@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.
- package/CHANGELOG.md +56 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +14 -19
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +510 -73
- package/src/edit/modes/hashline.ts +172 -91
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/lsp/client.ts +27 -35
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +57 -26
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +5 -3
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +175 -79
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/session/session-manager.ts +19 -2
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/bash.ts +9 -4
- package/src/tools/debug.ts +57 -70
- package/src/tools/gh.ts +267 -119
- package/src/tools/index.ts +7 -7
- package/src/tools/{run-command → recipe}/index.ts +19 -19
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/{run-command → recipe}/runner.ts +28 -7
- package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
- package/src/tools/renderers.ts +2 -2
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/src/tools/run-command/render.ts +0 -18
- /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
- /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
|
|
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(`
|
|
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
|
|
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
|
-
|
|
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
|
|
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[]):
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
114
|
+
cwd: options.namespaced ? options.packageDir : undefined,
|
|
143
115
|
commandName: shellQuote(scriptName),
|
|
144
116
|
}));
|
|
145
117
|
}
|
|
146
118
|
|
|
147
|
-
async function readPackageTasks(cwd: string
|
|
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
|
|
187
|
-
const tasks = await readPackageTasks(cwd
|
|
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
|
|
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;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
13
|
-
* searxng.token
|
|
14
|
-
* searxng.
|
|
15
|
-
* searxng.
|
|
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
|
|
19
|
-
* SEARXNG_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
|
-
|
|
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 (
|
|
126
|
-
headers.Authorization = `
|
|
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
|
-
|
|
200
|
+
auth: SearXNGAuth | null,
|
|
143
201
|
): Promise<SearXNGResponse> {
|
|
144
|
-
const { url, headers } = buildRequest(endpoint, params,
|
|
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
|
|
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
|
-
|
|
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([]);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|