@meshxdata/fops 0.0.1

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.
Files changed (57) hide show
  1. package/README.md +98 -0
  2. package/STRUCTURE.md +43 -0
  3. package/foundation.mjs +16 -0
  4. package/package.json +52 -0
  5. package/src/agent/agent.js +367 -0
  6. package/src/agent/agent.test.js +233 -0
  7. package/src/agent/context.js +143 -0
  8. package/src/agent/context.test.js +81 -0
  9. package/src/agent/index.js +2 -0
  10. package/src/agent/llm.js +127 -0
  11. package/src/agent/llm.test.js +139 -0
  12. package/src/auth/index.js +4 -0
  13. package/src/auth/keychain.js +58 -0
  14. package/src/auth/keychain.test.js +185 -0
  15. package/src/auth/login.js +421 -0
  16. package/src/auth/login.test.js +192 -0
  17. package/src/auth/oauth.js +203 -0
  18. package/src/auth/oauth.test.js +118 -0
  19. package/src/auth/resolve.js +78 -0
  20. package/src/auth/resolve.test.js +153 -0
  21. package/src/commands/index.js +268 -0
  22. package/src/config.js +24 -0
  23. package/src/config.test.js +70 -0
  24. package/src/doctor.js +487 -0
  25. package/src/doctor.test.js +134 -0
  26. package/src/plugins/api.js +37 -0
  27. package/src/plugins/api.test.js +95 -0
  28. package/src/plugins/discovery.js +78 -0
  29. package/src/plugins/discovery.test.js +92 -0
  30. package/src/plugins/hooks.js +13 -0
  31. package/src/plugins/hooks.test.js +118 -0
  32. package/src/plugins/index.js +3 -0
  33. package/src/plugins/loader.js +110 -0
  34. package/src/plugins/manifest.js +26 -0
  35. package/src/plugins/manifest.test.js +106 -0
  36. package/src/plugins/registry.js +14 -0
  37. package/src/plugins/registry.test.js +43 -0
  38. package/src/plugins/skills.js +126 -0
  39. package/src/plugins/skills.test.js +173 -0
  40. package/src/project.js +61 -0
  41. package/src/project.test.js +196 -0
  42. package/src/setup/aws.js +369 -0
  43. package/src/setup/aws.test.js +280 -0
  44. package/src/setup/index.js +3 -0
  45. package/src/setup/setup.js +161 -0
  46. package/src/setup/wizard.js +119 -0
  47. package/src/shell.js +9 -0
  48. package/src/shell.test.js +72 -0
  49. package/src/skills/foundation/SKILL.md +107 -0
  50. package/src/ui/banner.js +56 -0
  51. package/src/ui/banner.test.js +97 -0
  52. package/src/ui/confirm.js +97 -0
  53. package/src/ui/index.js +5 -0
  54. package/src/ui/input.js +199 -0
  55. package/src/ui/spinner.js +170 -0
  56. package/src/ui/spinner.test.js +29 -0
  57. package/src/ui/streaming.js +106 -0
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Foundation CLI
2
+
3
+ Install and manage **Foundation** data mesh platforms from the terminal.
4
+
5
+ ## Install
6
+
7
+ **npm / npx (recommended)**
8
+
9
+ ```bash
10
+ # Run without installing
11
+ npx foundation doctor
12
+
13
+ # Install globally
14
+ npm install -g foundation
15
+ # or
16
+ pnpm add -g foundation
17
+ ```
18
+
19
+ **From repo (development)**
20
+
21
+ ```bash
22
+ cd foundation-compose/foundation-cli
23
+ npm install
24
+ npm link # optional: use `foundation` from anywhere
25
+ ```
26
+
27
+ **Shell installer (optional)**
28
+
29
+ ```bash
30
+ curl -fsSL https://raw.githubusercontent.com/meshxdata/foundation-compose/main/foundation-cli/install.sh | bash
31
+ ```
32
+
33
+ ## Commands
34
+
35
+ | Command | Description |
36
+ |---------|-------------|
37
+ | `foundation setup` | Full automated setup (`.env`, netrc reminder, submodules; use `--download` to pull images). Replaces `setup.sh`. |
38
+ | `foundation init` | Initialize a Foundation project (submodules, `.env`); same as `setup` with default options |
39
+ | `foundation up` | Start all services (`make start`) |
40
+ | `foundation down` | Stop services; use `--clean` to remove volumes |
41
+ | `foundation status` | Show service status |
42
+ | `foundation logs [service]` | Stream logs (all or one service) |
43
+ | `foundation doctor` | Check Docker, git, `.env`, project layout; use `--fix` to apply fixes |
44
+ | `foundation config` | Launch interactive configuration |
45
+ | `foundation bootstrap` | Create demo data mesh |
46
+ | `foundation test` | Run health checks |
47
+ | `foundation agent` | AI assistant for the stack. Interactive or single-turn with `-m "message"`. |
48
+ | `foundation chat` | Same as `foundation agent` (interactive). |
49
+
50
+ ### AI Assistant (agent / chat)
51
+
52
+ - **Interactive:** `foundation agent` or `foundation chat` — in-CLI chat (no Claude Code app). Uses Anthropic or OpenAI APIs with streaming.
53
+ - **Single-turn:** `foundation agent -m "why are my containers failing?"` — one question, one reply; optional “run suggested command?” prompt.
54
+
55
+ **Auth** (checked in order):
56
+
57
+ 1. `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` environment variable
58
+ 2. `~/.claude/settings.json` → `apiKeyHelper` (script that prints the API key)
59
+ 3. `~/.claude/.credentials.json` → `anthropic_api_key` or `apiKey`
60
+ 4. `~/.claude.json` → `anthropic_api_key` or `apiKey`
61
+
62
+ On macOS, Claude Code stores keys in Keychain; use `apiKeyHelper` in `~/.claude/settings.json` pointing to a script that reads the key (e.g. from keychain) and prints it, or put a key in `~/.claude/.credentials.json`.
63
+
64
+ Install optional deps: `npm install @anthropic-ai/sdk openai`.
65
+
66
+ ## Usage
67
+
68
+ Run from a **foundation-compose** clone (or any directory that contains `docker-compose.yaml` and `Makefile`). You can also set `FOUNDATION_ROOT` to point to your repo root.
69
+
70
+ ```bash
71
+ cd /path/to/foundation-compose
72
+ foundation doctor
73
+ foundation up
74
+ foundation status
75
+ ```
76
+
77
+ First-time setup:
78
+
79
+ ```bash
80
+ git clone https://github.com/meshxdata/foundation-compose
81
+ cd foundation-compose
82
+ npx foundation init
83
+ npx foundation up
84
+ ```
85
+
86
+ ## Project structure
87
+
88
+ The CLI is split into modules under `src/` for long-term maintainability. See [STRUCTURE.md](STRUCTURE.md) for layout and how to add commands.
89
+
90
+ ## Requirements
91
+
92
+ - Node.js >= 18
93
+ - Docker (Docker Compose)
94
+ - Git (for `foundation init` submodules)
95
+
96
+ ## License
97
+
98
+ MIT
package/STRUCTURE.md ADDED
@@ -0,0 +1,43 @@
1
+ # Foundation CLI — Project structure
2
+
3
+ Long-term layout for a single package inside the foundation-compose repo. No git submodules required; the CLI lives in `foundation-cli/` and is versioned with the repo.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ foundation-cli/
9
+ ├── foundation.mjs # Entry point: parses argv, registers commands, runs
10
+ ├── package.json
11
+ ├── README.md
12
+ ├── STRUCTURE.md # This file
13
+ ├── install.sh # Optional curl | bash installer
14
+ └── src/
15
+ ├── config.js # PKG, CLI_BRAND, printFoundationBanner
16
+ ├── project.js # rootDir, requireRoot, isFoundationRoot, findComposeRootUp
17
+ ├── shell.js # make(), dockerCompose()
18
+ ├── auth.js # resolveAnthropicApiKey, resolveOpenAiApiKey, authHelp, offerClaudeLogin
19
+ ├── setup.js # runSetup, runInitWizard, checkEcrRepos
20
+ ├── agent.js # gatherStackContext, runAgentSingleTurn, runAgentInteractive, streaming
21
+ ├── doctor.js # runDoctor (checks + optional --fix)
22
+ └── commands/
23
+ └── index.js # registerCommands(program) — wires all CLI commands
24
+ ```
25
+
26
+ ## Conventions
27
+
28
+ - **Single entry**: `foundation.mjs` is the only file executed; it imports `src/commands/index.js` and calls `registerCommands(program)`.
29
+ - **No circular imports**: Flow is entry → commands → lib (config, project, shell, auth, setup, agent, doctor). Lib modules do not import commands.
30
+ - **ESM only**: `"type": "module"` in package.json; all sources use `import`/`export`.
31
+ - **Shared helpers**: Project root detection and `make`/docker are in `project.js` and `shell.js` so every command reuses the same semantics.
32
+
33
+ ## Adding a command
34
+
35
+ 1. Implement logic in the appropriate `src/*.js` (or a new one if it’s a new domain).
36
+ 2. In `src/commands/index.js`, add a `program.command(...).action(...)` that calls your function and uses `requireRoot(program)` or `rootDir()` as needed.
37
+
38
+ ## Optional: separate repo (submodule)
39
+
40
+ If you later move the CLI to its own repo and add it as a submodule under `foundation-compose/foundation-cli`:
41
+
42
+ - Keep this same layout inside the submodule.
43
+ - Root `setup.sh` and docs can still say “run `node foundation-cli/foundation.mjs`” or “`npx foundation`” if the package is published.
package/foundation.mjs ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { registerCommands } from "./src/commands/index.js";
5
+ import { loadPlugins } from "./src/plugins/index.js";
6
+
7
+ const program = new Command();
8
+ const registry = await loadPlugins();
9
+
10
+ registerCommands(program, registry);
11
+
12
+ for (const cmd of registry.commands) {
13
+ cmd.spec(program);
14
+ }
15
+
16
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@meshxdata/fops",
3
+ "version": "0.0.1",
4
+ "description": "CLI to install and manage Foundation data mesh platforms",
5
+ "keywords": [
6
+ "foundation",
7
+ "data-mesh",
8
+ "cli",
9
+ "installer"
10
+ ],
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "bin": {
14
+ "fops": "./foundation.mjs"
15
+ },
16
+ "files": [
17
+ "foundation.mjs",
18
+ "src/",
19
+ "README.md",
20
+ "STRUCTURE.md"
21
+ ],
22
+ "scripts": {
23
+ "start": "node foundation.mjs",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest"
26
+ },
27
+ "dependencies": {
28
+ "boxen": "^8.0.1",
29
+ "chalk": "^5.3.0",
30
+ "commander": "^12.0.0",
31
+ "execa": "^9.5.2",
32
+ "ink": "^6.7.0",
33
+ "ink-spinner": "^5.0.0",
34
+ "inquirer": "^9.2.23",
35
+ "ora": "^9.3.0",
36
+ "path-key": "^1.0.0",
37
+ "react": "^19.2.4"
38
+ },
39
+ "optionalDependencies": {
40
+ "@anthropic-ai/sdk": "^0.32.0",
41
+ "openai": "^4.73.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "devDependencies": {
50
+ "vitest": "^4.0.18"
51
+ }
52
+ }
@@ -0,0 +1,367 @@
1
+ import chalk from "chalk";
2
+ import { execa } from "execa";
3
+ import { resolveAnthropicApiKey, resolveOpenAiApiKey, authHelp, offerClaudeLogin } from "../auth/index.js";
4
+ import { renderSpinner, renderBanner, promptInput, selectOption } from "../ui/index.js";
5
+ import { FOUNDATION_SYSTEM_PROMPT, gatherStackContext } from "./context.js";
6
+ import { hasClaudeCode, hasClaudeCodeAuth, runViaClaudeCode, streamAssistantReply } from "./llm.js";
7
+
8
+ /**
9
+ * Render inline markdown: **bold**, *italic*, `code`
10
+ */
11
+ function renderInline(line) {
12
+ return line
13
+ .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
14
+ .replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
15
+ .replace(/`([^`]+)`/g, (_, t) => chalk.yellowBright(t));
16
+ }
17
+
18
+ /**
19
+ * Format AI response markdown for the terminal with ⏺ bullet prefix.
20
+ * Handles headings, bold, italic, inline code, code blocks, lists, blockquotes.
21
+ */
22
+ export function formatResponse(text) {
23
+ const lines = text.split("\n");
24
+ const out = [];
25
+ let inCode = false;
26
+ let codeLang = "";
27
+
28
+ let codeBlock = [];
29
+
30
+ for (const line of lines) {
31
+ // Code block fences
32
+ if (/^```/.test(line.trim())) {
33
+ if (!inCode) {
34
+ inCode = true;
35
+ codeLang = line.trim().replace(/^```/, "").trim();
36
+ codeBlock = [];
37
+ } else {
38
+ // Closing fence — render the collected block
39
+ const isShell = /^(bash|sh|shell|zsh)?$/.test(codeLang);
40
+ const maxLen = Math.max(...codeBlock.map((l) => l.length), 0);
41
+ const width = Math.max(maxLen + 4, 20);
42
+ const top = chalk.gray(" ┌" + "─".repeat(width) + "┐");
43
+ const bot = chalk.gray(" └" + "─".repeat(width) + "┘");
44
+ out.push(top);
45
+ for (const cl of codeBlock) {
46
+ const prefix = isShell ? chalk.green("$ ") : " ";
47
+ const pad = " ".repeat(Math.max(0, width - cl.length - (isShell ? 4 : 2)));
48
+ out.push(chalk.gray(" │ ") + prefix + chalk.white(cl) + chalk.gray(pad + " │"));
49
+ }
50
+ out.push(bot);
51
+ inCode = false;
52
+ codeLang = "";
53
+ codeBlock = [];
54
+ }
55
+ continue;
56
+ }
57
+
58
+ // Inside code block — collect lines
59
+ if (inCode) {
60
+ codeBlock.push(line);
61
+ continue;
62
+ }
63
+
64
+ // Headings
65
+ if (/^#{1,3}\s/.test(line)) {
66
+ const content = line.replace(/^#{1,3}\s+/, "");
67
+ out.push(chalk.bold.cyan(content));
68
+ continue;
69
+ }
70
+
71
+ // Blockquote
72
+ if (/^>\s?/.test(line)) {
73
+ const content = line.replace(/^>\s?/, "");
74
+ out.push(chalk.gray.italic(" " + renderInline(content)));
75
+ continue;
76
+ }
77
+
78
+ // Unordered list
79
+ if (/^[\s]*[-*+]\s/.test(line)) {
80
+ const content = line.replace(/^[\s]*[-*+]\s+/, "");
81
+ out.push(" " + chalk.cyan("•") + " " + renderInline(content));
82
+ continue;
83
+ }
84
+
85
+ // Ordered list
86
+ if (/^[\s]*\d+\.\s/.test(line)) {
87
+ const match = line.match(/^[\s]*(\d+)\.\s+(.*)/);
88
+ if (match) {
89
+ out.push(" " + chalk.cyan(match[1] + ".") + " " + renderInline(match[2]));
90
+ continue;
91
+ }
92
+ }
93
+
94
+ // Regular text (or blank line)
95
+ out.push(renderInline(line));
96
+ }
97
+
98
+ const rendered = out.join("\n").trimEnd();
99
+ const outputLines = rendered.split("\n");
100
+ const bullet = chalk.cyan("⏺");
101
+ return outputLines.map((l, i) => {
102
+ if (i === 0) return `${bullet} ${l}`;
103
+ return ` ${l}`;
104
+ }).join("\n");
105
+ }
106
+
107
+ export function extractSuggestedCommands(text) {
108
+ const commands = [];
109
+ const re = /```(?:bash|sh)\s*\n([\s\S]*?)```/gi;
110
+ let m;
111
+ while ((m = re.exec(text)) !== null) {
112
+ const block = m[1].trim().split("\n").map((l) => l.replace(/^\s*#.*$/, "").trim()).filter(Boolean);
113
+ if (block.length) commands.push(block[0]);
114
+ }
115
+ return commands;
116
+ }
117
+
118
+ /**
119
+ * Run a shell command string, capturing output.
120
+ * Returns { ok, output } — never throws.
121
+ */
122
+ async function runShellCommand(root, command, { isLast = true } = {}) {
123
+ const trimmed = command.replace(/\s+/g, " ").trim();
124
+ const branch = isLast ? "└─" : "├─";
125
+ const cont = isLast ? " " : "│ ";
126
+ const prefix = ` ${chalk.gray(cont)}`;
127
+
128
+ console.log(`\n ${chalk.gray(branch)} ${chalk.white(trimmed)} ${chalk.gray("· running")}`);
129
+
130
+ try {
131
+ // Pipe stdout/stderr so docker/compose use clean non-TTY output
132
+ // (no multi-line progress bars). Inherit stdin for interactive prompts.
133
+ const proc = execa("sh", ["-c", trimmed], {
134
+ cwd: root,
135
+ stdio: ["inherit", "pipe", "pipe"],
136
+ reject: false,
137
+ timeout: 120_000,
138
+ });
139
+
140
+ let output = "";
141
+ let lastWasCR = false;
142
+
143
+ const streamChunk = (chunk) => {
144
+ const text = chunk.toString();
145
+ output += text;
146
+ for (const raw of text.split("\n")) {
147
+ if (!raw) continue;
148
+ // Handle \r progress bars — overwrite previous line
149
+ const line = raw.includes("\r") ? raw.split("\r").filter(Boolean).pop() : raw;
150
+ if (!line?.trim()) continue;
151
+ if (lastWasCR) process.stdout.write("\n");
152
+ process.stdout.write(`${prefix}${chalk.gray(line)}`);
153
+ lastWasCR = raw.includes("\r") && !raw.endsWith("\n");
154
+ if (!lastWasCR) process.stdout.write("\n");
155
+ }
156
+ };
157
+
158
+ proc.stdout.on("data", streamChunk);
159
+ proc.stderr.on("data", streamChunk);
160
+
161
+ const result = await proc;
162
+ if (lastWasCR) process.stdout.write("\n");
163
+
164
+ if (result.exitCode !== 0) {
165
+ console.log(`${prefix}${chalk.yellow("⚠ Exit code " + result.exitCode)}`);
166
+ return { ok: false, output: output || `Exit code ${result.exitCode}` };
167
+ }
168
+ console.log(`${prefix}${chalk.green("✓ Done")}`);
169
+ return { ok: true, output };
170
+ } catch (err) {
171
+ const msg = err.message || String(err);
172
+ console.log(`${prefix}${chalk.red("✗ " + msg)}`);
173
+ return { ok: false, output: msg };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Present suggested commands in a single picker.
179
+ * Shows each command as a selectable option plus "Run all" / "Skip".
180
+ * Returns array of { command, ok, output } for commands that were run.
181
+ */
182
+ export async function askToRunCommand(root, replyText) {
183
+ const suggested = extractSuggestedCommands(replyText);
184
+ if (suggested.length === 0) return [];
185
+
186
+ console.log(chalk.cyan("⏺") + chalk.white(" Suggested commands:"));
187
+
188
+ const options = suggested.map((cmd) => ({ label: cmd, value: cmd }));
189
+ if (suggested.length > 1) {
190
+ options.push({ label: "Run all", value: "__run_all__" });
191
+ }
192
+ options.push({ label: "Skip", value: "__skip__" });
193
+
194
+ const choice = await selectOption("Run command?", options);
195
+
196
+ if (choice === null || choice === "__skip__") return [];
197
+
198
+ const toRun = choice === "__run_all__" ? suggested : [choice];
199
+ const results = [];
200
+
201
+ for (let i = 0; i < toRun.length; i++) {
202
+ const isLast = i === toRun.length - 1;
203
+ const result = await runShellCommand(root, toRun[i], { isLast });
204
+ results.push({ command: toRun[i], ...result });
205
+ }
206
+
207
+ return results;
208
+ }
209
+
210
+ export async function runAgentSingleTurn(root, message, opts = {}) {
211
+ const runSuggestions = opts.runSuggestions !== false;
212
+ const useClaudeCode = hasClaudeCode() && hasClaudeCodeAuth();
213
+ const anthropicKey = resolveAnthropicApiKey();
214
+ const openaiKey = resolveOpenAiApiKey();
215
+
216
+ if (!useClaudeCode && !anthropicKey && !openaiKey) {
217
+ authHelp();
218
+ process.exit(1);
219
+ }
220
+
221
+ const context = await gatherStackContext(root);
222
+ const systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + context;
223
+ let replyText = "";
224
+
225
+ const spinner = renderSpinner();
226
+
227
+ try {
228
+ if (useClaudeCode) {
229
+ // Use Claude Code CLI with OAuth auth
230
+ replyText = await runViaClaudeCode(message, systemContent);
231
+ } else if (anthropicKey) {
232
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
233
+ const client = new Anthropic({ apiKey: anthropicKey });
234
+ const response = await client.messages.create({
235
+ model: opts.model || "claude-sonnet-4-20250514",
236
+ max_tokens: 2048, system: systemContent,
237
+ messages: [{ role: "user", content: message }],
238
+ });
239
+ replyText = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
240
+ } else if (openaiKey) {
241
+ const OpenAI = (await import("openai")).default;
242
+ const client = new OpenAI({ apiKey: openaiKey });
243
+ const response = await client.chat.completions.create({
244
+ model: opts.model || "gpt-4o-mini", max_tokens: 2048,
245
+ messages: [{ role: "system", content: systemContent }, { role: "user", content: message }],
246
+ });
247
+ replyText = response.choices?.[0]?.message?.content ?? "";
248
+ }
249
+ } catch (err) {
250
+ spinner.stop();
251
+ if (err.code === "MODULE_NOT_FOUND" || err.message?.includes("Cannot find module")) {
252
+ console.log(chalk.yellow("Install optional deps for agent: npm install @anthropic-ai/sdk openai"));
253
+ process.exit(1);
254
+ }
255
+ const msg = String(err?.message ?? err?.body ?? "");
256
+ if (err?.status === 401 || msg.includes("401") || msg.includes("authentication_error") || msg.includes("invalid x-api-key")) {
257
+ console.log(chalk.red("Authentication failed: invalid or expired API key."));
258
+ const didLogin = await offerClaudeLogin();
259
+ if (didLogin) {
260
+ console.log(chalk.green("Login complete. Run your command again.\n"));
261
+ process.exit(0);
262
+ }
263
+ console.log(chalk.gray("Get a valid key from console.anthropic.com (we can open the page in your browser on next run).\n"));
264
+ process.exit(1);
265
+ }
266
+ throw err;
267
+ }
268
+
269
+ spinner.stop();
270
+ console.log("");
271
+ console.log(formatResponse(replyText));
272
+ console.log("");
273
+
274
+ if (runSuggestions) {
275
+ const cmdResults = await askToRunCommand(root, replyText);
276
+ if (cmdResults.some((r) => !r.ok)) {
277
+ console.log(chalk.yellow("\n Some commands failed. Run fops chat for interactive help.\n"));
278
+ }
279
+ }
280
+ }
281
+
282
+ export async function runAgentInteractive(root) {
283
+ const useClaudeCode = hasClaudeCode() && hasClaudeCodeAuth();
284
+ const anthropicKey = resolveAnthropicApiKey();
285
+ const openaiKey = resolveOpenAiApiKey();
286
+
287
+ if (!useClaudeCode && !anthropicKey && !openaiKey) {
288
+ authHelp();
289
+ console.log(chalk.gray(" npm install @anthropic-ai/sdk openai # optional deps\n"));
290
+ process.exit(1);
291
+ }
292
+
293
+ if (!useClaudeCode) {
294
+ try {
295
+ await import("@anthropic-ai/sdk");
296
+ } catch (e) {
297
+ if (e.code === "MODULE_NOT_FOUND") {
298
+ console.log(chalk.yellow("Install optional deps: npm install @anthropic-ai/sdk openai\n"));
299
+ process.exit(1);
300
+ }
301
+ }
302
+ }
303
+
304
+ const context = await gatherStackContext(root);
305
+ const systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + context;
306
+ const messages = [];
307
+
308
+ // Show banner
309
+ renderBanner();
310
+
311
+ // Show quick hints
312
+ console.log(chalk.gray(" Try: \"help me set up\" • \"what's running?\" • \"show logs\" • \"run diagnostics\"\n"));
313
+
314
+ // Main chat loop
315
+ while (true) {
316
+ const input = await promptInput();
317
+
318
+ // Exit on null (esc/ctrl+c) or exit command
319
+ if (input === null || /^\/(exit|quit|q)$/i.test(input)) {
320
+ console.log(chalk.gray("\nGoodbye!\n"));
321
+ break;
322
+ }
323
+
324
+ if (!input.trim()) continue;
325
+
326
+ messages.push({ role: "user", content: input });
327
+
328
+ try {
329
+ const replyText = await streamAssistantReply(root, messages, systemContent, {});
330
+ console.log("");
331
+ console.log(formatResponse(replyText));
332
+ console.log("");
333
+ messages.push({ role: "assistant", content: replyText });
334
+
335
+ const cmdResults = await askToRunCommand(root, replyText);
336
+ // Feed command outputs back into context so the AI knows what happened
337
+ if (cmdResults.length > 0) {
338
+ const summary = cmdResults.map((r) =>
339
+ `[Ran: ${r.command}] ${r.ok ? "Success" : "Failed"}:\n${r.output || "(no output)"}`
340
+ ).join("\n\n");
341
+ messages.push({ role: "user", content: `[Command results]\n${summary}` });
342
+
343
+ // Auto-follow-up if any command failed
344
+ if (cmdResults.some((r) => !r.ok)) {
345
+ console.log("");
346
+ const followUp = await streamAssistantReply(root, messages, systemContent, {});
347
+ console.log("");
348
+ console.log(formatResponse(followUp));
349
+ console.log("");
350
+ messages.push({ role: "assistant", content: followUp });
351
+ }
352
+ }
353
+ } catch (err) {
354
+ const msg = String(err?.message ?? err?.body ?? "");
355
+ const is401 = err?.status === 401 || err?.response?.status === 401 ||
356
+ msg.includes("401") || msg.includes("authentication_error") || msg.includes("invalid x-api-key");
357
+ if (is401) {
358
+ console.log(chalk.red("\nAuthentication failed: invalid or expired API key."));
359
+ const didLogin = await offerClaudeLogin();
360
+ if (didLogin) console.log(chalk.green("\nLogin complete. Try your message again.\n"));
361
+ else console.log(chalk.gray("Get a valid key from console.anthropic.com\n"));
362
+ } else {
363
+ console.log(chalk.red("\nError:"), err.message);
364
+ }
365
+ }
366
+ }
367
+ }