@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +7 -15
  3. package/scripts/build-binary.ts +1 -1
  4. package/src/cli/update-cli.ts +25 -1
  5. package/src/config/model-registry.ts +21 -19
  6. package/src/config/settings-schema.ts +14 -19
  7. package/src/discovery/claude-plugins.ts +28 -3
  8. package/src/edit/modes/atom.lark +7 -5
  9. package/src/edit/modes/atom.ts +510 -73
  10. package/src/edit/modes/hashline.ts +172 -91
  11. package/src/extensibility/extensions/runner.ts +34 -1
  12. package/src/extensibility/extensions/types.ts +8 -0
  13. package/src/lsp/client.ts +27 -35
  14. package/src/lsp/index.ts +2 -4
  15. package/src/lsp/render.ts +0 -3
  16. package/src/lsp/types.ts +1 -4
  17. package/src/lsp/utils.ts +18 -14
  18. package/src/memories/index.ts +5 -0
  19. package/src/modes/components/settings-defs.ts +1 -1
  20. package/src/modes/controllers/command-controller.ts +17 -0
  21. package/src/modes/controllers/input-controller.ts +7 -1
  22. package/src/modes/controllers/selector-controller.ts +2 -2
  23. package/src/modes/interactive-mode.ts +57 -26
  24. package/src/modes/theme/theme.ts +10 -1
  25. package/src/modes/types.ts +5 -3
  26. package/src/modes/utils/context-usage.ts +294 -0
  27. package/src/modes/utils/ui-helpers.ts +19 -6
  28. package/src/prompts/system/auto-continue.md +1 -0
  29. package/src/prompts/tools/atom.md +99 -44
  30. package/src/prompts/tools/exit-plan-mode.md +5 -39
  31. package/src/prompts/tools/github.md +3 -3
  32. package/src/prompts/tools/lsp.md +2 -3
  33. package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
  34. package/src/prompts/tools/task.md +34 -147
  35. package/src/prompts/tools/todo-write.md +22 -64
  36. package/src/sdk.ts +13 -2
  37. package/src/session/agent-session.ts +175 -79
  38. package/src/session/compaction/compaction.ts +35 -22
  39. package/src/session/session-dump-format.ts +1 -0
  40. package/src/session/session-manager.ts +19 -2
  41. package/src/slash-commands/builtin-registry.ts +12 -5
  42. package/src/tools/bash.ts +9 -4
  43. package/src/tools/debug.ts +57 -70
  44. package/src/tools/gh.ts +267 -119
  45. package/src/tools/index.ts +7 -7
  46. package/src/tools/{run-command → recipe}/index.ts +19 -19
  47. package/src/tools/recipe/render.ts +19 -0
  48. package/src/tools/{run-command → recipe}/runner.ts +28 -7
  49. package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
  50. package/src/tools/renderers.ts +2 -2
  51. package/src/utils/git.ts +61 -2
  52. package/src/web/search/providers/searxng.ts +71 -13
  53. package/src/tools/run-command/render.ts +0 -18
  54. /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
  55. /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
  56. /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
  57. /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
  58. /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
@@ -9,6 +9,8 @@ export interface RunnerTask {
9
9
  commandPrefix?: string;
10
10
  /** Token passed to the runner command; defaults to `name`. Used when display names are namespaced. */
11
11
  commandName?: string;
12
+ /** Working directory for the task, relative to the session cwd; absent means the runner's root cwd. */
13
+ cwd?: string;
12
14
  }
13
15
 
