@oh-my-pi/pi-coding-agent 15.0.1 → 15.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +8 -8
  3. package/src/commands/commit.ts +10 -0
  4. package/src/config/model-registry.ts +31 -1
  5. package/src/config/settings-schema.ts +11 -0
  6. package/src/discovery/claude-plugins.ts +19 -7
  7. package/src/eval/py/runner.py +42 -11
  8. package/src/eval/py/runtime.ts +1 -0
  9. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  10. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  11. package/src/hashline/input.ts +2 -1
  12. package/src/hashline/parser.ts +27 -3
  13. package/src/internal-urls/docs-index.generated.ts +8 -8
  14. package/src/internal-urls/router.ts +8 -0
  15. package/src/internal-urls/types.ts +21 -0
  16. package/src/lsp/config.ts +15 -6
  17. package/src/lsp/defaults.json +6 -2
  18. package/src/modes/acp/acp-agent.ts +248 -50
  19. package/src/modes/components/status-line/segments.ts +38 -4
  20. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  21. package/src/modes/rpc/host-uris.ts +235 -0
  22. package/src/modes/rpc/rpc-mode.ts +27 -1
  23. package/src/modes/rpc/rpc-types.ts +57 -0
  24. package/src/modes/runtime-init.ts +2 -1
  25. package/src/modes/theme/defaults/dark-poimandres.json +1 -0
  26. package/src/modes/theme/defaults/light-poimandres.json +1 -0
  27. package/src/modes/theme/theme.ts +6 -0
  28. package/src/prompts/tools/github.md +4 -4
  29. package/src/prompts/tools/hashline.md +22 -26
  30. package/src/prompts/tools/read.md +55 -37
  31. package/src/task/discovery.ts +5 -2
  32. package/src/task/executor.ts +2 -1
  33. package/src/tools/bash-command-fixup.ts +47 -0
  34. package/src/tools/bash.ts +39 -15
  35. package/src/tools/browser/render.ts +2 -2
  36. package/src/tools/eval.ts +10 -2
  37. package/src/tools/gh.ts +37 -4
  38. package/src/tools/job.ts +16 -7
  39. package/src/tools/output-meta.ts +26 -0
  40. package/src/tools/read.ts +32 -4
  41. package/src/tools/ssh.ts +3 -2
  42. package/src/tools/write.ts +20 -0
  43. package/src/web/search/providers/anthropic.ts +5 -0
  44. package/src/web/search/providers/exa.ts +3 -0
  45. package/src/web/search/providers/gemini.ts +5 -0
  46. package/src/web/search/providers/jina.ts +5 -2
  47. package/src/web/search/providers/zai.ts +5 -2
