@tuan_son.dinh/gsd 2.6.0
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/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +269 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +70 -0
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +418 -0
- package/dist/pi-migration.d.ts +14 -0
- package/dist/pi-migration.js +57 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +60 -0
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.js +25 -0
- package/package.json +60 -0
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +127 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +249 -0
- package/src/resources/extensions/bg-shell/index.ts +2808 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4989 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/google-search/index.ts +323 -0
- package/src/resources/extensions/google-search/package.json +9 -0
- package/src/resources/extensions/gsd/activity-log.ts +69 -0
- package/src/resources/extensions/gsd/auto.ts +2744 -0
- package/src/resources/extensions/gsd/commands.ts +313 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
- package/src/resources/extensions/gsd/doctor.ts +690 -0
- package/src/resources/extensions/gsd/files.ts +732 -0
- package/src/resources/extensions/gsd/git-service.ts +597 -0
- package/src/resources/extensions/gsd/gitignore.ts +168 -0
- package/src/resources/extensions/gsd/guided-flow.ts +817 -0
- package/src/resources/extensions/gsd/index.ts +558 -0
- package/src/resources/extensions/gsd/metrics.ts +374 -0
- package/src/resources/extensions/gsd/migrate/command.ts +218 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +757 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +187 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +460 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +131 -0
- package/src/resources/extensions/gsd/templates/preferences.md +24 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
- package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
- package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
- package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree-command.ts +845 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
- package/src/resources/extensions/gsd/worktree.ts +183 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/mcporter/index.ts +429 -0
- package/src/resources/extensions/remote-questions/config.ts +81 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/src/resources/extensions/remote-questions/format.ts +163 -0
- package/src/resources/extensions/remote-questions/manager.ts +192 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
- package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/src/resources/extensions/remote-questions/status.ts +31 -0
- package/src/resources/extensions/remote-questions/store.ts +77 -0
- package/src/resources/extensions/remote-questions/types.ts +75 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +65 -0
- package/src/resources/extensions/search-the-web/native-search.ts +157 -0
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/terminal.ts +23 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1020 -0
- package/src/resources/extensions/voice/index.ts +195 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get-secrets-from-user — paged secure env var collection + apply
|
|
3
|
+
*
|
|
4
|
+
* Collects secrets one-per-page via masked TUI input, then writes them
|
|
5
|
+
* to .env (local), Vercel, or Convex. No ctx.callTool, no external deps.
|
|
6
|
+
* Uses Node fs/promises for file I/O and pi.exec() for CLI sinks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { CURSOR_MARKER, Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
import { Type } from "@sinclair/typebox";
|
|
15
|
+
|
|
16
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface CollectedSecret {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string | null; // null = skipped
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ToolResultDetails {
|
|
24
|
+
destination: string;
|
|
25
|
+
environment?: string;
|
|
26
|
+
applied: string[];
|
|
27
|
+
skipped: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function maskPreview(value: string): string {
|
|
33
|
+
if (!value) return "";
|
|
34
|
+
if (value.length <= 8) return "*".repeat(value.length);
|
|
35
|
+
return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
|
|
40
|
+
*/
|
|
41
|
+
function maskEditorLine(line: string): string {
|
|
42
|
+
// Keep border / metadata lines readable.
|
|
43
|
+
if (line.startsWith("─")) {
|
|
44
|
+
return line;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let output = "";
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < line.length) {
|
|
50
|
+
if (line.startsWith(CURSOR_MARKER, i)) {
|
|
51
|
+
output += CURSOR_MARKER;
|
|
52
|
+
i += CURSOR_MARKER.length;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
57
|
+
if (ansiMatch) {
|
|
58
|
+
output += ansiMatch[0];
|
|
59
|
+
i += ansiMatch[0].length;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ch = line[i] as string;
|
|
64
|
+
output += ch === " " ? " " : "*";
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function shellEscapeSingle(value: string): string {
|
|
72
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeEnvKey(filePath: string, key: string, value: string): Promise<void> {
|
|
76
|
+
let content = "";
|
|
77
|
+
try {
|
|
78
|
+
content = await readFile(filePath, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
content = "";
|
|
81
|
+
}
|
|
82
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
83
|
+
const line = `${key}=${escaped}`;
|
|
84
|
+
const regex = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=.*$`, "m");
|
|
85
|
+
if (regex.test(content)) {
|
|
86
|
+
content = content.replace(regex, line);
|
|
87
|
+
} else {
|
|
88
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
89
|
+
content += `${line}\n`;
|
|
90
|
+
}
|
|
91
|
+
await writeFile(filePath, content, "utf8");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Paged secure input UI ────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show a single-key masked input page via ctx.ui.custom().
|
|
98
|
+
* Returns the entered value, or null if skipped/cancelled.
|
|
99
|
+
*/
|
|
100
|
+
async function collectOneSecret(
|
|
101
|
+
ctx: { ui: any; hasUI: boolean },
|
|
102
|
+
pageIndex: number,
|
|
103
|
+
totalPages: number,
|
|
104
|
+
keyName: string,
|
|
105
|
+
hint: string | undefined,
|
|
106
|
+
): Promise<string | null> {
|
|
107
|
+
if (!ctx.hasUI) return null;
|
|
108
|
+
|
|
109
|
+
return ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: (r: string | null) => void) => {
|
|
110
|
+
let value = "";
|
|
111
|
+
let cachedLines: string[] | undefined;
|
|
112
|
+
|
|
113
|
+
const editorTheme: EditorTheme = {
|
|
114
|
+
borderColor: (s: string) => theme.fg("accent", s),
|
|
115
|
+
selectList: {
|
|
116
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
117
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
118
|
+
description: (t: string) => theme.fg("muted", t),
|
|
119
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
120
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const editor = new Editor(tui, editorTheme, { paddingX: 1 });
|
|
124
|
+
|
|
125
|
+
function refresh() {
|
|
126
|
+
cachedLines = undefined;
|
|
127
|
+
tui.requestRender();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function handleInput(data: string) {
|
|
131
|
+
if (matchesKey(data, Key.enter)) {
|
|
132
|
+
value = editor.getText().trim();
|
|
133
|
+
done(value.length > 0 ? value : null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (matchesKey(data, Key.escape)) {
|
|
137
|
+
done(null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// ctrl+s = skip this key
|
|
141
|
+
if (data === "\x13") {
|
|
142
|
+
done(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
editor.handleInput(data);
|
|
146
|
+
refresh();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function render(width: number): string[] {
|
|
150
|
+
if (cachedLines) return cachedLines;
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
153
|
+
|
|
154
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
155
|
+
add(theme.fg("dim", ` Page ${pageIndex + 1}/${totalPages} · Secure Env Setup`));
|
|
156
|
+
lines.push("");
|
|
157
|
+
|
|
158
|
+
// Key name as big header
|
|
159
|
+
add(theme.fg("accent", theme.bold(` ${keyName}`)));
|
|
160
|
+
if (hint) {
|
|
161
|
+
add(theme.fg("muted", ` ${hint}`));
|
|
162
|
+
}
|
|
163
|
+
lines.push("");
|
|
164
|
+
|
|
165
|
+
// Masked preview
|
|
166
|
+
const raw = editor.getText();
|
|
167
|
+
const preview = raw.length > 0 ? maskPreview(raw) : theme.fg("dim", "(empty — press enter to skip)");
|
|
168
|
+
add(theme.fg("text", ` Preview: ${preview}`));
|
|
169
|
+
lines.push("");
|
|
170
|
+
|
|
171
|
+
// Editor
|
|
172
|
+
add(theme.fg("muted", " Enter value:"));
|
|
173
|
+
for (const line of editor.render(width - 2)) {
|
|
174
|
+
add(theme.fg("text", maskEditorLine(line)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lines.push("");
|
|
178
|
+
add(theme.fg("dim", ` enter to confirm | ctrl+s or esc to skip | esc cancels`));
|
|
179
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
180
|
+
|
|
181
|
+
cachedLines = lines;
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
render,
|
|
187
|
+
invalidate: () => { cachedLines = undefined; },
|
|
188
|
+
handleInput,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Extension ────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
export default function secureEnv(pi: ExtensionAPI) {
|
|
196
|
+
pi.registerTool({
|
|
197
|
+
name: "secure_env_collect",
|
|
198
|
+
label: "Secure Env Collect",
|
|
199
|
+
description:
|
|
200
|
+
"Collect one or more env vars through a paged masked-input UI, then write them to .env, Vercel, or Convex. " +
|
|
201
|
+
"Values are shown masked to the user (e.g. sk-ir***dgdh) and never echoed in tool output.",
|
|
202
|
+
promptSnippet: "Collect and apply env vars securely without asking user to edit files manually.",
|
|
203
|
+
promptGuidelines: [
|
|
204
|
+
"NEVER ask the user to manually edit .env files, copy-paste into a terminal, or open a dashboard to set env vars. Always use secure_env_collect instead.",
|
|
205
|
+
"When a command fails due to a missing env var (e.g. 'OPENAI_API_KEY is not set', 'Missing required environment variable', 'Invalid API key', 'authentication required'), immediately call secure_env_collect with the missing keys before retrying.",
|
|
206
|
+
"When starting a new project or running setup steps that require secrets (API keys, tokens, database URLs), proactively call secure_env_collect before the first command that needs them.",
|
|
207
|
+
"Detect the right destination: use 'dotenv' for local dev, 'vercel' when deploying to Vercel, 'convex' when using Convex backend.",
|
|
208
|
+
"After secure_env_collect completes, re-run the originally blocked command to verify the fix worked.",
|
|
209
|
+
"Never echo, log, or repeat secret values in your responses. Only report key names and applied/skipped status.",
|
|
210
|
+
],
|
|
211
|
+
parameters: Type.Object({
|
|
212
|
+
destination: Type.Union([
|
|
213
|
+
Type.Literal("dotenv"),
|
|
214
|
+
Type.Literal("vercel"),
|
|
215
|
+
Type.Literal("convex"),
|
|
216
|
+
], { description: "Where to write the collected secrets" }),
|
|
217
|
+
keys: Type.Array(
|
|
218
|
+
Type.Object({
|
|
219
|
+
key: Type.String({ description: "Env var name, e.g. OPENAI_API_KEY" }),
|
|
220
|
+
hint: Type.Optional(Type.String({ description: "Format hint shown to user, e.g. 'starts with sk-'" })),
|
|
221
|
+
required: Type.Optional(Type.Boolean()),
|
|
222
|
+
}),
|
|
223
|
+
{ minItems: 1 },
|
|
224
|
+
),
|
|
225
|
+
envFilePath: Type.Optional(Type.String({ description: "Path to .env file (dotenv only). Defaults to .env in cwd." })),
|
|
226
|
+
environment: Type.Optional(
|
|
227
|
+
Type.Union([
|
|
228
|
+
Type.Literal("development"),
|
|
229
|
+
Type.Literal("preview"),
|
|
230
|
+
Type.Literal("production"),
|
|
231
|
+
], { description: "Target environment (vercel only)" }),
|
|
232
|
+
),
|
|
233
|
+
}),
|
|
234
|
+
|
|
235
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
236
|
+
if (!ctx.hasUI) {
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: "Error: UI not available (interactive mode required for secure env collection)." }],
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const collected: CollectedSecret[] = [];
|
|
244
|
+
|
|
245
|
+
// Collect one key per page
|
|
246
|
+
for (let i = 0; i < params.keys.length; i++) {
|
|
247
|
+
const item = params.keys[i];
|
|
248
|
+
const value = await collectOneSecret(ctx, i, params.keys.length, item.key, item.hint);
|
|
249
|
+
collected.push({ key: item.key, value });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const provided = collected.filter((c) => c.value !== null) as Array<{ key: string; value: string }>;
|
|
253
|
+
const skipped = collected.filter((c) => c.value === null).map((c) => c.key);
|
|
254
|
+
const applied: string[] = [];
|
|
255
|
+
const errors: string[] = [];
|
|
256
|
+
|
|
257
|
+
// Apply to destination
|
|
258
|
+
if (params.destination === "dotenv") {
|
|
259
|
+
const filePath = resolve(ctx.cwd, params.envFilePath ?? ".env");
|
|
260
|
+
for (const { key, value } of provided) {
|
|
261
|
+
try {
|
|
262
|
+
await writeEnvKey(filePath, key, value);
|
|
263
|
+
applied.push(key);
|
|
264
|
+
} catch (err: any) {
|
|
265
|
+
errors.push(`${key}: ${err.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (params.destination === "vercel") {
|
|
271
|
+
const env = params.environment ?? "development";
|
|
272
|
+
for (const { key, value } of provided) {
|
|
273
|
+
try {
|
|
274
|
+
const result = await pi.exec("sh", [
|
|
275
|
+
"-c",
|
|
276
|
+
`printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`,
|
|
277
|
+
]);
|
|
278
|
+
if (result.code !== 0) {
|
|
279
|
+
errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
|
|
280
|
+
} else {
|
|
281
|
+
applied.push(key);
|
|
282
|
+
}
|
|
283
|
+
} catch (err: any) {
|
|
284
|
+
errors.push(`${key}: ${err.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (params.destination === "convex") {
|
|
290
|
+
for (const { key, value } of provided) {
|
|
291
|
+
try {
|
|
292
|
+
const result = await pi.exec("sh", [
|
|
293
|
+
"-c",
|
|
294
|
+
`npx convex env set ${key} ${shellEscapeSingle(value)}`,
|
|
295
|
+
]);
|
|
296
|
+
if (result.code !== 0) {
|
|
297
|
+
errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
|
|
298
|
+
} else {
|
|
299
|
+
applied.push(key);
|
|
300
|
+
}
|
|
301
|
+
} catch (err: any) {
|
|
302
|
+
errors.push(`${key}: ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const details: ToolResultDetails = {
|
|
308
|
+
destination: params.destination,
|
|
309
|
+
environment: params.environment,
|
|
310
|
+
applied,
|
|
311
|
+
skipped,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const lines = [
|
|
315
|
+
`destination: ${params.destination}${params.environment ? ` (${params.environment})` : ""}`,
|
|
316
|
+
...applied.map((k) => `✓ ${k}: applied`),
|
|
317
|
+
...skipped.map((k) => `• ${k}: skipped`),
|
|
318
|
+
...errors.map((e) => `✗ ${e}`),
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
323
|
+
details,
|
|
324
|
+
isError: errors.length > 0 && applied.length === 0,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
renderCall(args, theme) {
|
|
329
|
+
const count = Array.isArray(args.keys) ? args.keys.length : 0;
|
|
330
|
+
return new Text(
|
|
331
|
+
theme.fg("toolTitle", theme.bold("secure_env_collect ")) +
|
|
332
|
+
theme.fg("muted", `→ ${args.destination}`) +
|
|
333
|
+
theme.fg("dim", ` ${count} key${count !== 1 ? "s" : ""}`),
|
|
334
|
+
0, 0,
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
renderResult(result, _options, theme) {
|
|
339
|
+
const details = result.details as ToolResultDetails | undefined;
|
|
340
|
+
if (!details) {
|
|
341
|
+
const t = result.content[0];
|
|
342
|
+
return new Text(t?.type === "text" ? t.text : "", 0, 0);
|
|
343
|
+
}
|
|
344
|
+
const lines = [
|
|
345
|
+
`${theme.fg("success", "✓")} ${details.destination}${details.environment ? ` (${details.environment})` : ""}`,
|
|
346
|
+
...details.applied.map((k) => ` ${theme.fg("success", "✓")} ${k}: applied`),
|
|
347
|
+
...details.skipped.map((k) => ` ${theme.fg("warning", "•")} ${k}: skipped`),
|
|
348
|
+
];
|
|
349
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Search Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides a `google_search` tool that performs web searches via Gemini's
|
|
5
|
+
* Google Search grounding feature. Uses the user's existing GEMINI_API_KEY
|
|
6
|
+
* and Google Cloud GenAI credits.
|
|
7
|
+
*
|
|
8
|
+
* The tool sends queries to Gemini Flash with `googleSearch: {}` enabled.
|
|
9
|
+
* Gemini internally performs Google searches, synthesizes an answer, and
|
|
10
|
+
* returns it with source URLs from grounding metadata.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_MAX_BYTES,
|
|
16
|
+
DEFAULT_MAX_LINES,
|
|
17
|
+
formatSize,
|
|
18
|
+
truncateHead,
|
|
19
|
+
} from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
21
|
+
import { Type } from "@sinclair/typebox";
|
|
22
|
+
import { GoogleGenAI } from "@google/genai";
|
|
23
|
+
|
|
24
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface SearchSource {
|
|
27
|
+
title: string;
|
|
28
|
+
uri: string;
|
|
29
|
+
domain: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SearchResult {
|
|
33
|
+
answer: string;
|
|
34
|
+
sources: SearchSource[];
|
|
35
|
+
searchQueries: string[];
|
|
36
|
+
cached: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SearchDetails {
|
|
40
|
+
query: string;
|
|
41
|
+
sourceCount: number;
|
|
42
|
+
cached: boolean;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Lazy singleton client ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let client: GoogleGenAI | null = null;
|
|
50
|
+
|
|
51
|
+
function getClient(): GoogleGenAI {
|
|
52
|
+
if (!client) {
|
|
53
|
+
client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
|
|
54
|
+
}
|
|
55
|
+
return client;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── In-session cache ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const resultCache = new Map<string, SearchResult>();
|
|
61
|
+
|
|
62
|
+
function cacheKey(query: string): string {
|
|
63
|
+
return query.toLowerCase().trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Extension ────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export default function (pi: ExtensionAPI) {
|
|
69
|
+
pi.registerTool({
|
|
70
|
+
name: "google_search",
|
|
71
|
+
label: "Google Search",
|
|
72
|
+
description:
|
|
73
|
+
"Search the web using Google Search via Gemini. " +
|
|
74
|
+
"Returns an AI-synthesized answer grounded in Google Search results, plus source URLs. " +
|
|
75
|
+
"Use this when you need current information from the web: recent events, documentation, " +
|
|
76
|
+
"product details, technical references, news, etc. " +
|
|
77
|
+
"Requires GEMINI_API_KEY. Alternative to Brave-based search tools for users with Google Cloud credits.",
|
|
78
|
+
promptSnippet: "Search the web via Google Search to get current information with sources",
|
|
79
|
+
promptGuidelines: [
|
|
80
|
+
"Use google_search when you need up-to-date web information that isn't in your training data.",
|
|
81
|
+
"Be specific with queries for better results, e.g. 'Next.js 15 app router migration guide' not just 'Next.js'.",
|
|
82
|
+
"The tool returns both an answer and source URLs. Cite sources when sharing results with the user.",
|
|
83
|
+
"Results are cached per-session, so repeated identical queries are free.",
|
|
84
|
+
"You can still use fetch_page to read a specific URL if needed after getting results from google_search.",
|
|
85
|
+
],
|
|
86
|
+
parameters: Type.Object({
|
|
87
|
+
query: Type.String({
|
|
88
|
+
description: "The search query, e.g. 'latest Node.js LTS version' or 'how to configure Tailwind v4'",
|
|
89
|
+
}),
|
|
90
|
+
maxSources: Type.Optional(
|
|
91
|
+
Type.Number({
|
|
92
|
+
description: "Maximum number of source URLs to include (default 5, max 10).",
|
|
93
|
+
minimum: 1,
|
|
94
|
+
maximum: 10,
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
const maxSources = Math.min(Math.max(params.maxSources ?? 5, 1), 10);
|
|
102
|
+
|
|
103
|
+
// Check for API key
|
|
104
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: "Error: GEMINI_API_KEY is not set. Please set this environment variable to use Google Search.\n\nExample: export GEMINI_API_KEY=your_key",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
isError: true,
|
|
113
|
+
details: {
|
|
114
|
+
query: params.query,
|
|
115
|
+
sourceCount: 0,
|
|
116
|
+
cached: false,
|
|
117
|
+
durationMs: Date.now() - startTime,
|
|
118
|
+
error: "auth_error: GEMINI_API_KEY not set",
|
|
119
|
+
} as SearchDetails,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check cache
|
|
124
|
+
const key = cacheKey(params.query);
|
|
125
|
+
if (resultCache.has(key)) {
|
|
126
|
+
const cached = resultCache.get(key)!;
|
|
127
|
+
const output = formatOutput(cached, maxSources);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: output }],
|
|
130
|
+
details: {
|
|
131
|
+
query: params.query,
|
|
132
|
+
sourceCount: cached.sources.length,
|
|
133
|
+
cached: true,
|
|
134
|
+
durationMs: Date.now() - startTime,
|
|
135
|
+
} as SearchDetails,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Call Gemini with Google Search grounding
|
|
140
|
+
let result: SearchResult;
|
|
141
|
+
try {
|
|
142
|
+
const ai = getClient();
|
|
143
|
+
const response = await ai.models.generateContent({
|
|
144
|
+
model: process.env.GEMINI_SEARCH_MODEL || "gemini-2.5-flash",
|
|
145
|
+
contents: params.query,
|
|
146
|
+
config: {
|
|
147
|
+
tools: [{ googleSearch: {} }],
|
|
148
|
+
abortSignal: signal,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Extract answer text
|
|
153
|
+
const answer = response.text ?? "";
|
|
154
|
+
|
|
155
|
+
// Extract grounding metadata
|
|
156
|
+
const candidate = response.candidates?.[0];
|
|
157
|
+
const grounding = candidate?.groundingMetadata;
|
|
158
|
+
|
|
159
|
+
// Parse sources from grounding chunks
|
|
160
|
+
const sources: SearchSource[] = [];
|
|
161
|
+
const seenTitles = new Set<string>();
|
|
162
|
+
if (grounding?.groundingChunks) {
|
|
163
|
+
for (const chunk of grounding.groundingChunks) {
|
|
164
|
+
if (chunk.web) {
|
|
165
|
+
const title = chunk.web.title ?? "Untitled";
|
|
166
|
+
// Dedupe by title since URIs are redirect URLs that differ per call
|
|
167
|
+
if (seenTitles.has(title)) continue;
|
|
168
|
+
seenTitles.add(title);
|
|
169
|
+
// domain field is not available via Gemini API, use title as fallback
|
|
170
|
+
// (title is typically the domain name, e.g. "wikipedia.org")
|
|
171
|
+
const domain = chunk.web.domain ?? title;
|
|
172
|
+
sources.push({
|
|
173
|
+
title,
|
|
174
|
+
uri: chunk.web.uri ?? "",
|
|
175
|
+
domain,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract search queries Gemini actually performed
|
|
182
|
+
const searchQueries = grounding?.webSearchQueries ?? [];
|
|
183
|
+
|
|
184
|
+
result = { answer, sources, searchQueries, cached: false };
|
|
185
|
+
} catch (err: unknown) {
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
|
|
188
|
+
let errorType = "api_error";
|
|
189
|
+
if (msg.includes("401") || msg.includes("UNAUTHENTICATED")) {
|
|
190
|
+
errorType = "auth_error";
|
|
191
|
+
} else if (msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED") || msg.includes("quota")) {
|
|
192
|
+
errorType = "rate_limit";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Google Search failed (${errorType}): ${msg}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
isError: true,
|
|
203
|
+
details: {
|
|
204
|
+
query: params.query,
|
|
205
|
+
sourceCount: 0,
|
|
206
|
+
cached: false,
|
|
207
|
+
durationMs: Date.now() - startTime,
|
|
208
|
+
error: `${errorType}: ${msg}`,
|
|
209
|
+
} as SearchDetails,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Cache the result
|
|
214
|
+
resultCache.set(key, result);
|
|
215
|
+
|
|
216
|
+
// Format and truncate output
|
|
217
|
+
const rawOutput = formatOutput(result, maxSources);
|
|
218
|
+
const truncation = truncateHead(rawOutput, {
|
|
219
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
220
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
let finalText = truncation.content;
|
|
224
|
+
if (truncation.truncated) {
|
|
225
|
+
finalText +=
|
|
226
|
+
`\n\n[Truncated: showing ${truncation.outputLines}/${truncation.totalLines} lines` +
|
|
227
|
+
` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: finalText }],
|
|
232
|
+
details: {
|
|
233
|
+
query: params.query,
|
|
234
|
+
sourceCount: result.sources.length,
|
|
235
|
+
cached: false,
|
|
236
|
+
durationMs: Date.now() - startTime,
|
|
237
|
+
} as SearchDetails,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
renderCall(args, theme) {
|
|
242
|
+
let text = theme.fg("toolTitle", theme.bold("google_search "));
|
|
243
|
+
text += theme.fg("accent", `"${args.query}"`);
|
|
244
|
+
return new Text(text, 0, 0);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
renderResult(result, { isPartial, expanded }, theme) {
|
|
248
|
+
const d = result.details as SearchDetails | undefined;
|
|
249
|
+
|
|
250
|
+
if (isPartial) return new Text(theme.fg("warning", "Searching Google..."), 0, 0);
|
|
251
|
+
if (result.isError || d?.error) {
|
|
252
|
+
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let text = theme.fg("success", `${d?.sourceCount ?? 0} sources`);
|
|
256
|
+
text += theme.fg("dim", ` (${d?.durationMs ?? 0}ms)`);
|
|
257
|
+
if (d?.cached) text += theme.fg("dim", " · cached");
|
|
258
|
+
|
|
259
|
+
if (expanded) {
|
|
260
|
+
const content = result.content[0];
|
|
261
|
+
if (content?.type === "text") {
|
|
262
|
+
const preview = content.text.split("\n").slice(0, 8).join("\n");
|
|
263
|
+
text += "\n\n" + theme.fg("dim", preview);
|
|
264
|
+
if (content.text.split("\n").length > 8) {
|
|
265
|
+
text += "\n" + theme.fg("muted", "...");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return new Text(text, 0, 0);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Startup notification ─────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
277
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
278
|
+
ctx.ui.notify(
|
|
279
|
+
"Google Search: No GEMINI_API_KEY set. The google_search tool will not work until this is configured.",
|
|
280
|
+
"warning",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Output formatting ────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function formatOutput(result: SearchResult, maxSources: number): string {
|
|
289
|
+
const lines: string[] = [];
|
|
290
|
+
|
|
291
|
+
// Answer
|
|
292
|
+
if (result.answer) {
|
|
293
|
+
lines.push(result.answer);
|
|
294
|
+
} else {
|
|
295
|
+
lines.push("(No answer text returned from search)");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Sources
|
|
299
|
+
if (result.sources.length > 0) {
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push("Sources:");
|
|
302
|
+
const sourcesToShow = result.sources.slice(0, maxSources);
|
|
303
|
+
for (let i = 0; i < sourcesToShow.length; i++) {
|
|
304
|
+
const s = sourcesToShow[i];
|
|
305
|
+
lines.push(`[${i + 1}] ${s.title} - ${s.domain}`);
|
|
306
|
+
lines.push(` ${s.uri}`);
|
|
307
|
+
}
|
|
308
|
+
if (result.sources.length > maxSources) {
|
|
309
|
+
lines.push(`(${result.sources.length - maxSources} more sources omitted)`);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
lines.push("");
|
|
313
|
+
lines.push("(No source URLs found in grounding metadata)");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Search queries
|
|
317
|
+
if (result.searchQueries.length > 0) {
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push(`Searches performed: ${result.searchQueries.map((q) => `"${q}"`).join(", ")}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|