14
16
  export interface DetectedRunner {
@@ -39,16 +41,20 @@ interface PromptTaskModel {
39
41
  paramSig?: string;
40
42
  command?: string;
41
43
  doc?: string;
44
+ cwd?: string;
42
45
  }
43
46
 
47
+ const PROMPT_TASK_LIMIT = 20;
48
+
44
49
  interface PromptRunnerModel {
45
50
  id: string;
46
51
  label: string;
47
52
  commandPrefix: string;
48
53
  tasks: PromptTaskModel[];
54
+ hiddenTaskCount?: number;
49
55
  }
50
56
 
51
- export interface RunCommandPromptModel {
57
+ export interface RecipePromptModel {
52
58
  [key: string]: unknown;
53
59
  hasMultipleRunners: boolean;
54
60
  ambiguityExampleRunner?: string;
@@ -101,7 +107,7 @@ function resolveRunnerAndTask(
101
107
  ): { runner: DetectedRunner; task: RunnerTask; tail: string } {
102
108
  const { head, tail } = parseOp(op);
103
109
  if (!head) {
104
- throw new ToolError(`run_command op is empty. Available tasks:\n${formatAvailableTasks(runners)}`);
110
+ throw new ToolError(`recipe op is empty. Available tasks:\n${formatAvailableTasks(runners)}`);
105
111
  }
106
112
 
107
113
  const colonIndex = head.indexOf(":");
@@ -136,12 +142,18 @@ function resolveRunnerAndTask(
136
142
  );
137
143
  }
138
144
 
139
- export function resolveCommand(op: string, runners: DetectedRunner[]): string {
145
+ export interface ResolvedTask {
146
+ command: string;
147
+ cwd?: string;
148
+ }
149
+
150
+ export function resolveCommand(op: string, runners: DetectedRunner[]): ResolvedTask {
140
151
  const { runner, task, tail } = resolveRunnerAndTask(op, runners);
141
- return buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, tail);
152
+ const command = buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, tail);
153
+ return task.cwd ? { command, cwd: task.cwd } : { command };
142
154
  }
143
155
 
144
- export function commandFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined {
156
+ export function resolveTaskFromOp(op: string | undefined, runners: DetectedRunner[]): ResolvedTask | undefined {
145
157
  if (!op) return undefined;
146
158
  try {
147
159
  return resolveCommand(op, runners);
@@ -150,6 +162,14 @@ export function commandFromOp(op: string | undefined, runners: DetectedRunner[])
150
162
  }
151
163
  }
152
164
 
165
+ export function commandFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined {
166
+ return resolveTaskFromOp(op, runners)?.command;
167
+ }
168
+
169
+ export function cwdFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined {
170
+ return resolveTaskFromOp(op, runners)?.cwd;
171
+ }
172
+
153
173
  export function titleFromOp(op: string | undefined, runners: DetectedRunner[]): string {
154
174
  if (!op) return "Run";
155
175
  const { head } = parseOp(op);
@@ -177,7 +197,7 @@ function findAmbiguityExample(runners: DetectedRunner[]): { runner: string; task
177
197
  return firstRunner && firstTask ? { runner: firstRunner.id, task: firstTask.name } : undefined;
178
198
  }
179
199
 
180
- export function buildPromptModel(runners: DetectedRunner[]): RunCommandPromptModel {
200
+ export function buildPromptModel(runners: DetectedRunner[]): RecipePromptModel {
181
201
  const ambiguityExample = findAmbiguityExample(runners);
182
202
  return {
183
203
  hasMultipleRunners: runners.length > 1,
@@ -187,11 +207,12 @@ export function buildPromptModel(runners: DetectedRunner[]): RunCommandPromptMod
187
207
  id: runner.id,
188
208
  label: runner.label,
189
209
  commandPrefix: runner.commandPrefix,
190
- tasks: runner.tasks.map(task => ({
210
+ tasks: runner.tasks.slice(0, PROMPT_TASK_LIMIT).map(task => ({
191
211
  name: task.name,
192
212
  paramSig: task.parameters.length > 0 ? task.parameters.join(" ") : undefined,
193
213
  command: buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, ""),
194
214
  doc: task.doc,
215
+ cwd: task.cwd,
195
216
  })),
196
217
  })),
197
218
  };
@@ -9,9 +9,23 @@ interface PackageJsonInfo {
9
9
  workspaces: string[];
10
10
  }
11
11
 
12
- interface PackageCommandInfo {
13
- rootCommandPrefix: string;
14
- workspaceCommandPrefix: (relativeDir: string) => string;
12
+ async function resolvePackageRunner(cwd: string): Promise<string> {
13
+ if ((await isFile(path.join(cwd, "bun.lock"))) || (await isFile(path.join(cwd, "bun.lockb")))) {
14
+ return "bun run";
15
+ }
16
+ if (await isFile(path.join(cwd, "pnpm-lock.yaml"))) {
17
+ return "pnpm run";
18
+ }
19
+ if (await isFile(path.join(cwd, "yarn.lock"))) {
20
+ return "yarn";
21
+ }
22
+ if ((await isFile(path.join(cwd, "package-lock.json"))) || (await isFile(path.join(cwd, "npm-shrinkwrap.json")))) {
23
+ return "npm run";
24
+ }
25
+ if ($which("bun")) {
26
+ return "bun run";
27
+ }
28
+ return "npm run";
15
29
  }
16
30
 
17
31
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -32,43 +46,6 @@ async function isFile(filePath: string): Promise<boolean> {
32
46
  }
33
47
  }
34
48
 
35
- async function resolvePackageRunner(cwd: string): Promise<PackageCommandInfo> {
36
- if ((await isFile(path.join(cwd, "bun.lock"))) || (await isFile(path.join(cwd, "bun.lockb")))) {
37
- return {
38
- rootCommandPrefix: "bun run",
39
- workspaceCommandPrefix: relativeDir => `bun --cwd ${shellQuote(relativeDir)} run`,
40
- };
41
- }
42
- if (await isFile(path.join(cwd, "pnpm-lock.yaml"))) {
43
- return {
44
- rootCommandPrefix: "pnpm run",
45
- workspaceCommandPrefix: relativeDir => `pnpm --dir ${shellQuote(relativeDir)} run`,
46
- };
47
- }
48
- if (await isFile(path.join(cwd, "yarn.lock"))) {
49
- return {
50
- rootCommandPrefix: "yarn",
51
- workspaceCommandPrefix: relativeDir => `yarn --cwd ${shellQuote(relativeDir)}`,
52
- };
53
- }
54
- if ((await isFile(path.join(cwd, "package-lock.json"))) || (await isFile(path.join(cwd, "npm-shrinkwrap.json")))) {
55
- return {
56
- rootCommandPrefix: "npm run",
57
- workspaceCommandPrefix: relativeDir => `npm --prefix ${shellQuote(relativeDir)} run`,
58
- };
59
- }
60
- if ($which("bun")) {
61
- return {
62
- rootCommandPrefix: "bun run",
63
- workspaceCommandPrefix: relativeDir => `bun --cwd ${shellQuote(relativeDir)} run`,
64
- };
65
- }
66
- return {
67
- rootCommandPrefix: "npm run",
68
- workspaceCommandPrefix: relativeDir => `npm --prefix ${shellQuote(relativeDir)} run`,
69
- };
70
- }
71
-
72
49
  function parseWorkspacePatterns(pkg: Record<string, unknown>): string[] {
73
50
  const { workspaces } = pkg;
74
51
  if (Array.isArray(workspaces)) return workspaces.filter((entry): entry is string => typeof entry === "string");
@@ -129,22 +106,17 @@ function packageTaskName(packageName: string | undefined, packageDir: string, sc
129
106
  return `${packageName ?? packageDir}/${scriptName}`;
130
107
  }
131
108
 
132
- function tasksForPackage(options: {
133
- pkg: PackageJsonInfo;
134
- packageDir: string;
135
- commandPrefix: string;
136
- namespaced: boolean;
137
- }): RunnerTask[] {
109
+ function tasksForPackage(options: { pkg: PackageJsonInfo; packageDir: string; namespaced: boolean }): RunnerTask[] {
138
110
  return options.pkg.scripts.map(scriptName => ({
139
111
  name: options.namespaced ? packageTaskName(options.pkg.name, options.packageDir, scriptName) : scriptName,
140
112
  doc: options.namespaced ? options.packageDir : undefined,
141
113
  parameters: [],
142
- commandPrefix: options.commandPrefix,
114
+ cwd: options.namespaced ? options.packageDir : undefined,
143
115
  commandName: shellQuote(scriptName),
144
116
  }));
145
117
  }
146
118
 
147
- async function readPackageTasks(cwd: string, commandInfo: PackageCommandInfo): Promise<RunnerTask[] | null> {
119
+ async function readPackageTasks(cwd: string): Promise<RunnerTask[] | null> {
148
120
  const rootPkg = await readPackageJson(path.join(cwd, "package.json"));
149
121
  if (!rootPkg) return null;
150
122
  const workspacePackageJsons = await findWorkspacePackageJsons(cwd, rootPkg.workspaces);
@@ -155,7 +127,6 @@ async function readPackageTasks(cwd: string, commandInfo: PackageCommandInfo): P
155
127
  ...tasksForPackage({
156
128
  pkg: rootPkg,
157
129
  packageDir: ".",
158
- commandPrefix: commandInfo.rootCommandPrefix,
159
130
  namespaced: false,
160
131
  }),
161
132
  );
@@ -169,7 +140,6 @@ async function readPackageTasks(cwd: string, commandInfo: PackageCommandInfo): P
169
140
  ...tasksForPackage({
170
141
  pkg,
171
142
  packageDir,
172
- commandPrefix: commandInfo.workspaceCommandPrefix(packageDir),
173
143
  namespaced: true,
174
144
  }),
175
145
  );
@@ -183,10 +153,10 @@ export const pkgRunner: TaskRunner = {
183
153
  label: "Pkg",
184
154
  async detect(cwd: string): Promise<DetectedRunner | null> {
185
155
  try {
186
- const commandInfo = await resolvePackageRunner(cwd);
187
- const tasks = await readPackageTasks(cwd, commandInfo);
156
+ const commandPrefix = await resolvePackageRunner(cwd);
157
+ const tasks = await readPackageTasks(cwd);
188
158
  if (!tasks || tasks.length === 0) return null;
189
- return { id: "pkg", label: "Pkg", commandPrefix: commandInfo.rootCommandPrefix, tasks };
159
+ return { id: "pkg", label: "Pkg", commandPrefix, tasks };
190
160
  } catch (err) {
191
161
  logger.debug("package runner probe failed", { error: err instanceof Error ? err.message : String(err) });
192
162
  return null;
@@ -23,8 +23,8 @@ import { jobToolRenderer } from "./job";
23
23
  import { notebookToolRenderer } from "./notebook";
24
24
  import { pythonToolRenderer } from "./python";
25
25
  import { readToolRenderer } from "./read";
26
+ import { recipeToolRenderer } from "./recipe/render";
26
27
  import { resolveToolRenderer } from "./resolve";
27
- import { runCommandToolRenderer } from "./run-command/render";
28
28
  import { searchToolRenderer } from "./search";
29
29
  import { searchToolBm25Renderer } from "./search-tool-bm25";
30
30
  import { sshToolRenderer } from "./ssh";
@@ -49,7 +49,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
49
49
  ast_grep: astGrepToolRenderer as ToolRenderer,
50
50
  ast_edit: astEditToolRenderer as ToolRenderer,
51
51
  bash: bashToolRenderer as ToolRenderer,
52
- run_command: runCommandToolRenderer as ToolRenderer,
52
+ recipe: recipeToolRenderer as ToolRenderer,
53
53
  debug: debugToolRenderer as ToolRenderer,
54
54
  python: pythonToolRenderer as ToolRenderer,
55
55
  calc: calculatorToolRenderer as ToolRenderer,
package/src/utils/git.ts CHANGED
@@ -150,6 +150,7 @@ const SHORT_LIVED_GIT_CONFIG: readonly (readonly [key: string, value: string])[]
150
150
  ["core.fsmonitor", "false"],
151
151
  ["core.untrackedCache", "false"],
152
152
  ];
153
+ const REMOTE_ALREADY_EXISTS = /remote .* already exists/i;
153
154
 
154
155
  interface CommandOptions {
155
156
  readonly env?: Record<string, string | undefined>;
@@ -267,6 +268,51 @@ async function tryText(
267
268
  return result.stdout;
268
269
  }
269
270
 
271
+ // ════════════════════════════════════════════════════════════════════════════
272
+ // Internal: per-repo write serialization
273
+ // ════════════════════════════════════════════════════════════════════════════
274
+
275
+ // Git uses lock files (`.git/config.lock`, commit-graph chain locks,
276
+ // `packed-refs.lock`, …) for many of its mutating operations. Each is created
277
+ // O_EXCL with no waiter, so concurrent in-process git invocations against the
278
+ // same repository fail immediately rather than block. Worktrees share the
279
+ // primary repo's `.git` directory, so racing across worktrees has the same
280
+ // failure mode. We give callers a single per-repo serialization point keyed by
281
+ // the primary repo root: any block that mutates repo state should hold this
282
+ // lock so unrelated callers cannot collide on git's internal locks.
283
+ const repoWriteChain = new Map<string, Promise<unknown>>();
284
+
285
+ /**
286
+ * Serialize an async block that mutates a git repository against other
287
+ * in-process callers operating on the same repository. The lock is keyed by
288
+ * the primary repo root so worktrees of the same repo share a single queue.
289
+ * Failures in one block do not poison the queue for the next caller.
290
+ *
291
+ * Not reentrant: do NOT nest acquisitions for the same repo. Helpers in this
292
+ * module never auto-acquire — callers wrap the critical section themselves.
293
+ */
294
+ export async function withRepoLock<T>(cwd: string, fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
295
+ const key = (await repo.primaryRoot(cwd, signal)) ?? cwd;
296
+ const prior = repoWriteChain.get(key);
297
+ const run = (async () => {
298
+ if (prior) {
299
+ try {
300
+ await prior;
301
+ } catch {
302
+ // A prior caller failing must not block us from running.
303
+ }
304
+ }
305
+ throwIfAborted(signal);
306
+ return fn();
307
+ })();
308
+ repoWriteChain.set(key, run);
309
+ try {
310
+ return await run;
311
+ } finally {
312
+ if (repoWriteChain.get(key) === run) repoWriteChain.delete(key);
313
+ }
314
+ }
315
+
270
316
  function splitLines(text: string): string[] {
271
317
  return text
272
318
  .split("\n")
@@ -955,9 +1001,22 @@ export const remote = {
955
1001
  return trimScalar(await tryText(cwd, ["remote", "get-url", name], { readOnly: true, signal }));
956
1002
  },
957
1003
 
958
- /** Add a new remote. */
1004
+ /**
1005
+ * Add a remote pointing at `url`. Idempotent: if a remote named `name`
1006
+ * already exists with the same URL (e.g. an in-process race or a leftover
1007
+ * remote from a previous run), this is treated as success. Throws when the
1008
+ * remote exists with a different URL — that's a real conflict the caller
1009
+ * needs to resolve, not paper over.
1010
+ */
959
1011
  async add(cwd: string, name: string, url: string, signal?: AbortSignal): Promise<void> {
960
- await runEffect(cwd, ["remote", "add", name, url], { signal });
1012
+ const result = await runCommand(cwd, ["remote", "add", name, url], { signal });
1013
+ if (result.exitCode === 0) return;
1014
+ if (REMOTE_ALREADY_EXISTS.test(result.stderr)) {
1015
+ const existing = await remote.url(cwd, name, signal);
1016
+ if (existing === url) return;
1017
+ throw new ToolError(`remote ${name} already exists with URL ${existing ?? "(unset)"}, expected ${url}`);
1018
+ }
1019
+ throw new GitCommandError(["remote", "add", name, url], result);
961
1020
  },
962
1021
  };
963
1022
 
@@ -9,14 +9,18 @@
9
9
  * and various authentication methods (bearer token, basic auth, or none).
10
10
  *
11
11
  * Configuration via settings:
12
- * searxng.endpoint - Base URL of the SearXNG instance (e.g. https://searx.example.org)
13
- * searxng.token - Optional bearer token for authentication
14
- * searxng.categories - Optional comma-separated categories filter
15
- * searxng.language - Optional language code (e.g. en, zh-CN)
12
+ * searxng.endpoint - Base URL of the SearXNG instance (e.g. https://searx.example.org)
13
+ * searxng.token - Optional bearer token for authentication
14
+ * searxng.basicUsername - Optional RFC 7617 Basic auth username
15
+ * searxng.basicPassword - Optional RFC 7617 Basic auth password
16
+ * searxng.categories - Optional comma-separated categories filter
17
+ * searxng.language - Optional language code (e.g. en, zh-CN)
16
18
  *
17
19
  * Environment variable fallbacks:
18
- * SEARXNG_ENDPOINT - Base URL of the SearXNG instance
19
- * SEARXNG_TOKEN - Optional bearer token
20
+ * SEARXNG_ENDPOINT - Base URL of the SearXNG instance
21
+ * SEARXNG_TOKEN - Optional bearer token
22
+ * SEARXNG_BASIC_USERNAME - Optional RFC 7617 Basic auth username
23
+ * SEARXNG_BASIC_PASSWORD - Optional RFC 7617 Basic auth password
20
24
  *
21
25
  * Reference: https://docs.searxng.org/dev/search_api.html
22
26
  */
@@ -61,6 +65,11 @@ interface SearXNGResponse {
61
65
  unresponsive_engines?: Array<[string, string]>;
62
66
  }
63
67
 
68
+ interface SearXNGAuth {
69
+ type: "basic" | "bearer";
70
+ value: string;
71
+ }
72
+
64
73
  /** Find SearXNG endpoint from settings or environment. */
65
74
  function findEndpoint(): string | null {
66
75
  try {
@@ -83,6 +92,53 @@ function findToken(): string | null {
83
92
  return process.env.SEARXNG_TOKEN ?? null;
84
93
  }
85
94
 
95
+ /** Find SearXNG Basic auth username from settings or environment. */
96
+ function findBasicUsername(): string | null {
97
+ try {
98
+ const username = settings.get("searxng.basicUsername");
99
+ if (username !== undefined) return username;
100
+ } catch {
101
+ // Settings not initialized yet
102
+ }
103
+ return process.env.SEARXNG_BASIC_USERNAME ?? null;
104
+ }
105
+
106
+ /** Find SearXNG Basic auth password from settings or environment. */
107
+ function findBasicPassword(): string | null {
108
+ try {
109
+ const password = settings.get("searxng.basicPassword");
110
+ if (password !== undefined) return password;
111
+ } catch {
112
+ // Settings not initialized yet
113
+ }
114
+ return process.env.SEARXNG_BASIC_PASSWORD ?? null;
115
+ }
116
+
117
+ /** Build the RFC 7617 Basic auth credential using UTF-8 bytes. */
118
+ function buildBasicAuthValue(username: string, password: string): string {
119
+ return Buffer.from(`${username}:${password}`, "utf-8").toString("base64");
120
+ }
121
+
122
+ /** Find SearXNG authentication from settings or environment. Basic auth takes precedence over bearer tokens. */
123
+ function findAuth(): SearXNGAuth | null {
124
+ const basicUsername = findBasicUsername();
125
+ const basicPassword = findBasicPassword();
126
+ if (basicUsername !== null || basicPassword !== null) {
127
+ if (basicUsername === null || basicPassword === null) {
128
+ throw new Error(
129
+ "SearXNG Basic auth requires both searxng.basicUsername and searxng.basicPassword, or SEARXNG_BASIC_USERNAME and SEARXNG_BASIC_PASSWORD.",
130
+ );
131
+ }
132
+ if (basicUsername.includes(":")) {
133
+ throw new Error("SearXNG Basic auth username cannot contain ':' because RFC 7617 uses it as the separator.");
134
+ }
135
+ return { type: "basic", value: buildBasicAuthValue(basicUsername, basicPassword) };
136
+ }
137
+
138
+ const token = findToken();
139
+ return token ? { type: "bearer", value: token } : null;
140
+ }
141
+
86
142
  /** Build the search URL and headers for a SearXNG request */
87
143
  function buildRequest(
88
144
  endpoint: string,
@@ -94,7 +150,7 @@ function buildRequest(
94
150
  language?: string;
95
151
  signal?: AbortSignal;
96
152
  },
97
- token: string | null,
153
+ auth: SearXNGAuth | null,
98
154
  ): { url: URL; headers: Record<string, string> } {
99
155
  const base = endpoint.replace(/\/+$/, "");
100
156
  const url = new URL(`${base}/search`);
@@ -122,8 +178,10 @@ function buildRequest(
122
178
  Accept: "application/json",
123
179
  };
124
180
 
125
- if (token) {
126
- headers.Authorization = `Bearer ${token}`;
181
+ if (auth?.type === "basic") {
182
+ headers.Authorization = `Basic ${auth.value}`;
183
+ } else if (auth?.type === "bearer") {
184
+ headers.Authorization = `Bearer ${auth.value}`;
127
185
  }
128
186
 
129
187
  return { url, headers };
@@ -139,9 +197,9 @@ async function callSearXNGSearch(
139
197
  language?: string;
140
198
  signal?: AbortSignal;
141
199
  },
142
- token: string | null,
200
+ auth: SearXNGAuth | null,
143
201
  ): Promise<SearXNGResponse> {
144
- const { url, headers } = buildRequest(endpoint, params, token);
202
+ const { url, headers } = buildRequest(endpoint, params, auth);
145
203
 
146
204
  const response = await fetch(url, {
147
205
  headers,
@@ -172,7 +230,7 @@ export async function searchSearXNG(params: {
172
230
  );
173
231
  }
174
232
 
175
- const token = findToken();
233
+ const auth = findAuth();
176
234
 
177
235
  let categories: string | undefined;
178
236
  let language: string | undefined;
@@ -190,7 +248,7 @@ export async function searchSearXNG(params: {
190
248
  categories,
191
249
  language,
192
250
  },
193
- token,
251
+ auth,
194
252
  );
195
253
 
196
254
  const sources: SearchSource[] = [];
@@ -1,18 +0,0 @@
1
- import { createShellRenderer } from "../bash";
2
- import type { DetectedRunner } from "./runner";
3
- import { commandFromOp, titleFromOp } from "./runner";
4
-
5
- export interface RunCommandRenderArgs {
6
- op?: string;
7
- __partialJson?: string;
8
- [key: string]: unknown;
9
- }
10
-
11
- export function createRunCommandToolRenderer(runners: DetectedRunner[]) {
12
- return createShellRenderer<RunCommandRenderArgs>({
13
- resolveTitle: args => titleFromOp(args?.op, runners),
14
- resolveCommand: args => commandFromOp(args?.op, runners),
15
- });
16
- }
17
-
18
- export const runCommandToolRenderer = createRunCommandToolRenderer([]);