@@ -0,0 +1,235 @@
1
+ import { Snowflake } from "@oh-my-pi/pi-utils";
2
+ import { InternalUrlRouter } from "../../internal-urls";
3
+ import type {
4
+ InternalResource,
5
+ InternalUrl,
6
+ ProtocolHandler,
7
+ ResolveContext,
8
+ WriteContext,
9
+ } from "../../internal-urls/types";
10
+ import type {
11
+ RpcHostUriCancelRequest,
12
+ RpcHostUriRequest,
13
+ RpcHostUriResult,
14
+ RpcHostUriSchemeDefinition,
15
+ } from "./rpc-types";
16
+
17
+ type RpcHostUriOutput = (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
18
+
19
+ type PendingUriRequest = {
20
+ operation: "read" | "write";
21
+ url: string;
22
+ resolve: (frame: RpcHostUriResult) => void;
23
+ reject: (error: Error) => void;
24
+ };
25
+
26
+ /** Type guard for inbound `host_uri_result` frames coming from the host. */
27
+ export function isRpcHostUriResult(value: unknown): value is RpcHostUriResult {
28
+ if (!value || typeof value !== "object") return false;
29
+ const frame = value as { type?: unknown; id?: unknown };
30
+ return frame.type === "host_uri_result" && typeof frame.id === "string";
31
+ }
32
+
33
+ /**
34
+ * One handler instance per host-registered scheme. Delegates reads and (when
35
+ * the scheme was registered as writable) writes to the bridge, which serializes
36
+ * them over the RPC transport.
37
+ */
38
+ class RpcHostUriProtocolHandler implements ProtocolHandler {
39
+ readonly scheme: string;
40
+ readonly immutable: boolean;
41
+ readonly write?: (url: InternalUrl, content: string, context?: WriteContext) => Promise<void>;
42
+ readonly #bridge: RpcHostUriBridge;
43
+
44
+ constructor(definition: RpcHostUriSchemeDefinition, bridge: RpcHostUriBridge) {
45
+ this.scheme = definition.scheme;
46
+ this.immutable = definition.immutable === true;
47
+ this.#bridge = bridge;
48
+ if (definition.writable === true) {
49
+ this.write = (url, content, context) => this.#bridge.requestWrite(this.scheme, url, content, context);
50
+ }
51
+ }
52
+
53
+ resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
54
+ return this.#bridge.requestRead(this.scheme, url, context);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Bidirectional bridge that lets the RPC host own a set of URI schemes.
60
+ *
61
+ * The host registers schemes via `set_host_uri_schemes`; the bridge installs
62
+ * a `RpcHostUriProtocolHandler` per scheme into the process-global
63
+ * {@link InternalUrlRouter}. Reads land on the read tool through the existing
64
+ * router; writes are intercepted by the write tool and dispatched through
65
+ * `requestWrite`.
66
+ */
67
+ export class RpcHostUriBridge {
68
+ #output: RpcHostUriOutput;
69
+ #router: InternalUrlRouter;
70
+ #definitions = new Map<string, RpcHostUriSchemeDefinition>();
71
+ #pending = new Map<string, PendingUriRequest>();
72
+
73
+ constructor(output: RpcHostUriOutput, router: InternalUrlRouter = InternalUrlRouter.instance()) {
74
+ this.#output = output;
75
+ this.#router = router;
76
+ }
77
+
78
+ getSchemes(): string[] {
79
+ return Array.from(this.#definitions.keys());
80
+ }
81
+
82
+ /**
83
+ * Replace the registered set of host URI schemes. Previously registered
84
+ * schemes that no longer appear in the new set are unregistered from the
85
+ * router; surviving and new schemes get fresh handler instances.
86
+ */
87
+ setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[] {
88
+ const normalized = new Map<string, RpcHostUriSchemeDefinition>();
89
+ for (const raw of schemes) {
90
+ const scheme = typeof raw?.scheme === "string" ? raw.scheme.trim().toLowerCase() : "";
91
+ if (!scheme) {
92
+ throw new Error("Host URI scheme must be a non-empty string");
93
+ }
94
+ if (!/^[a-z][a-z0-9+.-]*$/.test(scheme)) {
95
+ throw new Error(`Host URI scheme contains invalid characters: ${raw.scheme}`);
96
+ }
97
+ normalized.set(scheme, {
98
+ scheme,
99
+ description: typeof raw.description === "string" ? raw.description : undefined,
100
+ writable: raw.writable === true,
101
+ immutable: raw.immutable === true,
102
+ });
103
+ }
104
+
105
+ for (const previous of this.#definitions.keys()) {
106
+ if (!normalized.has(previous)) {
107
+ this.#router.unregister(previous);
108
+ }
109
+ }
110
+ for (const definition of normalized.values()) {
111
+ this.#router.register(new RpcHostUriProtocolHandler(definition, this));
112
+ }
113
+ this.#definitions = normalized;
114
+ return Array.from(normalized.keys());
115
+ }
116
+
117
+ /**
118
+ * Unregister every host scheme from the router and reject any in-flight
119
+ * requests. Called on RPC shutdown to keep the global router clean for
120
+ * subsequent sessions in the same process (used by tests).
121
+ */
122
+ clear(message: string = "Host URI bridge shut down"): void {
123
+ for (const scheme of this.#definitions.keys()) {
124
+ this.#router.unregister(scheme);
125
+ }
126
+ this.#definitions.clear();
127
+ this.rejectAllPending(message);
128
+ }
129
+
130
+ /** Resolve a pending request by id; called by `rpc-mode` on inbound results. */
131
+ handleResult(frame: RpcHostUriResult): boolean {
132
+ const pending = this.#pending.get(frame.id);
133
+ if (!pending) return false;
134
+ this.#pending.delete(frame.id);
135
+ pending.resolve(frame);
136
+ return true;
137
+ }
138
+
139
+ rejectAllPending(message: string): void {
140
+ const error = new Error(message);
141
+ const pending = Array.from(this.#pending.values());
142
+ this.#pending.clear();
143
+ for (const entry of pending) {
144
+ entry.reject(error);
145
+ }
146
+ }
147
+
148
+ async requestRead(scheme: string, url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
149
+ const result = await this.#dispatch("read", url.href, undefined, context?.signal);
150
+ if (result.isError) {
151
+ throw new Error(result.error || result.content || `Host URI read failed for ${url.href}`);
152
+ }
153
+ const content = result.content ?? "";
154
+ const contentType = result.contentType ?? "text/plain";
155
+ const definition = this.#definitions.get(scheme);
156
+ return {
157
+ url: url.href,
158
+ content,
159
+ contentType,
160
+ size: Buffer.byteLength(content, "utf-8"),
161
+ notes: result.notes && result.notes.length > 0 ? [...result.notes] : undefined,
162
+ immutable: result.immutable ?? definition?.immutable === true,
163
+ };
164
+ }
165
+
166
+ async requestWrite(_scheme: string, url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
167
+ const result = await this.#dispatch("write", url.href, content, context?.signal);
168
+ if (result.isError) {
169
+ throw new Error(result.error || result.content || `Host URI write failed for ${url.href}`);
170
+ }
171
+ }
172
+
173
+ #dispatch(
174
+ operation: "read" | "write",
175
+ url: string,
176
+ content: string | undefined,
177
+ signal: AbortSignal | undefined,
178
+ ): Promise<RpcHostUriResult> {
179
+ if (signal?.aborted) {
180
+ return Promise.reject(new Error(`Host URI ${operation} for ${url} was aborted`));
181
+ }
182
+
183
+ const id = Snowflake.next() as string;
184
+ const { promise, resolve, reject } = Promise.withResolvers<RpcHostUriResult>();
185
+ let settled = false;
186
+
187
+ const cleanup = () => {
188
+ signal?.removeEventListener("abort", onAbort);
189
+ this.#pending.delete(id);
190
+ };
191
+
192
+ const onAbort = () => {
193
+ if (settled) return;
194
+ settled = true;
195
+ cleanup();
196
+ this.#output({
197
+ type: "host_uri_cancel",
198
+ id: Snowflake.next() as string,
199
+ targetId: id,
200
+ });
201
+ reject(new Error(`Host URI ${operation} for ${url} was aborted`));
202
+ };
203
+
204
+ signal?.addEventListener("abort", onAbort, { once: true });
205
+ this.#pending.set(id, {
206
+ operation,
207
+ url,
208
+ resolve: frame => {
209
+ if (settled) return;
210
+ settled = true;
211
+ cleanup();
212
+ resolve(frame);
213
+ },
214
+ reject: err => {
215
+ if (settled) return;
216
+ settled = true;
217
+ cleanup();
218
+ reject(err);
219
+ },
220
+ });
221
+
222
+ const frame: RpcHostUriRequest = {
223
+ type: "host_uri_request",
224
+ id,
225
+ operation,
226
+ url,
227
+ };
228
+ if (operation === "write") {
229
+ frame.content = content ?? "";
230
+ }
231
+ this.#output(frame);
232
+
233
+ return promise;
234
+ }
235
+ }
@@ -21,6 +21,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
21
21
  import type { AgentSession } from "../../session/agent-session";
