@apmantza/greedysearch-pi 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +17 -1
- package/bin/launch.mjs +366 -288
- package/bin/search.mjs +148 -20
- package/extractors/common.mjs +291 -279
- package/extractors/gemini.mjs +146 -145
- package/extractors/google-ai.mjs +125 -124
- package/extractors/perplexity.mjs +145 -141
- package/extractors/selectors.mjs +54 -52
- package/index.ts +179 -35
- package/package.json +53 -46
- package/src/github.mjs +237 -237
- package/src/search/chrome.mjs +222 -222
- package/src/search/constants.mjs +37 -37
- package/src/search/defaults.mjs +14 -14
- package/src/search/engines.mjs +6 -2
- package/src/search/fetch-source.mjs +229 -229
- package/src/search/output.mjs +58 -58
- package/src/search/sources.mjs +445 -445
- package/src/search/synthesis-runner.mjs +63 -63
- package/src/search/synthesis.mjs +51 -40
- package/src/tools/deep-research-handler.ts +36 -36
- package/src/tools/greedy-search-handler.ts +57 -57
- package/src/tools/shared.ts +130 -130
- package/src/types.ts +103 -103
- package/test.mjs +377 -0
|
@@ -1,141 +1,145 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// extractors/perplexity.mjs
|
|
4
|
-
// Navigate Perplexity, wait for streaming to complete, return clean answer + sources.
|
|
5
|
-
//
|
|
6
|
-
// Usage:
|
|
7
|
-
// node extractors/perplexity.mjs "<query>" [--tab <prefix>]
|
|
8
|
-
//
|
|
9
|
-
// Output (stdout): JSON { answer, sources, query, url }
|
|
10
|
-
// Errors go to stderr only — stdout is always clean JSON for piping.
|
|
11
|
-
//
|
|
12
|
-
// TODO: Refactor - this file has 42 lines duplicated with google-ai.mjs (line 28)
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
cdp,
|
|
16
|
-
formatAnswer,
|
|
17
|
-
getOrOpenTab,
|
|
18
|
-
handleError,
|
|
19
|
-
injectClipboardInterceptor,
|
|
20
|
-
outputJson,
|
|
21
|
-
parseArgs,
|
|
22
|
-
parseSourcesFromMarkdown,
|
|
23
|
-
validateQuery,
|
|
24
|
-
waitForStreamComplete,
|
|
25
|
-
} from "./common.mjs";
|
|
26
|
-
import { dismissConsent } from "./consent.mjs";
|
|
27
|
-
import { SELECTORS } from "./selectors.mjs";
|
|
28
|
-
|
|
29
|
-
const S = SELECTORS.perplexity;
|
|
30
|
-
const GLOBAL_VAR = "__pplxClipboard";
|
|
31
|
-
|
|
32
|
-
// ============================================================================
|
|
33
|
-
//
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await new Promise((r) => setTimeout(r,
|
|
103
|
-
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await cdp([
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// extractors/perplexity.mjs
|
|
4
|
+
// Navigate Perplexity, wait for streaming to complete, return clean answer + sources.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node extractors/perplexity.mjs "<query>" [--tab <prefix>]
|
|
8
|
+
//
|
|
9
|
+
// Output (stdout): JSON { answer, sources, query, url }
|
|
10
|
+
// Errors go to stderr only — stdout is always clean JSON for piping.
|
|
11
|
+
//
|
|
12
|
+
// TODO: Refactor - this file has 42 lines duplicated with google-ai.mjs (line 28)
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
cdp,
|
|
16
|
+
formatAnswer,
|
|
17
|
+
getOrOpenTab,
|
|
18
|
+
handleError,
|
|
19
|
+
injectClipboardInterceptor,
|
|
20
|
+
outputJson,
|
|
21
|
+
parseArgs,
|
|
22
|
+
parseSourcesFromMarkdown,
|
|
23
|
+
validateQuery,
|
|
24
|
+
waitForStreamComplete,
|
|
25
|
+
} from "./common.mjs";
|
|
26
|
+
import { dismissConsent } from "./consent.mjs";
|
|
27
|
+
import { SELECTORS } from "./selectors.mjs";
|
|
28
|
+
|
|
29
|
+
const S = SELECTORS.perplexity;
|
|
30
|
+
const GLOBAL_VAR = "__pplxClipboard";
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Language-agnostic copy button finder
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
function findCopyButtonJsExpression() {
|
|
37
|
+
// Perplexity uses SVG icons via <use xlink:href="#pplx-icon-copy">
|
|
38
|
+
// This works across all locales since it doesn't depend on aria-label text
|
|
39
|
+
return `Array.from(document.querySelectorAll('button')).find(b => b.innerHTML.includes('#pplx-icon-copy'))`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Extraction
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
async function extractAnswer(tab) {
|
|
47
|
+
const copyBtnExpr = findCopyButtonJsExpression();
|
|
48
|
+
|
|
49
|
+
await cdp(["eval", tab, `${copyBtnExpr}?.click()`]);
|
|
50
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
51
|
+
|
|
52
|
+
let answer = await cdp(["eval", tab, `window.${GLOBAL_VAR} || ''`]);
|
|
53
|
+
|
|
54
|
+
// Retry once if clipboard is empty (Perplexity might be slow to write)
|
|
55
|
+
if (!answer) {
|
|
56
|
+
console.error("[perplexity] Clipboard empty, retrying in 2s...");
|
|
57
|
+
await cdp(["eval", tab, `${copyBtnExpr}?.click()`]);
|
|
58
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
59
|
+
answer = await cdp(["eval", tab, `window.${GLOBAL_VAR} || ''`]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!answer) throw new Error("Clipboard interceptor returned empty text");
|
|
63
|
+
|
|
64
|
+
const sources = parseSourcesFromMarkdown(answer);
|
|
65
|
+
return { answer: answer.trim(), sources };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Main
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
const USAGE =
|
|
73
|
+
'Usage: node extractors/perplexity.mjs "<query>" [--tab <prefix>]\n';
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
const args = process.argv.slice(2);
|
|
77
|
+
validateQuery(args, USAGE);
|
|
78
|
+
|
|
79
|
+
const { query, tabPrefix, short } = parseArgs(args);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Refresh page list so cache is current
|
|
83
|
+
await cdp(["list"]);
|
|
84
|
+
|
|
85
|
+
const tab = await getOrOpenTab(tabPrefix);
|
|
86
|
+
|
|
87
|
+
// Navigate to homepage and use the search box (direct ?q= URLs trigger bot redirect)
|
|
88
|
+
await cdp(["nav", tab, "https://www.perplexity.ai/"], 35000);
|
|
89
|
+
await dismissConsent(tab, cdp);
|
|
90
|
+
|
|
91
|
+
// Wait for React app to mount input (up to 8s)
|
|
92
|
+
const deadline = Date.now() + 8000;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const found = await cdp([
|
|
95
|
+
"eval",
|
|
96
|
+
tab,
|
|
97
|
+
`!!document.querySelector('${S.input}')`,
|
|
98
|
+
]).catch(() => "false");
|
|
99
|
+
if (found === "true") break;
|
|
100
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
101
|
+
}
|
|
102
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
103
|
+
|
|
104
|
+
await injectClipboardInterceptor(tab, GLOBAL_VAR);
|
|
105
|
+
await cdp(["click", tab, S.input]);
|
|
106
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
107
|
+
await cdp(["type", tab, query]);
|
|
108
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
109
|
+
|
|
110
|
+
// Submit with Enter (most reliable across Chrome instances)
|
|
111
|
+
await cdp([
|
|
112
|
+
"eval",
|
|
113
|
+
tab,
|
|
114
|
+
`document.querySelector('${S.input}')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,keyCode:13})), 'ok'`,
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
await waitForStreamComplete(tab, {
|
|
118
|
+
timeout: 30000,
|
|
119
|
+
interval: 600,
|
|
120
|
+
stableRounds: 3,
|
|
121
|
+
selector: "document.body",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const { answer, sources } = await extractAnswer(tab);
|
|
125
|
+
|
|
126
|
+
if (!answer)
|
|
127
|
+
throw new Error(
|
|
128
|
+
"No answer extracted — Perplexity may not have responded",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const finalUrl = await cdp(["eval", tab, "document.location.href"]).catch(
|
|
132
|
+
() => "",
|
|
133
|
+
);
|
|
134
|
+
outputJson({
|
|
135
|
+
query,
|
|
136
|
+
url: finalUrl,
|
|
137
|
+
answer: formatAnswer(answer, short),
|
|
138
|
+
sources,
|
|
139
|
+
});
|
|
140
|
+
} catch (e) {
|
|
141
|
+
handleError(e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main();
|
package/extractors/selectors.mjs
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
// extractors/selectors.mjs
|
|
2
|
-
// Centralized CSS selectors for all engines.
|
|
3
|
-
// Update selectors here when a site changes its UI.
|
|
4
|
-
|
|
5
|
-
export const SELECTORS = {
|
|
6
|
-
// ──────────────────────────────────────────────
|
|
7
|
-
// Perplexity (perplexity.ai)
|
|
8
|
-
// ──────────────────────────────────────────────
|
|
9
|
-
perplexity: {
|
|
10
|
-
input: "#ask-input",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
// extractors/selectors.mjs
|
|
2
|
+
// Centralized CSS selectors for all engines.
|
|
3
|
+
// Update selectors here when a site changes its UI.
|
|
4
|
+
|
|
5
|
+
export const SELECTORS = {
|
|
6
|
+
// ──────────────────────────────────────────────
|
|
7
|
+
// Perplexity (perplexity.ai)
|
|
8
|
+
// ──────────────────────────────────────────────
|
|
9
|
+
perplexity: {
|
|
10
|
+
input: "#ask-input",
|
|
11
|
+
// Note: copy button found via JS in extractor (language-agnostic)
|
|
12
|
+
copyButton: null,
|
|
13
|
+
sourceItem: "[data-pplx-citation-url]",
|
|
14
|
+
sourceLink: "a",
|
|
15
|
+
consent: "#onetrust-accept-btn-handler",
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// ──────────────────────────────────────────────
|
|
19
|
+
// Bing Copilot (copilot.microsoft.com)
|
|
20
|
+
// ──────────────────────────────────────────────
|
|
21
|
+
bing: {
|
|
22
|
+
input: "#userInput",
|
|
23
|
+
copyButton: 'button[data-testid="copy-ai-message-button"]',
|
|
24
|
+
sourceLink: 'a[href^="http"][target="_blank"]',
|
|
25
|
+
sourceExclude: "copilot.microsoft.com",
|
|
26
|
+
consent: "#onetrust-accept-btn-handler",
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// ──────────────────────────────────────────────
|
|
30
|
+
// Google AI Mode (google.com/search?udm=50)
|
|
31
|
+
// ──────────────────────────────────────────────
|
|
32
|
+
google: {
|
|
33
|
+
answerContainer: ".pWvJNd",
|
|
34
|
+
sourceLink: 'a[href^="http"]',
|
|
35
|
+
sourceExclude: ["google.", "gstatic", "googleapis"],
|
|
36
|
+
sourceHeadingParent: "[data-snhf]",
|
|
37
|
+
consent: '#L2AGLb, button[jsname="b3VHJd"], .tHlp8d',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// ──────────────────────────────────────────────
|
|
41
|
+
// Gemini (gemini.google.com/app)
|
|
42
|
+
// ──────────────────────────────────────────────
|
|
43
|
+
gemini: {
|
|
44
|
+
input: "rich-textarea .ql-editor",
|
|
45
|
+
// Language-agnostic: use Material icon data attributes (work across locales)
|
|
46
|
+
copyButton: 'button:has(mat-icon[data-mat-icon-name="content_copy"])',
|
|
47
|
+
sendButton: 'button:has(mat-icon[data-mat-icon-name="send"]), .send-button',
|
|
48
|
+
sourcesSidebarButton: "button.legacy-sources-sidebar-button",
|
|
49
|
+
sourcesExclude: ["gemini.google", "gstatic", "google.com/search"],
|
|
50
|
+
citationButtonPattern: 'button[aria-label*="citation from"]',
|
|
51
|
+
// For parsing citation aria-labels: "View source details for citation from {name}. Opens side panel."
|
|
52
|
+
citationNameRegex: /from\s+(.+?)\.\s/,
|
|
53
|
+
},
|
|
54
|
+
};
|
package/index.ts
CHANGED
|
@@ -15,10 +15,10 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
15
15
|
import { Type } from "@sinclair/typebox";
|
|
16
16
|
|
|
17
17
|
import { formatCodingTask } from "./src/formatters/coding.js";
|
|
18
|
-
import {
|
|
18
|
+
import { DEFAULTS } from "./src/search/defaults.mjs";
|
|
19
19
|
import { registerDeepResearchTool } from "./src/tools/deep-research-handler.js";
|
|
20
|
+
import { registerGreedySearchTool } from "./src/tools/greedy-search-handler.js";
|
|
20
21
|
import { cdpAvailable, type ProgressUpdate } from "./src/tools/shared.js";
|
|
21
|
-
import { DEFAULTS } from "./src/search/defaults.js";
|
|
22
22
|
|
|
23
23
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
|
|
@@ -62,22 +62,43 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
62
62
|
},
|
|
63
63
|
),
|
|
64
64
|
mode: Type.Union(
|
|
65
|
-
[
|
|
65
|
+
[
|
|
66
|
+
Type.Literal("code"),
|
|
67
|
+
Type.Literal("review"),
|
|
68
|
+
Type.Literal("plan"),
|
|
69
|
+
Type.Literal("test"),
|
|
70
|
+
Type.Literal("debug"),
|
|
71
|
+
],
|
|
66
72
|
{
|
|
67
|
-
description:
|
|
73
|
+
description:
|
|
74
|
+
"Task mode: code (default), review (code review), plan (architect review), test (write tests), debug (root cause analysis)",
|
|
68
75
|
default: "code",
|
|
69
76
|
},
|
|
70
77
|
),
|
|
71
|
-
context: Type.Optional(
|
|
78
|
+
context: Type.Optional(
|
|
79
|
+
Type.String({
|
|
80
|
+
description: "Optional code context/snippet to include with the task",
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
72
83
|
}),
|
|
73
84
|
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
74
|
-
const {
|
|
75
|
-
task
|
|
85
|
+
const {
|
|
86
|
+
task,
|
|
87
|
+
engine = "gemini",
|
|
88
|
+
mode = "code",
|
|
89
|
+
context,
|
|
90
|
+
} = params as {
|
|
91
|
+
task: string;
|
|
92
|
+
engine: string;
|
|
93
|
+
mode: string;
|
|
94
|
+
context?: string;
|
|
76
95
|
};
|
|
77
96
|
|
|
78
97
|
if (!cdpAvailable(__dir)) {
|
|
79
98
|
return {
|
|
80
|
-
content: [
|
|
99
|
+
content: [
|
|
100
|
+
{ type: "text", text: "cdp.mjs missing — try reinstalling." },
|
|
101
|
+
],
|
|
81
102
|
details: {} as { raw?: Record<string, unknown> },
|
|
82
103
|
};
|
|
83
104
|
}
|
|
@@ -87,35 +108,67 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
87
108
|
|
|
88
109
|
try {
|
|
89
110
|
onUpdate?.({
|
|
90
|
-
content: [
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `**Coding task...** 🔄 ${engine === "all" ? "Gemini + Copilot" : engine} (${mode} mode)`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
91
117
|
details: { _progress: true },
|
|
92
118
|
} satisfies ProgressUpdate);
|
|
93
119
|
|
|
94
|
-
const data = await new Promise<Record<string, unknown>>(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const data = await new Promise<Record<string, unknown>>(
|
|
121
|
+
(resolve, reject) => {
|
|
122
|
+
const proc = spawn(
|
|
123
|
+
"node",
|
|
124
|
+
[join(__dir, "bin", "coding-task.mjs"), task, ...flags],
|
|
125
|
+
{
|
|
126
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
let out = "";
|
|
130
|
+
let err = "";
|
|
131
|
+
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
proc.kill("SIGTERM");
|
|
134
|
+
reject(new Error("Aborted"));
|
|
135
|
+
};
|
|
136
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
|
|
138
|
+
proc.stdout.on("data", (d: Buffer) => (out += d));
|
|
139
|
+
proc.stderr.on("data", (d: Buffer) => (err += d));
|
|
140
|
+
proc.on("close", (code: number) => {
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
if (code !== 0) {
|
|
143
|
+
reject(
|
|
144
|
+
new Error(
|
|
145
|
+
err.trim() || `coding-task.mjs exited with code ${code}`,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
try {
|
|
150
|
+
resolve(JSON.parse(out.trim()));
|
|
151
|
+
} catch {
|
|
152
|
+
reject(
|
|
153
|
+
new Error(
|
|
154
|
+
`Invalid JSON from coding-task.mjs: ${out.slice(0, 200)}`,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Timeout after 3 minutes
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
proc.kill("SIGTERM");
|
|
164
|
+
reject(
|
|
165
|
+
new Error(
|
|
166
|
+
`Coding task timed out after ${DEFAULTS.CODING_TASK_TIMEOUT / 1000}s`,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}, DEFAULTS.CODING_TASK_TIMEOUT);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
119
172
|
|
|
120
173
|
const text = formatCodingTask(data);
|
|
121
174
|
return {
|
|
@@ -131,4 +184,95 @@ export default function greedySearchExtension(pi: ExtensionAPI) {
|
|
|
131
184
|
}
|
|
132
185
|
},
|
|
133
186
|
});
|
|
134
|
-
|
|
187
|
+
|
|
188
|
+
// ─── /set-greedy-locale command ───────────────────────────────────────────
|
|
189
|
+
pi.registerCommand("set-greedy-locale", {
|
|
190
|
+
description:
|
|
191
|
+
"Set default locale for GreedySearch results (e.g., /set-greedy-locale de, /set-greedy-locale --clear, /set-greedy-locale --show)",
|
|
192
|
+
handler: async (args, ctx) => {
|
|
193
|
+
const arg = args.trim() || "--show";
|
|
194
|
+
|
|
195
|
+
if (arg === "--show") {
|
|
196
|
+
const config = loadUserConfig();
|
|
197
|
+
if (config.locale) {
|
|
198
|
+
ctx.ui.notify(`Default locale: ${config.locale}`, "info");
|
|
199
|
+
} else {
|
|
200
|
+
ctx.ui.notify("No default locale (uses: en)", "info");
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (arg === "--clear") {
|
|
206
|
+
const config = loadUserConfig();
|
|
207
|
+
delete config.locale;
|
|
208
|
+
saveUserConfig(config);
|
|
209
|
+
ctx.ui.notify("Default locale cleared (now uses: en).", "info");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Set locale
|
|
214
|
+
const locale = arg.toLowerCase();
|
|
215
|
+
const VALID_LOCALES = [
|
|
216
|
+
"en",
|
|
217
|
+
"de",
|
|
218
|
+
"fr",
|
|
219
|
+
"es",
|
|
220
|
+
"it",
|
|
221
|
+
"pt",
|
|
222
|
+
"nl",
|
|
223
|
+
"pl",
|
|
224
|
+
"ru",
|
|
225
|
+
"ja",
|
|
226
|
+
"ko",
|
|
227
|
+
"zh",
|
|
228
|
+
"ar",
|
|
229
|
+
"hi",
|
|
230
|
+
"tr",
|
|
231
|
+
"sv",
|
|
232
|
+
"da",
|
|
233
|
+
"no",
|
|
234
|
+
"fi",
|
|
235
|
+
"cs",
|
|
236
|
+
"hu",
|
|
237
|
+
"ro",
|
|
238
|
+
"el",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
if (!VALID_LOCALES.includes(locale)) {
|
|
242
|
+
ctx.ui.notify(
|
|
243
|
+
`Invalid locale "${locale}". Valid: ${VALID_LOCALES.join(", ")}`,
|
|
244
|
+
"error",
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const config = loadUserConfig();
|
|
250
|
+
config.locale = locale;
|
|
251
|
+
saveUserConfig(config);
|
|
252
|
+
ctx.ui.notify(`Default locale set to: ${locale}`, "info");
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
258
|
+
// Config helpers for /set-greedy-locale command
|
|
259
|
+
import { homedir } from "node:os";
|
|
260
|
+
|
|
261
|
+
const USER_CONFIG_DIR = join(homedir(), ".config", "greedysearch");
|
|
262
|
+
const USER_CONFIG_FILE = join(USER_CONFIG_DIR, "config.json");
|
|
263
|
+
|
|
264
|
+
function loadUserConfig(): Record<string, string> {
|
|
265
|
+
try {
|
|
266
|
+
if (existsSync(USER_CONFIG_FILE)) {
|
|
267
|
+
return JSON.parse(readFileSync(USER_CONFIG_FILE, "utf8"));
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Ignore parse errors
|
|
271
|
+
}
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function saveUserConfig(config: Record<string, string>): void {
|
|
276
|
+
mkdirSync(USER_CONFIG_DIR, { recursive: true });
|
|
277
|
+
writeFileSync(USER_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
278
|
+
}
|