Skip to content

Commit acc646c

Browse files
aadesh18BilalG1
andauthored
stack-cli: cloud/local init flow, auto-create on empty projects, post-setup next-steps (#1383)
### Summary Reworks `stack init` UX, adds Sentry error reporting to the CLI, polishes the emulator start flow, and overhauls the local-emulator dashboard's "Open config file" dialog. #### `stack init` flow - **New top-level flow.** Drops the old "link existing vs. create new local" fork. `init` now asks *where* to create the project — "Stack Auth Cloud" or "Local". Adds a new `create-cloud` mode that logs the user in, creates a cloud project, mints keys, and writes `.env` — no round-trip through the dashboard. - **Conditional emulator-install warning.** The "Local" choice label only shows "(requires local emulator installation, ~1.3gb storage required)" when the QEMU image isn't already on disk; otherwise it shows "(emulator already installed)". Driven by a new `isEmulatorImageInstalled()` helper in `commands/emulator.ts`. - **Auto-create on zero-projects.** When the link-from-cloud path hits an empty project list, the CLI now prompts *"You don't have any Stack Auth projects yet. Would you like to create one?"* and, on yes, runs the same flow as `stack project create`. Skips the pointless "select a project" prompt when we just created one. - **MCP-server notice.** Before invoking the coding agent, the CLI announces that it's also registering the Stack Auth MCP server (`mcp.stack-auth.com`) so the agent can answer Stack-specific questions going forward. - **Local-emulator env header.** When `writeProjectKeysToEnv` runs in `local` mode it writes a 3-line comment header above the keys explaining they're emulator-only and only valid while the emulator is running. - **"What's next" footer.** After setup finishes, prints a short orientation block: where the sign-up/sign-in routes live (`/handler/sign-up`, `/handler/sign-in`), how to start the local emulator (for `create` mode), a dashboard deep link for cloud projects (respects `STACK_DASHBOARD_URL`), and a docs link. #### Sentry error reporting (`lib/sentry.ts`, `index.ts`, `tsdown.config.ts`) - New `lib/sentry.ts` initializes `@sentry/node` with PII scrubbing (Stack key prefixes, JWTs, home-dir paths, sensitive field names like `token`/`secret`/`password`/`dsn`). - DSN is baked at build time via a tsdown `define` sentinel (`__STACK_CLI_SENTRY_DSN__`) — no DSN in source, no runtime env-var dependency for installed users. CI sets `STACK_CLI_SENTRY_DSN_BUILD` before `pnpm build`. - Disabled when `NODE_ENV=development` or `CI`. No user opt-out. - Wired into `main()`'s catch (only for unexpected errors — `CliError`/`AuthError` still print and exit cleanly) plus `uncaughtException` and `unhandledRejection` handlers via a `handleFatal` helper. #### `stack emulator start` welcome - After a fresh start (not when reusing a running VM, not when `--config-file` keeps stdout JSON-only), prints a short "Emulator is up" block with service URLs (dashboard / backend / inbucket) and common commands (`status`, `stop`, `reset`, `run`). #### Local-emulator dashboard "Open config file" dialog The dialog at `http://localhost:26700` (when no project is loaded) used to be a single text input asking for an absolute path, with no explanation of where that path comes from. **Backend** (`apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx`): - POST is now tolerant of directory paths or paths that don't end in `.ts`/`.js`/`.mjs` — it appends `stack.config.ts` and creates the file if missing (`writeConfigToFile` mkdir's parents). Lets users paste a project folder instead of hunting for the config file. - New GET endpoint returns up to 20 most-recent `LocalEmulatorProject` rows joined with their display names, sorted by `updatedAt` desc. Same `isLocalEmulatorEnabled()` + client-auth gating as POST. **Dashboard** (`apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx`): - Title changed to "Open your Stack Auth project". Description now explicitly ties the file to `stack init`: *"Point the local dashboard at the `stack.config.ts` in your project. If you just ran `stack init`, it was created at the root of that project."* - Added: *"Don't have one yet? Paste your project folder path instead and we'll create stack.config.ts for you."* - Recent-projects list (clickable rows that prefill the input) fetched from the new GET endpoint when the dialog opens. - OS-specific copy-path tip below the input (macOS ⌥-Copy as Pathname, Windows Shift+RC Copy as path, Linux `realpath`). - "Open project" button is disabled when the input is empty. - All error paths (empty input, non-absolute path, server errors, exceptions) surface via destructive toasts instead of throwing. Why no native file picker: browsers do not expose absolute filesystem paths from `<input type="file">`, drag-and-drop, or the File System Access API. The backend requires an absolute path, so a Finder-style picker isn't possible from a web page. The recent list + OS tips are the workaround. ### Goal The previous `init` flow dead-ended new users: if you had no project you got an error telling you to go create one in the dashboard and come back. The happy path also forced a choice between "link existing" and "create local emulator" — not the question most users are trying to answer. The emulator dashboard's open-project dialog had similar friction: an unexplained path field with no recall of previously-opened projects. And the CLI silently swallowed unexpected errors with no telemetry. This branch makes the first-run path work end-to-end from the terminal, gives the emulator dashboard a usable open-project surface, and turns CLI crashes into actionable bug reports. ### How to review - Start with `packages/stack-cli/src/commands/init.ts` — the whole user-facing flow lives in `runInit`. Mode dispatch at the top, `handleCreateCloud` is the new cloud branch, `printNextSteps` is the footer, the MCP notice prints right before `runClaudeAgent`. - `packages/stack-cli/src/lib/sentry.ts` is small and self-contained; the sentinel-replacement contract is in `tsdown.config.ts`'s `define` block. Confirm `dist/index.js` contains zero `__STACK_CLI_SENTRY_DSN__` occurrences after a build with the env var unset, and the actual DSN host after a build with it set. - `packages/stack-cli/src/commands/emulator.ts` — `printEmulatorWelcome()` is the welcome block; `isEmulatorImageInstalled()` is the new exported helper used by `init.ts`. - `apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx` — the directory-tolerance branch is in the POST handler around the `looksLikeConfigFile` check; the GET handler is appended at the bottom. - `apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx` — dialog markup, recent-list fetch effect, `pathCopyTip` memo, and the toast-based error handling in `handleOpenConfigFile`. - Non-interactive (CI) paths stay strict: empty-project list still errors with a pointer to `stack project create --display-name`. No surprise project creation in CI. - No tests. The CLI has no harness for the interactive flow; verification is manual. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Recent local emulator projects listed in the config dialog for quick selection. * New CLI create-cloud mode and --display-name flag; interactive cloud project creation and clearer next steps. * Emulator start shows a welcome banner with service URLs when a new instance starts. * **Improvements** * Config dialog UX, validation, error-toasting, and platform-aware copy refined; “Open project” disabled for empty/invalid paths. * CLI: centralized interactive project creation and improved fatal error handling. * **Chores** * Sentry added and initialized for CLI error reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Bilal Godil <bg2002@gmail.com>
1 parent 6eaf492 commit acc646c

11 files changed

Lines changed: 665 additions & 171 deletions

File tree

apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
LOCAL_EMULATOR_ADMIN_USER_ID,
55
LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE,
66
LOCAL_EMULATOR_OWNER_TEAM_ID,
7-
isLocalEmulatorOnboardingEnabledInConfig,
87
isLocalEmulatorEnabled,
8+
isLocalEmulatorOnboardingEnabledInConfig,
99
readConfigFromFile,
1010
resolveEmulatorPath,
11+
writeConfigToFile,
1112
writeShowOnboardingConfigToFile,
1213
} from "@/lib/local-emulator";
1314
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
@@ -18,6 +19,7 @@ import {
1819
projectOnboardingStatusSchema,
1920
projectOnboardingStatusValues,
2021
type ProjectOnboardingStatus,
22+
yupArray,
2123
yupBoolean,
2224
yupNumber,
2325
yupObject,
@@ -37,6 +39,14 @@ function isProjectOnboardingStatus(value: string): value is ProjectOnboardingSta
3739
return projectOnboardingStatusValues.some((status) => status === value);
3840
}
3941

