tau-coding-agent 0.1.3 → 0.1.5
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/README.md +1 -1
- package/extensions/answer.ts +49 -43
- package/extensions/branch-term.ts +11 -16
- package/extensions/btw.ts +8 -8
- package/extensions/ghostty.ts +65 -12
- package/extensions/git-diff-stats.ts +1 -1
- package/extensions/git-pr-status.ts +1 -1
- package/extensions/insights.ts +6 -5
- package/extensions/loop.ts +39 -11
- package/extensions/memory.ts +6 -5
- package/extensions/notify.ts +25 -6
- package/extensions/openai-fast.ts +6 -4
- package/extensions/openai-verbosity.ts +6 -4
- package/extensions/review/fix.ts +239 -0
- package/extensions/review/git.ts +517 -0
- package/extensions/review/index.ts +232 -0
- package/extensions/review/message-queue.ts +249 -0
- package/extensions/review/models.ts +589 -0
- package/extensions/review/prompts.ts +337 -0
- package/extensions/review/renderers/inline.ts +126 -0
- package/extensions/review/request.ts +504 -0
- package/extensions/review/review.ts +1305 -0
- package/extensions/review/runner.ts +522 -0
- package/extensions/review/runtime.ts +275 -0
- package/extensions/review/schema.ts +160 -0
- package/extensions/review/submit-review-tool.ts +66 -0
- package/extensions/review/submit-triage-tool.ts +59 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +428 -103
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +173 -17
- package/extensions/usage/anthropic.ts +1 -1
- package/extensions/usage/github-copilot.ts +1 -1
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/minimax.ts +1 -1
- package/extensions/usage/openai-codex.ts +1 -1
- package/extensions/usage/openrouter.ts +1 -1
- package/extensions/usage/providers.ts +0 -2
- package/extensions/usage/types.ts +1 -2
- package/extensions/usage/zai.ts +1 -1
- package/extensions/websearch/README.md +1 -1
- package/extensions/websearch/browser/chromium.ts +42 -31
- package/extensions/websearch/browser/discovery.ts +11 -9
- package/extensions/websearch/browser/firefox.ts +10 -7
- package/extensions/websearch/browser/fs.ts +10 -0
- package/extensions/websearch/browser/sqlite.ts +22 -15
- package/extensions/websearch/config.ts +2 -2
- package/extensions/websearch/index.ts +9 -9
- package/extensions/websearch/providers/anthropic.pi.ts +1 -1
- package/extensions/websearch/providers/gemini.pi.ts +1 -1
- package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
- package/extensions/websearch/providers/pi-model.shared.ts +2 -2
- package/extensions/worktree.ts +36 -36
- package/package.json +7 -7
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/scripts/oracle +28 -12
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4347
- package/extensions/usage/google-gemini-cli.ts +0 -232
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
8
|
|
|
6
9
|
const DEFAULT_SHORTCUTS = ["alt+x"] as const;
|
|
7
|
-
const STATUS_KEY = "
|
|
8
|
-
const KEYBINDINGS_PATH = path.join(
|
|
10
|
+
const STATUS_KEY = "stash";
|
|
11
|
+
const KEYBINDINGS_PATH = path.join(getAgentDir(), "keybindings.json");
|
|
9
12
|
|
|
10
13
|
type ShortcutConfig = string | string[] | undefined;
|
|
11
14
|
|
|
12
15
|
type KeybindingsConfig = {
|
|
13
|
-
|
|
16
|
+
stash?: ShortcutConfig;
|
|
14
17
|
[key: string]: unknown;
|
|
15
18
|
};
|
|
16
19
|
|
|
@@ -37,9 +40,9 @@ function normalizeShortcuts(value: ShortcutConfig): string[] {
|
|
|
37
40
|
return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_SHORTCUTS];
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
export default function
|
|
43
|
+
export default function stashExtension(pi: ExtensionAPI): void {
|
|
41
44
|
const keybindings = readKeybindings(KEYBINDINGS_PATH);
|
|
42
|
-
const shortcuts = normalizeShortcuts(keybindings.
|
|
45
|
+
const shortcuts = normalizeShortcuts(keybindings.stash);
|
|
43
46
|
|
|
44
47
|
let stashedDraft: string | null = null;
|
|
45
48
|
let armed = false;
|
|
@@ -52,7 +55,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
|
|
|
52
55
|
if (!ctx.hasUI) return;
|
|
53
56
|
|
|
54
57
|
if (armed && stashedDraft !== null) {
|
|
55
|
-
ctx.ui.setStatus(STATUS_KEY, `
|
|
58
|
+
ctx.ui.setStatus(STATUS_KEY, `stash (${shortcutLabel()})`);
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -80,7 +83,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
|
|
|
80
83
|
|
|
81
84
|
if (armed && stashedDraft !== null) {
|
|
82
85
|
restoreDraft(ctx);
|
|
83
|
-
ctx.ui.notify("
|
|
86
|
+
ctx.ui.notify("Stashed draft restored", "info");
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -101,8 +104,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
|
|
|
101
104
|
|
|
102
105
|
for (const shortcut of shortcuts) {
|
|
103
106
|
pi.registerShortcut(shortcut as never, {
|
|
104
|
-
description:
|
|
105
|
-
"Stash the current message draft, send one interlude message, then restore the draft",
|
|
107
|
+
description: "Stash the current message draft, send one message, then restore it",
|
|
106
108
|
handler: async (ctx) => {
|
|
107
109
|
stashOrRestore(ctx);
|
|
108
110
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type AppKeybinding,
|
|
5
6
|
CustomEditor,
|
|
6
7
|
createBashToolDefinition,
|
|
7
8
|
createFindToolDefinition,
|
|
@@ -16,8 +17,15 @@ import {
|
|
|
16
17
|
type ToolDefinition,
|
|
17
18
|
type ToolInfo,
|
|
18
19
|
type ToolRenderResultOptions,
|
|
19
|
-
} from "@
|
|
20
|
-
import {
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import {
|
|
22
|
+
Container,
|
|
23
|
+
Text,
|
|
24
|
+
type AutocompleteProvider,
|
|
25
|
+
type Component,
|
|
26
|
+
type EditorComponent,
|
|
27
|
+
type Focusable,
|
|
28
|
+
} from "@earendil-works/pi-tui";
|
|
21
29
|
|
|
22
30
|
// --- Constants ---
|
|
23
31
|
|
|
@@ -46,11 +54,22 @@ type ToolName = keyof typeof TOOL_FACTORIES;
|
|
|
46
54
|
type AnyToolDefinition = ToolDefinition<any, any, any>;
|
|
47
55
|
type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
|
|
48
56
|
type JsonObject = Record<string, unknown>;
|
|
57
|
+
type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
|
|
49
58
|
|
|
50
59
|
type ToolDisplayModeConfig = {
|
|
51
60
|
mode: Mode;
|
|
52
61
|
};
|
|
53
62
|
|
|
63
|
+
type CustomEditorLike = EditorComponent &
|
|
64
|
+
Partial<Focusable> & {
|
|
65
|
+
actionHandlers?: Map<AppKeybinding, () => void>;
|
|
66
|
+
onEscape?: () => void;
|
|
67
|
+
onCtrlD?: () => void;
|
|
68
|
+
onPasteImage?: () => void;
|
|
69
|
+
onExtensionShortcut?: (data: string) => boolean;
|
|
70
|
+
isShowingAutocomplete?: () => boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
54
73
|
// --- Config ---
|
|
55
74
|
|
|
56
75
|
function emptyConfig(): ToolDisplayModeConfig {
|
|
@@ -58,8 +77,7 @@ function emptyConfig(): ToolDisplayModeConfig {
|
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function getConfigPath(): string {
|
|
61
|
-
|
|
62
|
-
return path.join(agentDir, CONFIG_FILE);
|
|
80
|
+
return path.join(getAgentDir(), CONFIG_FILE);
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
function isObject(value: unknown): value is JsonObject {
|
|
@@ -242,11 +260,11 @@ function renderMinimalResult(
|
|
|
242
260
|
break;
|
|
243
261
|
}
|
|
244
262
|
|
|
245
|
-
return renderMinimalText(theme, "muted",
|
|
263
|
+
return renderMinimalText(theme, "muted", `↳ ${summary}`);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
|
|
249
|
-
return new Text(theme.fg(color,
|
|
267
|
+
return new Text(theme.fg(color, text), 0, 0);
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
function bashSummary(result: AgentToolResult<any>): string {
|
|
@@ -328,29 +346,152 @@ function emptyComponent(): Container {
|
|
|
328
346
|
|
|
329
347
|
// --- Editor ---
|
|
330
348
|
|
|
331
|
-
class ToolDisplayEditor
|
|
349
|
+
class ToolDisplayEditor implements EditorComponent, Focusable {
|
|
350
|
+
readonly actionHandlers: Map<AppKeybinding, () => void>;
|
|
351
|
+
private readonly customBase: CustomEditorLike;
|
|
352
|
+
private fallbackFocused = false;
|
|
353
|
+
private fallbackOnEscape?: () => void;
|
|
354
|
+
private fallbackOnCtrlD?: () => void;
|
|
355
|
+
private fallbackOnPasteImage?: () => void;
|
|
356
|
+
private fallbackOnExtensionShortcut?: (data: string) => boolean;
|
|
357
|
+
|
|
332
358
|
constructor(
|
|
333
|
-
|
|
334
|
-
theme: EditorTheme,
|
|
359
|
+
private readonly base: EditorComponent,
|
|
335
360
|
private readonly appKeybindings: KeybindingsManager,
|
|
336
361
|
private readonly cycleMode: () => void,
|
|
337
362
|
) {
|
|
338
|
-
|
|
363
|
+
this.customBase = base as CustomEditorLike;
|
|
364
|
+
this.actionHandlers = this.customBase.actionHandlers ?? new Map();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
get focused(): boolean {
|
|
368
|
+
return this.customBase.focused ?? this.fallbackFocused;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
set focused(value: boolean) {
|
|
372
|
+
this.fallbackFocused = value;
|
|
373
|
+
if ("focused" in this.customBase) this.customBase.focused = value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get onSubmit(): ((text: string) => void) | undefined {
|
|
377
|
+
return this.base.onSubmit;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
set onSubmit(handler: ((text: string) => void) | undefined) {
|
|
381
|
+
this.base.onSubmit = handler;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
get onChange(): ((text: string) => void) | undefined {
|
|
385
|
+
return this.base.onChange;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
set onChange(handler: ((text: string) => void) | undefined) {
|
|
389
|
+
this.base.onChange = handler;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
get wantsKeyRelease(): boolean | undefined {
|
|
393
|
+
return this.base.wantsKeyRelease;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
get borderColor(): ((str: string) => string) | undefined {
|
|
397
|
+
return this.base.borderColor;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
set borderColor(handler: ((str: string) => string) | undefined) {
|
|
401
|
+
this.base.borderColor = handler;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
get onEscape(): (() => void) | undefined {
|
|
405
|
+
return this.customBase.onEscape ?? this.fallbackOnEscape;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
set onEscape(handler: (() => void) | undefined) {
|
|
409
|
+
this.fallbackOnEscape = handler;
|
|
410
|
+
if ("onEscape" in this.customBase) this.customBase.onEscape = handler;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
get onCtrlD(): (() => void) | undefined {
|
|
414
|
+
return this.customBase.onCtrlD ?? this.fallbackOnCtrlD;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
set onCtrlD(handler: (() => void) | undefined) {
|
|
418
|
+
this.fallbackOnCtrlD = handler;
|
|
419
|
+
if ("onCtrlD" in this.customBase) this.customBase.onCtrlD = handler;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
get onPasteImage(): (() => void) | undefined {
|
|
423
|
+
return this.customBase.onPasteImage ?? this.fallbackOnPasteImage;
|
|
339
424
|
}
|
|
340
425
|
|
|
341
|
-
|
|
426
|
+
set onPasteImage(handler: (() => void) | undefined) {
|
|
427
|
+
this.fallbackOnPasteImage = handler;
|
|
428
|
+
if ("onPasteImage" in this.customBase) this.customBase.onPasteImage = handler;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
get onExtensionShortcut(): ((data: string) => boolean) | undefined {
|
|
432
|
+
return this.customBase.onExtensionShortcut ?? this.fallbackOnExtensionShortcut;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
|
|
436
|
+
this.fallbackOnExtensionShortcut = handler;
|
|
437
|
+
if ("onExtensionShortcut" in this.customBase) this.customBase.onExtensionShortcut = handler;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getText(): string {
|
|
441
|
+
return this.base.getText();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
setText(text: string): void {
|
|
445
|
+
this.base.setText(text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
addToHistory(text: string): void {
|
|
449
|
+
this.base.addToHistory?.(text);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
insertTextAtCursor(text: string): void {
|
|
453
|
+
this.base.insertTextAtCursor?.(text);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
getExpandedText(): string {
|
|
457
|
+
return this.base.getExpandedText?.() ?? this.base.getText();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
461
|
+
this.base.setAutocompleteProvider?.(provider);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
setPaddingX(padding: number): void {
|
|
465
|
+
this.base.setPaddingX?.(padding);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
setAutocompleteMaxVisible(maxVisible: number): void {
|
|
469
|
+
this.base.setAutocompleteMaxVisible?.(maxVisible);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
render(width: number): string[] {
|
|
473
|
+
return this.base.render(width);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
invalidate(): void {
|
|
477
|
+
this.base.invalidate();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
handleInput(data: string): void {
|
|
342
481
|
if (this.appKeybindings.matches(data, "app.tools.expand")) {
|
|
343
482
|
this.cycleMode();
|
|
344
483
|
return;
|
|
345
484
|
}
|
|
346
485
|
|
|
347
|
-
|
|
486
|
+
this.base.handleInput(data);
|
|
348
487
|
}
|
|
349
488
|
}
|
|
350
489
|
|
|
351
490
|
export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
|
|
352
491
|
let mode: Mode = DEFAULT_MODE;
|
|
353
492
|
let registeredToolRenderers = false;
|
|
493
|
+
let installedEditorFactory: EditorFactory | undefined;
|
|
494
|
+
let previousEditorFactory: EditorFactory | undefined;
|
|
354
495
|
const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
|
|
355
496
|
|
|
356
497
|
const setMode = (ctx: ExtensionContext, next: Mode): void => {
|
|
@@ -379,9 +520,24 @@ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
|
|
|
379
520
|
applyMode(ctx, mode);
|
|
380
521
|
if (!ctx.hasUI) return;
|
|
381
522
|
|
|
382
|
-
ctx.ui.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
523
|
+
if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
|
|
524
|
+
|
|
525
|
+
previousEditorFactory = ctx.ui.getEditorComponent();
|
|
526
|
+
installedEditorFactory = (tui, theme, keybindings) => {
|
|
527
|
+
const baseEditor =
|
|
528
|
+
previousEditorFactory?.(tui, theme, keybindings) ??
|
|
529
|
+
new CustomEditor(tui, theme, keybindings);
|
|
530
|
+
return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
|
|
531
|
+
};
|
|
532
|
+
ctx.ui.setEditorComponent(installedEditorFactory);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
536
|
+
if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
|
|
537
|
+
ctx.ui.setEditorComponent(previousEditorFactory);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
installedEditorFactory = undefined;
|
|
541
|
+
previousEditorFactory = undefined;
|
|
386
542
|
});
|
|
387
543
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
BorderedLoader,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
7
|
import {
|
|
4
8
|
Key,
|
|
5
9
|
matchesKey,
|
|
@@ -7,7 +11,7 @@ import {
|
|
|
7
11
|
type TUI,
|
|
8
12
|
truncateToWidth,
|
|
9
13
|
visibleWidth,
|
|
10
|
-
} from "@
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
11
15
|
import { createReadStream, type Dirent } from "node:fs";
|
|
12
16
|
import fs from "node:fs/promises";
|
|
13
17
|
import os from "node:os";
|
|
@@ -31,7 +35,7 @@ import type {
|
|
|
31
35
|
} from "./types.js";
|
|
32
36
|
|
|
33
37
|
const ALL_TAB_ID = "all";
|
|
34
|
-
const SESSION_ROOT = path.join(
|
|
38
|
+
const SESSION_ROOT = path.join(getAgentDir(), "sessions");
|
|
35
39
|
const RANGE_DAYS = [7, 30, 90] as const;
|
|
36
40
|
const FILE_PARSE_CONCURRENCY = 16;
|
|
37
41
|
|
|
@@ -2205,7 +2209,7 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
|
|
|
2205
2209
|
handler: async (_args, ctx: ExtensionContext) => {
|
|
2206
2210
|
const liveUsageAvailability = await getLiveUsageAvailabilityMap(ctx);
|
|
2207
2211
|
|
|
2208
|
-
if (
|
|
2212
|
+
if (ctx.mode !== "tui") {
|
|
2209
2213
|
const [data, liveUsage] = await Promise.all([
|
|
2210
2214
|
computeBreakdown(liveUsageAvailability, undefined),
|
|
2211
2215
|
fetchLiveUsage(SUPPORTED_PROVIDERS, liveUsageAvailability, ctx, undefined),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import anthropicProvider from "./anthropic.js";
|
|
2
2
|
import githubCopilotProvider from "./github-copilot.js";
|
|
3
|
-
import googleGeminiCliProvider from "./google-gemini-cli.js";
|
|
4
3
|
import minimaxProvider from "./minimax.js";
|
|
5
4
|
import openAICodexProvider from "./openai-codex.js";
|
|
6
5
|
import openRouterProvider from "./openrouter.js";
|
|
@@ -11,7 +10,6 @@ export const SUPPORTED_PROVIDERS: readonly UsageProviderDefinition[] = [
|
|
|
11
10
|
openAICodexProvider,
|
|
12
11
|
anthropicProvider,
|
|
13
12
|
githubCopilotProvider,
|
|
14
|
-
googleGeminiCliProvider,
|
|
15
13
|
openRouterProvider,
|
|
16
14
|
zaiProvider,
|
|
17
15
|
minimaxProvider,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export interface RGB {
|
|
4
4
|
r: number;
|
|
@@ -10,7 +10,6 @@ export type SupportedProviderId =
|
|
|
10
10
|
| "openai-codex"
|
|
11
11
|
| "anthropic"
|
|
12
12
|
| "github-copilot"
|
|
13
|
-
| "google-gemini-cli"
|
|
14
13
|
| "openrouter"
|
|
15
14
|
| "zai"
|
|
16
15
|
| "minimax";
|
package/extensions/usage/zai.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createDecipheriv, createHash,
|
|
3
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createDecipheriv, createHash, pbkdf2 } from "node:crypto";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
7
8
|
|
|
8
9
|
import type { BrowserCookie, BrowserProfile } from "../types.js";
|
|
10
|
+
import { pathExists } from "./fs.js";
|
|
9
11
|
import { withSqliteSnapshot } from "./sqlite.js";
|
|
10
12
|
|
|
11
13
|
const MACOS_BROWSER_CONFIGS: ChromiumBrowserConfig[] = [
|
|
@@ -72,6 +74,8 @@ interface ChromiumBrowserConfig {
|
|
|
72
74
|
secretToolApp?: string;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
const execFileAsync = promisify(execFile);
|
|
78
|
+
const pbkdf2Async = promisify(pbkdf2);
|
|
75
79
|
const passwordCache = new Map<string, string | null>();
|
|
76
80
|
|
|
77
81
|
function browserConfigs(): ChromiumBrowserConfig[] {
|
|
@@ -80,13 +84,15 @@ function browserConfigs(): ChromiumBrowserConfig[] {
|
|
|
80
84
|
return [];
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
export function discoverChromiumProfiles(
|
|
87
|
+
export async function discoverChromiumProfiles(
|
|
88
|
+
preferredProfileName?: string,
|
|
89
|
+
): Promise<BrowserProfile[]> {
|
|
84
90
|
const profiles: BrowserProfile[] = [];
|
|
85
91
|
|
|
86
92
|
for (const config of browserConfigs()) {
|
|
87
|
-
if (!
|
|
93
|
+
if (!(await pathExists(config.baseDir))) continue;
|
|
88
94
|
|
|
89
|
-
const candidates = ["Default", ...discoverProfileDirectories(config.baseDir)];
|
|
95
|
+
const candidates = ["Default", ...(await discoverProfileDirectories(config.baseDir))];
|
|
90
96
|
const seen = new Set<string>();
|
|
91
97
|
for (const directoryName of candidates) {
|
|
92
98
|
if (!directoryName || seen.has(directoryName)) continue;
|
|
@@ -94,7 +100,7 @@ export function discoverChromiumProfiles(preferredProfileName?: string): Browser
|
|
|
94
100
|
|
|
95
101
|
const profilePath = path.join(config.baseDir, directoryName);
|
|
96
102
|
const cookiesPath = path.join(profilePath, "Cookies");
|
|
97
|
-
if (!
|
|
103
|
+
if (!(await pathExists(cookiesPath))) continue;
|
|
98
104
|
|
|
99
105
|
profiles.push({
|
|
100
106
|
family: "chromium",
|
|
@@ -136,9 +142,9 @@ function isPreferredChromiumProfile(
|
|
|
136
142
|
return profile.profileName === preferredProfileName;
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
function discoverProfileDirectories(baseDir: string): string[] {
|
|
145
|
+
async function discoverProfileDirectories(baseDir: string): Promise<string[]> {
|
|
140
146
|
try {
|
|
141
|
-
return
|
|
147
|
+
return (await readdir(baseDir, { withFileTypes: true }))
|
|
142
148
|
.filter((entry) => entry.isDirectory())
|
|
143
149
|
.map((entry) => entry.name)
|
|
144
150
|
.filter((name) => name === "Default" || /^Profile\s+\d+$/.test(name));
|
|
@@ -147,27 +153,28 @@ function discoverProfileDirectories(baseDir: string): string[] {
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
export function loadChromiumCookies(profile: BrowserProfile): BrowserCookie[] {
|
|
156
|
+
export async function loadChromiumCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
|
|
151
157
|
const browserConfig = browserConfigs().find((config) =>
|
|
152
158
|
profile.profilePath.startsWith(config.baseDir),
|
|
153
159
|
);
|
|
154
160
|
if (!browserConfig) return [];
|
|
155
161
|
|
|
156
|
-
const
|
|
162
|
+
const cookiesPath = path.join(profile.profilePath, "Cookies");
|
|
163
|
+
if (!(await pathExists(cookiesPath))) return [];
|
|
164
|
+
|
|
165
|
+
const password = await readBrowserPassword(browserConfig);
|
|
157
166
|
if (!password) return [];
|
|
158
167
|
|
|
159
|
-
const keyMaterial =
|
|
168
|
+
const keyMaterial = await pbkdf2Async(
|
|
160
169
|
password,
|
|
161
170
|
"saltysalt",
|
|
162
171
|
process.platform === "darwin" ? 1003 : 1,
|
|
163
172
|
16,
|
|
164
173
|
"sha1",
|
|
165
174
|
);
|
|
166
|
-
const cookiesPath = path.join(profile.profilePath, "Cookies");
|
|
167
|
-
if (!existsSync(cookiesPath)) return [];
|
|
168
175
|
|
|
169
176
|
try {
|
|
170
|
-
return withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
|
|
177
|
+
return await withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
|
|
171
178
|
const db = new DatabaseSync(tempDbPath, { readOnly: true });
|
|
172
179
|
try {
|
|
173
180
|
const rows = db
|
|
@@ -277,7 +284,7 @@ function stripHostKeyDigest(buffer: Buffer, hostKey: string | null): Buffer {
|
|
|
277
284
|
return buffer.subarray(0, 32).equals(digest) ? buffer.subarray(32) : buffer;
|
|
278
285
|
}
|
|
279
286
|
|
|
280
|
-
function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
|
|
287
|
+
async function readBrowserPassword(config: ChromiumBrowserConfig): Promise<string | null> {
|
|
281
288
|
const cacheKey = `${config.name}:${config.baseDir}`;
|
|
282
289
|
if (passwordCache.has(cacheKey)) {
|
|
283
290
|
return passwordCache.get(cacheKey) ?? null;
|
|
@@ -286,9 +293,13 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
|
|
|
286
293
|
let password: string | null = null;
|
|
287
294
|
if (process.platform === "darwin") {
|
|
288
295
|
if (!config.keychainAccount || !config.keychainService) return null;
|
|
289
|
-
password = readMacKeychainPassword(
|
|
296
|
+
password = await readMacKeychainPassword(
|
|
297
|
+
config.name,
|
|
298
|
+
config.keychainAccount,
|
|
299
|
+
config.keychainService,
|
|
300
|
+
);
|
|
290
301
|
} else if (process.platform === "linux") {
|
|
291
|
-
password = readLinuxPassword(config.secretToolApp);
|
|
302
|
+
password = await readLinuxPassword(config.secretToolApp);
|
|
292
303
|
}
|
|
293
304
|
|
|
294
305
|
if (password) {
|
|
@@ -297,33 +308,33 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
|
|
|
297
308
|
return password;
|
|
298
309
|
}
|
|
299
310
|
|
|
300
|
-
function readMacKeychainPassword(
|
|
311
|
+
async function readMacKeychainPassword(
|
|
301
312
|
browserName: string,
|
|
302
313
|
account: string,
|
|
303
314
|
service: string,
|
|
304
|
-
): string | null {
|
|
315
|
+
): Promise<string | null> {
|
|
305
316
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}).trim() || null
|
|
317
|
+
const { stdout } = await execFileAsync(
|
|
318
|
+
"security",
|
|
319
|
+
["find-generic-password", "-w", "-a", account, "-s", service],
|
|
320
|
+
{ encoding: "utf8" },
|
|
311
321
|
);
|
|
322
|
+
return stdout.trim() || null;
|
|
312
323
|
} catch {
|
|
313
324
|
throw new Error(`Could not read ${browserName} cookie decryption password from Keychain.`);
|
|
314
325
|
}
|
|
315
326
|
}
|
|
316
327
|
|
|
317
|
-
function readLinuxPassword(secretToolApp?: string): string | null {
|
|
328
|
+
async function readLinuxPassword(secretToolApp?: string): Promise<string | null> {
|
|
318
329
|
if (!secretToolApp) return "peanuts";
|
|
319
330
|
|
|
320
331
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}).trim() || "peanuts"
|
|
332
|
+
const { stdout } = await execFileAsync(
|
|
333
|
+
"secret-tool",
|
|
334
|
+
["lookup", "application", secretToolApp],
|
|
335
|
+
{ encoding: "utf8" },
|
|
326
336
|
);
|
|
337
|
+
return stdout.trim() || "peanuts";
|
|
327
338
|
} catch {
|
|
328
339
|
return "peanuts";
|
|
329
340
|
}
|
|
@@ -11,32 +11,34 @@ import type {
|
|
|
11
11
|
const COOKIE_CACHE_TTL_MS = 30_000;
|
|
12
12
|
const cookieCache = new Map<string, { cookies: BrowserCookie[]; expiresAt: number }>();
|
|
13
13
|
|
|
14
|
-
export function discoverProfiles(
|
|
14
|
+
export async function discoverProfiles(
|
|
15
15
|
browserFamilies: WebsearchBrowserFamily[],
|
|
16
16
|
preferredProfiles: WebsearchConfig["profiles"],
|
|
17
|
-
): BrowserProfile[] {
|
|
17
|
+
): Promise<BrowserProfile[]> {
|
|
18
18
|
const profiles: BrowserProfile[] = [];
|
|
19
19
|
|
|
20
20
|
for (const family of browserFamilies) {
|
|
21
21
|
if (family === "firefox") {
|
|
22
|
-
profiles.push(...discoverFirefoxProfiles(preferredProfiles.firefox));
|
|
22
|
+
profiles.push(...(await discoverFirefoxProfiles(preferredProfiles.firefox)));
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
profiles.push(...discoverChromiumProfiles(preferredProfiles.chromium));
|
|
26
|
+
profiles.push(...(await discoverChromiumProfiles(preferredProfiles.chromium)));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
return profiles;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
|
|
32
|
+
export async function loadCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
|
|
33
33
|
const cacheKey = `${profile.family}:${profile.profilePath}`;
|
|
34
34
|
const cached = cookieCache.get(cacheKey);
|
|
35
35
|
if (cached && cached.expiresAt > Date.now()) return cached.cookies;
|
|
36
36
|
if (cached) cookieCache.delete(cacheKey);
|
|
37
37
|
|
|
38
38
|
const cookies =
|
|
39
|
-
profile.family === "firefox"
|
|
39
|
+
profile.family === "firefox"
|
|
40
|
+
? await loadFirefoxCookies(profile)
|
|
41
|
+
: await loadChromiumCookies(profile);
|
|
40
42
|
|
|
41
43
|
if (cookies.length > 0) {
|
|
42
44
|
cookieCache.set(cacheKey, {
|
|
@@ -48,11 +50,11 @@ export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
|
|
|
48
50
|
return cookies;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export function createBrowserSession(
|
|
53
|
+
export async function createBrowserSession(
|
|
52
54
|
profile: BrowserProfile,
|
|
53
55
|
domains: string[],
|
|
54
|
-
): BrowserSession | null {
|
|
55
|
-
const cookies = loadCookies(profile).filter((cookie) => {
|
|
56
|
+
): Promise<BrowserSession | null> {
|
|
57
|
+
const cookies = (await loadCookies(profile)).filter((cookie) => {
|
|
56
58
|
return domains.some((domain) => matchesBrowserDomain(domain, cookie.domain));
|
|
57
59
|
});
|
|
58
60
|
if (cookies.length === 0) return null;
|