stack-agent 0.3.0 → 0.3.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.
- package/README.md +7 -19
- package/dist/index.js +86 -51
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -5,22 +5,16 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
AI-powered CLI that helps developers choose and scaffold full-stack applications through
|
|
8
|
+
AI-powered CLI that helps developers choose and scaffold full-stack applications through a fullscreen terminal UI.
|
|
9
9
|
|
|
10
|
-
A senior software architect in your terminal — it
|
|
10
|
+
A senior software architect in your terminal — it recommends your entire stack, lets you review and refine each decision, then scaffolds the project with integration code.
|
|
11
11
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
1. **Conversation** — The agent asks what you're building, then guides you through frontend, backend, database, auth, payments, AI/LLM, and deployment choices
|
|
15
|
-
2. **Recommendations** — Each stage presents 2-3 options with a recommended pick and trade-off context
|
|
16
|
-
3. **Review** — Once all decisions are made, the agent presents your full stack for approval
|
|
17
|
-
4. **Scaffold** — The agent runs official tools (create-next-app, create-vite, etc.) and generates integration code grounded by current documentation
|
|
18
|
-
|
|
19
|
-
## Quick start
|
|
12
|
+
## Usage
|
|
20
13
|
|
|
21
14
|
```bash
|
|
22
15
|
export ANTHROPIC_API_KEY=your-key-here
|
|
23
|
-
npx stack-agent
|
|
16
|
+
npx stack-agent # Start or resume a session
|
|
17
|
+
npx stack-agent --fresh # Clear saved progress and start over
|
|
24
18
|
```
|
|
25
19
|
|
|
26
20
|
## Requirements
|
|
@@ -28,19 +22,13 @@ npx stack-agent
|
|
|
28
22
|
- Node.js 20+
|
|
29
23
|
- An [Anthropic API key](https://console.anthropic.com/settings/keys)
|
|
30
24
|
|
|
31
|
-
## What it does
|
|
32
|
-
|
|
33
|
-
- Delegates base scaffolding to official framework CLIs (create-next-app, create-vite, etc.)
|
|
34
|
-
- Generates integration code (auth, database, payments) using Claude, grounded by up-to-date documentation via MCP
|
|
35
|
-
- Writes `.env.example` with required environment variables
|
|
36
|
-
- Installs dependencies automatically
|
|
37
|
-
|
|
38
25
|
## Development
|
|
39
26
|
|
|
40
27
|
```bash
|
|
41
28
|
npm install
|
|
42
29
|
npm run dev # Run with tsx
|
|
43
|
-
npm
|
|
30
|
+
npm run mockup # Run interactive TUI mockup (no LLM calls)
|
|
31
|
+
npm test # Run tests (176 tests)
|
|
44
32
|
npm run build # Build with tsup
|
|
45
33
|
```
|
|
46
34
|
|
package/dist/index.js
CHANGED
|
@@ -262,12 +262,12 @@ function deserializeSession(json) {
|
|
|
262
262
|
|
|
263
263
|
// src/cli/components/stage-list.tsx
|
|
264
264
|
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
265
|
-
function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
265
|
+
function StageListView({ stages, currentStageId, progress, showFresh = false, onResult }) {
|
|
266
266
|
const [showConfirm, setShowConfirm] = useState3(false);
|
|
267
267
|
const [showWarning, setShowWarning] = useState3("");
|
|
268
268
|
const [cursor, setCursor] = useState3(0);
|
|
269
269
|
const canBuild = isComplete(progress);
|
|
270
|
-
const itemCount = stages.length + 1;
|
|
270
|
+
const itemCount = stages.length + 1 + (showFresh ? 1 : 0);
|
|
271
271
|
useInput2((input, key) => {
|
|
272
272
|
if (showConfirm) return;
|
|
273
273
|
if (key.upArrow) {
|
|
@@ -281,13 +281,15 @@ function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
|
281
281
|
if (key.return) {
|
|
282
282
|
if (cursor < stages.length) {
|
|
283
283
|
onResult({ kind: "select", stageId: stages[cursor].id });
|
|
284
|
-
} else {
|
|
284
|
+
} else if (cursor === stages.length) {
|
|
285
285
|
if (canBuild) {
|
|
286
286
|
setShowConfirm(true);
|
|
287
287
|
} else {
|
|
288
288
|
const missing = getMissingDecisions(progress);
|
|
289
289
|
setShowWarning(`Complete ${missing.join(", ")} first.`);
|
|
290
290
|
}
|
|
291
|
+
} else {
|
|
292
|
+
onResult({ kind: "fresh" });
|
|
291
293
|
}
|
|
292
294
|
}
|
|
293
295
|
if (key.escape) {
|
|
@@ -325,6 +327,10 @@ function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
|
325
327
|
requiredRemaining(progress),
|
|
326
328
|
" remaining)"
|
|
327
329
|
] })
|
|
330
|
+
] }),
|
|
331
|
+
showFresh && /* @__PURE__ */ jsxs3(Box5, { children: [
|
|
332
|
+
/* @__PURE__ */ jsx5(Text5, { children: cursor === stages.length + 1 ? "\u276F " : " " }),
|
|
333
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Start fresh" })
|
|
328
334
|
] })
|
|
329
335
|
] });
|
|
330
336
|
}
|
|
@@ -471,10 +477,10 @@ function ScaffoldView({ steps }) {
|
|
|
471
477
|
function createBridge() {
|
|
472
478
|
const listeners = /* @__PURE__ */ new Map();
|
|
473
479
|
let pendingResolve = null;
|
|
474
|
-
function emit(event, ...
|
|
480
|
+
function emit(event, ...args) {
|
|
475
481
|
const set = listeners.get(event);
|
|
476
482
|
if (set) {
|
|
477
|
-
for (const fn of set) fn(...
|
|
483
|
+
for (const fn of set) fn(...args);
|
|
478
484
|
}
|
|
479
485
|
}
|
|
480
486
|
return {
|
|
@@ -511,20 +517,48 @@ import { join as join2 } from "path";
|
|
|
511
517
|
import Anthropic from "@anthropic-ai/sdk";
|
|
512
518
|
|
|
513
519
|
// src/util/logger.ts
|
|
514
|
-
|
|
515
|
-
var
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
520
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
521
|
+
var currentLevel = process.env.LOG_LEVEL ?? "info";
|
|
522
|
+
function log(level, component, msg, data) {
|
|
523
|
+
if (LEVELS[level] < LEVELS[currentLevel]) return;
|
|
524
|
+
const prefix = `[${level}] [${component}]`;
|
|
525
|
+
const line = data ? `${prefix} ${msg} ${JSON.stringify(data)}` : `${prefix} ${msg}`;
|
|
526
|
+
process.stderr.write(line + "\n");
|
|
527
|
+
}
|
|
519
528
|
function createLogger(component) {
|
|
520
|
-
return
|
|
529
|
+
return {
|
|
530
|
+
debug: (data, msg) => log("debug", component, msg, data),
|
|
531
|
+
info: (data, msg) => log("info", component, msg, data),
|
|
532
|
+
warn: (data, msg) => log("warn", component, msg, data),
|
|
533
|
+
error: (data, msg) => log("error", component, msg, data)
|
|
534
|
+
};
|
|
521
535
|
}
|
|
522
536
|
|
|
523
537
|
// src/llm/client.ts
|
|
524
|
-
var
|
|
538
|
+
var log2 = createLogger("llm");
|
|
525
539
|
function summarizeMessages(messages) {
|
|
526
540
|
return `${messages.length} messages, last: ${messages.at(-1)?.role ?? "none"}`;
|
|
527
541
|
}
|
|
542
|
+
function withMessageCaching(messages) {
|
|
543
|
+
if (messages.length < 2) return messages;
|
|
544
|
+
const cached = messages.map((m, i) => {
|
|
545
|
+
if (i !== messages.length - 2) return m;
|
|
546
|
+
const content = m.content;
|
|
547
|
+
if (typeof content === "string") {
|
|
548
|
+
return { ...m, content: [{ type: "text", text: content, cache_control: { type: "ephemeral" } }] };
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(content)) {
|
|
551
|
+
const blocks = [...content];
|
|
552
|
+
const last = blocks[blocks.length - 1];
|
|
553
|
+
if (last) {
|
|
554
|
+
blocks[blocks.length - 1] = { ...last, cache_control: { type: "ephemeral" } };
|
|
555
|
+
}
|
|
556
|
+
return { ...m, content: blocks };
|
|
557
|
+
}
|
|
558
|
+
return m;
|
|
559
|
+
});
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
528
562
|
function getClient() {
|
|
529
563
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
530
564
|
if (!apiKey) {
|
|
@@ -543,7 +577,7 @@ function client() {
|
|
|
543
577
|
}
|
|
544
578
|
async function chat(options) {
|
|
545
579
|
const { system, messages, tools, maxTokens, mcpServers } = options;
|
|
546
|
-
|
|
580
|
+
log2.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chat request");
|
|
547
581
|
let response;
|
|
548
582
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
549
583
|
const mcpServerList = Object.entries(mcpServers).map(([name, config]) => ({
|
|
@@ -573,39 +607,39 @@ async function chat(options) {
|
|
|
573
607
|
response = await client().messages.create({
|
|
574
608
|
model: "claude-sonnet-4-6",
|
|
575
609
|
max_tokens: maxTokens,
|
|
576
|
-
system,
|
|
577
|
-
messages,
|
|
610
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
611
|
+
messages: withMessageCaching(messages),
|
|
578
612
|
...tools && tools.length > 0 && { tools }
|
|
579
613
|
});
|
|
580
614
|
}
|
|
581
|
-
|
|
615
|
+
log2.info({
|
|
582
616
|
stopReason: response.stop_reason,
|
|
583
617
|
contentBlocks: response.content.length,
|
|
584
618
|
usage: response.usage
|
|
585
619
|
}, "chat response");
|
|
586
|
-
|
|
620
|
+
log2.debug({ content: response.content }, "chat response content");
|
|
587
621
|
return response;
|
|
588
622
|
}
|
|
589
623
|
async function chatStream(options, callbacks) {
|
|
590
624
|
const { system, messages, tools, maxTokens } = options;
|
|
591
|
-
|
|
625
|
+
log2.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chatStream request");
|
|
592
626
|
const stream = client().messages.stream({
|
|
593
627
|
model: "claude-sonnet-4-6",
|
|
594
628
|
max_tokens: maxTokens,
|
|
595
|
-
system,
|
|
596
|
-
messages,
|
|
629
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
630
|
+
messages: withMessageCaching(messages),
|
|
597
631
|
...tools && tools.length > 0 && { tools }
|
|
598
632
|
});
|
|
599
633
|
stream.on("text", (text) => {
|
|
600
634
|
callbacks.onText(text);
|
|
601
635
|
});
|
|
602
636
|
const finalMessage = await stream.finalMessage();
|
|
603
|
-
|
|
637
|
+
log2.info({
|
|
604
638
|
stopReason: finalMessage.stop_reason,
|
|
605
639
|
contentBlocks: finalMessage.content.length,
|
|
606
640
|
usage: finalMessage.usage
|
|
607
641
|
}, "chatStream response");
|
|
608
|
-
|
|
642
|
+
log2.debug({ content: finalMessage.content }, "chatStream response content");
|
|
609
643
|
for (const block of finalMessage.content) {
|
|
610
644
|
if (block.type === "tool_use") {
|
|
611
645
|
callbacks.onToolUse(block);
|
|
@@ -905,7 +939,7 @@ Do not ask for confirmation. Proceed through all steps automatically.`;
|
|
|
905
939
|
import { execFileSync } from "child_process";
|
|
906
940
|
import { readdirSync, existsSync } from "fs";
|
|
907
941
|
import { join } from "path";
|
|
908
|
-
function runScaffold(tool,
|
|
942
|
+
function runScaffold(tool, args, projectName, cwd) {
|
|
909
943
|
const outputDir = join(cwd, projectName);
|
|
910
944
|
if (existsSync(outputDir)) {
|
|
911
945
|
const entries = readdirSync(outputDir);
|
|
@@ -915,7 +949,7 @@ function runScaffold(tool, args2, projectName, cwd) {
|
|
|
915
949
|
);
|
|
916
950
|
}
|
|
917
951
|
}
|
|
918
|
-
const spawnArgs = [`${tool}@latest`, projectName, ...
|
|
952
|
+
const spawnArgs = [`${tool}@latest`, projectName, ...args];
|
|
919
953
|
const opts = { cwd, stdio: "pipe" };
|
|
920
954
|
execFileSync("npx", spawnArgs, opts);
|
|
921
955
|
return outputDir;
|
|
@@ -1203,7 +1237,7 @@ async function runScaffoldLoop(progress, onProgress, mcpServers) {
|
|
|
1203
1237
|
}
|
|
1204
1238
|
|
|
1205
1239
|
// src/agent/recommend.ts
|
|
1206
|
-
var
|
|
1240
|
+
var log3 = createLogger("recommend");
|
|
1207
1241
|
var RECOMMEND_PROMPT = `You are a senior software architect. Based on the project description, recommend a complete technology stack.
|
|
1208
1242
|
|
|
1209
1243
|
For each category, provide your recommendation as a JSON object. If a category is not needed for this project, set it to null.
|
|
@@ -1237,15 +1271,15 @@ Description: ${description}`
|
|
|
1237
1271
|
maxTokens: 1024
|
|
1238
1272
|
});
|
|
1239
1273
|
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1240
|
-
|
|
1241
|
-
|
|
1274
|
+
log3.info({ rawLength: text.length }, "received recommendation response");
|
|
1275
|
+
log3.debug({ rawText: text }, "recommendation raw text");
|
|
1242
1276
|
const jsonStr = text.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1243
1277
|
const parsed = JSON.parse(jsonStr);
|
|
1244
1278
|
const categories = Object.keys(parsed).filter((k) => parsed[k] !== null);
|
|
1245
|
-
|
|
1279
|
+
log3.info({ recommended: categories, skipped: Object.keys(parsed).filter((k) => parsed[k] === null) }, "parsed recommendations");
|
|
1246
1280
|
return parsed;
|
|
1247
1281
|
} catch (err) {
|
|
1248
|
-
|
|
1282
|
+
log3.error({ err }, "recommendation pass failed");
|
|
1249
1283
|
return {};
|
|
1250
1284
|
}
|
|
1251
1285
|
}
|
|
@@ -1290,7 +1324,7 @@ function applyRecommendations(progress, stages, recommendations) {
|
|
|
1290
1324
|
|
|
1291
1325
|
// src/cli/app.tsx
|
|
1292
1326
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1293
|
-
function App({ manager, onBuild, onExit }) {
|
|
1327
|
+
function App({ manager, isResumed, onBuild, onFresh, onExit }) {
|
|
1294
1328
|
const app = useApp();
|
|
1295
1329
|
const { width, height } = useScreenSize();
|
|
1296
1330
|
const [view, setView] = useState5(
|
|
@@ -1372,6 +1406,9 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1372
1406
|
syncState();
|
|
1373
1407
|
}
|
|
1374
1408
|
runStage(result.stageId);
|
|
1409
|
+
} else if (result.kind === "fresh") {
|
|
1410
|
+
onFresh();
|
|
1411
|
+
app.exit();
|
|
1375
1412
|
} else if (result.kind === "build") {
|
|
1376
1413
|
setView("scaffold");
|
|
1377
1414
|
setScaffoldSteps([]);
|
|
@@ -1458,6 +1495,7 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1458
1495
|
stages,
|
|
1459
1496
|
currentStageId: currentStage?.id ?? null,
|
|
1460
1497
|
progress,
|
|
1498
|
+
showFresh: isResumed,
|
|
1461
1499
|
onResult: handleStageResult
|
|
1462
1500
|
}
|
|
1463
1501
|
),
|
|
@@ -1992,63 +2030,60 @@ What needs to change?`;
|
|
|
1992
2030
|
}
|
|
1993
2031
|
};
|
|
1994
2032
|
}
|
|
1995
|
-
async function main(
|
|
2033
|
+
async function main(startFresh = false) {
|
|
1996
2034
|
const cwd = process.cwd();
|
|
1997
2035
|
const invalidationFn = createInvalidationFn();
|
|
1998
2036
|
process.on("SIGINT", () => {
|
|
1999
2037
|
process.exit(0);
|
|
2000
2038
|
});
|
|
2001
|
-
if (
|
|
2039
|
+
if (startFresh) {
|
|
2002
2040
|
const tempManager = StageManager.resume(cwd);
|
|
2003
2041
|
tempManager?.cleanup();
|
|
2004
|
-
console.log("Session cleared. Starting fresh.\n");
|
|
2005
2042
|
}
|
|
2006
2043
|
let manager;
|
|
2007
|
-
const
|
|
2008
|
-
if (
|
|
2009
|
-
console.log(`
|
|
2010
|
-
Found saved progress for "${existingSession.progress.projectName ?? "unnamed"}"`);
|
|
2011
|
-
console.log("Run with --fresh to start over.\n");
|
|
2044
|
+
const isResumed = !startFresh && StageManager.detect(cwd) !== null;
|
|
2045
|
+
if (isResumed) {
|
|
2012
2046
|
const resumed = StageManager.resume(cwd, invalidationFn);
|
|
2013
|
-
|
|
2014
|
-
console.log("Could not restore session. Starting fresh.");
|
|
2015
|
-
manager = StageManager.start(cwd, invalidationFn);
|
|
2016
|
-
} else {
|
|
2017
|
-
manager = resumed;
|
|
2018
|
-
}
|
|
2047
|
+
manager = resumed ?? StageManager.start(cwd, invalidationFn);
|
|
2019
2048
|
} else {
|
|
2020
2049
|
manager = StageManager.start(cwd, invalidationFn);
|
|
2021
2050
|
}
|
|
2022
2051
|
let shouldBuild = false;
|
|
2052
|
+
let shouldStartFresh = false;
|
|
2023
2053
|
const ink = withFullScreen(
|
|
2024
2054
|
React6.createElement(App, {
|
|
2025
2055
|
manager,
|
|
2056
|
+
isResumed,
|
|
2026
2057
|
onBuild: () => {
|
|
2027
2058
|
shouldBuild = true;
|
|
2028
2059
|
},
|
|
2060
|
+
onFresh: () => {
|
|
2061
|
+
shouldStartFresh = true;
|
|
2062
|
+
},
|
|
2029
2063
|
onExit: () => {
|
|
2030
|
-
shouldBuild = false;
|
|
2031
2064
|
}
|
|
2032
2065
|
})
|
|
2033
2066
|
);
|
|
2034
2067
|
await ink.start();
|
|
2035
2068
|
await ink.waitUntilExit();
|
|
2069
|
+
if (shouldStartFresh) {
|
|
2070
|
+
manager.cleanup();
|
|
2071
|
+
return main(true);
|
|
2072
|
+
}
|
|
2036
2073
|
if (shouldBuild) {
|
|
2037
2074
|
const readiness = manager.progress.deployment ? checkDeployReadiness(manager.progress.deployment.component) : null;
|
|
2038
2075
|
renderPostScaffold(manager.progress.projectName, readiness);
|
|
2039
2076
|
console.log("\nHappy building!\n");
|
|
2040
2077
|
}
|
|
2041
2078
|
}
|
|
2042
|
-
var
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
if (!command || command === "init" || command === "--fresh") {
|
|
2046
|
-
main(isFresh).catch((err) => {
|
|
2079
|
+
var command = process.argv[2];
|
|
2080
|
+
if (!command || command === "init") {
|
|
2081
|
+
main().catch((err) => {
|
|
2047
2082
|
console.error(err);
|
|
2048
2083
|
process.exit(1);
|
|
2049
2084
|
});
|
|
2050
2085
|
} else {
|
|
2051
2086
|
console.error(`Unknown command: ${command}`);
|
|
2052
|
-
console.error("Usage: stack-agent [init]
|
|
2087
|
+
console.error("Usage: stack-agent [init]");
|
|
2053
2088
|
process.exit(1);
|
|
2054
2089
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stack-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "AI-powered CLI that helps developers choose and scaffold full-stack applications through conversational interaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"ink": "^6.8.0",
|
|
47
47
|
"marked": "^15.0.12",
|
|
48
48
|
"marked-terminal": "^7.3.0",
|
|
49
|
-
"pino": "^10.3.1",
|
|
50
49
|
"react": "^19.2.4",
|
|
51
50
|
"zod": "^4.3.6"
|
|
52
51
|
},
|
|
@@ -55,7 +54,6 @@
|
|
|
55
54
|
"@types/node": "^25.5.0",
|
|
56
55
|
"@types/react": "^19.2.14",
|
|
57
56
|
"ink-testing-library": "^4.0.0",
|
|
58
|
-
"pino-pretty": "^13.1.3",
|
|
59
57
|
"tsup": "^8.5.1",
|
|
60
58
|
"tsx": "^4.21.0",
|
|
61
59
|
"typescript": "^5.9.3",
|