tau-coding-agent 0.1.4 → 0.1.6
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 +59 -47
- package/extensions/branch-term.ts +8 -13
- package/extensions/btw.ts +77 -6
- package/extensions/ghostty.ts +64 -11
- package/extensions/insights.ts +42 -4
- package/extensions/loop.ts +51 -12
- package/extensions/memory.ts +35 -17
- package/extensions/notify.ts +24 -5
- 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 +62 -0
- package/extensions/review/submit-triage-tool.ts +60 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +976 -185
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +2 -3
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/openai-codex.ts +2 -2
- 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 +6 -6
- package/extensions/websearch/providers/pi-model.shared.ts +19 -1
- package/extensions/worktree.ts +33 -33
- package/package.json +4 -4
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +82 -61
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4420
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compatibility workaround for sandbox-runtime on macOS.
|
|
5
|
+
*
|
|
6
|
+
* The runtime routes Git-over-SSH through BSD nc, which cannot authenticate to
|
|
7
|
+
* its SOCKS5 proxy. This shell preserves normal bash execution while replacing
|
|
8
|
+
* that transport with an authenticated HTTP CONNECT tunnel.
|
|
9
|
+
*
|
|
10
|
+
* Remove once the upstream fix ships:
|
|
11
|
+
* https://github.com/anthropic-experimental/sandbox-runtime/pull/385
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { connect } from "node:net";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const PROXY_CONNECT_MODE = "--proxy-connect";
|
|
19
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
20
|
+
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
if (process.argv[2] === PROXY_CONNECT_MODE) {
|
|
23
|
+
connectThroughSandboxProxy(process.argv[3], process.argv[4]);
|
|
24
|
+
} else {
|
|
25
|
+
runBash(process.argv.slice(2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runBash(args) {
|
|
29
|
+
if (hasAuthenticatedSandboxProxy()) {
|
|
30
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
31
|
+
const proxyCommand = [
|
|
32
|
+
shellQuote(escapeSshPercent(process.execPath)),
|
|
33
|
+
shellQuote(escapeSshPercent(scriptPath)),
|
|
34
|
+
PROXY_CONNECT_MODE,
|
|
35
|
+
"%h",
|
|
36
|
+
"%p",
|
|
37
|
+
].join(" ");
|
|
38
|
+
process.env.GIT_SSH_COMMAND = [
|
|
39
|
+
"ssh",
|
|
40
|
+
"-o ControlMaster=no",
|
|
41
|
+
"-o ControlPath=none",
|
|
42
|
+
`-o ProxyCommand=${shellQuote(proxyCommand)}`,
|
|
43
|
+
].join(" ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const child = spawn("bash", args, {
|
|
47
|
+
env: process.env,
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
child.once("error", (error) => {
|
|
51
|
+
console.error(error.message);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
});
|
|
54
|
+
child.once("exit", (code, signal) => {
|
|
55
|
+
if (signal) {
|
|
56
|
+
process.kill(process.pid, signal);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
process.exitCode = code ?? 1;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasAuthenticatedSandboxProxy() {
|
|
64
|
+
try {
|
|
65
|
+
const proxy = getSandboxProxyUrl();
|
|
66
|
+
return proxy.protocol === "http:" && Boolean(proxy.username || proxy.password);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connectThroughSandboxProxy(targetHost, targetPortText) {
|
|
73
|
+
const targetPort = Number(targetPortText);
|
|
74
|
+
if (
|
|
75
|
+
!targetHost ||
|
|
76
|
+
/[\r\n]/u.test(targetHost) ||
|
|
77
|
+
!Number.isInteger(targetPort) ||
|
|
78
|
+
targetPort < 1 ||
|
|
79
|
+
targetPort > 65_535
|
|
80
|
+
) {
|
|
81
|
+
fail("Invalid SSH proxy target.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let proxy;
|
|
85
|
+
try {
|
|
86
|
+
proxy = getSandboxProxyUrl();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
fail(error);
|
|
89
|
+
}
|
|
90
|
+
if (proxy.protocol !== "http:") {
|
|
91
|
+
fail(`Unsupported sandbox proxy protocol: ${proxy.protocol}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const proxyPort = Number(proxy.port || 80);
|
|
95
|
+
const authority = targetHost.includes(":")
|
|
96
|
+
? `[${targetHost}]:${targetPort}`
|
|
97
|
+
: `${targetHost}:${targetPort}`;
|
|
98
|
+
let username;
|
|
99
|
+
let password;
|
|
100
|
+
try {
|
|
101
|
+
username = decodeURIComponent(proxy.username);
|
|
102
|
+
password = decodeURIComponent(proxy.password);
|
|
103
|
+
} catch {
|
|
104
|
+
fail("Sandbox HTTP proxy credentials are invalid.");
|
|
105
|
+
}
|
|
106
|
+
const authorization =
|
|
107
|
+
username || password
|
|
108
|
+
? `Proxy-Authorization: Basic ${Buffer.from(`${username}:${password}`).toString("base64")}\r\n`
|
|
109
|
+
: "";
|
|
110
|
+
|
|
111
|
+
const socket = connect({ host: proxy.hostname, port: proxyPort });
|
|
112
|
+
const connectTimeout = setTimeout(() => {
|
|
113
|
+
socket.destroy(new Error("Sandbox proxy connection timed out."));
|
|
114
|
+
}, CONNECT_TIMEOUT_MS);
|
|
115
|
+
let response = Buffer.alloc(0);
|
|
116
|
+
let tunnelEstablished = false;
|
|
117
|
+
let connectionFailed = false;
|
|
118
|
+
|
|
119
|
+
socket.once("connect", () => {
|
|
120
|
+
clearTimeout(connectTimeout);
|
|
121
|
+
socket.write(
|
|
122
|
+
`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${authorization}Proxy-Connection: Keep-Alive\r\n\r\n`,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const failConnection = (error) => {
|
|
127
|
+
if (connectionFailed) return;
|
|
128
|
+
connectionFailed = true;
|
|
129
|
+
clearTimeout(connectTimeout);
|
|
130
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
socket.destroy();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleResponse = (chunk) => {
|
|
136
|
+
response = Buffer.concat([response, chunk]);
|
|
137
|
+
if (response.length > MAX_RESPONSE_HEADER_BYTES) {
|
|
138
|
+
failConnection(new Error("Sandbox proxy returned oversized headers."));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const headerEnd = response.indexOf("\r\n\r\n");
|
|
143
|
+
if (headerEnd === -1) return;
|
|
144
|
+
|
|
145
|
+
const header = response.subarray(0, headerEnd).toString("latin1");
|
|
146
|
+
const status = header.match(/^HTTP\/\d(?:\.\d)?\s+(\d{3})/u)?.[1];
|
|
147
|
+
if (status !== "200") {
|
|
148
|
+
failConnection(new Error(`Sandbox proxy CONNECT failed${status ? ` (${status})` : ""}.`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tunnelEstablished = true;
|
|
153
|
+
socket.off("data", handleResponse);
|
|
154
|
+
const remainder = response.subarray(headerEnd + 4);
|
|
155
|
+
if (remainder.length > 0) process.stdout.write(remainder);
|
|
156
|
+
process.stdin.pipe(socket);
|
|
157
|
+
socket.pipe(process.stdout);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
socket.on("data", handleResponse);
|
|
161
|
+
socket.once("error", failConnection);
|
|
162
|
+
socket.once("close", () => {
|
|
163
|
+
clearTimeout(connectTimeout);
|
|
164
|
+
process.stdin.unpipe(socket);
|
|
165
|
+
process.stdin.pause();
|
|
166
|
+
if (!tunnelEstablished && !connectionFailed) {
|
|
167
|
+
failConnection(new Error("Sandbox proxy closed before establishing the SSH tunnel."));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
process.stdin.on("error", failConnection);
|
|
171
|
+
process.stdout.on("error", (error) => {
|
|
172
|
+
if (error?.code === "EPIPE") {
|
|
173
|
+
socket.destroy();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
failConnection(error);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getSandboxProxyUrl() {
|
|
181
|
+
const value = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
|
|
182
|
+
if (!value) throw new Error("Sandbox HTTP proxy is unavailable.");
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return new URL(value);
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error("Sandbox HTTP proxy URL is invalid.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function escapeSshPercent(value) {
|
|
192
|
+
return value.replaceAll("%", "%%");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shellQuote(value) {
|
|
196
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function fail(error) {
|
|
200
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
@@ -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,7 @@
|
|
|
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
5
|
type AppKeybinding,
|
|
6
6
|
CustomEditor,
|
|
7
7
|
createBashToolDefinition,
|
|
@@ -77,8 +77,7 @@ function emptyConfig(): ToolDisplayModeConfig {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function getConfigPath(): string {
|
|
80
|
-
|
|
81
|
-
return path.join(agentDir, CONFIG_FILE);
|
|
80
|
+
return path.join(getAgentDir(), CONFIG_FILE);
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
function isObject(value: unknown): value is JsonObject {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
clampPercent,
|
|
4
4
|
formatCurrency,
|
|
@@ -111,7 +111,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
111
111
|
const token = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
|
|
112
112
|
if (!token) throw new LiveUsageUnavailableError("no auth configured");
|
|
113
113
|
|
|
114
|
-
const credential =
|
|
114
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
115
115
|
if (credential?.type === "api_key") {
|
|
116
116
|
throw new LiveUsageUnavailableError("API-key auth does not expose Claude subscription quota");
|
|
117
117
|
}
|
|
@@ -170,12 +170,11 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
async function getLiveUsageAvailability(
|
|
174
|
-
|
|
173
|
+
async function getLiveUsageAvailability(_ctx: ExtensionContext) {
|
|
174
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
175
|
+
if (!credential) {
|
|
175
176
|
return { available: false as const, reason: "no auth configured" };
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
-
const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
|
|
179
178
|
if (credential?.type === "oauth") {
|
|
180
179
|
return { available: true as const };
|
|
181
180
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
2
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
clampPercent,
|
|
4
5
|
formatCount,
|
|
@@ -8,7 +9,12 @@ import {
|
|
|
8
9
|
readNumber,
|
|
9
10
|
readString,
|
|
10
11
|
} from "./shared.js";
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
LiveUsageAvailability,
|
|
14
|
+
LiveUsageItem,
|
|
15
|
+
LiveUsageSnapshot,
|
|
16
|
+
UsageProviderDefinition,
|
|
17
|
+
} from "./types.js";
|
|
12
18
|
|
|
13
19
|
const PROVIDER_ID = "github-copilot";
|
|
14
20
|
|
|
@@ -136,13 +142,18 @@ function buildUsageUrl(enterpriseUrl?: string): string {
|
|
|
136
142
|
return `https://${host}/copilot_internal/user`;
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
async function getLiveUsageAvailability(_ctx: ExtensionContext): Promise<LiveUsageAvailability> {
|
|
146
|
+
return getStoredCopilotCredential()
|
|
147
|
+
? { available: true }
|
|
148
|
+
: { available: false, reason: "not logged in" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function fetchUsage(
|
|
152
|
+
_ctx: ExtensionContext,
|
|
153
|
+
signal?: AbortSignal,
|
|
154
|
+
): Promise<LiveUsageSnapshot> {
|
|
155
|
+
const credential = getStoredCopilotCredential();
|
|
156
|
+
if (!credential) {
|
|
146
157
|
throw new Error("Not logged in to GitHub Copilot.");
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -193,11 +204,21 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
193
204
|
};
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
function getStoredCopilotCredential(): OAuthCredential | undefined {
|
|
208
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
209
|
+
return credential?.type === "oauth" &&
|
|
210
|
+
typeof credential.refresh === "string" &&
|
|
211
|
+
credential.refresh.trim()
|
|
212
|
+
? credential
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
196
216
|
const githubCopilotProvider: UsageProviderDefinition = {
|
|
197
217
|
id: PROVIDER_ID,
|
|
198
218
|
label: "GitHub Copilot",
|
|
199
219
|
shortLabel: "copilot",
|
|
200
220
|
color: { r: 168, g: 85, b: 247 },
|
|
221
|
+
getLiveUsageAvailability,
|
|
201
222
|
fetchLiveUsage: fetchUsage,
|
|
202
223
|
};
|
|
203
224
|
|
|
@@ -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,
|
|
@@ -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
|
|
|
@@ -1429,7 +1433,7 @@ async function getLiveUsageAvailability(
|
|
|
1429
1433
|
return provider.getLiveUsageAvailability(ctx);
|
|
1430
1434
|
}
|
|
1431
1435
|
|
|
1432
|
-
return ctx.modelRegistry.
|
|
1436
|
+
return ctx.modelRegistry.getProviderAuthStatus(provider.id).configured
|
|
1433
1437
|
? { available: true }
|
|
1434
1438
|
: { available: false, reason: "no auth configured" };
|
|
1435
1439
|
}
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
clampPercent,
|
|
4
4
|
formatCurrency,
|
|
@@ -112,7 +112,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
112
112
|
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
|
|
113
113
|
if (!apiKey) throw new Error("Not logged in to OpenAI Codex.");
|
|
114
114
|
|
|
115
|
-
const credential =
|
|
115
|
+
const credential = readStoredCredential(PROVIDER_ID);
|
|
116
116
|
const accountId =
|
|
117
117
|
credential?.type === "oauth" && typeof credential.accountId === "string"
|
|
118
118
|
? credential.accountId
|
|
@@ -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;
|