@herbertgao/pi-extensions 2026.8.7 → 2026.8.9
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 +8 -6
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
- package/node_modules/@czottmann/pi-automode/README.md +78 -112
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
- package/node_modules/@czottmann/pi-automode/package.json +9 -2
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
- package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
- package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
- package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
- package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
- package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
- package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
- package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
- package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
- package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
- package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
- package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
- package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-recap/README.md +5 -3
- package/node_modules/@herbertgao/pi-recap/package.json +9 -3
- package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
- package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
- package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
- package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
- package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
- package/node_modules/@herbertgao/pi-rename/README.md +13 -11
- package/node_modules/@herbertgao/pi-rename/package.json +13 -3
- package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
- package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
- package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
- package/node_modules/@herbertgao/pi-stash/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +27 -0
- package/node_modules/pi-web-access/README.md +8 -7
- package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +28 -10
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +43 -14
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +19 -18
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/warm-loader-cache.mjs (#1926)
|
|
3
|
+
//
|
|
4
|
+
// Pay pi's jiti transform of dist/index.js at install time, so no interactive
|
|
5
|
+
// session pays it.
|
|
6
|
+
//
|
|
7
|
+
// pi loads an extension with jiti, which Babel-transforms the entry and caches
|
|
8
|
+
// the result under <tmpdir>/jiti. pi-lens's entry is a ~4MB esbuild bundle, so
|
|
9
|
+
// the transform is the dominant startup cost exactly once per build: the first
|
|
10
|
+
// session after a `git:` install or update measured 4847ms of `module import`,
|
|
11
|
+
// against 138ms once the cache is warm. `prepare` has just built that bundle,
|
|
12
|
+
// so this script transforms it through the same jiti and writes the same cache
|
|
13
|
+
// entry pi will read.
|
|
14
|
+
//
|
|
15
|
+
// See scripts/lib/warm-loader-cache.mjs for why the entry this writes is the
|
|
16
|
+
// entry pi reads — the cache key, the transform-version marker that makes a
|
|
17
|
+
// mismatch a miss instead of a corruption, and the measurement that proved the
|
|
18
|
+
// output byte-identical to pi's own.
|
|
19
|
+
//
|
|
20
|
+
// POSTURE: best-effort, last in the chain, always exit 0. `prepare` also runs
|
|
21
|
+
// `build:dist` and `download-grammars`, which consumers depend on and which
|
|
22
|
+
// MUST fail loudly. A cache warm is an optimisation and must never share their
|
|
23
|
+
// failure path — the same split `scripts/setup-git-hooks.mjs` makes (#1804).
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
import { HOST_PROVIDED_PACKAGES } from "./lib/host-provided-deps.mjs";
|
|
30
|
+
import {
|
|
31
|
+
appendBounded,
|
|
32
|
+
buildStubAliases,
|
|
33
|
+
expectedCacheFileName,
|
|
34
|
+
resolveJitiCacheDir,
|
|
35
|
+
verifyCacheEntry,
|
|
36
|
+
warmSkipReason,
|
|
37
|
+
} from "./lib/warm-loader-cache.mjs";
|
|
38
|
+
|
|
39
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
|
|
42
|
+
function log(message) {
|
|
43
|
+
process.stderr.write(`[warm-loader-cache] ${message}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Append one JSONL record to ~/.pi-lens/install.log.
|
|
48
|
+
*
|
|
49
|
+
* pi's install output scrolls away, so without this there is no record that the
|
|
50
|
+
* warm ran, was skipped, or failed. One bounded line per install keeps the
|
|
51
|
+
* question answerable after the fact. `PI_LENS_INSTALL_LOG` redirects the file,
|
|
52
|
+
* which is how the tests read the record back.
|
|
53
|
+
*/
|
|
54
|
+
function record(entry) {
|
|
55
|
+
try {
|
|
56
|
+
const override = process.env.PI_LENS_INSTALL_LOG;
|
|
57
|
+
const file =
|
|
58
|
+
typeof override === "string" && override.length > 0
|
|
59
|
+
? override
|
|
60
|
+
: path.join(os.homedir(), ".pi-lens", "install.log");
|
|
61
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
62
|
+
const existing = fs.existsSync(file)
|
|
63
|
+
? fs.readFileSync(file, "utf8").split("\n")
|
|
64
|
+
: [];
|
|
65
|
+
const line = JSON.stringify({
|
|
66
|
+
ts: new Date().toISOString(),
|
|
67
|
+
event: "warm_loader_cache",
|
|
68
|
+
...entry,
|
|
69
|
+
});
|
|
70
|
+
fs.writeFileSync(file, `${appendBounded(existing, line).join("\n")}\n`);
|
|
71
|
+
} catch {
|
|
72
|
+
// The log is diagnostic. Losing it must not change the install outcome.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Entries pi will load, read from the manifest rather than hardcoded. */
|
|
77
|
+
function extensionEntries() {
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(
|
|
80
|
+
fs.readFileSync(path.join(root, "package.json"), "utf8"),
|
|
81
|
+
);
|
|
82
|
+
return (pkg.pi?.extensions ?? []).map((entry) => path.resolve(root, entry));
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function loadCreateJiti() {
|
|
89
|
+
// `jiti/static` is the subpath pi's loader imports, and it is export-mapped
|
|
90
|
+
// for `import` only — `require.resolve` reports it as not exported. Resolve
|
|
91
|
+
// it the ESM way, and fall back to the package root for older runtimes.
|
|
92
|
+
for (const specifier of ["jiti/static", "jiti"]) {
|
|
93
|
+
try {
|
|
94
|
+
const url = import.meta.resolve
|
|
95
|
+
? import.meta.resolve(specifier)
|
|
96
|
+
: pathToFileURL(require.resolve(specifier)).href;
|
|
97
|
+
const mod = await import(url);
|
|
98
|
+
if (typeof mod.createJiti === "function") return mod.createJiti;
|
|
99
|
+
} catch {
|
|
100
|
+
// Try the next specifier; absence is a skip, not a failure.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The installed jiti's version, so a host jiti bump — which changes the marker
|
|
108
|
+
* and silently turns every warm into a cold session — is diagnosable from the
|
|
109
|
+
* record rather than from a bisect.
|
|
110
|
+
*/
|
|
111
|
+
function jitiVersion() {
|
|
112
|
+
try {
|
|
113
|
+
const url = import.meta.resolve
|
|
114
|
+
? import.meta.resolve("jiti/package.json")
|
|
115
|
+
: pathToFileURL(require.resolve("jiti/package.json")).href;
|
|
116
|
+
return JSON.parse(fs.readFileSync(fileURLToPath(url), "utf8")).version;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Real-filesystem probes for `verifyCacheEntry`. */
|
|
123
|
+
const fsDeps = {
|
|
124
|
+
existsSync: (p) => fs.existsSync(p),
|
|
125
|
+
readFileSync: (p) => fs.readFileSync(p, "utf8"),
|
|
126
|
+
isWritable: (p) => {
|
|
127
|
+
try {
|
|
128
|
+
fs.accessSync(p, fs.constants.W_OK);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export async function main() {
|
|
137
|
+
const entries = extensionEntries();
|
|
138
|
+
const createJiti = await loadCreateJiti();
|
|
139
|
+
const skip = warmSkipReason({
|
|
140
|
+
env: process.env,
|
|
141
|
+
distEntryExists:
|
|
142
|
+
entries.length > 0 && entries.every((e) => fs.existsSync(e)),
|
|
143
|
+
jitiResolvable: typeof createJiti === "function",
|
|
144
|
+
});
|
|
145
|
+
if (skip) {
|
|
146
|
+
log(`skipped: ${skip}`);
|
|
147
|
+
record({ status: "skipped", reason: skip });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cacheDir = resolveJitiCacheDir({
|
|
152
|
+
tmpdir: os.tmpdir,
|
|
153
|
+
env: process.env,
|
|
154
|
+
cwd: process.cwd,
|
|
155
|
+
join: path.join,
|
|
156
|
+
});
|
|
157
|
+
const alias = buildStubAliases(HOST_PROVIDED_PACKAGES);
|
|
158
|
+
const version = jitiVersion();
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const started = Date.now();
|
|
162
|
+
const cacheFile = expectedCacheFileName(entry);
|
|
163
|
+
const jiti = createJiti(pathToFileURL(entry).href, {
|
|
164
|
+
// Mirror pi's loader: no in-process module cache, aliases present.
|
|
165
|
+
// Neither affects the transform or the cache key, but staying close to
|
|
166
|
+
// pi's call keeps the two easy to compare.
|
|
167
|
+
moduleCache: false,
|
|
168
|
+
fsCache: cacheDir,
|
|
169
|
+
alias,
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
// jiti writes the cache entry BEFORE it evaluates the module, so the
|
|
173
|
+
// warm is complete even though evaluation then fails on the first
|
|
174
|
+
// host-provided import — those only resolve inside pi.
|
|
175
|
+
await jiti.import(entry, { default: true });
|
|
176
|
+
} catch {
|
|
177
|
+
// Expected. Evaluation is not the goal; the transform is.
|
|
178
|
+
}
|
|
179
|
+
const ms = Date.now() - started;
|
|
180
|
+
const relative = path.relative(root, entry);
|
|
181
|
+
|
|
182
|
+
// The import returning proves nothing. Several exit-0 paths leave no usable
|
|
183
|
+
// entry — an unwritable cache directory, an import that never reached the
|
|
184
|
+
// transform, a stale file that fails jiti's marker check — so read the file
|
|
185
|
+
// back before claiming anything was cached.
|
|
186
|
+
const verdict = verifyCacheEntry({
|
|
187
|
+
cacheDir,
|
|
188
|
+
fileName: cacheFile,
|
|
189
|
+
source: fs.readFileSync(entry, "utf8"),
|
|
190
|
+
fsDeps,
|
|
191
|
+
});
|
|
192
|
+
const common = {
|
|
193
|
+
entry: relative,
|
|
194
|
+
ms,
|
|
195
|
+
cacheDir,
|
|
196
|
+
cacheFile,
|
|
197
|
+
jitiVersion: version,
|
|
198
|
+
};
|
|
199
|
+
if (verdict.ok) {
|
|
200
|
+
log(`warmed ${relative} in ${ms}ms (cache: ${cacheDir}/${cacheFile})`);
|
|
201
|
+
record({
|
|
202
|
+
status: "warmed",
|
|
203
|
+
...common,
|
|
204
|
+
transformVersion: verdict.transformVersion,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
log(`not cached: ${relative} after ${ms}ms — ${verdict.reason}`);
|
|
209
|
+
record({ status: "not_cached", ...common, reason: verdict.reason });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run `work`, absorbing any failure. Exported so a test can inject a throwing
|
|
215
|
+
* body and assert the process still exits 0 and still leaves a record.
|
|
216
|
+
*/
|
|
217
|
+
export async function run(work = main) {
|
|
218
|
+
try {
|
|
219
|
+
await work();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
222
|
+
log(`failed: ${message}`);
|
|
223
|
+
record({ status: "failed", reason: message });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const invokedPath = process.argv[1];
|
|
228
|
+
const invokedDirectly =
|
|
229
|
+
typeof invokedPath === "string" &&
|
|
230
|
+
pathToFileURL(path.resolve(invokedPath)).href === import.meta.url;
|
|
231
|
+
if (invokedDirectly) {
|
|
232
|
+
await run();
|
|
233
|
+
}
|
|
@@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.24.2] - 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
- `auto` search now uses Codex-backed OpenAI search when Pi is running a Codex model.
|
|
11
|
+
- Non-Codex sessions still prefer Exa first, so zero-config search keeps its fast keyless path.
|
|
12
|
+
- Gemini Web browser-cookie access is more reliable on Windows Chrome and Edge profiles.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Prefer Codex-backed OpenAI search in `auto` mode when the active Pi model is `openai-codex`; otherwise prefer Exa before OpenAI.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed Windows Gemini Web browser-cookie extraction when Chromium cookie expiry values are too large for JavaScript numbers or PowerShell DPAPI key unprotect needs encoded command transport. Thanks to [@laixuanthoi](https://github.com/laixuanthoi) for issue #290.
|
|
19
|
+
|
|
20
|
+
## [0.24.1] - 2026-08-21
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Added `pdf.maxPages` to limit Datalab, Gemini, and local PDF extraction to the first N pages. Thanks to [@jaudiger](https://github.com/jaudiger) for issue #277.
|
|
24
|
+
- Added optional `openaiSearchProviders` config to choose which Pi model providers fund OpenAI `web_search`, in priority order. Thanks to [@hank-warren](https://github.com/hank-warren) for PR #276.
|
|
25
|
+
- Added Windows Chrome and Edge browser-cookie support for Gemini Web. Thanks to [@laixuanthoi](https://github.com/laixuanthoi) for issue #286.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Hardened GitHub clone cache path handling. Thanks to [@spikelab](https://github.com/spikelab) for the responsible disclosure.
|
|
29
|
+
- Updated the local `fetch_content` HTTP User-Agent for wider compatible content retrieval. Thanks to [@_can1357](https://x.com/_can1357) for [the User-Agent observation](https://x.com/_can1357/status/2090837707069014224).
|
|
30
|
+
- Replaced inline RFC 2397 `data:` URIs in extracted page content with explicit bounded omission markers (MIME type, encoding, encoded/decoded byte counts, SHA-256 digest, `retrieval=not-retained`) before content reaches tool results, the fetch cache, or session persistence. Readable prose and Markdown image alt text are preserved; typed thumbnail/frame image blocks are unaffected. Thanks to [@bbbRye007](https://github.com/bbbRye007) for #281 and #282.
|
|
31
|
+
- Detached Linux curator browser launches so `xdg-open` cannot block `web_search` until the browser exits. Thanks to [@nguyenphivn](https://github.com/nguyenphivn) for issue #279.
|
|
32
|
+
- Allowed configured Firecrawl API base URLs to use loopback addresses without opening loopback for submitted fetch targets. Thanks to [@ackalker](https://github.com/ackalker) for issue #280.
|
|
33
|
+
|
|
7
34
|
## [0.24.0] - 2026-08-18
|
|
8
35
|
|
|
9
36
|
### Highlights
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
**Video Understanding** — Point it at a YouTube video or local screen recording and ask questions about what's on screen. Full transcripts, visual descriptions, and frame extraction at exact timestamps.
|
|
20
20
|
|
|
21
|
-
**Smart Fallbacks** — Every capability has a fallback chain. Search tries configured SearXNG first for local/private search
|
|
21
|
+
**Smart Fallbacks** — Every capability has a fallback chain. Search tries configured SearXNG first for local/private search. When the active Pi model is `openai-codex`, it then tries Codex-backed OpenAI search. Otherwise it tries Exa before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, and Gemini Web when browser cookies are enabled. YouTube tries Gemini Web when enabled, then API, then Perplexity. Blocked pages try configured self-hosted Firecrawl first. Third-party hosted page fetchers require explicit `fetchRouting.allowRemoteHostedProviders` opt-in for remote HTTP(S) targets.
|
|
22
22
|
|
|
23
23
|
**GitHub Cloning** — GitHub URLs are cloned locally instead of scraped. The agent gets real file contents and a local path to explore, not rendered HTML.
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ Works immediately with no API keys — Exa MCP provides zero-config search. If P
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
In `auto` mode (default), `web_search` tries a configured SearXNG endpoint first for local/private search
|
|
49
|
+
In `auto` mode (default), `web_search` tries a configured SearXNG endpoint first for local/private search. When the active Pi model is `openai-codex`, it then tries Codex-backed OpenAI search. Otherwise it tries Exa (direct API if keyed, MCP if not) before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Perplexity, Gemini API, and Gemini Web when browser-cookie access is enabled. Exa handles search; curator summary drafts are generated separately by the configured Pi summary model, defaulting to Claude Haiku, Codex Luna, Codex Terra, Gemini 3.6 Flash, GPT-5 mini, then DeepSeek V4 Flash when available. Slow summary drafts fall back to a deterministic result summary after a bounded deadline.
|
|
50
50
|
|
|
51
51
|
If your OpenAI key belongs to a third-party Responses-compatible gateway, set `openaiResponsesUrl` to that gateway's full Responses endpoint. The default remains `https://api.openai.com/v1/responses`.
|
|
52
52
|
|
|
@@ -233,6 +233,7 @@ Configure Datalab via the web-search config:
|
|
|
233
233
|
"datalabApiKey": "$DATALAB_API_KEY",
|
|
234
234
|
"pdf": {
|
|
235
235
|
"maxSizeMB": 20,
|
|
236
|
+
"maxPages": 100,
|
|
236
237
|
"provider": "auto", // "auto" | "gemini" | "datalab" | "unpdf"
|
|
237
238
|
"datalabMode": "balanced", // "fast" | "balanced" | "accurate"
|
|
238
239
|
"datalabTimeoutMs": 120000
|
|
@@ -458,7 +459,7 @@ Set `searxngBaseUrl` or `SEARXNG_BASE_URL` to use a self-hosted SearXNG JSON API
|
|
|
458
459
|
|
|
459
460
|
**DuckDuckGo.** DuckDuckGo HTML search is keyless and explicit-only. Select it with `provider: "duckduckgo"` or place it in `searchRouting`; it is never chosen by `auto` and never participates in `provider: "all"`. Domain filters are enforced locally after DuckDuckGo redirect URLs are decoded. `recencyFilter` is not guaranteed because the HTML endpoint has no documented stable time parameter. A 200 page with no parseable results is reported as an invalid response, so routing can continue when `fallbackOn` includes `"invalid-response"`.
|
|
460
461
|
|
|
461
|
-
Set `firecrawlBaseUrl` or `FIRECRAWL_BASE_URL` to use Firecrawl for `web_search` and as an extraction fallback for `fetch_content`. Search calls `/v2/search` by default, requests `sources: ["web"]`, maps `numResults` to `limit`, maps `recencyFilter` to Firecrawl's `tbs`, and maps domain filters to `includeDomains` or `excludeDomains` when possible. With `includeContent: true`, Firecrawl search adds Markdown scrape options and returns successful result Markdown as inline content. Fetch extraction calls `/v2/scrape` by default. Set `firecrawlApiVersion` or `FIRECRAWL_API_VERSION` to `v1` for older self-hosted images. Firecrawl page scraping is cache-only by default (`lockdown: true`), so the Firecrawl server does not make fresh outbound target requests unless you explicitly set `firecrawlFreshScrape: true` or `FIRECRAWL_FRESH_SCRAPE=1`. Enable fresh scraping only for a Firecrawl deployment whose own egress, redirects, DNS rebinding behavior, and internal-network access are isolated or allowlisted; this extension can preflight submitted fetch URLs but cannot control network requests made by the Firecrawl server. The configured Firecrawl API base URL and redirects are still validated by the same SSRF guard as other remote requests, and Firecrawl credentials are stripped from cross-origin API redirects.
|
|
462
|
+
Set `firecrawlBaseUrl` or `FIRECRAWL_BASE_URL` to use Firecrawl for `web_search` and as an extraction fallback for `fetch_content`. Search calls `/v2/search` by default, requests `sources: ["web"]`, maps `numResults` to `limit`, maps `recencyFilter` to Firecrawl's `tbs`, and maps domain filters to `includeDomains` or `excludeDomains` when possible. With `includeContent: true`, Firecrawl search adds Markdown scrape options and returns successful result Markdown as inline content. Fetch extraction calls `/v2/scrape` by default. Set `firecrawlApiVersion` or `FIRECRAWL_API_VERSION` to `v1` for older self-hosted images. Firecrawl page scraping is cache-only by default (`lockdown: true`), so the Firecrawl server does not make fresh outbound target requests unless you explicitly set `firecrawlFreshScrape: true` or `FIRECRAWL_FRESH_SCRAPE=1`. Enable fresh scraping only for a Firecrawl deployment whose own egress, redirects, DNS rebinding behavior, and internal-network access are isolated or allowlisted; this extension can preflight submitted fetch URLs but cannot control network requests made by the Firecrawl server. A configured Firecrawl API base URL may use `localhost`, `127.0.0.0/8`, or `::1` without adding those loopback ranges to global `ssrf.allowRanges`; that exception is only for Firecrawl API calls, not submitted fetch/search target URLs. The configured Firecrawl API base URL and redirects are otherwise still validated by the same SSRF guard as other remote requests, and Firecrawl credentials are stripped from cross-origin API redirects.
|
|
462
463
|
|
|
463
464
|
**Bright Data.** Set `brightdataApiKey` or `BRIGHTDATA_API_KEY` to use Bright Data-backed features. The SERP search provider also requires `brightdataSerpZone` or `BRIGHTDATA_SERP_ZONE`, and the Web Unlocker extraction fallback also requires `brightdataUnlockerZone` or `BRIGHTDATA_UNLOCKER_ZONE`. These zone settings are separate and are never substituted for each other: SERP requires a Bright Data zone of type `serp`, while Web Unlocker requires a zone of type `unblocker`. Leaving either zone unset keeps that product unavailable, so enabling one Bright Data feature does not opt into the other.
|
|
464
465
|
|
|
@@ -478,7 +479,7 @@ Bright Data Web Unlocker is a paid `fetch_content` fallback after Parallel and b
|
|
|
478
479
|
|
|
479
480
|
**Parallel MCP.** Select `provider: "parallel-mcp"` to use Parallel Search MCP without an API key, or add it to `searchRouting`. It is explicit-only and is never chosen by `auto` or `provider: "all"`; the existing `parallel` provider remains the key-required REST API. A configured `parallelApiKey` or `PARALLEL_API_KEY` is sent as an optional Bearer token for higher MCP limits. To use MCP `web_fetch`, add `parallel-mcp` to `fetchRouting.providers` and set `fetchRouting.allowRemoteHostedProviders` to `true`; it is not part of the default fetch route.
|
|
480
481
|
|
|
481
|
-
Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `ANYSEARCH_API_KEY`, `XAI_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"valyu"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"serper"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, `"network"`, and/or `"invalid-response"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"summary-review"` (default, opens curator with auto-generated summary draft), `"auto-summary"` (returns a model-generated summary without opening the curator), or `"none"` (raw results, no curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `chromeProfile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `summaryGenerationDeadlineMs` sets the maximum time for one summary model attempt in the curator UI and `auto-summary` mode. It defaults to `30000`, must be a positive integer, and is capped at `600000`. `maxInlineContentChars` sets the direct `fetch_content` content slice and the default and maximum `get_search_content` slice. It defaults to `30000`, must be a positive integer, and is capped at `200000`; full fetched content remains stored for later retrieval. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IPs, and `NO_PROXY` matches. It does not configure proxy transport.
|
|
482
|
+
Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `ANYSEARCH_API_KEY`, `XAI_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. `openaiSearchProviders` sets which Pi model providers OpenAI `web_search` resolves login credentials from, in priority order; it defaults to `["openai-codex", "openai"]`, entries that are not registered or not signed in are skipped, and an empty array skips Pi credentials entirely so the `openaiApiKey` / `OPENAI_API_KEY` fallback applies. Useful for choosing between multiple Codex accounts (for example a second account registered by an extension) or forcing API-key billing while signed into Codex. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"valyu"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"serper"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, `"network"`, and/or `"invalid-response"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"summary-review"` (default, opens curator with auto-generated summary draft), `"auto-summary"` (returns a model-generated summary without opening the curator), or `"none"` (raw results, no curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `chromeProfile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `summaryGenerationDeadlineMs` sets the maximum time for one summary model attempt in the curator UI and `auto-summary` mode. It defaults to `30000`, must be a positive integer, and is capped at `600000`. `maxInlineContentChars` sets the direct `fetch_content` content slice and the default and maximum `get_search_content` slice. It defaults to `30000`, must be a positive integer, and is capped at `200000`; full fetched content remains stored for later retrieval. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IPs, and `NO_PROXY` matches. It does not configure proxy transport.
|
|
482
483
|
`VALYU_API_KEY` and `SERPER_API_KEY` also retain this precedence. `provider` and `searchProvider` also accept `"parallel-mcp"`, `"valyu"`, and `"serper"`; all three remain explicit-only.
|
|
483
484
|
|
|
484
485
|
### All providers
|
|
@@ -810,12 +811,12 @@ Values use the same format as pi keybindings (e.g. `ctrl+s`, `ctrl+shift+s`, `al
|
|
|
810
811
|
|
|
811
812
|
Set `"enabled": false` under `tools`, `commands`, `image`, or `pdf` to disable that feature. Tool-specific settings override the legacy `webSearch.enabled` shorthand; without an override, it still disables `web_search` and `source_check`. `image.enabled: false` blocks direct image fetches and video frame extraction, and prevents video thumbnails. `pdf.enabled: false` blocks PDF extraction. For GitHub specifically, `githubClone.enabled: false` only skips clone/API specialization; it does not unregister `fetch_content` or block generic URL extraction. Pi restart is required for tool and command registration changes.
|
|
812
813
|
|
|
813
|
-
Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL. Remote extraction fallbacks carry their own budgets and are not covered by that number: Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50.
|
|
814
|
+
Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL. Remote extraction fallbacks carry their own budgets and are not covered by that number: Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50. `pdf.maxPages` defaults to 100 and limits every PDF provider to the first N pages.
|
|
814
815
|
|
|
815
816
|
## Limitations
|
|
816
817
|
|
|
817
818
|
- If the curator cannot open a browser automatically, such as in Docker, WSL, SSH, or headless environments, the running curator URL is shown in the tool output. Copy it into a browser that can reach the Pi host, or use a tunnel/port-forward when needed.
|
|
818
|
-
- Chromium cookie extraction for Gemini Web is opt-in via `allowBrowserCookies: true` or `PI_ALLOW_BROWSER_COOKIES=1`; no browser data or password store is touched while it is disabled. On macOS, enabling it may trigger a Keychain dialog. Required cookie names are checked before password-store access, and browser encryption passwords are cached only in-process. If `node:sqlite` is unavailable, the reader falls back to the `sqlite3` CLI or Python stdlib; `/google-account` reports a sanitized SQLite/profile/password diagnostic when extraction fails.
|
|
819
|
+
- Chromium cookie extraction for Gemini Web is opt-in via `allowBrowserCookies: true` or `PI_ALLOW_BROWSER_COOKIES=1`; no browser data or password store is touched while it is disabled. On macOS, enabling it may trigger a Keychain dialog. On Windows, Chrome and Edge v10 cookies use the current user's DPAPI key; v20 app-bound cookies are not supported. Required cookie names are checked before password-store access, and browser encryption passwords are cached only in-process. If `node:sqlite` is unavailable, the reader falls back to the `sqlite3` CLI or Python stdlib; `/google-account` reports a sanitized SQLite/profile/password diagnostic when extraction fails.
|
|
819
820
|
- YouTube private/age-restricted videos may fail on all extraction paths.
|
|
820
821
|
- Gemini can process videos up to ~1 hour; longer videos may be truncated.
|
|
821
822
|
- PDFs are text-extracted only (no OCR for scanned documents).
|
|
@@ -862,7 +863,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Sear
|
|
|
862
863
|
| `gemini-web.ts` | Gemini Web client (cookie auth, StreamGenerate) |
|
|
863
864
|
| `gemini-web-config.ts` | Gemini Web profile and browser-cookie opt-in config |
|
|
864
865
|
| `gemini-api.ts` | Gemini REST API client (generateContent) |
|
|
865
|
-
| `chrome-cookies.ts` |
|
|
866
|
+
| `chrome-cookies.ts` | Chromium-based cookie extraction (macOS Keychain, Linux secret-tool, Windows DPAPI + SQLite) |
|
|
866
867
|
| `youtube-extract.ts` | YouTube detection, three-tier extraction, frame extraction |
|
|
867
868
|
| `video-extract.ts` | Local video detection, Files API upload, Gemini analysis |
|
|
868
869
|
| `github-extract.ts` | GitHub URL parsing, clone cache, content generation |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
|
3
|
-
import { copyFileSync, existsSync, mkdtempSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
4
4
|
import { tmpdir, homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, sep } from "node:path";
|
|
6
6
|
import { isBrowserCookieAccessAllowed } from "./gemini-web-config.ts";
|
|
@@ -10,6 +10,7 @@ export type CookieMap = Record<string, string>;
|
|
|
10
10
|
interface BrowserConfig {
|
|
11
11
|
name: string;
|
|
12
12
|
baseDir: string;
|
|
13
|
+
usesLocalAppData?: boolean;
|
|
13
14
|
keychainService?: string;
|
|
14
15
|
keychainAccount?: string;
|
|
15
16
|
secretToolApp?: string;
|
|
@@ -48,6 +49,11 @@ const LINUX_BROWSER_CONFIGS: BrowserConfig[] = [
|
|
|
48
49
|
{ name: "Chrome", baseDir: ".config/google-chrome", secretToolApp: "chrome" },
|
|
49
50
|
];
|
|
50
51
|
|
|
52
|
+
const WINDOWS_BROWSER_CONFIGS: BrowserConfig[] = [
|
|
53
|
+
{ name: "Chrome", baseDir: "Google/Chrome/User Data", usesLocalAppData: true },
|
|
54
|
+
{ name: "Edge", baseDir: "Microsoft/Edge/User Data", usesLocalAppData: true },
|
|
55
|
+
];
|
|
56
|
+
|
|
51
57
|
const browserPasswordCache = new Map<string, Promise<string | null>>();
|
|
52
58
|
let lastCookieDiagnostic: string | null = null;
|
|
53
59
|
let sqliteModule: typeof import("node:sqlite") | null = null;
|
|
@@ -83,7 +89,7 @@ export async function getBrowserCookiesForHosts(
|
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
const currentPlatform = process.platform;
|
|
86
|
-
const configs = currentPlatform === "darwin" ? MACOS_BROWSER_CONFIGS : currentPlatform === "linux" ? LINUX_BROWSER_CONFIGS : [];
|
|
92
|
+
const configs = currentPlatform === "darwin" ? MACOS_BROWSER_CONFIGS : currentPlatform === "linux" ? LINUX_BROWSER_CONFIGS : currentPlatform === "win32" ? WINDOWS_BROWSER_CONFIGS : [];
|
|
87
93
|
if (configs.length === 0) {
|
|
88
94
|
lastCookieDiagnostic = "Chromium cookie extraction is unsupported on this platform.";
|
|
89
95
|
return null;
|
|
@@ -103,12 +109,13 @@ export async function getBrowserCookiesForHosts(
|
|
|
103
109
|
lastCookieDiagnostic = "No valid cookie hosts were requested.";
|
|
104
110
|
return null;
|
|
105
111
|
}
|
|
106
|
-
const home = homedir();
|
|
112
|
+
const home = currentPlatform === "win32" ? process.env.USERPROFILE || homedir() : homedir();
|
|
107
113
|
let sawCookieDatabase = false;
|
|
108
114
|
let sawRequiredCookies = false;
|
|
109
115
|
let sawAnyHostCookie = false;
|
|
110
116
|
let sawBackendFailure: SqliteFailure | undefined;
|
|
111
117
|
let sawUnsafeProfilePath = false;
|
|
118
|
+
let sawWindowsAppBoundCookie = false;
|
|
112
119
|
|
|
113
120
|
for (const config of configs) {
|
|
114
121
|
const profiles = requestedProfile ? [requestedProfile] : listBrowserProfiles(home, config);
|
|
@@ -119,7 +126,8 @@ export async function getBrowserCookiesForHosts(
|
|
|
119
126
|
continue;
|
|
120
127
|
}
|
|
121
128
|
if (!profilePath) continue;
|
|
122
|
-
const cookiesPath =
|
|
129
|
+
const cookiesPath = cookieDatabasePath(profilePath, config);
|
|
130
|
+
if (!cookiesPath) continue;
|
|
123
131
|
sawCookieDatabase = true;
|
|
124
132
|
|
|
125
133
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-chrome-cookies-"));
|
|
@@ -136,13 +144,15 @@ export async function getBrowserCookiesForHosts(
|
|
|
136
144
|
sawRequiredCookies = true;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
const key = currentPlatform === "win32"
|
|
148
|
+
? await readWindowsEncryptionKey(config, home)
|
|
149
|
+
: await readBrowserPassword(config, currentPlatform).then((password) => password ? pbkdf2Sync(password, "saltysalt", currentPlatform === "darwin" ? 1003 : 1, 16, "sha1") : null);
|
|
150
|
+
if (!key) {
|
|
151
|
+
warningSet.add(currentPlatform === "win32"
|
|
152
|
+
? `Could not read ${config.name} Windows cookie encryption key`
|
|
153
|
+
: `Could not read ${config.name} cookie encryption password`);
|
|
142
154
|
continue;
|
|
143
155
|
}
|
|
144
|
-
|
|
145
|
-
const key = pbkdf2Sync(password, "saltysalt", currentPlatform === "darwin" ? 1003 : 1, 16, "sha1");
|
|
146
156
|
const metaVersion = await readMetaVersion(tempDb);
|
|
147
157
|
if (metaVersion.failure) sawBackendFailure = metaVersion.failure;
|
|
148
158
|
if (metaVersion.value === null) continue;
|
|
@@ -159,7 +169,11 @@ export async function getBrowserCookiesForHosts(
|
|
|
159
169
|
if (!name) continue;
|
|
160
170
|
let value = typeof row.value === "string" && row.value.length > 0 ? row.value : null;
|
|
161
171
|
if (!value && typeof row.encrypted_value_hex === "string" && /^[0-9a-f]*$/i.test(row.encrypted_value_hex)) {
|
|
162
|
-
|
|
172
|
+
const encrypted = Buffer.from(row.encrypted_value_hex, "hex");
|
|
173
|
+
if (currentPlatform === "win32" && encrypted.subarray(0, 3).toString("utf8") === "v20") sawWindowsAppBoundCookie = true;
|
|
174
|
+
value = currentPlatform === "win32"
|
|
175
|
+
? decryptWindowsCookieValue(encrypted, key, metaVersion.value >= 24)
|
|
176
|
+
: decryptCookieValue(encrypted, key, metaVersion.value >= 24);
|
|
163
177
|
}
|
|
164
178
|
if (!value) continue;
|
|
165
179
|
const path = typeof row.path === "string" && row.path.startsWith("/") ? row.path : "/";
|
|
@@ -194,6 +208,8 @@ export async function getBrowserCookiesForHosts(
|
|
|
194
208
|
: "No detected Chromium profile contains a cookie database.";
|
|
195
209
|
} else if (requiredCookies?.length && !sawRequiredCookies) {
|
|
196
210
|
lastCookieDiagnostic = `No detected Chromium profile contains the required ${options.requiredLabel ?? "browser"} cookies.`;
|
|
211
|
+
} else if (sawWindowsAppBoundCookie) {
|
|
212
|
+
lastCookieDiagnostic = "Windows Chromium v20 app-bound cookies are not supported.";
|
|
197
213
|
} else if (!sawAnyHostCookie) {
|
|
198
214
|
lastCookieDiagnostic = options.requestUrl
|
|
199
215
|
? "No detected Chromium profile contains cookies for the requested URL."
|
|
@@ -217,10 +233,9 @@ function normalizeProfileName(value: string | undefined): string | undefined {
|
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
function resolveProfilePath(home: string, config: BrowserConfig, profile: string): string | "outside-root" | null {
|
|
220
|
-
const basePath =
|
|
236
|
+
const basePath = browserBasePath(home, config);
|
|
221
237
|
const profilePath = join(basePath, profile);
|
|
222
|
-
|
|
223
|
-
if (!existsSync(cookiesPath)) return null;
|
|
238
|
+
if (!cookieDatabasePath(profilePath, config)) return null;
|
|
224
239
|
try {
|
|
225
240
|
const baseRealPath = realpathSync(basePath);
|
|
226
241
|
const profileRealPath = realpathSync(profilePath);
|
|
@@ -231,6 +246,19 @@ function resolveProfilePath(home: string, config: BrowserConfig, profile: string
|
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
|
|
249
|
+
function cookieDatabasePath(profilePath: string, config: BrowserConfig): string | null {
|
|
250
|
+
const networkCookies = join(profilePath, "Network", "Cookies");
|
|
251
|
+
if (config.usesLocalAppData && existsSync(networkCookies)) return networkCookies;
|
|
252
|
+
const legacyCookies = join(profilePath, "Cookies");
|
|
253
|
+
return existsSync(legacyCookies) ? legacyCookies : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function browserBasePath(home: string, config: BrowserConfig): string {
|
|
257
|
+
return config.usesLocalAppData
|
|
258
|
+
? join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), config.baseDir)
|
|
259
|
+
: join(home, config.baseDir);
|
|
260
|
+
}
|
|
261
|
+
|
|
234
262
|
function normalizeCookieNames(names: string[] | undefined): string[] | undefined {
|
|
235
263
|
if (!names?.length) return undefined;
|
|
236
264
|
const normalized = names.filter((name): name is string => typeof name === "string").map((name) => name.trim()).filter(Boolean);
|
|
@@ -242,12 +270,12 @@ function normalizeHosts(hosts: string[]): string[] {
|
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
function listBrowserProfiles(home: string, config: BrowserConfig): string[] {
|
|
245
|
-
const basePath =
|
|
273
|
+
const basePath = browserBasePath(home, config);
|
|
246
274
|
if (!existsSync(basePath)) return ["Default"];
|
|
247
275
|
const profiles = new Set<string>();
|
|
248
276
|
try {
|
|
249
277
|
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
|
|
250
|
-
if (entry.isDirectory() &&
|
|
278
|
+
if (entry.isDirectory() && cookieDatabasePath(join(basePath, entry.name), config)) profiles.add(entry.name);
|
|
251
279
|
}
|
|
252
280
|
} catch {
|
|
253
281
|
}
|
|
@@ -288,6 +316,21 @@ function decryptCookieValue(encrypted: Uint8Array, key: Buffer, stripHash: boole
|
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
|
|
319
|
+
function decryptWindowsCookieValue(encrypted: Uint8Array, key: Buffer, stripHash: boolean): string | null {
|
|
320
|
+
const buf = Buffer.from(encrypted);
|
|
321
|
+
if (buf.subarray(0, 3).toString("utf8") !== "v10" || buf.length < 3 + 12 + 16) return null;
|
|
322
|
+
try {
|
|
323
|
+
const nonce = buf.subarray(3, 15);
|
|
324
|
+
const ciphertext = buf.subarray(15, -16);
|
|
325
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
326
|
+
decipher.setAuthTag(buf.subarray(-16));
|
|
327
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
328
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(stripHash && plaintext.length >= 32 ? plaintext.subarray(32) : plaintext);
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
291
334
|
function removePkcs7Padding(buf: Buffer): Buffer {
|
|
292
335
|
if (!buf.length) return buf;
|
|
293
336
|
const padding = buf[buf.length - 1];
|
|
@@ -316,6 +359,35 @@ function readBrowserPassword(config: BrowserConfig, currentPlatform: typeof proc
|
|
|
316
359
|
return passwordPromise;
|
|
317
360
|
}
|
|
318
361
|
|
|
362
|
+
async function readWindowsEncryptionKey(config: BrowserConfig, home: string): Promise<Buffer | null> {
|
|
363
|
+
try {
|
|
364
|
+
const localState = JSON.parse(readFileSync(join(browserBasePath(home, config), "Local State"), "utf8")) as { os_crypt?: { encrypted_key?: unknown } };
|
|
365
|
+
const encodedKey = localState.os_crypt?.encrypted_key;
|
|
366
|
+
if (typeof encodedKey !== "string") return null;
|
|
367
|
+
const protectedKey = Buffer.from(encodedKey, "base64");
|
|
368
|
+
if (protectedKey.subarray(0, 5).toString("utf8") !== "DPAPI") return null;
|
|
369
|
+
const decrypted = await unprotectWindowsData(protectedKey.subarray(5));
|
|
370
|
+
return decrypted?.length === 32 ? decrypted : null;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function unprotectWindowsData(protectedData: Buffer): Promise<Buffer | null> {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const script = "Add-Type -AssemblyName System.Security;$data=[Convert]::FromBase64String($env:PIWA_PROTECTED);$clear=[System.Security.Cryptography.ProtectedData]::Unprotect($data,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Write([Convert]::ToBase64String($clear))";
|
|
379
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
380
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { timeout: 5000, maxBuffer: 1024 * 1024, env: { ...process.env, PIWA_PROTECTED: protectedData.toString("base64") } }, (err, stdout) => {
|
|
381
|
+
if (err) { resolve(null); return; }
|
|
382
|
+
try {
|
|
383
|
+
resolve(Buffer.from(stdout.trim(), "base64"));
|
|
384
|
+
} catch {
|
|
385
|
+
resolve(null);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
319
391
|
function readKeychainPassword(account: string, service: string): Promise<string | null> {
|
|
320
392
|
return new Promise((resolve) => {
|
|
321
393
|
execFile("security", ["find-generic-password", "-w", "-a", account, "-s", service], { timeout: 5000 }, (err, stdout) => {
|
|
@@ -438,10 +510,10 @@ async function queryCookieRows(dbPath: string, hosts: string[], names: Iterable<
|
|
|
438
510
|
const columns = await readCookieColumns(dbPath);
|
|
439
511
|
if (columns.status === "failure") return columns;
|
|
440
512
|
const pathExpr = columns.columns.has("path") ? "path" : "'/' AS path";
|
|
441
|
-
const expiresExpr = columns.columns.has("expires_utc") ?
|
|
442
|
-
const expiryFilter = filterExpired && columns.columns.has("expires_utc") ? ` AND (
|
|
513
|
+
const expiresExpr = columns.columns.has("expires_utc") ? chromeExpiryMillisExpr() : "0";
|
|
514
|
+
const expiryFilter = filterExpired && columns.columns.has("expires_utc") ? ` AND (${expiresExpr} = 0 OR ${expiresExpr} > ${chromeExpiryNowMillis()})` : "";
|
|
443
515
|
const partitionFilter = filterExpired ? unpartitionedCookieFilter(columns.columns) : "";
|
|
444
|
-
return runSqliteQuery(dbPath, `SELECT name, value, host_key, ${pathExpr}, ${expiresExpr}, hex(encrypted_value) AS encrypted_value_hex FROM cookies WHERE ${buildCookieWhere(hosts, names ?? undefined)}${expiryFilter}${partitionFilter} ORDER BY length(path) DESC,
|
|
516
|
+
return runSqliteQuery(dbPath, `SELECT name, value, host_key, ${pathExpr}, ${expiresExpr} AS expires_utc, hex(encrypted_value) AS encrypted_value_hex FROM cookies WHERE ${buildCookieWhere(hosts, names ?? undefined)}${expiryFilter}${partitionFilter} ORDER BY length(path) DESC, ${expiresExpr} ASC`);
|
|
445
517
|
}
|
|
446
518
|
|
|
447
519
|
async function readCookieColumns(dbPath: string): Promise<{ status: "success"; columns: Set<string> } | { status: "failure"; failure: SqliteFailure }> {
|
|
@@ -450,8 +522,12 @@ async function readCookieColumns(dbPath: string): Promise<{ status: "success"; c
|
|
|
450
522
|
return { status: "success", columns: new Set(result.rows.map(row => typeof row.name === "string" ? row.name : "")) };
|
|
451
523
|
}
|
|
452
524
|
|
|
453
|
-
function
|
|
454
|
-
return (
|
|
525
|
+
function chromeExpiryMillisExpr(): string {
|
|
526
|
+
return "CASE WHEN expires_utc = 0 THEN 0 ELSE CAST(expires_utc / 1000 AS INTEGER) + CASE WHEN expires_utc % 1000 = 0 THEN 0 ELSE 1 END END";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function chromeExpiryNowMillis(): number {
|
|
530
|
+
return Date.now() + 11644473600000;
|
|
455
531
|
}
|
|
456
532
|
|
|
457
533
|
function unpartitionedCookieFilter(columns: Set<string>): string {
|