22
22
  import { initializeExtensions } from "../runtime-init";
23
23
  import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
24
+ import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
24
25
  import type {
25
26
  RpcCommand,
26
27
  RpcExtensionUIRequest,
@@ -28,6 +29,8 @@ import type {
28
29
  RpcHostToolCallRequest,
29
30
  RpcHostToolCancelRequest,
30
31
  RpcHostToolDefinition,
32
+ RpcHostUriCancelRequest,
33
+ RpcHostUriRequest,
31
34
  RpcResponse,
32
35
  RpcSessionState,
33
36
  } from "./rpc-types";
@@ -41,7 +44,14 @@ export type PendingExtensionRequest = {
41
44
  };
42
45
 
43
46
  type RpcOutput = (
44
- obj: RpcResponse | RpcExtensionUIRequest | RpcHostToolCallRequest | RpcHostToolCancelRequest | object,
47
+ obj:
48
+ | RpcResponse
49
+ | RpcExtensionUIRequest
50
+ | RpcHostToolCallRequest
51
+ | RpcHostToolCancelRequest
52
+ | RpcHostUriRequest
53
+ | RpcHostUriCancelRequest
54
+ | object,
45
55
  ) => void;
46
56
 
47
57
  function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
@@ -188,6 +198,7 @@ export async function runRpcMode(
188
198
 
189
199
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
190
200
  const hostToolBridge = new RpcHostToolBridge(output);
201
+ const hostUriBridge = new RpcHostUriBridge(output);
191
202
 
192
203
  // Shutdown request flag (wrapped in object to allow mutation with const)
193
204
  const shutdownState = { requested: false };
@@ -533,6 +544,15 @@ export async function runRpcMode(
533
544
  return success(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
534
545
  }
535
546
 
547
+ case "set_host_uri_schemes": {
548
+ try {
549
+ const schemes = hostUriBridge.setSchemes(command.schemes);
550
+ return success(id, "set_host_uri_schemes", { schemes });
551
+ } catch (err) {
552
+ return error(id, "set_host_uri_schemes", err instanceof Error ? err.message : String(err));
553
+ }
554
+ }
555
+
536
556
  // =================================================================
537
557
  // Model
538
558
  // =================================================================
@@ -807,6 +827,11 @@ export async function runRpcMode(
807
827
  continue;
808
828
  }
809
829
 
830
+ if (isRpcHostUriResult(parsed)) {
831
+ hostUriBridge.handleResult(parsed);
832
+ continue;
833
+ }
834
+
810
835
  // Handle regular commands
811
836
  const command = parsed as RpcCommand;
812
837
  const response = await handleCommand(command);
@@ -821,5 +846,6 @@ export async function runRpcMode(
821
846
 
822
847
  // stdin closed — RPC client is gone, exit cleanly
823
848
  hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
849
+ hostUriBridge.clear("RPC client disconnected before host URI request completed");
824
850
  process.exit(0);
825
851
  }
@@ -29,6 +29,7 @@ export type RpcCommand =
29
29
  | { id?: string; type: "get_state" }
30
30
  | { id?: string; type: "set_todos"; phases: TodoPhase[] }
31
31
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
32
+ | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
32
33
 
33
34
  // Model
34
35
  | { id?: string; type: "set_model"; provider: string; modelId: string }
@@ -121,6 +122,7 @@ export type RpcResponse =
121
122
  | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
122
123
  | { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
123
124
  | { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
125
+ | { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
124
126
 
125
127
  // Model
126
128
  | {
@@ -304,6 +306,61 @@ export interface RpcHostToolResult {
304
306
  isError?: boolean;
305
307
  }
306
308
 
309
+ // ============================================================================
310
+ // Host URI Frames (bidirectional)
311
+ // ============================================================================
312
+
313
+ export interface RpcHostUriSchemeDefinition {
314
+ /** URL scheme without trailing `://` (e.g. `db`, `notion`). */
315
+ scheme: string;
316
+ /** Optional human-readable description for logs/diagnostics. */
317
+ description?: string;
318
+ /** When true, the write tool is allowed to dispatch writes to this scheme. */
319
+ writable?: boolean;
320
+ /** When true, downstream callers suppress hashline anchors for resolved content. */
321
+ immutable?: boolean;
322
+ }
323
+
324
+ export type RpcHostUriOperation = "read" | "write";
325
+
326
+ /** Emitted by the RPC server when it needs the host to satisfy a URI operation. */
327
+ export interface RpcHostUriRequest {
328
+ type: "host_uri_request";
329
+ id: string;
330
+ operation: RpcHostUriOperation;
331
+ url: string;
332
+ /** Present for write operations. */
333
+ content?: string;
334
+ }
335
+
336
+ /** Emitted by the RPC server when a pending URI request should be aborted. */
337
+ export interface RpcHostUriCancelRequest {
338
+ type: "host_uri_cancel";
339
+ id: string;
340
+ targetId: string;
341
+ }
342
+
343
+ /** Sent by the host to complete a pending URI request. */
344
+ export interface RpcHostUriResult {
345
+ type: "host_uri_result";
346
+ id: string;
347
+ /**
348
+ * Required for successful `read` results. Ignored for `write` success.
349
+ * Set on errors when a textual explanation accompanies `isError`.
350
+ */
351
+ content?: string;
352
+ /** Defaults to `text/plain` when omitted. */
353
+ contentType?: "text/markdown" | "application/json" | "text/plain";
354
+ /** Optional resolution notes propagated to the read tool. */
355
+ notes?: string[];
356
+ /** Overrides the scheme-level `immutable` flag for this single resolution. */
357
+ immutable?: boolean;
358
+ /** When true, surface the result content as an error to the caller. */
359
+ isError?: boolean;
360
+ /** Optional error message; preferred over `content` for error surfacing. */
361
+ error?: string;
362
+ }
363
+
307
364
  // ============================================================================
308
365
  // Extension UI Commands (stdin)
309
366
  // ============================================================================
@@ -7,6 +7,7 @@
7
7
  * caller-supplied hooks.
8
8
  */
9
9
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
10
+ import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
10
11
  import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
11
12
  import type { AgentSession } from "../session/agent-session";
12
13
 
@@ -59,7 +60,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
59
60
  getActiveTools: () => session.getActiveToolNames(),
60
61
  getAllTools: () => session.getAllToolNames(),
61
62
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
62
- getCommands: () => [],
63
+ getCommands: () => getSessionSlashCommands(session),
63
64
  setModel: model => runExtensionSetModel(session, model),
64
65
  getThinkingLevel: () => session.thinkingLevel,
65
66
  setThinkingLevel: level => session.setThinkingLevel(level),
@@ -133,6 +133,7 @@
133
133
  "icon.pause": "‖",
134
134
  "icon.loop": "↻",
135
135
  "icon.folder": "▸",
136
+ "icon.scratchFolder": "◌",
136
137
  "icon.pi": "π",
137
138
  "format.bullet": "◦",
138
139
  "md.bullet": "◦"
@@ -133,6 +133,7 @@
133
133
  "icon.pause": "‖",
134
134
  "icon.loop": "↻",
135
135
  "icon.folder": "▸",
136
+ "icon.scratchFolder": "◌",
136
137
  "icon.pi": "π",
137
138
  "format.bullet": "◦",
138
139
  "md.bullet": "◦"
@@ -95,6 +95,7 @@ export type SymbolKey =
95
95
  | "icon.pause"
96
96
  | "icon.loop"
97
97
  | "icon.folder"
98
+ | "icon.scratchFolder"
98
99
  | "icon.file"
99
100
  | "icon.git"
100
101
  | "icon.branch"
@@ -258,6 +259,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
258
259
  "icon.pause": "⏸",
259
260
  "icon.loop": "↻",
260
261
  "icon.folder": "📁",
262
+ "icon.scratchFolder": "🗑",
261
263
  "icon.file": "📄",
262
264
  "icon.git": "⎇",
263
265
  "icon.branch": "⑂",
@@ -476,6 +478,8 @@ const NERD_SYMBOLS: SymbolMap = {
476
478
  "icon.loop": "\uf021",
477
479
  // pick:  | alt:  
478
480
  "icon.folder": "\uf115",
481
+ // pick: | alt:
482
+ "icon.scratchFolder": "\uf014",
479
483
  // pick:  | alt:  
480
484
  "icon.file": "\uf15b",
481
485
  // pick:  | alt:  ⎇
@@ -678,6 +682,7 @@ const ASCII_SYMBOLS: SymbolMap = {
678
682
  "icon.pause": "||",
679
683
  "icon.loop": "loop",
680
684
  "icon.folder": "[D]",
685
+ "icon.scratchFolder": "[T]",
681
686
  "icon.file": "[F]",
682
687
  "icon.git": "git:",
683
688
  "icon.branch": "@",
@@ -1457,6 +1462,7 @@ export class Theme {
1457
1462
  pause: this.#symbols["icon.pause"],
1458
1463
  loop: this.#symbols["icon.loop"],
1459
1464
  folder: this.#symbols["icon.folder"],
1465
+ scratchFolder: this.#symbols["icon.scratchFolder"],
1460
1466
  file: this.#symbols["icon.file"],
1461
1467
  git: this.#symbols["icon.git"],
1462
1468
  branch: this.#symbols["icon.branch"],
@@ -6,10 +6,10 @@ Pick the operation via `op`. Each op uses a subset of the parameters:
6
6
  - `pr_create` — Create a pull request. Either provide `title` (and optional `body`) or set `fill: true` to auto-fill from commits. Optional `base` (target, defaults to repo default), `head` (source, defaults to current branch), `draft`, `repo`, `reviewer[]`, `assignee[]`, `label[]`. Returns the new PR URL plus a summary.
7
7
  - `pr_checkout` — Check one or more pull requests out into dedicated git worktrees. Optional `pr` (number, URL, branch, or array of any of those — pass an array to batch-check-out multiple PRs in one call), `repo`, `force` (reset existing local branch).
8
8
  - `pr_push` — Push a checked-out PR branch back to its source branch. Requires the branch to have been checked out via `op: pr_checkout` (carries push metadata). Optional `branch`; defaults to the current checked-out git branch. Optional `forceWithLease`.
9
- - `search_issues` — Search issues using normal GitHub issue search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`.
10
- - `search_prs` — Search pull requests using normal GitHub PR search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`.
11
- - `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments. Date filtering (`since`/`until`) is **not** supported by GitHub code search.
12
- - `search_commits` — Search commits across GitHub. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`. `dateField` is ignored — always uses `committer-date`.
9
+ - `search_issues` — Search issues using normal GitHub issue search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
10
+ - `search_prs` — Search pull requests using normal GitHub PR search syntax. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`, `dateField`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
11
+ - `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it. Date filtering (`since`/`until`) is **not** supported by GitHub code search.
12
+ - `search_commits` — Search commits across GitHub. Optional `query` (required unless `since`/`until` is set), `repo`, `limit`, `since`, `until`. `dateField` is ignored — always uses `committer-date`. Defaults `repo` to the current checkout's `owner/repo` when omitted; pass an explicit `repo:`/`org:`/`user:` qualifier in `query` to search outside it.
13
13
  - `search_repos` — Search repositories across GitHub. Optional `query` (required unless `since`/`until` is set), `limit`, `since`, `until`, `dateField` (use query qualifiers like `org:`, `language:` instead of `repo`).
14
14
  - Date filter format for `since` / `until`: relative duration `<n><unit>` (`m`/`h`/`d`/`w`/`mo`/`y`, e.g. `3d`, `12h`, `2w`), an ISO date `YYYY-MM-DD`, or an ISO datetime. Translated to a single GitHub-search qualifier (`created:≥…`, `created:≤…`, or `created:since..until`). `dateField: "updated"` maps to `updated:` for issues/prs and `pushed:` for repos. When you only want a date filter and no keywords, omit `query` entirely.
15
15
  - `run_watch` — Watch a GitHub Actions workflow run. Optional `run` (id or URL). Omitting `run` watches all workflow runs for the current HEAD commit; `branch` falls back to the current branch. Optional `tail` (log lines per failed job). Streams snapshots, fast-fails on the first detected job failure (with a brief grace period to capture concurrent failures), then fetches tailed logs for the failed jobs. The full failed-job logs are saved as a session artifact for on-demand reads.
@@ -66,36 +66,35 @@ When braces bound your edit, you SHOULD prefer these shapes:
66
66
  </common-failures>
67
67
 
68
68
  <case file="mod.ts">
69
- {{hline 1 "const DEF = \"guest\";"}}
70
- {{hline 2 "export function label(name) {"}}
69
+ {{hline 1 "const TITLE = \"Mr\";"}}
70
+ {{hline 2 "export function greet(name) {"}}
71
71
  {{hline 3 "\treturn ["}}
72
- {{hline 4 "\t\tname?.trim() || DEF,"}}
73
- {{hline 5 "\t\t\" \","}}
74
- {{hline 6 "\t].join(\"\");"}}
72
+ {{hline 4 "\t\tTITLE,"}}
73
+ {{hline 5 "\t\tname?.trim() || \"guest\","}}
74
+ {{hline 6 "\t].join(\" \");"}}
75
75
  {{hline 7 "}"}}
76
76
  </case>
77
77
 
78
78
  <examples>
79
79
  # Replace one line (the payload must re-emit the original indentation)
80
80
  @@ mod.ts
81
- = {{hrefr 4}}..{{hrefr 4}}
82
- {{hsep}} name?.trim().toUpperCase() || DEF,
81
+ = {{hrefr 1}}..{{hrefr 1}}
82
+ {{hsep}}const TITLE = "Mrs";
83
83
 
84
84
  # Replace a full multiline statement (widen to a self-contained boundary)
85
85
  @@ mod.ts
86
86
  = {{hrefr 3}}..{{hrefr 6}}
87
87
  {{hsep}} return [
88
- {{hsep}} name?.trim() || DEF,
89
- {{hsep}} "·",
90
- {{hsep}} " ",
91
- {{hsep}} ].join("");
88
+ {{hsep}} "Mrs",
89
+ {{hsep}} name?.trim() || "guest",
90
+ {{hsep}} ].join(" ");
92
91
 
93
92
  # Insert AFTER/BEFORE a line
94
93
  @@ mod.ts
95
- + {{hrefr 3}}
96
- {{hsep}} "·",
94
+ + {{hrefr 4}}
95
+ {{hsep}} "Dr",
97
96
  < {{hrefr 5}}
98
- {{hsep}} "·",
97
+ {{hsep}} "Dr",
99
98
 
100
99
  # Append to file
101
100
  @@ mod.ts
@@ -112,13 +111,12 @@ When braces bound your edit, you SHOULD prefer these shapes:
112
111
  </examples>
113
112
 
114
113
  <anti-pattern>
115
- # WRONG — replaces 3 lines just to add one.
114
+ # WRONG — replaces 2 lines just to add one.
116
115
  @@ mod.ts
117
- = {{hrefr 1}}..{{hrefr 3}}
118
- {{hsep}}const DEF = "guest";
116
+ = {{hrefr 1}}..{{hrefr 2}}
117
+ {{hsep}}const TITLE = "Mr";
119
118
  {{hsep}}const DEBUG = false;
120
- {{hsep}}export function label(name) {
121
- {{hsep}} return [
119
+ {{hsep}}export function greet(name) {
122
120
  # RIGHT — same effect, one-line insert
123
121
  @@ mod.ts
124
122
  + {{hrefr 1}}
@@ -127,17 +125,15 @@ When braces bound your edit, you SHOULD prefer these shapes:
127
125
  # WRONG — replace from the middle of a larger statement (error-prone)
128
126
  @@ mod.ts
129
127
  = {{hrefr 4}}..{{hrefr 5}}
130
- {{hsep}} name?.trim() || DEF,
131
- {{hsep}} "·",
132
- {{hsep}} " • ",
128
+ {{hsep}} "Dr",
129
+ {{hsep}} name?.trim() || "guest",
133
130
  # RIGHT — widen to the full statement
134
131
  @@ mod.ts
135
132
  = {{hrefr 3}}..{{hrefr 6}}
136
133
  {{hsep}} return [
137
- {{hsep}} name?.trim() || DEF,
138
- {{hsep}} "·",
139
- {{hsep}} " ",
140
- {{hsep}} ].join("");
134
+ {{hsep}} "Dr",
135
+ {{hsep}} name?.trim() || "guest",
136
+ {{hsep}} ].join(" ");
141
137
  </anti-pattern>
142
138
 
143
139
  <critical>