@kata-sh/cli 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +156 -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 +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- 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 +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-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 +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -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 +4916 -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/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -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/search-the-web/cache.ts +78 -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 +68 -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 +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -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 +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -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 +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -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
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters — produce text summaries for issues, PRs, comments, etc.
|
|
3
|
+
*
|
|
4
|
+
* Used by both tools (LLM context) and renderers (TUI display).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GhIssue, GhPullRequest, GhComment, GhReview, GhLabel, GhMilestone } from "./gh-api.js";
|
|
8
|
+
|
|
9
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function timeAgo(dateStr: string): string {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const then = new Date(dateStr).getTime();
|
|
14
|
+
const diff = now - then;
|
|
15
|
+
const mins = Math.floor(diff / 60000);
|
|
16
|
+
if (mins < 1) return "just now";
|
|
17
|
+
if (mins < 60) return `${mins}m ago`;
|
|
18
|
+
const hours = Math.floor(mins / 60);
|
|
19
|
+
if (hours < 24) return `${hours}h ago`;
|
|
20
|
+
const days = Math.floor(hours / 24);
|
|
21
|
+
if (days < 30) return `${days}d ago`;
|
|
22
|
+
const months = Math.floor(days / 30);
|
|
23
|
+
if (months < 12) return `${months}mo ago`;
|
|
24
|
+
return `${Math.floor(months / 12)}y ago`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stateIcon(state: string, draft?: boolean): string {
|
|
28
|
+
if (draft) return "◇";
|
|
29
|
+
switch (state) {
|
|
30
|
+
case "open":
|
|
31
|
+
return "●";
|
|
32
|
+
case "closed":
|
|
33
|
+
return "✓";
|
|
34
|
+
case "merged":
|
|
35
|
+
return "⊕";
|
|
36
|
+
default:
|
|
37
|
+
return "○";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function truncateBody(body: string | null, maxLines = 10): string {
|
|
42
|
+
if (!body) return "(no description)";
|
|
43
|
+
const lines = body.split("\n");
|
|
44
|
+
if (lines.length <= maxLines) return body;
|
|
45
|
+
return lines.slice(0, maxLines).join("\n") + `\n... (${lines.length - maxLines} more lines)`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Issue formatting ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function formatIssueOneLiner(issue: GhIssue): string {
|
|
51
|
+
const icon = stateIcon(issue.state);
|
|
52
|
+
const labels = issue.labels.map((l) => l.name).join(", ");
|
|
53
|
+
const labelStr = labels ? ` [${labels}]` : "";
|
|
54
|
+
const assignee = issue.assignees.length ? ` → ${issue.assignees.map((a) => a.login).join(", ")}` : "";
|
|
55
|
+
return `${icon} #${issue.number} ${issue.title}${labelStr}${assignee} (${timeAgo(issue.updated_at)})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatIssueDetail(issue: GhIssue): string {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
lines.push(`# Issue #${issue.number}: ${issue.title}`);
|
|
61
|
+
lines.push(`State: ${issue.state} | Author: @${issue.user.login} | Created: ${timeAgo(issue.created_at)} | Updated: ${timeAgo(issue.updated_at)}`);
|
|
62
|
+
|
|
63
|
+
if (issue.assignees.length) {
|
|
64
|
+
lines.push(`Assignees: ${issue.assignees.map((a) => `@${a.login}`).join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
if (issue.labels.length) {
|
|
67
|
+
lines.push(`Labels: ${issue.labels.map((l) => l.name).join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
if (issue.milestone) {
|
|
70
|
+
lines.push(`Milestone: ${issue.milestone.title}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push(`Comments: ${issue.comments}`);
|
|
73
|
+
lines.push(`URL: ${issue.html_url}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(truncateBody(issue.body, 30));
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatIssueList(issues: GhIssue[]): string {
|
|
80
|
+
if (!issues.length) return "No issues found.";
|
|
81
|
+
return issues.map(formatIssueOneLiner).join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── PR formatting ────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function formatPROneLiner(pr: GhPullRequest): string {
|
|
87
|
+
const icon = stateIcon(pr.merged_at ? "merged" : pr.state, pr.draft);
|
|
88
|
+
const labels = pr.labels.map((l) => l.name).join(", ");
|
|
89
|
+
const labelStr = labels ? ` [${labels}]` : "";
|
|
90
|
+
const draftStr = pr.draft ? " (draft)" : "";
|
|
91
|
+
const reviewers = pr.requested_reviewers.map((r) => r.login).join(", ");
|
|
92
|
+
const reviewerStr = reviewers ? ` ⟵ ${reviewers}` : "";
|
|
93
|
+
return `${icon} #${pr.number} ${pr.title}${draftStr}${labelStr}${reviewerStr} (${timeAgo(pr.updated_at)})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatPRDetail(pr: GhPullRequest): string {
|
|
97
|
+
const lines: string[] = [];
|
|
98
|
+
const mergedState = pr.merged_at ? "merged" : pr.state;
|
|
99
|
+
lines.push(`# PR #${pr.number}: ${pr.title}`);
|
|
100
|
+
lines.push(`State: ${mergedState}${pr.draft ? " (draft)" : ""} | Author: @${pr.user.login} | Created: ${timeAgo(pr.created_at)} | Updated: ${timeAgo(pr.updated_at)}`);
|
|
101
|
+
lines.push(`Branch: ${pr.head.ref} → ${pr.base.ref}`);
|
|
102
|
+
|
|
103
|
+
if (pr.assignees.length) {
|
|
104
|
+
lines.push(`Assignees: ${pr.assignees.map((a) => `@${a.login}`).join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
if (pr.labels.length) {
|
|
107
|
+
lines.push(`Labels: ${pr.labels.map((l) => l.name).join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
if (pr.milestone) {
|
|
110
|
+
lines.push(`Milestone: ${pr.milestone.title}`);
|
|
111
|
+
}
|
|
112
|
+
if (pr.requested_reviewers.length) {
|
|
113
|
+
lines.push(`Reviewers: ${pr.requested_reviewers.map((r) => `@${r.login}`).join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines.push(`Mergeable: ${pr.mergeable === null ? "checking..." : pr.mergeable ? "yes" : "no"} (${pr.mergeable_state})`);
|
|
117
|
+
lines.push(`Comments: ${pr.comments} | Review comments: ${pr.review_comments}`);
|
|
118
|
+
lines.push(`URL: ${pr.html_url}`);
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push(truncateBody(pr.body, 30));
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatPRList(prs: GhPullRequest[]): string {
|
|
125
|
+
if (!prs.length) return "No pull requests found.";
|
|
126
|
+
return prs.map(formatPROneLiner).join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Comment formatting ──────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function formatComment(comment: GhComment): string {
|
|
132
|
+
return `@${comment.user.login} (${timeAgo(comment.created_at)}):\n${truncateBody(comment.body, 8)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatCommentList(comments: GhComment[]): string {
|
|
136
|
+
if (!comments.length) return "No comments.";
|
|
137
|
+
return comments.map(formatComment).join("\n\n---\n\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Review formatting ───────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function reviewStateIcon(state: string): string {
|
|
143
|
+
switch (state) {
|
|
144
|
+
case "APPROVED":
|
|
145
|
+
return "✓";
|
|
146
|
+
case "CHANGES_REQUESTED":
|
|
147
|
+
return "✗";
|
|
148
|
+
case "COMMENTED":
|
|
149
|
+
return "💬";
|
|
150
|
+
case "DISMISSED":
|
|
151
|
+
return "—";
|
|
152
|
+
case "PENDING":
|
|
153
|
+
return "…";
|
|
154
|
+
default:
|
|
155
|
+
return "?";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatReview(review: GhReview): string {
|
|
160
|
+
const icon = reviewStateIcon(review.state);
|
|
161
|
+
const body = review.body ? `\n${truncateBody(review.body, 5)}` : "";
|
|
162
|
+
return `${icon} @${review.user.login}: ${review.state} (${timeAgo(review.submitted_at)})${body}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function formatReviewList(reviews: GhReview[]): string {
|
|
166
|
+
if (!reviews.length) return "No reviews.";
|
|
167
|
+
return reviews.map(formatReview).join("\n\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Label / Milestone formatting ─────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
export function formatLabel(label: GhLabel): string {
|
|
173
|
+
const desc = label.description ? ` — ${label.description}` : "";
|
|
174
|
+
return `• ${label.name} (#${label.color})${desc}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatLabelList(labels: GhLabel[]): string {
|
|
178
|
+
if (!labels.length) return "No labels.";
|
|
179
|
+
return labels.map(formatLabel).join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function formatMilestone(ms: GhMilestone): string {
|
|
183
|
+
const progress = ms.open_issues + ms.closed_issues > 0 ? Math.round((ms.closed_issues / (ms.open_issues + ms.closed_issues)) * 100) : 0;
|
|
184
|
+
const due = ms.due_on ? ` | Due: ${new Date(ms.due_on).toISOString().split("T")[0]}` : "";
|
|
185
|
+
return `• ${ms.title} (${ms.state}) — ${progress}% complete (${ms.closed_issues}/${ms.open_issues + ms.closed_issues})${due}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function formatMilestoneList(milestones: GhMilestone[]): string {
|
|
189
|
+
if (!milestones.length) return "No milestones.";
|
|
190
|
+
return milestones.map(formatMilestone).join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── File change formatting ───────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
export function formatFileChanges(
|
|
196
|
+
files: { filename: string; status: string; additions: number; deletions: number; changes: number }[],
|
|
197
|
+
): string {
|
|
198
|
+
if (!files.length) return "No files changed.";
|
|
199
|
+
const lines = files.map((f) => {
|
|
200
|
+
const statusIcon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
|
|
201
|
+
return `${statusIcon} ${f.filename} (+${f.additions} -${f.deletions})`;
|
|
202
|
+
});
|
|
203
|
+
const totalAdd = files.reduce((s, f) => s + f.additions, 0);
|
|
204
|
+
const totalDel = files.reduce((s, f) => s + f.deletions, 0);
|
|
205
|
+
lines.push(`\n${files.length} files changed, +${totalAdd} -${totalDel}`);
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|