42+
function deriveDisplayLabel(absoluteFilePath: string): string {
43+
const base = path.basename(absoluteFilePath);
44+
if (base.toLowerCase() === "stack.config.ts") {
45+
return path.basename(path.dirname(absoluteFilePath)) || base;
46+
}
47+
return base;
48+
}
49+
4050
async function assertLocalEmulatorOwnerTeamReadiness() {
4151
const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
4252
const internalPrisma = await getPrismaClientForTenancy(internalTenancy);
@@ -90,7 +100,7 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom
90100
update: {},
91101
create: {
92102
id: projectId,
93-
displayName: `Local Emulator: ${path.basename(absoluteFilePath) || "Project"}`,
103+
displayName: `Local Emulator: ${deriveDisplayLabel(absoluteFilePath) || "Project"}`,
94104
description: `Local emulator project for ${absoluteFilePath}`,
95105
isProductionMode: false,
96106
ownerTeamId: LOCAL_EMULATOR_OWNER_TEAM_ID,
@@ -287,14 +297,30 @@ export const POST = createSmartRouteHandler({
287297
if (!isLocalEmulatorEnabled()) {
288298
throw new StatusError(StatusError.BadRequest, LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE);
289299
}
290-
if (!path.isAbsolute(req.body.absolute_file_path)) {
291-
throw new StatusError(StatusError.BadRequest, "absolute_file_path must be an absolute path.");
300+
if (!path.posix.isAbsolute(req.body.absolute_file_path)) {
301+
const looksWindows = path.win32.isAbsolute(req.body.absolute_file_path);
302+
throw new StatusError(
303+
StatusError.BadRequest,
304+
looksWindows
305+
? "absolute_file_path must be a POSIX absolute path. The local emulator runs in a Linux VM and does not accept Windows-style paths. Use the in-VM path or run the emulator from WSL."
306+
: "absolute_file_path must be an absolute path.",
307+
);
292308
}
293309

294-
const absoluteFilePath = path.resolve(req.body.absolute_file_path);
295-
const resolvedFilePath = resolveEmulatorPath(absoluteFilePath);
310+
const inputPath = path.resolve(req.body.absolute_file_path);
311+
let inputStat;
312+
try {
313+
inputStat = await fs.stat(resolveEmulatorPath(inputPath));
314+
} catch {
315+
inputStat = undefined;
316+
}
296317

297-
// Validate file exists before creating a project
318+
const looksLikeConfigFile = /\.(ts|js|mjs)$/i.test(inputPath);
319+
const absoluteFilePath = (inputStat?.isDirectory() || (!inputStat && !looksLikeConfigFile))
320+
? path.join(inputPath, "stack.config.ts")
321+
: inputPath;
322+
323+
const resolvedFilePath = resolveEmulatorPath(absoluteFilePath);
298324
let fileExists: boolean;
299325
try {
300326
await fs.access(resolvedFilePath);
@@ -303,7 +329,7 @@ export const POST = createSmartRouteHandler({
303329
fileExists = false;
304330
}
305331
if (!fileExists) {
306-
throw new StatusError(StatusError.BadRequest, `Config file not found: ${absoluteFilePath}`);
332+
await writeConfigToFile(absoluteFilePath, {});
307333
}
308334

309335
const fileContent = await fs.readFile(resolvedFilePath, "utf-8");
@@ -335,3 +361,71 @@ export const POST = createSmartRouteHandler({
335361
};
336362
},
337363
});
364+
365+
type LocalEmulatorProjectListRow = {
366+
projectId: string,
367+
absoluteFilePath: string,
368+
updatedAt: Date,
369+
};
370+
371+
export const GET = createSmartRouteHandler({
372+
metadata: {
373+
hidden: true,
374+
summary: "List recent local emulator projects",
375+
description: "Returns previously opened local emulator project mappings, most-recent first.",
376+
tags: ["Local Emulator"],
377+
},
378+
request: yupObject({
379+
auth: yupObject({
380+
type: clientOrHigherAuthTypeSchema.defined(),
381+
project: yupObject({
382+
id: yupString().oneOf(["internal"]).defined(),
383+
}).defined(),
384+
}).defined(),
385+
method: yupString().oneOf(["GET"]).defined(),
386+
}),
387+
response: yupObject({
388+
statusCode: yupNumber().oneOf([200]).defined(),
389+
bodyType: yupString().oneOf(["json"]).defined(),
390+
body: yupObject({
391+
projects: yupArray(yupObject({
392+
project_id: yupString().defined(),
393+
absolute_file_path: yupString().defined(),
394+
display_name: yupString().defined(),
395+
}).defined()).defined(),
396+
}).defined(),
397+
}),
398+
handler: async () => {
399+
if (!isLocalEmulatorEnabled()) {
400+
throw new StatusError(StatusError.BadRequest, LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE);
401+
}
402+
403+
const rows = await globalPrismaClient.$queryRaw<LocalEmulatorProjectListRow[]>(Prisma.sql`
404+
SELECT "projectId", "absoluteFilePath", "updatedAt"
405+
FROM "LocalEmulatorProject"
406+
ORDER BY "updatedAt" DESC
407+
LIMIT 20
408+
`);
409+
410+
const projectIds = rows.map((r) => r.projectId);
411+
const projects = projectIds.length > 0
412+
? await globalPrismaClient.project.findMany({
413+
where: { id: { in: projectIds } },
414+
select: { id: true, displayName: true },
415+
})
416+
: [];
417+
const displayNameById = new Map(projects.map((p) => [p.id, p.displayName]));
418+
419+
return {
420+
statusCode: 200 as const,
421+
bodyType: "json" as const,
422+
body: {
423+
projects: rows.map((r) => ({
424+
project_id: r.projectId,
425+
absolute_file_path: r.absoluteFilePath,
426+
display_name: displayNameById.get(r.projectId) ?? deriveDisplayLabel(r.absoluteFilePath),
427+
})),
428+
},
429+
};
430+
},
431+
});

apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export default function PageClient() {
6666
const [openConfigFileDialog, setOpenConfigFileDialog] = useState(false);
6767
const [absoluteConfigFilePath, setAbsoluteConfigFilePath] = useState("");
6868
const [openingConfigFile, setOpeningConfigFile] = useState(false);
69+
const [recentConfigProjects, setRecentConfigProjects] = useState<Array<{ project_id: string, absolute_file_path: string, display_name: string }>>([]);
70+
const [recentConfigProjectsError, setRecentConfigProjectsError] = useState(false);
6971
const [projectStatuses, setProjectStatuses] = useState<Map<string, ProjectOnboardingStatus>>(new Map());
7072
const [loadingProjectStatuses, setLoadingProjectStatuses] = useState(true);
7173
const [projectDau, setProjectDau] = useState<Map<string, { date: string, activity: number }[]>>(new Map());
@@ -157,17 +159,77 @@ export default function PageClient() {
157159
};
158160
}, [appInternals, rawProjects.length]);
159161

162+
useEffect(() => {
163+
if (!openConfigFileDialog || !isLocalEmulator) return;
164+
let cancelled = false;
165+
setRecentConfigProjectsError(false);
166+
runAsynchronously(async () => {
167+
try {
168+
const response = await appInternals.sendRequest("/internal/local-emulator/project", { method: "GET" }, "client");
169+
if (!response.ok) {
170+
if (!cancelled) {
171+
setRecentConfigProjects([]);
172+
setRecentConfigProjectsError(true);
173+
}
174+
return;
175+
}
176+
const body = await response.json() as { projects?: unknown };
177+
if (cancelled) return;
178+
if (!Array.isArray(body.projects)) {
179+
throw new Error("Invalid recent-projects payload");
180+
}
181+
const parsed = body.projects.map((p: unknown): { project_id: string, absolute_file_path: string, display_name: string } => {
182+
if (
183+
!p || typeof p !== "object"
184+
|| typeof (p as Record<string, unknown>).project_id !== "string"
185+
|| typeof (p as Record<string, unknown>).absolute_file_path !== "string"
186+
|| typeof (p as Record<string, unknown>).display_name !== "string"
187+
) {
188+
throw new Error("Invalid recent-projects payload");
189+
}
190+
const r = p as Record<string, string>;
191+
return { project_id: r.project_id, absolute_file_path: r.absolute_file_path, display_name: r.display_name };
192+
});
193+
setRecentConfigProjects(parsed);
194+
} catch {
195+
if (!cancelled) {
196+
setRecentConfigProjects([]);
197+
setRecentConfigProjectsError(true);
198+
}
199+
}
200+
});
201+
return () => {
202+
cancelled = true;
203+
};
204+
}, [openConfigFileDialog, isLocalEmulator, appInternals]);
205+
206+
const pathCopyTip = useMemo(() => {
207+
const p = typeof navigator !== "undefined" ? navigator.platform : "";
208+
if (/Mac|iPhone|iPad|iPod/i.test(p)) {
209+
return "Tip: in Finder, right-click the file → hold ⌥ Option → Copy as Pathname, then paste here.";
210+
}
211+
if (/Win/i.test(p)) {
212+
return "Note: the emulator runs in a Linux VM and needs a POSIX path. From WSL, run `wslpath -a stack.config.ts` (or `realpath stack.config.ts`) and paste that here.";
213+
}
214+
return "Tip: from your project folder, run `realpath stack.config.ts` in a terminal.";
215+
}, []);
216+
160217
const handleOpenConfigFile = async () => {
161218
const trimmedPath = absoluteConfigFilePath.trim();
162219
if (trimmedPath.length === 0) {
163-
throw new Error("Please enter an absolute config file path.");
220+
toast({ description: "Please enter a path to your project or stack.config.ts.", variant: "destructive" });
221+
return;
164222
}
165223

166-
const hasUnixAbsolutePath = trimmedPath.startsWith("/");
167-
const hasWindowsAbsolutePath = /^[a-zA-Z]:[\\/]/.test(trimmedPath);
168-
const hasWindowsUncPath = trimmedPath.startsWith("\\\\");
169-
if (!hasUnixAbsolutePath && !hasWindowsAbsolutePath && !hasWindowsUncPath) {
170-
throw new Error("Config file path must be absolute.");
224+
if (!trimmedPath.startsWith("/")) {
225+
const looksWindows = /^[a-zA-Z]:[\\/]/.test(trimmedPath) || trimmedPath.startsWith("\\\\");
226+
toast({
227+
description: looksWindows
228+
? "The local emulator runs in a Linux VM and only accepts POSIX paths (e.g. /Users/you/project). Windows paths aren't supported — use WSL or the in-VM path."
229+
: "The path must be absolute (e.g. /Users/you/project or /Users/you/project/stack.config.ts).",
230+
variant: "destructive",
231+
});
232+
return;
171233
}
172234

173235
setOpeningConfigFile(true);
@@ -188,19 +250,20 @@ export default function PageClient() {
188250
const responseBody = await response.json();
189251

190252
if (!response.ok) {
253+
let message = "Couldn't open that path. Make sure it points to your project folder or a valid stack.config.ts.";
191254
if (typeof responseBody === "string" && responseBody.length > 0) {
192-
throw new Error(responseBody);
193-
}
194-
if (
255+
message = responseBody;
256+
} else if (
195257
responseBody != null &&
196258
typeof responseBody === "object" &&
197259
"error" in responseBody &&
198260
typeof responseBody.error === "string" &&
199261
responseBody.error.length > 0
200262
) {
201-
throw new Error(responseBody.error);
263+
message = responseBody.error;
202264
}
203-
throw new Error("Failed to open config file project in local emulator.");
265+
toast({ description: message, variant: "destructive" });
266+
return;
204267
}
205268

206269
if (
@@ -209,7 +272,8 @@ export default function PageClient() {
209272
!("project_id" in responseBody) ||
210273
typeof responseBody.project_id !== "string"
211274
) {
212-
throw new Error("Local emulator endpoint returned an invalid response.");
275+
toast({ description: "Local emulator endpoint returned an invalid response.", variant: "destructive" });
276+
return;
213277
}
214278
const onboardingStatus = "onboarding_status" in responseBody
215279
? responseBody.onboarding_status
@@ -232,6 +296,11 @@ export default function PageClient() {
232296
router.push(`/new-project?project_id=${encodeURIComponent(responseBody.project_id)}`);
233297
}
234298
await wait(2000);
299+
} catch (e) {
300+
toast({
301+
description: e instanceof Error ? e.message : "Something went wrong opening that project.",
302+
variant: "destructive",
303+
});
235304
} finally {
236305
setOpeningConfigFile(false);
237306
}
@@ -302,7 +371,7 @@ export default function PageClient() {
302371
router.push("/new-project");
303372
return await wait(2000);
304373
}}
305-
>{isLocalEmulator ? "Open config file" : "Create Project"}
374+
>{isLocalEmulator ? "Open a project" : "Create Project"}
306375
</Button>
307376
</div>
308377
</div>
@@ -318,24 +387,57 @@ export default function PageClient() {
318387
>
319388
<DialogContent className="sm:max-w-[520px]">
320389
<DialogHeader>
321-
<DialogTitle>Open config file</DialogTitle>
390+
<DialogTitle>Open your Stack Auth project</DialogTitle>
322391
</DialogHeader>
323392
<div className="space-y-3">
324393
<Typography variant="secondary">
325-
Enter the absolute path to your local Stack config file. The local emulator will create or reuse the mapped project and open it in the dashboard.
394+
Point the local dashboard at the <code>stack.config.ts</code> in your project. If you just ran <code>stack init</code>, it was created at the root of that project.
395+
</Typography>
396+
<Typography variant="secondary" className="text-xs">
397+
Don&apos;t have one yet? Paste your project folder path instead and we&apos;ll create <code>stack.config.ts</code> for you.
326398
</Typography>
399+
{recentConfigProjects.length > 0 && (
400+
<div className="space-y-1">
401+
<Typography variant="secondary" className="text-xs uppercase tracking-wide">Recent</Typography>
402+
<div className="max-h-40 overflow-y-auto rounded-md border">
403+
{recentConfigProjects.map((p) => (
404+
<button
405+
key={p.project_id}
406+
type="button"
407+
className="block w-full truncate px-3 py-2 text-left text-sm hover:bg-muted"
408+
onClick={() => setAbsoluteConfigFilePath(p.absolute_file_path)}
409+
title={p.absolute_file_path}
410+
>
411+
{p.absolute_file_path}
412+
</button>
413+
))}
414+
</div>
415+
</div>
416+
)}
417+
{recentConfigProjectsError && recentConfigProjects.length === 0 && (
418+
<Typography variant="secondary" className="text-xs text-destructive">
419+
Couldn&apos;t load recent projects. Paste a path below to continue.
420+
</Typography>
421+
)}
327422
<Input
328423
autoFocus
329424
placeholder="/Users/you/project/stack.config.ts"
330425
value={absoluteConfigFilePath}
331426
onChange={(event) => setAbsoluteConfigFilePath(event.target.value)}
332427
/>
428+
<Typography variant="secondary" className="text-xs">
429+
{pathCopyTip}
430+
</Typography>
333431
</div>
334432
<DialogFooter className="pt-2">
335433
<Button variant="outline" onClick={() => setOpenConfigFileDialog(false)} disabled={openingConfigFile}>
336434
Cancel
337435
</Button>
338-
<Button onClick={handleOpenConfigFile} loading={openingConfigFile}>
436+
<Button
437+
onClick={handleOpenConfigFile}
438+
loading={openingConfigFile}
439+
disabled={absoluteConfigFilePath.trim().length === 0}
440+
>
339441
Open project
340442
</Button>
341443
</DialogFooter>

packages/stack-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dependencies": {
3030
"@anthropic-ai/claude-agent-sdk": "^0.2.73",
3131
"@inquirer/prompts": "^7.0.0",
32+
"@sentry/node": "^10.42.0",
3233
"@stackframe/js": "workspace:*",
3334
"@stackframe/stack-shared": "workspace:*",
3435
"commander": "^13.1.0",

0 commit comments

Comments
 (0)