@meshxdata/fops 0.0.3 → 0.0.5
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/package.json +2 -1
- package/src/agent/agent.js +139 -38
- package/src/agent/agents.js +224 -0
- package/src/agent/context.js +146 -12
- package/src/agent/index.js +1 -0
- package/src/agent/llm.js +84 -13
- package/src/auth/coda.js +10 -10
- package/src/auth/login.js +13 -13
- package/src/auth/oauth.js +4 -4
- package/src/commands/index.js +121 -15
- package/src/config.js +2 -2
- package/src/doctor.js +215 -26
- package/src/feature-flags.js +197 -0
- package/src/plugins/api.js +14 -0
- package/src/plugins/builtins/stack-api.js +36 -0
- package/src/plugins/loader.js +67 -0
- package/src/plugins/registry.js +2 -0
- package/src/project.js +20 -1
- package/src/setup/aws.js +58 -45
- package/src/setup/setup.js +10 -9
- package/src/setup/wizard.js +195 -15
- package/src/ui/confirm.js +3 -2
- package/src/ui/input.js +2 -2
- package/src/ui/spinner.js +4 -4
- package/src/ui/streaming.js +2 -2
package/src/agent/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { execa } from "execa";
|
|
4
5
|
import { loadSkills, searchKnowledge } from "../plugins/index.js";
|
|
@@ -17,8 +18,23 @@ When suggesting commands, ALWAYS use \`fops\` commands, not raw \`make\` or \`gi
|
|
|
17
18
|
**Always suggest 2–3 commands** so the user can pick. Pair a primary action with a follow-up (e.g. restart + logs, doctor + up).
|
|
18
19
|
|
|
19
20
|
## Accuracy Rules
|
|
20
|
-
- ALWAYS check
|
|
21
|
-
-
|
|
21
|
+
- ALWAYS check BOTH container status AND service health context. Container "running (healthy)" only means the Docker healthcheck passed — the service may still be initializing, have failed migrations, or be unresponsive.
|
|
22
|
+
- Cross-reference the "Service health" section (HTTP endpoint checks) with container status. If any endpoint is DOWN or unreachable, the stack is NOT fully ready — report this even if containers look healthy.
|
|
23
|
+
- If ANY container is exited, unhealthy, or failed, report it — never claim "all healthy" when failures exist.
|
|
24
|
+
- When containers have failed or services are unreachable, lead with the failures and suggest diagnostics.
|
|
25
|
+
- If "Missing images" context is present, report which images are missing and whether they need building or pulling. Suggest \`fops build\` for buildable images or \`make download\` (after ECR auth) for registry images.
|
|
26
|
+
|
|
27
|
+
## Auto-Fix Rules
|
|
28
|
+
When you detect failing containers, DO NOT just report them — diagnose and fix:
|
|
29
|
+
1. Read the container logs provided in context. Look for: missing files/volumes, permission errors, config issues, dependency failures, image problems.
|
|
30
|
+
2. Apply the RIGHT fix based on the diagnosis:
|
|
31
|
+
- **Restarting/crash-loop with no logs**: likely a missing volume mount or stale image → suggest \`fops build\` then \`docker compose up -d <service>\`
|
|
32
|
+
- **Image not found / pull access denied**: missing image → \`fops build\` (for buildable) or \`fops download\` (for registry images)
|
|
33
|
+
- **Dependency unhealthy**: fix the dependency first, then restart dependents → \`docker compose up -d <dep-service>\`
|
|
34
|
+
- **Port conflict**: another process using the port → identify and kill or change port in .env
|
|
35
|
+
- **Migration failed**: database issue → \`docker compose restart <service>-migrations\`
|
|
36
|
+
- **Config error / env missing**: check .env file → \`fops setup\`
|
|
37
|
+
3. Output the fix commands in fenced bash blocks so they auto-execute. Be decisive — don't hedge with "you might try", just say what to do and give the command.
|
|
22
38
|
|
|
23
39
|
## Security Rules
|
|
24
40
|
- Never output API keys, passwords, or tokens in responses
|
|
@@ -42,8 +58,10 @@ async function gatherDockerStatus(root) {
|
|
|
42
58
|
if (!parsed.length) return "No containers running.";
|
|
43
59
|
|
|
44
60
|
let healthy = 0, unhealthy = 0, exited = 0, running = 0;
|
|
61
|
+
const failingContainers = [];
|
|
45
62
|
const services = parsed.map((o) => {
|
|
46
63
|
const name = o.Name || o.name || o.Service || "?";
|
|
64
|
+
const service = o.Service || name;
|
|
47
65
|
const state = (o.State || "").toLowerCase();
|
|
48
66
|
const status = o.Status || "";
|
|
49
67
|
const health = (o.Health || "").toLowerCase();
|
|
@@ -51,11 +69,13 @@ async function gatherDockerStatus(root) {
|
|
|
51
69
|
|
|
52
70
|
if (state === "exited" || state === "dead") {
|
|
53
71
|
exited++;
|
|
72
|
+
failingContainers.push(service);
|
|
54
73
|
return `${name}: EXITED (code ${exitCode}) — ${status}`;
|
|
55
74
|
}
|
|
56
|
-
if (health === "unhealthy") {
|
|
75
|
+
if (health === "unhealthy" || state === "restarting") {
|
|
57
76
|
unhealthy++;
|
|
58
|
-
|
|
77
|
+
failingContainers.push(service);
|
|
78
|
+
return `${name}: ${state === "restarting" ? "RESTARTING" : "UNHEALTHY"} — ${status}`;
|
|
59
79
|
}
|
|
60
80
|
if (state === "running" && (health === "healthy" || !health)) {
|
|
61
81
|
healthy++;
|
|
@@ -69,15 +89,36 @@ async function gatherDockerStatus(root) {
|
|
|
69
89
|
const summary = [];
|
|
70
90
|
if (running) summary.push(`${running} running`);
|
|
71
91
|
if (healthy) summary.push(`${healthy} healthy`);
|
|
72
|
-
if (unhealthy) summary.push(`${unhealthy} UNHEALTHY`);
|
|
92
|
+
if (unhealthy) summary.push(`${unhealthy} UNHEALTHY/RESTARTING`);
|
|
73
93
|
if (exited) summary.push(`${exited} EXITED/FAILED`);
|
|
74
94
|
|
|
75
95
|
let header = `Container summary: ${parsed.length} total — ${summary.join(", ")}`;
|
|
76
96
|
if (unhealthy || exited) {
|
|
77
|
-
header += "\n⚠ ATTENTION: Some containers are failing. Diagnose and
|
|
97
|
+
header += "\n⚠ ATTENTION: Some containers are failing. Diagnose and fix the failures.";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let result = header + "\n\nContainer details:\n" + services.join("\n");
|
|
101
|
+
|
|
102
|
+
// Auto-collect logs from failing containers (last 15 lines each, max 3)
|
|
103
|
+
if (failingContainers.length > 0) {
|
|
104
|
+
const logsToFetch = failingContainers.slice(0, 3);
|
|
105
|
+
const logResults = await Promise.all(
|
|
106
|
+
logsToFetch.map(async (svc) => {
|
|
107
|
+
try {
|
|
108
|
+
const { stdout, stderr } = await execa(
|
|
109
|
+
"docker", ["compose", "logs", svc, "--tail", "15", "--no-color"],
|
|
110
|
+
{ cwd: root, reject: false, timeout: 5000 },
|
|
111
|
+
);
|
|
112
|
+
const output = (stdout || "") + (stderr || "");
|
|
113
|
+
if (output.trim()) return `\n--- Logs: ${svc} (last 15 lines) ---\n${output.trim()}`;
|
|
114
|
+
} catch { /* skip */ }
|
|
115
|
+
return `\n--- Logs: ${svc} ---\n(no logs available)`;
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
result += "\n" + logResults.join("\n");
|
|
78
119
|
}
|
|
79
120
|
|
|
80
|
-
return
|
|
121
|
+
return result;
|
|
81
122
|
}
|
|
82
123
|
} catch {
|
|
83
124
|
return "Docker: not available or not running.";
|
|
@@ -85,6 +126,95 @@ async function gatherDockerStatus(root) {
|
|
|
85
126
|
return null;
|
|
86
127
|
}
|
|
87
128
|
|
|
129
|
+
/**
|
|
130
|
+
* HTTP GET with timeout. Returns { ok, status } or { ok: false, error }.
|
|
131
|
+
*/
|
|
132
|
+
function httpPing(url, timeout = 3000) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const req = http.get(url, { timeout }, (res) => {
|
|
135
|
+
res.resume(); // drain
|
|
136
|
+
resolve({ ok: res.statusCode < 500, status: res.statusCode });
|
|
137
|
+
});
|
|
138
|
+
req.on("error", (err) => resolve({ ok: false, error: err.code || err.message }));
|
|
139
|
+
req.on("timeout", () => { req.destroy(); resolve({ ok: false, error: "TIMEOUT" }); });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const SERVICE_ENDPOINTS = [
|
|
144
|
+
{ name: "Backend API", url: "http://localhost:9001/api/data/mesh/list?per_page=1", port: 9001 },
|
|
145
|
+
{ name: "Frontend", url: "http://localhost:3002", port: 3002 },
|
|
146
|
+
{ name: "Storage Engine (MinIO)", url: "http://localhost:9002/minio/health/live", port: 9002 },
|
|
147
|
+
{ name: "Trino", url: "http://localhost:8081/v1/info", port: 8081 },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
async function gatherServiceHealth() {
|
|
151
|
+
const results = await Promise.all(
|
|
152
|
+
SERVICE_ENDPOINTS.map(async ({ name, url }) => {
|
|
153
|
+
const r = await httpPing(url);
|
|
154
|
+
if (r.ok) return `${name}: UP (HTTP ${r.status})`;
|
|
155
|
+
return `${name}: DOWN (${r.error || "HTTP " + r.status})`;
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
return "Service health (HTTP checks):\n" + results.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Detect which compose images are missing locally.
|
|
163
|
+
* Compares `docker compose config --images` against `docker images`.
|
|
164
|
+
*/
|
|
165
|
+
async function gatherMissingImages(root) {
|
|
166
|
+
try {
|
|
167
|
+
// Get all images required by compose
|
|
168
|
+
const { stdout: configImages } = await execa(
|
|
169
|
+
"docker", ["compose", "config", "--images"],
|
|
170
|
+
{ cwd: root, reject: false, timeout: 10000 },
|
|
171
|
+
);
|
|
172
|
+
if (!configImages?.trim()) return null;
|
|
173
|
+
|
|
174
|
+
const required = [...new Set(configImages.trim().split("\n").filter(Boolean))];
|
|
175
|
+
|
|
176
|
+
// Get locally available images
|
|
177
|
+
const { stdout: localImages } = await execa(
|
|
178
|
+
"docker", ["images", "--format", "{{.Repository}}:{{.Tag}}"],
|
|
179
|
+
{ reject: false, timeout: 5000 },
|
|
180
|
+
);
|
|
181
|
+
const localSet = new Set((localImages || "").trim().split("\n").filter(Boolean));
|
|
182
|
+
|
|
183
|
+
// Also check by repo without tag (docker sometimes lists <none> tag)
|
|
184
|
+
const localRepos = new Set(
|
|
185
|
+
(localImages || "").trim().split("\n").filter(Boolean).map((i) => i.split(":")[0]),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Find which compose services have build contexts
|
|
189
|
+
const { stdout: configJson } = await execa(
|
|
190
|
+
"docker", ["compose", "config", "--format", "json"],
|
|
191
|
+
{ cwd: root, reject: false, timeout: 10000 },
|
|
192
|
+
);
|
|
193
|
+
const buildableImages = new Set();
|
|
194
|
+
if (configJson?.trim()) {
|
|
195
|
+
try {
|
|
196
|
+
const config = JSON.parse(configJson);
|
|
197
|
+
for (const [, svc] of Object.entries(config.services || {})) {
|
|
198
|
+
if (svc.build && svc.image) buildableImages.add(svc.image);
|
|
199
|
+
}
|
|
200
|
+
} catch { /* ignore */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const missing = required.filter((img) => !localSet.has(img) && !localRepos.has(img.split(":")[0]));
|
|
204
|
+
if (missing.length === 0) return null;
|
|
205
|
+
|
|
206
|
+
const lines = missing.map((img) => {
|
|
207
|
+
const action = buildableImages.has(img) ? "needs BUILD (has build context)" : "needs PULL from registry";
|
|
208
|
+
return ` ${img} — ${action}`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return `Missing images (${missing.length}/${required.length}):\n` + lines.join("\n") +
|
|
212
|
+
"\n\nTo build local images: make build\nTo pull registry images: make download (requires ECR auth)";
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
88
218
|
async function gatherImageAges(root) {
|
|
89
219
|
try {
|
|
90
220
|
const { stdout: imgOut } = await execa("docker", ["compose", "images", "--format", "json"], { cwd: root, reject: false, timeout: 5000 });
|
|
@@ -152,9 +282,9 @@ function checkEnvFile(root) {
|
|
|
152
282
|
return null;
|
|
153
283
|
}
|
|
154
284
|
|
|
155
|
-
async function gatherSkills() {
|
|
285
|
+
async function gatherSkills(registry) {
|
|
156
286
|
try {
|
|
157
|
-
const skills = await loadSkills();
|
|
287
|
+
const skills = await loadSkills(registry);
|
|
158
288
|
if (skills.length) {
|
|
159
289
|
return "## Additional Skills\n" + skills.map((s) => s.content).join("\n\n");
|
|
160
290
|
}
|
|
@@ -169,16 +299,20 @@ export async function gatherStackContext(root, { registry, message } = {}) {
|
|
|
169
299
|
|
|
170
300
|
if (root) {
|
|
171
301
|
// Run all independent checks in parallel
|
|
172
|
-
const [dockerStatus, imageAges, prereqs, envInfo, skills, knowledge] = await Promise.all([
|
|
302
|
+
const [dockerStatus, serviceHealth, missingImages, imageAges, prereqs, envInfo, skills, knowledge] = await Promise.all([
|
|
173
303
|
gatherDockerStatus(root),
|
|
304
|
+
gatherServiceHealth(),
|
|
305
|
+
gatherMissingImages(root),
|
|
174
306
|
gatherImageAges(root),
|
|
175
307
|
gatherPrereqs(),
|
|
176
308
|
Promise.resolve(checkEnvFile(root)),
|
|
177
|
-
gatherSkills(),
|
|
309
|
+
gatherSkills(registry),
|
|
178
310
|
registry && message ? searchKnowledge(registry, message) : Promise.resolve(null),
|
|
179
311
|
]);
|
|
180
312
|
|
|
181
313
|
if (dockerStatus) parts.push(dockerStatus);
|
|
314
|
+
if (serviceHealth) parts.push(serviceHealth);
|
|
315
|
+
if (missingImages) parts.push(missingImages);
|
|
182
316
|
if (imageAges) parts.push(imageAges);
|
|
183
317
|
parts.push(prereqs);
|
|
184
318
|
if (envInfo) parts.push(envInfo);
|
|
@@ -188,7 +322,7 @@ export async function gatherStackContext(root, { registry, message } = {}) {
|
|
|
188
322
|
// No root — still check prereqs, skills, and knowledge
|
|
189
323
|
const [prereqs, skills, knowledge] = await Promise.all([
|
|
190
324
|
gatherPrereqs(),
|
|
191
|
-
gatherSkills(),
|
|
325
|
+
gatherSkills(registry),
|
|
192
326
|
registry && message ? searchKnowledge(registry, message) : Promise.resolve(null),
|
|
193
327
|
]);
|
|
194
328
|
parts.push(prereqs);
|
package/src/agent/index.js
CHANGED
package/src/agent/llm.js
CHANGED
|
@@ -37,10 +37,10 @@ function buildConversationPrompt(messages) {
|
|
|
37
37
|
/**
|
|
38
38
|
* Run a prompt through Claude Code CLI (uses OAuth auth)
|
|
39
39
|
*/
|
|
40
|
-
export async function runViaClaudeCode(prompt, systemPrompt) {
|
|
40
|
+
export async function runViaClaudeCode(prompt, systemPrompt, { replaceSystemPrompt = false } = {}) {
|
|
41
41
|
const args = ["-p", "--no-session-persistence"];
|
|
42
42
|
if (systemPrompt) {
|
|
43
|
-
args.push("--append-system-prompt", systemPrompt);
|
|
43
|
+
args.push(replaceSystemPrompt ? "--system-prompt" : "--append-system-prompt", systemPrompt);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const { stdout } = await execa("claude", args, {
|
|
@@ -53,12 +53,20 @@ export async function runViaClaudeCode(prompt, systemPrompt) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Stream response via Claude Code CLI with thinking display
|
|
56
|
+
* Stream response via Claude Code CLI with thinking display.
|
|
57
|
+
* Uses --output-format stream-json --include-partial-messages to get
|
|
58
|
+
* token-level streaming events (content_block_delta for text and thinking).
|
|
57
59
|
*/
|
|
58
|
-
export async function streamViaClaudeCode(prompt, systemPrompt, onChunk, onThinking) {
|
|
59
|
-
const args = [
|
|
60
|
+
export async function streamViaClaudeCode(prompt, systemPrompt, onChunk, onThinking, onBlockStart, { replaceSystemPrompt = false } = {}) {
|
|
61
|
+
const args = [
|
|
62
|
+
"-p",
|
|
63
|
+
"--output-format", "stream-json",
|
|
64
|
+
"--verbose",
|
|
65
|
+
"--include-partial-messages",
|
|
66
|
+
"--no-session-persistence",
|
|
67
|
+
];
|
|
60
68
|
if (systemPrompt) {
|
|
61
|
-
args.push("--append-system-prompt", systemPrompt);
|
|
69
|
+
args.push(replaceSystemPrompt ? "--system-prompt" : "--append-system-prompt", systemPrompt);
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
const proc = execa("claude", args, {
|
|
@@ -68,14 +76,62 @@ export async function streamViaClaudeCode(prompt, systemPrompt, onChunk, onThink
|
|
|
68
76
|
});
|
|
69
77
|
|
|
70
78
|
let fullText = "";
|
|
79
|
+
let lineBuf = "";
|
|
71
80
|
|
|
72
81
|
proc.stdout.on("data", (chunk) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
lineBuf += chunk.toString();
|
|
83
|
+
const lines = lineBuf.split("\n");
|
|
84
|
+
lineBuf = lines.pop(); // keep incomplete line
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
let evt;
|
|
89
|
+
try { evt = JSON.parse(line); } catch { continue; }
|
|
90
|
+
|
|
91
|
+
if (evt.type === "stream_event") {
|
|
92
|
+
const inner = evt.event;
|
|
93
|
+
if (inner?.type === "content_block_start") {
|
|
94
|
+
const blockType = inner.content_block?.type;
|
|
95
|
+
if (onBlockStart) onBlockStart(blockType);
|
|
96
|
+
} else if (inner?.type === "content_block_delta") {
|
|
97
|
+
if (inner.delta?.type === "thinking_delta" && inner.delta.thinking) {
|
|
98
|
+
if (onThinking) onThinking(inner.delta.thinking);
|
|
99
|
+
}
|
|
100
|
+
if (inner.delta?.type === "text_delta" && inner.delta.text) {
|
|
101
|
+
fullText += inner.delta.text;
|
|
102
|
+
if (onChunk) onChunk(inner.delta.text);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (evt.type === "assistant") {
|
|
106
|
+
// Final assistant message — extract any remaining text
|
|
107
|
+
const content = evt.message?.content || [];
|
|
108
|
+
const textParts = content.filter(b => b.type === "text").map(b => b.text).join("");
|
|
109
|
+
// Only use this if streaming didn't capture everything
|
|
110
|
+
if (textParts && !fullText) {
|
|
111
|
+
fullText = textParts;
|
|
112
|
+
if (onChunk) onChunk(textParts);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
76
116
|
});
|
|
77
117
|
|
|
78
118
|
await proc;
|
|
119
|
+
|
|
120
|
+
// Process any remaining buffer
|
|
121
|
+
if (lineBuf.trim()) {
|
|
122
|
+
try {
|
|
123
|
+
const evt = JSON.parse(lineBuf);
|
|
124
|
+
if (evt.type === "assistant") {
|
|
125
|
+
const content = evt.message?.content || [];
|
|
126
|
+
const textParts = content.filter(b => b.type === "text").map(b => b.text).join("");
|
|
127
|
+
if (textParts && !fullText) {
|
|
128
|
+
fullText = textParts;
|
|
129
|
+
if (onChunk) onChunk(textParts);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
return fullText;
|
|
80
136
|
}
|
|
81
137
|
|
|
@@ -91,10 +147,25 @@ export async function streamAssistantReply(root, messages, systemContent, opts)
|
|
|
91
147
|
try {
|
|
92
148
|
if (useClaudeCode) {
|
|
93
149
|
const prompt = buildConversationPrompt(messages);
|
|
94
|
-
fullText = await streamViaClaudeCode(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
150
|
+
fullText = await streamViaClaudeCode(
|
|
151
|
+
prompt,
|
|
152
|
+
systemContent,
|
|
153
|
+
(chunk) => {
|
|
154
|
+
display.appendContent(chunk);
|
|
155
|
+
},
|
|
156
|
+
(thinking) => {
|
|
157
|
+
display.appendThinking(thinking);
|
|
158
|
+
},
|
|
159
|
+
(blockType) => {
|
|
160
|
+
if (blockType === "thinking") {
|
|
161
|
+
display.setStatus("Reasoning");
|
|
162
|
+
} else if (blockType === "text") {
|
|
163
|
+
display.setThinking(""); // clear thinking preview
|
|
164
|
+
display.setStatus("Responding");
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{ replaceSystemPrompt: !!opts.replaceSystemPrompt },
|
|
168
|
+
);
|
|
98
169
|
} else if (anthropicKey) {
|
|
99
170
|
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
100
171
|
const client = new Anthropic({ apiKey: anthropicKey });
|
package/src/auth/coda.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function runCodaLogin() {
|
|
|
67
67
|
default: false,
|
|
68
68
|
}]);
|
|
69
69
|
if (!overwrite) {
|
|
70
|
-
console.log(chalk.
|
|
70
|
+
console.log(chalk.dim("Keeping existing Coda token."));
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -77,11 +77,11 @@ export async function runCodaLogin() {
|
|
|
77
77
|
console.log("");
|
|
78
78
|
console.log(chalk.white(" To connect Foundation to Coda, you need an API token."));
|
|
79
79
|
console.log(chalk.white(" Here's how to get one:\n"));
|
|
80
|
-
console.log(chalk.
|
|
81
|
-
console.log(chalk.
|
|
82
|
-
console.log(chalk.
|
|
83
|
-
console.log(chalk.
|
|
84
|
-
console.log(chalk.
|
|
80
|
+
console.log(chalk.dim(" 1. Go to ") + chalk.cyan(CODA_ACCOUNT_URL));
|
|
81
|
+
console.log(chalk.dim(" 2. Scroll to ") + chalk.white("\"API Settings\""));
|
|
82
|
+
console.log(chalk.dim(" 3. Click ") + chalk.white("\"Generate API token\""));
|
|
83
|
+
console.log(chalk.dim(" 4. Give it a name (e.g. \"Foundation CLI\")"));
|
|
84
|
+
console.log(chalk.dim(" 5. Copy the token and paste it below"));
|
|
85
85
|
console.log("");
|
|
86
86
|
|
|
87
87
|
const { openIt } = await inquirer.prompt([{
|
|
@@ -111,18 +111,18 @@ export async function runCodaLogin() {
|
|
|
111
111
|
},
|
|
112
112
|
}]);
|
|
113
113
|
|
|
114
|
-
console.log(chalk.
|
|
114
|
+
console.log(chalk.dim("\n Validating token..."));
|
|
115
115
|
const result = await validateCodaToken(token.trim());
|
|
116
116
|
|
|
117
117
|
if (!result.valid) {
|
|
118
118
|
console.log(chalk.red(`\n ${result.error}`));
|
|
119
|
-
console.log(chalk.
|
|
119
|
+
console.log(chalk.dim(" Check the token and try again with: fops login coda\n"));
|
|
120
120
|
return false;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
saveCodaToken(token);
|
|
124
124
|
console.log(chalk.green(`\n Coda login successful!`));
|
|
125
|
-
console.log(chalk.
|
|
126
|
-
console.log(chalk.
|
|
125
|
+
console.log(chalk.dim(` Logged in as: ${result.name || result.loginId}`));
|
|
126
|
+
console.log(chalk.dim(" Token saved to ~/.fops.json\n"));
|
|
127
127
|
return true;
|
|
128
128
|
}
|
package/src/auth/login.js
CHANGED
|
@@ -9,10 +9,10 @@ const ANTHROPIC_KEYS_URL = "https://console.anthropic.com/settings/keys";
|
|
|
9
9
|
|
|
10
10
|
export function authHelp() {
|
|
11
11
|
console.log(chalk.yellow("No API key found. Try one of:"));
|
|
12
|
-
console.log(chalk.
|
|
13
|
-
console.log(chalk.
|
|
14
|
-
console.log(chalk.
|
|
15
|
-
console.log(chalk.
|
|
12
|
+
console.log(chalk.dim(" • Open " + chalk.cyan(ANTHROPIC_KEYS_URL) + " in your browser to sign in and create a key"));
|
|
13
|
+
console.log(chalk.dim(" • ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
|
|
14
|
+
console.log(chalk.dim(" • ~/.claude/.credentials.json with anthropic_api_key or apiKey"));
|
|
15
|
+
console.log(chalk.dim(" • ~/.claude/settings.json with apiKeyHelper (script that prints the key)\n"));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function openBrowser(url) {
|
|
@@ -46,7 +46,7 @@ export function saveApiKey(apiKey) {
|
|
|
46
46
|
fs.writeFileSync(CLAUDE_CREDENTIALS, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
47
47
|
|
|
48
48
|
console.log(chalk.green("\nLogin successful"));
|
|
49
|
-
console.log(chalk.
|
|
49
|
+
console.log(chalk.dim("Key saved to ~/.claude/.credentials.json\n"));
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -65,11 +65,11 @@ export async function offerClaudeLogin() {
|
|
|
65
65
|
if (!opened) {
|
|
66
66
|
console.log(chalk.yellow(" Could not open browser. Visit: " + ANTHROPIC_KEYS_URL + "\n"));
|
|
67
67
|
}
|
|
68
|
-
console.log(chalk.
|
|
69
|
-
console.log(chalk.
|
|
70
|
-
console.log(chalk.
|
|
71
|
-
console.log(chalk.
|
|
72
|
-
console.log(chalk.
|
|
68
|
+
console.log(chalk.dim(" 1. Sign in and create an API key"));
|
|
69
|
+
console.log(chalk.dim(" 2. Add it to ~/.claude/.credentials.json:"));
|
|
70
|
+
console.log(chalk.dim(' { "anthropic_api_key": "sk-ant-..." }'));
|
|
71
|
+
console.log(chalk.dim(" 3. Or run: export ANTHROPIC_API_KEY=\"sk-ant-...\""));
|
|
72
|
+
console.log(chalk.dim(" 4. Then run foundation chat again.\n"));
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -310,7 +310,7 @@ export async function runLogin(options = {}) {
|
|
|
310
310
|
},
|
|
311
311
|
]);
|
|
312
312
|
if (!overwrite) {
|
|
313
|
-
console.log(chalk.
|
|
313
|
+
console.log(chalk.dim("Keeping existing credentials."));
|
|
314
314
|
return true;
|
|
315
315
|
}
|
|
316
316
|
}
|
|
@@ -405,8 +405,8 @@ async function runDeviceLogin() {
|
|
|
405
405
|
const url = `http://127.0.0.1:${port}`;
|
|
406
406
|
|
|
407
407
|
console.log(chalk.blue("\nOpening browser for authentication...\n"));
|
|
408
|
-
console.log(chalk.
|
|
409
|
-
console.log(chalk.
|
|
408
|
+
console.log(chalk.dim(` If browser doesn't open, visit: ${chalk.cyan(url)}\n`));
|
|
409
|
+
console.log(chalk.dim(" Waiting for authentication..."));
|
|
410
410
|
|
|
411
411
|
openBrowser(url);
|
|
412
412
|
});
|
package/src/auth/oauth.js
CHANGED
|
@@ -153,9 +153,9 @@ export async function runOAuthLogin() {
|
|
|
153
153
|
|
|
154
154
|
console.log(chalk.green("\nLogin successful"));
|
|
155
155
|
if (process.platform === "darwin") {
|
|
156
|
-
console.log(chalk.
|
|
156
|
+
console.log(chalk.dim("Token saved to macOS Keychain\n"));
|
|
157
157
|
} else {
|
|
158
|
-
console.log(chalk.
|
|
158
|
+
console.log(chalk.dim("Token saved to ~/.claude/.credentials.json\n"));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
server.close();
|
|
@@ -187,8 +187,8 @@ export async function runOAuthLogin() {
|
|
|
187
187
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
188
188
|
|
|
189
189
|
console.log(chalk.blue("\nOpening browser for Claude login...\n"));
|
|
190
|
-
console.log(chalk.
|
|
191
|
-
console.log(chalk.
|
|
190
|
+
console.log(chalk.dim(` If browser doesn't open, visit:\n ${chalk.cyan(authUrl.toString())}\n`));
|
|
191
|
+
console.log(chalk.dim(" Waiting for authentication..."));
|
|
192
192
|
|
|
193
193
|
openBrowser(authUrl.toString());
|
|
194
194
|
});
|