@tostudy-ai/cli 0.11.0 → 0.11.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/dist/cli.js CHANGED
@@ -104,7 +104,7 @@ var require_picocolors = __commonJS({
104
104
  }
105
105
  });
106
106
 
107
- // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-CHajMTDD.js
107
+ // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-CG4i4G4l.mjs
108
108
  function formatJson(entry) {
109
109
  const { timestamp: timestamp2, level, message, service, environment, context, data, error: error49 } = entry;
110
110
  const output3 = {
@@ -284,8 +284,8 @@ function extractErrorInfo(error49, includeStack) {
284
284
  return info;
285
285
  }
286
286
  var import_picocolors, LOG_LEVELS, LEVEL_COLORS, LEVEL_WIDTH, SENSITIVE_FIELDS, sanitize, BaseLogger;
287
- var init_logger_base_CHajMTDD = __esm({
288
- "../../node_modules/@parisgroup-ai/logger/dist/logger.base-CHajMTDD.js"() {
287
+ var init_logger_base_CG4i4G4l = __esm({
288
+ "../../node_modules/@parisgroup-ai/logger/dist/logger.base-CG4i4G4l.mjs"() {
289
289
  import_picocolors = __toESM(require_picocolors(), 1);
290
290
  LOG_LEVELS = {
291
291
  trace: 0,
@@ -541,7 +541,7 @@ var init_logger_base_CHajMTDD = __esm({
541
541
  }
542
542
  });
543
543
 
544
- // ../../node_modules/@parisgroup-ai/logger/dist/index.js
544
+ // ../../node_modules/@parisgroup-ai/logger/dist/index.mjs
545
545
  import { AsyncLocalStorage } from "node:async_hooks";
546
546
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
547
547
  import { join } from "node:path";
@@ -610,8 +610,8 @@ function createLogger(module, options) {
610
610
  }
611
611
  var asyncLocalStorage, ConsoleTransport, FileTransport, HttpTransport, SINGLETON_KEY, Logger, logger;
612
612
  var init_dist = __esm({
613
- "../../node_modules/@parisgroup-ai/logger/dist/index.js"() {
614
- init_logger_base_CHajMTDD();
613
+ "../../node_modules/@parisgroup-ai/logger/dist/index.mjs"() {
614
+ init_logger_base_CG4i4G4l();
615
615
  asyncLocalStorage = new AsyncLocalStorage();
616
616
  ConsoleTransport = class {
617
617
  log(entry, formatted) {
@@ -859,6 +859,21 @@ function createHttpProvider(apiUrl, token2) {
859
859
  const res = await apiFetch(`${base}/courses`, token2);
860
860
  return res.courses;
861
861
  },
862
+ listCreatorCourses: async (filters) => {
863
+ const params = new URLSearchParams();
864
+ params.append("status", filters.status || "all");
865
+ if (filters.search) params.append("search", filters.search);
866
+ if (filters.sortBy) params.append("sortBy", filters.sortBy);
867
+ if (filters.sortOrder) params.append("sortOrder", filters.sortOrder);
868
+ if (filters.limit) params.append("limit", filters.limit.toString());
869
+ if (filters.offset) params.append("offset", filters.offset.toString());
870
+ if (filters.projectId) params.append("projectId", filters.projectId);
871
+ const res = await apiFetch(
872
+ `${base}/creator-courses?${params.toString()}`,
873
+ token2
874
+ );
875
+ return res.courses;
876
+ },
862
877
  select: async (_userId, courseId) => {
863
878
  const res = await apiFetch(`${base}/courses/select`, token2, {
864
879
  method: "POST",
@@ -1265,12 +1280,16 @@ function formatCourseList(courses3) {
1265
1280
  month: "short",
1266
1281
  day: "numeric"
1267
1282
  }) : null;
1268
- lines.push(` ${idx + 1}. ${course.title}`);
1283
+ const archivedBadge = course.status === "archived" ? " [ARQUIVADO]" : "";
1284
+ lines.push(` ${idx + 1}. ${course.title}${archivedBadge}`);
1269
1285
  lines.push(` ${bar}`);
1270
1286
  lines.push(` Professor: ${course.creatorName}`);
1271
1287
  if (enrolledDate) {
1272
1288
  lines.push(` Inscrito em: ${enrolledDate}`);
1273
1289
  }
1290
+ if (course.status === "archived") {
1291
+ lines.push(` \u26A0 Arquivado \u2014 somente leitura (use \`tostudy lesson\`)`);
1292
+ }
1274
1293
  lines.push("");
1275
1294
  });
1276
1295
  lines.push("\u2192 tostudy select <n\xFAmero> para ativar um curso");
@@ -2303,7 +2322,8 @@ var init_errors_pt_br = __esm({
2303
2322
  vaultNotFound: "\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n",
2304
2323
  logoutSuccess: "\n Deslogado com sucesso.\n",
2305
2324
  workspaceCommandDescription: "Gerenciar workspace de estudo local",
2306
- workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo"
2325
+ workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo",
2326
+ insufficientCredits: "\u274C Voc\xEA est\xE1 sem cr\xE9ditos. Cada resposta do tutor de IA consome cr\xE9ditos para cobrir o processamento. Recarregue em https://tostudy.ai/student/credits para continuar de onde parou.\n"
2307
2327
  };
2308
2328
  }
2309
2329
  });
@@ -2320,7 +2340,8 @@ var init_errors_en_us = __esm({
2320
2340
  vaultNotFound: "\u274C Vault not found. Run 'tostudy vault init' first.\n",
2321
2341
  logoutSuccess: "\n Logged out successfully.\n",
2322
2342
  workspaceCommandDescription: "Manage local study workspace",
2323
- workspaceSetupDescription: "Create the workspace structure for the active course"
2343
+ workspaceSetupDescription: "Create the workspace structure for the active course",
2344
+ insufficientCredits: "\u274C You're out of credits. Every reply from the AI tutor uses credits to cover processing. Top up at https://tostudy.ai/student/credits to continue from where you stopped.\n"
2324
2345
  };
2325
2346
  }
2326
2347
  });
@@ -2348,6 +2369,10 @@ function resolveLocale() {
2348
2369
  function getErrors(locale) {
2349
2370
  return BUNDLES[locale ?? resolveLocale()];
2350
2371
  }
2372
+ function isInsufficientCreditsError(err) {
2373
+ const msg = err instanceof Error ? err.message : String(err);
2374
+ return msg.includes("INSUFFICIENT_CREDITS");
2375
+ }
2351
2376
  var BUNDLES, _cachedLocale;
2352
2377
  var init_errors = __esm({
2353
2378
  "src/errors/index.ts"() {
@@ -2544,7 +2569,7 @@ var CLI_VERSION;
2544
2569
  var init_version = __esm({
2545
2570
  "src/version.ts"() {
2546
2571
  "use strict";
2547
- CLI_VERSION = true ? "0.11.0" : "0.7.1";
2572
+ CLI_VERSION = true ? "0.11.1" : "0.7.1";
2548
2573
  }
2549
2574
  });
2550
2575
 
@@ -2947,16 +2972,24 @@ var init_courses2 = __esm({
2947
2972
  init_guards();
2948
2973
  init_formatter();
2949
2974
  logger4 = createLogger("cli:courses");
2950
- coursesCommand = new Command5("courses").description("List your enrolled courses with progress").option("--json", "Output structured JSON").action(async (opts) => {
2975
+ coursesCommand = new Command5("courses").description("List your enrolled courses with progress").option("--json", "Output structured JSON").option("--mine", "List only your own courses, including drafts").action(async (opts) => {
2951
2976
  try {
2952
2977
  const session = await requireSession();
2953
2978
  const data = createHttpProvider(session.apiUrl, session.token);
2954
2979
  const deps = { data, logger: logger4 };
2955
- const courses3 = await listCourses({ userId: session.userId }, deps);
2980
+ let coursesToList;
2981
+ if (opts.mine) {
2982
+ coursesToList = await data.courses.listCreatorCourses({
2983
+ userId: session.userId,
2984
+ status: "all"
2985
+ });
2986
+ } else {
2987
+ coursesToList = await listCourses({ userId: session.userId }, deps);
2988
+ }
2956
2989
  if (opts.json) {
2957
- output(courses3, { json: true });
2990
+ output(coursesToList, { json: true });
2958
2991
  } else {
2959
- output(formatCourseList(courses3), { json: false });
2992
+ output(formatCourseList(coursesToList), { json: false });
2960
2993
  }
2961
2994
  } catch (err) {
2962
2995
  const msg = err instanceof Error ? err.message : String(err);
@@ -28729,6 +28762,12 @@ var init_users = __esm({
28729
28762
  // Stripe Integration
28730
28763
  stripeCustomerId: varchar("stripe_customer_id", { length: 255 }),
28731
28764
  // Stripe Customer ID (cus_xxx) for payment method reuse
28765
+ // Paris Group integration: server-to-server provisioning attribution.
28766
+ // Set by `POST /api/internal/v1/cohort-enrollments` so the same paris
28767
+ // participant maps idempotently to a single tostudy user, and outbound
28768
+ // webhooks can scope themselves to `provisioning_source='paris-immersion'`.
28769
+ provisioningSource: text("provisioning_source"),
28770
+ externalUserId: text("external_user_id"),
28732
28771
  metadata: jsonb("metadata").$type().default({}),
28733
28772
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
28734
28773
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
@@ -28747,7 +28786,12 @@ var init_users = __esm({
28747
28786
  // FEAT-754: Portfolio index for public portfolio queries
28748
28787
  portfolioEnabledIdx: index("users_portfolio_enabled_idx").on(table.portfolioEnabled),
28749
28788
  // Admin analytics: date-range queries on users.createdAt (gte/lte)
28750
- createdAtIdx: index("users_created_at_idx").on(table.createdAt)
28789
+ createdAtIdx: index("users_created_at_idx").on(table.createdAt),
28790
+ // Paris Group integration: composite lookup for idempotent provisioning.
28791
+ externalIdIdx: index("idx_users_external_id").on(
28792
+ table.provisioningSource,
28793
+ table.externalUserId
28794
+ )
28751
28795
  })
28752
28796
  );
28753
28797
  }
@@ -28888,6 +28932,8 @@ var init_user_preferences = __esm({
28888
28932
  emailNewReplies: boolean("email_new_replies").default(true).notNull(),
28889
28933
  /** Whether user wants push notifications for mentorship (via MCP) */
28890
28934
  pushMentoria: boolean("push_mentoria").default(true).notNull(),
28935
+ // i18n nickname pilot column (dogfooding)
28936
+ nicknameI18n: jsonb("nickname_i18n").$type(),
28891
28937
  // Timestamps
28892
28938
  createdAt: timestamp("created_at").defaultNow().notNull(),
28893
28939
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -29071,6 +29117,20 @@ var init_course_categories = __esm({
29071
29117
  }
29072
29118
  });
29073
29119
 
29120
+ // ../../packages/database/src/schema/course-visibility.ts
29121
+ var courseVisibilityEnum;
29122
+ var init_course_visibility = __esm({
29123
+ "../../packages/database/src/schema/course-visibility.ts"() {
29124
+ "use strict";
29125
+ init_pg_core();
29126
+ courseVisibilityEnum = pgEnum("course_visibility", [
29127
+ "public",
29128
+ "unlisted",
29129
+ "private_allowlist"
29130
+ ]);
29131
+ }
29132
+ });
29133
+
29074
29134
  // ../../packages/database/src/schema/courses.ts
29075
29135
  var courseStatusEnum, courseFormatEnum, courseLevelEnum, creatorReviewStatusEnum, studyChannelEnum, courses;
29076
29136
  var init_courses3 = __esm({
@@ -29079,6 +29139,7 @@ var init_courses3 = __esm({
29079
29139
  init_pg_core();
29080
29140
  init_users();
29081
29141
  init_course_categories();
29142
+ init_course_visibility();
29082
29143
  courseStatusEnum = pgEnum("course_status", [
29083
29144
  "draft",
29084
29145
  "pending_review",
@@ -29111,6 +29172,7 @@ var init_courses3 = __esm({
29111
29172
  type: varchar("type", { length: 50 }),
29112
29173
  // "self-paced" | "with-mentorship"
29113
29174
  status: courseStatusEnum("status").default("draft"),
29175
+ visibility: courseVisibilityEnum("visibility").default("public").notNull(),
29114
29176
  featured: boolean("featured").default(false).notNull(),
29115
29177
  verified: boolean("verified").default(false).notNull(),
29116
29178
  isEssential: boolean("is_essential").default(false).notNull(),
@@ -29206,7 +29268,20 @@ var init_courses3 = __esm({
29206
29268
  // Widened to 800 in BUG-1346 (GH #382) — `deriveCertificateSummary` can
29207
29269
  // return up to 203 chars (truncated + "..."), and AI-path edge cases
29208
29270
  // exceeded varchar(200) silently. 800 covers worst observed (~660) + buffer.
29209
- certificateSummary: varchar("certificate_summary", { length: 800 })
29271
+ certificateSummary: varchar("certificate_summary", { length: 800 }),
29272
+ /**
29273
+ * paris-immersion onboarding gate: true quando o curso foi endossado
29274
+ * pela equipe Paris para ser servido como onboarding de turmas. Default
29275
+ * false. NÃO afeta o marketplace ou a publicação normal — apenas o
29276
+ * endpoint `/api/internal/v1/courses?tag=paris-onboarding&endorsed=true`
29277
+ * filtra por este campo.
29278
+ *
29279
+ * Auto-marcado true quando o creator está em `paris_authorized_creators`
29280
+ * E o curso tem `tags` contendo 'paris-onboarding' (ver application/paris-endorsement.ts).
29281
+ * Admins paris-immersion podem endossar manualmente via
29282
+ * POST /api/internal/v1/courses/:slug/endorse.
29283
+ */
29284
+ parisEndorsed: boolean("paris_endorsed").default(false).notNull()
29210
29285
  },
29211
29286
  (table) => ({
29212
29287
  creatorIdIdx: index("courses_creator_id_idx").on(table.creatorId),
@@ -29233,6 +29308,8 @@ var init_courses3 = __esm({
29233
29308
  // B-tree for FIFO ordering
29234
29309
  statusSubmittedIdx: index("courses_status_submitted_idx").on(table.status, table.submittedAt),
29235
29310
  // Composite for queue queries
29311
+ visibilityIdx: index("courses_visibility_idx").on(table.visibility),
29312
+ statusVisibilityIdx: index("courses_status_visibility_idx").on(table.status, table.visibility),
29236
29313
  // Course completeness tracking index
29237
29314
  completenessStatusIdx: index("courses_completeness_status_idx").on(table.completenessStatus),
29238
29315
  // Verified Seal Quality System - Indexes for quality metrics filtering
@@ -29770,6 +29847,9 @@ var init_badges = __esm({
29770
29847
  // Link to specific module (for module badges)
29771
29848
  isEssentialBadge: boolean("is_essential_badge").default(false).notNull(),
29772
29849
  // Flag for essential course badges
29850
+ // i18n dynamic translation pilot columns (Expand pattern)
29851
+ nameI18n: jsonb("name_i18n").$type(),
29852
+ descriptionI18n: jsonb("description_i18n").$type(),
29773
29853
  // Display
29774
29854
  order: integer("order").notNull().default(0),
29775
29855
  // Display order
@@ -30203,6 +30283,34 @@ var init_course_reports = __esm({
30203
30283
  }
30204
30284
  });
30205
30285
 
30286
+ // ../../packages/database/src/schema/cohorts.ts
30287
+ var cohorts;
30288
+ var init_cohorts = __esm({
30289
+ "../../packages/database/src/schema/cohorts.ts"() {
30290
+ "use strict";
30291
+ init_pg_core();
30292
+ cohorts = pgTable(
30293
+ "cohorts",
30294
+ {
30295
+ id: uuid("id").defaultRandom().primaryKey(),
30296
+ slug: text("slug").notNull().unique(),
30297
+ name: text("name").notNull(),
30298
+ /** e.g. "paris-immersion" — namespace for the external system that owns this cohort. */
30299
+ source: text("source").notNull(),
30300
+ /** Stable identifier in the source system (e.g. immersion slug). */
30301
+ sourceExternalId: text("source_external_id").notNull(),
30302
+ startsAt: timestamp("starts_at", { withTimezone: true }).notNull(),
30303
+ endsAt: timestamp("ends_at", { withTimezone: true }).notNull(),
30304
+ metadata: jsonb("metadata").$type().notNull().default({}),
30305
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
30306
+ },
30307
+ (table) => ({
30308
+ sourceIdx: index("idx_cohorts_source").on(table.source, table.sourceExternalId)
30309
+ })
30310
+ );
30311
+ }
30312
+ });
30313
+
30206
30314
  // ../../packages/database/src/schema/enrollments.ts
30207
30315
  var enrollmentStatusEnum, enrollmentSourceEnum, exerciseEntryLevelEnum, EXERCISE_ENTRY_LEVELS, enrollments;
30208
30316
  var init_enrollments = __esm({
@@ -30211,6 +30319,7 @@ var init_enrollments = __esm({
30211
30319
  init_pg_core();
30212
30320
  init_users();
30213
30321
  init_courses3();
30322
+ init_cohorts();
30214
30323
  enrollmentStatusEnum = pgEnum("enrollment_status", [
30215
30324
  "active",
30216
30325
  "preview",
@@ -30264,6 +30373,10 @@ var init_enrollments = __esm({
30264
30373
  // adjusts via the `enrollments.setExerciseLevel` mutation.
30265
30374
  entryLevel: exerciseEntryLevelEnum("entry_level").notNull().default("L4"),
30266
30375
  currentLevel: exerciseEntryLevelEnum("current_level").notNull().default("L4"),
30376
+ // Paris Group integration: links a provisioned enrollment to its cohort.
30377
+ // Set when tostudy provisions an account via `POST /api/internal/v1/cohort-enrollments`.
30378
+ // Nullable for non-cohort enrollments (self-enrollment, subscriptions, etc.).
30379
+ cohortId: uuid("cohort_id").references(() => cohorts.id, { onDelete: "set null" }),
30267
30380
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
30268
30381
  metadata: jsonb("metadata").$type().default({})
30269
30382
  },
@@ -30282,7 +30395,9 @@ var init_enrollments = __esm({
30282
30395
  courseCurrentLevelIdx: index("enrollments_course_current_level_idx").on(
30283
30396
  table.courseId,
30284
30397
  table.currentLevel
30285
- )
30398
+ ),
30399
+ // Paris Group integration: partial index for cohort lookups.
30400
+ cohortIdIdx: index("idx_enrollments_cohort_id").on(table.cohortId)
30286
30401
  })
30287
30402
  );
30288
30403
  }
@@ -30443,6 +30558,29 @@ var init_course_index = __esm({
30443
30558
  }
30444
30559
  });
30445
30560
 
30561
+ // ../../packages/database/src/schema/paris-authorized-creators.ts
30562
+ var parisAuthorizedCreators;
30563
+ var init_paris_authorized_creators = __esm({
30564
+ "../../packages/database/src/schema/paris-authorized-creators.ts"() {
30565
+ "use strict";
30566
+ init_pg_core();
30567
+ init_users();
30568
+ parisAuthorizedCreators = pgTable(
30569
+ "paris_authorized_creators",
30570
+ {
30571
+ userId: uuid("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }),
30572
+ /** Quem aprovou (admin Paris). NULL se setado via seed/SQL direto. */
30573
+ authorizedBy: uuid("authorized_by").references(() => users.id, { onDelete: "set null" }),
30574
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
30575
+ notes: text("notes")
30576
+ },
30577
+ (table) => ({
30578
+ authorizedByIdx: index("idx_paris_authorized_creators_authorized_by").on(table.authorizedBy)
30579
+ })
30580
+ );
30581
+ }
30582
+ });
30583
+
30446
30584
  // ../../packages/database/src/schema/enrollment-channel-changes.ts
30447
30585
  var enrollmentChannelChanges;
30448
30586
  var init_enrollment_channel_changes = __esm({
@@ -33693,6 +33831,8 @@ var init_mentorship_services = __esm({
33693
33831
  // 30, 45, 60, 90
33694
33832
  priceCents: integer("price_cents").notNull(),
33695
33833
  // min 5000 for 1:1, 3000 for group/workshop
33834
+ currency: varchar("currency", { length: 3 }).notNull().default("BRL"),
33835
+ // ISO 4217 uppercase; Phase 1 threads through DTOs
33696
33836
  priceCredits: integer("price_credits"),
33697
33837
  // nullable during migration, NOT NULL after Phase 3
33698
33838
  maxParticipants: integer("max_participants").default(1).notNull(),
@@ -33943,37 +34083,50 @@ var init_mentorship_packages = __esm({
33943
34083
  init_users();
33944
34084
  init_mentorship_services();
33945
34085
  init_zod();
33946
- mentorshipPackages2 = pgTable("mentorship_packages", {
33947
- id: uuid("id").defaultRandom().primaryKey(),
33948
- mentorId: uuid("mentor_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
33949
- serviceId: uuid("service_id").references(() => mentorshipServices2.id, { onDelete: "cascade" }).notNull(),
33950
- // Package configuration
33951
- numSessions: integer("num_sessions").notNull(),
33952
- // 4, 8, or 12 (enforced by check constraint)
33953
- discountPercent: integer("discount_percent").notNull().default(0),
33954
- // 0-50 (enforced by check constraint)
33955
- validityMonths: integer("validity_months").notNull().default(6),
33956
- // 3, 6, or 12
33957
- // Calculated prices (stored for consistency and to avoid recalculation issues)
33958
- pricePerSessionCents: integer("price_per_session_cents").notNull(),
33959
- // Price per session after discount
33960
- totalPriceCents: integer("total_price_cents").notNull(),
33961
- // Total package price
33962
- // Status
33963
- isActive: boolean("is_active").notNull().default(true),
33964
- // Timestamps
33965
- createdAt: timestamp("created_at").defaultNow().notNull(),
33966
- updatedAt: timestamp("updated_at").defaultNow().notNull()
33967
- }, (table) => ({
33968
- mentorIdIdx: index("mentorship_packages_mentor_id_idx").on(table.mentorId),
33969
- serviceIdIdx: index("mentorship_packages_service_id_idx").on(table.serviceId),
33970
- isActiveIdx: index("mentorship_packages_is_active_idx").on(table.isActive),
33971
- mentorServiceActiveIdx: index("mentorship_packages_mentor_service_active_idx").on(table.mentorId, table.serviceId, table.isActive),
33972
- // Check constraints for business rules
33973
- numSessionsCheck: check("num_sessions_check", sql`${table.numSessions} IN (4, 8, 12)`),
33974
- discountPercentCheck: check("discount_percent_check", sql`${table.discountPercent} >= 0 AND ${table.discountPercent} <= 50`),
33975
- validityMonthsCheck: check("validity_months_check", sql`${table.validityMonths} IN (3, 6, 12)`)
33976
- }));
34086
+ mentorshipPackages2 = pgTable(
34087
+ "mentorship_packages",
34088
+ {
34089
+ id: uuid("id").defaultRandom().primaryKey(),
34090
+ mentorId: uuid("mentor_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
34091
+ serviceId: uuid("service_id").references(() => mentorshipServices2.id, { onDelete: "cascade" }).notNull(),
34092
+ // Package configuration
34093
+ numSessions: integer("num_sessions").notNull(),
34094
+ // 4, 8, or 12 (enforced by check constraint)
34095
+ discountPercent: integer("discount_percent").notNull().default(0),
34096
+ // 0-50 (enforced by check constraint)
34097
+ validityMonths: integer("validity_months").notNull().default(6),
34098
+ // 3, 6, or 12
34099
+ // Calculated prices (stored for consistency and to avoid recalculation issues)
34100
+ pricePerSessionCents: integer("price_per_session_cents").notNull(),
34101
+ // Price per session after discount
34102
+ totalPriceCents: integer("total_price_cents").notNull(),
34103
+ // Total package price
34104
+ currency: varchar("currency", { length: 3 }).notNull().default("BRL"),
34105
+ // ISO 4217 uppercase; Phase 2 threads through DTOs (derived from service.currency)
34106
+ // Status
34107
+ isActive: boolean("is_active").notNull().default(true),
34108
+ // Timestamps
34109
+ createdAt: timestamp("created_at").defaultNow().notNull(),
34110
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
34111
+ },
34112
+ (table) => ({
34113
+ mentorIdIdx: index("mentorship_packages_mentor_id_idx").on(table.mentorId),
34114
+ serviceIdIdx: index("mentorship_packages_service_id_idx").on(table.serviceId),
34115
+ isActiveIdx: index("mentorship_packages_is_active_idx").on(table.isActive),
34116
+ mentorServiceActiveIdx: index("mentorship_packages_mentor_service_active_idx").on(
34117
+ table.mentorId,
34118
+ table.serviceId,
34119
+ table.isActive
34120
+ ),
34121
+ // Check constraints for business rules
34122
+ numSessionsCheck: check("num_sessions_check", sql`${table.numSessions} IN (4, 8, 12)`),
34123
+ discountPercentCheck: check(
34124
+ "discount_percent_check",
34125
+ sql`${table.discountPercent} >= 0 AND ${table.discountPercent} <= 50`
34126
+ ),
34127
+ validityMonthsCheck: check("validity_months_check", sql`${table.validityMonths} IN (3, 6, 12)`)
34128
+ })
34129
+ );
33977
34130
  PACKAGE_SESSION_OPTIONS = [4, 8, 12];
33978
34131
  PACKAGE_VALIDITY_OPTIONS = [3, 6, 12];
33979
34132
  MAX_DISCOUNT_PERCENT = 50;
@@ -34207,7 +34360,7 @@ var init_mentorship_transactions = __esm({
34207
34360
  transactionType: mentorshipTransactionTypeEnum("transaction_type").default("session").notNull(),
34208
34361
  packageId: uuid("package_id").references(() => mentorshipPackages2.id, { onDelete: "set null" }),
34209
34362
  serviceId: uuid("service_id").references(() => mentorshipServices2.id, { onDelete: "set null" }),
34210
- // Financial amounts in cents (BRL) - Story 19.3: Split tracking
34363
+ // Financial amounts in cents - Story 19.3: Split tracking
34211
34364
  grossAmountCents: integer("gross_amount_cents").notNull(),
34212
34365
  // Original price before discount
34213
34366
  discountCents: integer("discount_cents").default(0).notNull(),
@@ -34216,6 +34369,8 @@ var init_mentorship_transactions = __esm({
34216
34369
  // 15% of net amount
34217
34370
  mentorPayoutCents: integer("mentor_payout_cents").notNull(),
34218
34371
  // 85% of net amount (netAmountCents)
34372
+ currency: varchar("currency", { length: 3 }).notNull().default("BRL"),
34373
+ // ISO 4217 uppercase; Phase 3 threads through DTOs (derived from package.currency / service.currency)
34219
34374
  // Transaction status
34220
34375
  status: mentorshipTransactionStatusEnum("status").default("pending").notNull(),
34221
34376
  // Stripe integration
@@ -34462,53 +34617,66 @@ var init_transactions = __esm({
34462
34617
  init_pg_core();
34463
34618
  init_courses3();
34464
34619
  init_users();
34465
- transactions2 = pgTable("transactions", {
34466
- id: uuid("id").defaultRandom().primaryKey(),
34467
- // Stripe references
34468
- stripePaymentIntentId: varchar("stripe_payment_intent_id", { length: 255 }).notNull().unique(),
34469
- stripeTransferId: varchar("stripe_transfer_id", { length: 255 }),
34470
- // Stripe Transfer ID (if separate transfer)
34471
- // Foreign keys
34472
- courseId: uuid("course_id").notNull().references(() => courses.id, { onDelete: "restrict" }),
34473
- creatorId: uuid("creator_id").notNull().references(() => users.id, { onDelete: "restrict" }),
34474
- studentId: uuid("student_id").notNull().references(() => users.id, { onDelete: "restrict" }),
34475
- // Financial details (all amounts in cents)
34476
- amount: integer("amount").notNull(),
34477
- // Total amount charged
34478
- platformFee: integer("platform_fee").notNull(),
34479
- // Amount kept by platform
34480
- creatorPayout: integer("creator_payout").notNull(),
34481
- // Amount transferred to creator
34482
- stripeFee: integer("stripe_fee"),
34483
- // Stripe processing fee (nullable, updated from webhook)
34484
- netPlatformRevenue: integer("net_platform_revenue"),
34485
- // platformFee - stripeFee
34486
- // Transaction metadata
34487
- type: varchar("type", { length: 20 }).notNull(),
34488
- // 'mentoria' or 'self-paced'
34489
- paymentMethod: varchar("payment_method", { length: 50 }),
34490
- // 'card' or 'pix'
34491
- status: varchar("status", { length: 20 }).notNull().default("pending"),
34492
- // 'pending', 'succeeded', 'failed', 'refunded'
34493
- // Timestamps
34494
- createdAt: timestamp("created_at").defaultNow().notNull(),
34495
- processedAt: timestamp("processed_at"),
34496
- refundedAt: timestamp("refunded_at"),
34497
- updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
34498
- }, (table) => ({
34499
- // Indexes for fast lookups
34500
- stripePaymentIntentIdIdx: index("transactions_stripe_payment_intent_id_idx").on(table.stripePaymentIntentId),
34501
- creatorIdIdx: index("transactions_creator_id_idx").on(table.creatorId),
34502
- studentIdIdx: index("transactions_student_id_idx").on(table.studentId),
34503
- statusIdx: index("transactions_status_idx").on(table.status),
34504
- paymentMethodIdx: index("transactions_payment_method_idx").on(table.paymentMethod),
34505
- // For PIX analytics
34506
- createdAtIdx: index("transactions_created_at_idx").on(table.createdAt),
34507
- // For analytics queries
34508
- refundedAtIdx: index("transactions_refunded_at_idx").on(table.refundedAt),
34509
- // Issue #4: Database Index Gaps - Composite index for refund lookups
34510
- refundLookupIdx: index("transactions_refund_lookup_idx").on(table.courseId, table.studentId, table.status, table.createdAt)
34511
- }));
34620
+ transactions2 = pgTable(
34621
+ "transactions",
34622
+ {
34623
+ id: uuid("id").defaultRandom().primaryKey(),
34624
+ // Stripe references
34625
+ stripePaymentIntentId: varchar("stripe_payment_intent_id", { length: 255 }).notNull().unique(),
34626
+ stripeTransferId: varchar("stripe_transfer_id", { length: 255 }),
34627
+ // Stripe Transfer ID (if separate transfer)
34628
+ // Foreign keys
34629
+ courseId: uuid("course_id").notNull().references(() => courses.id, { onDelete: "restrict" }),
34630
+ creatorId: uuid("creator_id").notNull().references(() => users.id, { onDelete: "restrict" }),
34631
+ studentId: uuid("student_id").notNull().references(() => users.id, { onDelete: "restrict" }),
34632
+ // Financial details (all amounts in cents)
34633
+ amount: integer("amount").notNull(),
34634
+ // Total amount charged
34635
+ currency: varchar("currency", { length: 3 }).notNull().default("BRL"),
34636
+ // ISO 4217 uppercase; seeded from Stripe charge.currency on webhook insert
34637
+ platformFee: integer("platform_fee").notNull(),
34638
+ // Amount kept by platform
34639
+ creatorPayout: integer("creator_payout").notNull(),
34640
+ // Amount transferred to creator
34641
+ stripeFee: integer("stripe_fee"),
34642
+ // Stripe processing fee (nullable, updated from webhook)
34643
+ netPlatformRevenue: integer("net_platform_revenue"),
34644
+ // platformFee - stripeFee
34645
+ // Transaction metadata
34646
+ type: varchar("type", { length: 20 }).notNull(),
34647
+ // 'mentoria' or 'self-paced'
34648
+ paymentMethod: varchar("payment_method", { length: 50 }),
34649
+ // 'card' or 'pix'
34650
+ status: varchar("status", { length: 20 }).notNull().default("pending"),
34651
+ // 'pending', 'succeeded', 'failed', 'refunded'
34652
+ // Timestamps
34653
+ createdAt: timestamp("created_at").defaultNow().notNull(),
34654
+ processedAt: timestamp("processed_at"),
34655
+ refundedAt: timestamp("refunded_at"),
34656
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
34657
+ },
34658
+ (table) => ({
34659
+ // Indexes for fast lookups
34660
+ stripePaymentIntentIdIdx: index("transactions_stripe_payment_intent_id_idx").on(
34661
+ table.stripePaymentIntentId
34662
+ ),
34663
+ creatorIdIdx: index("transactions_creator_id_idx").on(table.creatorId),
34664
+ studentIdIdx: index("transactions_student_id_idx").on(table.studentId),
34665
+ statusIdx: index("transactions_status_idx").on(table.status),
34666
+ paymentMethodIdx: index("transactions_payment_method_idx").on(table.paymentMethod),
34667
+ // For PIX analytics
34668
+ createdAtIdx: index("transactions_created_at_idx").on(table.createdAt),
34669
+ // For analytics queries
34670
+ refundedAtIdx: index("transactions_refunded_at_idx").on(table.refundedAt),
34671
+ // Issue #4: Database Index Gaps - Composite index for refund lookups
34672
+ refundLookupIdx: index("transactions_refund_lookup_idx").on(
34673
+ table.courseId,
34674
+ table.studentId,
34675
+ table.status,
34676
+ table.createdAt
34677
+ )
34678
+ })
34679
+ );
34512
34680
  }
34513
34681
  });
34514
34682
 
@@ -36827,7 +36995,16 @@ var init_security_audit_log = __esm({
36827
36995
  "admin.impersonate_start",
36828
36996
  "admin.impersonate_end",
36829
36997
  "admin.config_change",
36830
- "admin.student_progress_reset"
36998
+ "admin.student_progress_reset",
36999
+ // Paris integration events (PR #575)
37000
+ "account.claimed_by_external",
37001
+ "account.claim_denied_by_policy",
37002
+ "user.created_by_external",
37003
+ "authorized_creator.added",
37004
+ "authorized_creator.add_attempt_unknown_user",
37005
+ "authorized_creator.removed",
37006
+ "course.paris_endorsed",
37007
+ "course.paris_endorsement_revoked"
36831
37008
  ]);
36832
37009
  securityEventResultEnum = pgEnum("security_event_result", [
36833
37010
  "success",
@@ -40896,6 +41073,80 @@ var init_brainstorm_knowledge_bases = __esm({
40896
41073
  }
40897
41074
  });
40898
41075
 
41076
+ // ../../packages/database/src/schema/course-allowlist.ts
41077
+ var courseAllowlist;
41078
+ var init_course_allowlist = __esm({
41079
+ "../../packages/database/src/schema/course-allowlist.ts"() {
41080
+ "use strict";
41081
+ init_pg_core();
41082
+ init_courses3();
41083
+ init_users();
41084
+ courseAllowlist = pgTable(
41085
+ "course_allowlist",
41086
+ {
41087
+ id: uuid("id").defaultRandom().primaryKey(),
41088
+ courseId: uuid("course_id").references(() => courses.id, { onDelete: "cascade" }).notNull(),
41089
+ email: varchar("email", { length: 255 }).notNull(),
41090
+ userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
41091
+ invitedBy: uuid("invited_by").references(() => users.id, {
41092
+ onDelete: "set null"
41093
+ }),
41094
+ invitedAt: timestamp("invited_at", { withTimezone: true }).defaultNow().notNull(),
41095
+ resolvedAt: timestamp("resolved_at", { withTimezone: true })
41096
+ },
41097
+ (table) => ({
41098
+ courseEmailUnique: uniqueIndex("course_allowlist_course_email_unique").on(
41099
+ table.courseId,
41100
+ table.email
41101
+ ),
41102
+ courseIdIdx: index("course_allowlist_course_id_idx").on(table.courseId)
41103
+ // partial indexes (user_id IS NOT NULL / IS NULL) are created via raw SQL in the migration — see drizzle-kit $N gotcha in packages/database/AGENTS.md
41104
+ })
41105
+ );
41106
+ }
41107
+ });
41108
+
41109
+ // ../../node_modules/@parisgroup-ai/pg-backend/dist/idempotency-schema.mjs
41110
+ var pgBackendIdempotencyKeys;
41111
+ var init_idempotency_schema = __esm({
41112
+ "../../node_modules/@parisgroup-ai/pg-backend/dist/idempotency-schema.mjs"() {
41113
+ init_drizzle_orm();
41114
+ init_pg_core();
41115
+ pgBackendIdempotencyKeys = pgTable("pg_backend_idempotency_keys", {
41116
+ reservationId: uuid("reservation_id").primaryKey().defaultRandom(),
41117
+ idempotencyKey: text("idempotency_key").notNull(),
41118
+ resource: text("resource").notNull(),
41119
+ operation: text("operation").notNull(),
41120
+ fingerprint: text("fingerprint").notNull(),
41121
+ scopeKey: text("scope_key"),
41122
+ status: text("status").notNull(),
41123
+ response: jsonb("response"),
41124
+ reservedAt: timestamp("reserved_at", { withTimezone: true }).notNull().defaultNow(),
41125
+ committedAt: timestamp("committed_at", { withTimezone: true }),
41126
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
41127
+ }, (t) => ({
41128
+ uniqueKey: uniqueIndex("pg_backend_idempotency_unique").on(t.idempotencyKey, t.resource, t.operation, sql`COALESCE(${t.scopeKey}, '')`),
41129
+ expiresIdx: index("pg_backend_idempotency_expires").on(t.expiresAt),
41130
+ pendingIdx: index("pg_backend_idempotency_pending").on(t.status, t.reservedAt)
41131
+ }));
41132
+ }
41133
+ });
41134
+
41135
+ // ../../node_modules/@parisgroup-ai/pg-backend/dist/index.mjs
41136
+ var init_dist2 = __esm({
41137
+ "../../node_modules/@parisgroup-ai/pg-backend/dist/index.mjs"() {
41138
+ init_idempotency_schema();
41139
+ }
41140
+ });
41141
+
41142
+ // ../../packages/database/src/schema/idempotency.ts
41143
+ var init_idempotency = __esm({
41144
+ "../../packages/database/src/schema/idempotency.ts"() {
41145
+ "use strict";
41146
+ init_dist2();
41147
+ }
41148
+ });
41149
+
40899
41150
  // ../../packages/database/src/schema/index.ts
40900
41151
  var schema_exports = {};
40901
41152
  __export(schema_exports, {
@@ -40943,6 +41194,7 @@ __export(schema_exports, {
40943
41194
  challengeType: () => challengeType,
40944
41195
  challenges: () => challenges,
40945
41196
  challengesRelations: () => challengesRelations,
41197
+ cohorts: () => cohorts,
40946
41198
  communityComments: () => communityComments2,
40947
41199
  communityCommentsRelations: () => communityCommentsRelations2,
40948
41200
  communityLikes: () => communityLikes2,
@@ -40968,6 +41220,7 @@ __export(schema_exports, {
40968
41220
  couponUsagesRelations: () => couponUsagesRelations2,
40969
41221
  coupons: () => coupons2,
40970
41222
  couponsRelations: () => couponsRelations2,
41223
+ courseAllowlist: () => courseAllowlist,
40971
41224
  courseAssets: () => courseAssets,
40972
41225
  courseAssetsRelations: () => courseAssetsRelations,
40973
41226
  courseCategories: () => courseCategories,
@@ -41000,6 +41253,7 @@ __export(schema_exports, {
41000
41253
  courseValidations: () => courseValidations,
41001
41254
  courseVideoPrompts: () => courseVideoPrompts,
41002
41255
  courseVideoPromptsRelations: () => courseVideoPromptsRelations,
41256
+ courseVisibilityEnum: () => courseVisibilityEnum,
41003
41257
  courses: () => courses,
41004
41258
  coursesRelations: () => coursesRelations2,
41005
41259
  creatorApplications: () => creatorApplications2,
@@ -41158,6 +41412,7 @@ __export(schema_exports, {
41158
41412
  onboardingAnalytics: () => onboardingAnalytics2,
41159
41413
  onboardingAnalyticsRelations: () => onboardingAnalyticsRelations2,
41160
41414
  optimizerRoleEnum: () => optimizerRoleEnum,
41415
+ parisAuthorizedCreators: () => parisAuthorizedCreators,
41161
41416
  parseSettingValue: () => parseSettingValue,
41162
41417
  participantStatusEnum: () => participantStatusEnum,
41163
41418
  pathCourses: () => pathCourses2,
@@ -41177,6 +41432,7 @@ __export(schema_exports, {
41177
41432
  payoutWithdrawals: () => payoutWithdrawals,
41178
41433
  payouts: () => payouts2,
41179
41434
  payoutsRelations: () => payoutsRelations2,
41435
+ pgBackendIdempotencyKeys: () => pgBackendIdempotencyKeys,
41180
41436
  pixKeyTypeEnum: () => pixKeyTypeEnum,
41181
41437
  platformConfig: () => platformConfig,
41182
41438
  platformFeeTypeEnum: () => platformFeeTypeEnum,
@@ -41367,6 +41623,8 @@ var init_schema3 = __esm({
41367
41623
  "use strict";
41368
41624
  init_auth_index();
41369
41625
  init_course_index();
41626
+ init_cohorts();
41627
+ init_paris_authorized_creators();
41370
41628
  init_enrollment_channel_changes();
41371
41629
  init_badges();
41372
41630
  init_user_badges();
@@ -41457,6 +41715,9 @@ var init_schema3 = __esm({
41457
41715
  init_announcements();
41458
41716
  init_creator_brainstorm();
41459
41717
  init_brainstorm_knowledge_bases();
41718
+ init_course_visibility();
41719
+ init_course_allowlist();
41720
+ init_idempotency();
41460
41721
  }
41461
41722
  });
41462
41723
 
@@ -41469,6 +41730,14 @@ var init_check_module_unlocked = __esm({
41469
41730
  }
41470
41731
  });
41471
41732
 
41733
+ // ../../packages/database/src/utils/courses-filter.ts
41734
+ var init_courses_filter = __esm({
41735
+ "../../packages/database/src/utils/courses-filter.ts"() {
41736
+ "use strict";
41737
+ init_courses3();
41738
+ }
41739
+ });
41740
+
41472
41741
  // ../../packages/database/src/utils/date-helpers.ts
41473
41742
  var init_date_helpers = __esm({
41474
41743
  "../../packages/database/src/utils/date-helpers.ts"() {
@@ -41494,6 +41763,7 @@ var init_src4 = __esm({
41494
41763
  init_schema3();
41495
41764
  init_schema3();
41496
41765
  init_check_module_unlocked();
41766
+ init_courses_filter();
41497
41767
  init_date_helpers();
41498
41768
  init_lessons();
41499
41769
  init_mentorship_services();
@@ -41791,6 +42061,11 @@ async function runStart(opts, deps = defaultDeps3) {
41791
42061
  process.stderr.write("Acesse tostudy.ai para estudar este curso pelo ChatStudy.\n");
41792
42062
  process.exit(1);
41793
42063
  }
42064
+ if (err instanceof CliApiError && err.status === 403 && err.message === "COURSE_ARCHIVED") {
42065
+ process.stderr.write("Este curso foi arquivado e est\xE1 dispon\xEDvel somente para leitura.\n");
42066
+ process.stderr.write("Use `tostudy lesson` para revisar o conte\xFAdo j\xE1 estudado.\n");
42067
+ process.exit(1);
42068
+ }
41794
42069
  const msg = err instanceof Error ? err.message : String(err);
41795
42070
  deps.error(msg);
41796
42071
  }
@@ -41869,6 +42144,11 @@ var init_start_next = __esm({
41869
42144
  process.stderr.write("\u2192 tostudy courses para ver outros cursos\n");
41870
42145
  process.exit(0);
41871
42146
  }
42147
+ if (err instanceof CliApiError && err.status === 403 && err.message === "COURSE_ARCHIVED") {
42148
+ process.stderr.write("Este curso foi arquivado e est\xE1 dispon\xEDvel somente para leitura.\n");
42149
+ process.stderr.write("Use `tostudy lesson` para revisar o conte\xFAdo j\xE1 estudado.\n");
42150
+ process.exit(1);
42151
+ }
41872
42152
  const msg = err instanceof Error ? err.message : String(err);
41873
42153
  if (opts.json) jsonError(msg);
41874
42154
  error(msg);
@@ -41996,6 +42276,7 @@ var init_lesson = __esm({
41996
42276
  init_course_state();
41997
42277
  init_formatter();
41998
42278
  init_resolve();
42279
+ init_errors();
41999
42280
  logger12 = createLogger("cli:lesson");
42000
42281
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").action(async (opts) => {
42001
42282
  try {
@@ -42029,6 +42310,12 @@ var init_lesson = __esm({
42029
42310
  }
42030
42311
  }
42031
42312
  } catch (err) {
42313
+ if (isInsufficientCreditsError(err)) {
42314
+ const friendly = getErrors().insufficientCredits;
42315
+ if (opts.json) jsonError("insufficient_credits", { message: friendly });
42316
+ error(friendly);
42317
+ return;
42318
+ }
42032
42319
  const msg = err instanceof Error ? err.message : String(err);
42033
42320
  if (opts.json) jsonError(msg);
42034
42321
  error(msg);
@@ -42049,6 +42336,7 @@ var init_hint = __esm({
42049
42336
  init_guards();
42050
42337
  init_course_state();
42051
42338
  init_formatter();
42339
+ init_errors();
42052
42340
  logger13 = createLogger("cli:hint");
42053
42341
  hintCommand = new Command12("hint").description("Get a progressive hint for the current exercise").option("--json", "Output structured JSON").action(async (opts) => {
42054
42342
  try {
@@ -42068,6 +42356,12 @@ var init_hint = __esm({
42068
42356
  output(formatHint(hint), { json: false });
42069
42357
  }
42070
42358
  } catch (err) {
42359
+ if (isInsufficientCreditsError(err)) {
42360
+ const friendly = getErrors().insufficientCredits;
42361
+ if (opts.json) jsonError("insufficient_credits", { message: friendly });
42362
+ error(friendly);
42363
+ return;
42364
+ }
42071
42365
  const msg = err instanceof Error ? err.message : String(err);
42072
42366
  if (opts.json) jsonError(msg);
42073
42367
  error(msg);
@@ -42404,6 +42698,7 @@ var init_validate = __esm({
42404
42698
  init_course_state();
42405
42699
  init_formatter();
42406
42700
  init_init_template();
42701
+ init_errors();
42407
42702
  logger14 = createLogger("cli:validate");
42408
42703
  validateCommand = new Command13("validate").description("Validate your solution for the current exercise").argument("[file]", "Path to the solution file to read").option("--stdin", "Read solution from stdin instead of a file").option("--json", "Output structured JSON").action(async (file2, opts) => {
42409
42704
  try {
@@ -42485,6 +42780,12 @@ var init_validate = __esm({
42485
42780
  }
42486
42781
  process.exit(result.passed ? 0 : 1);
42487
42782
  } catch (err) {
42783
+ if (isInsufficientCreditsError(err)) {
42784
+ const friendly = getErrors().insufficientCredits;
42785
+ if (opts.json) jsonError("insufficient_credits", { message: friendly });
42786
+ error(friendly);
42787
+ return;
42788
+ }
42488
42789
  const msg = err instanceof Error ? err.message : String(err);
42489
42790
  if (opts.json) jsonError(msg);
42490
42791
  error(msg);
@@ -44286,6 +44587,7 @@ var init_theory = __esm({
44286
44587
  init_src();
44287
44588
  init_guards();
44288
44589
  init_formatter();
44590
+ init_errors();
44289
44591
  logger27 = createLogger("cli:theory");
44290
44592
  EXERCISE_LEVELS2 = ["L0", "L1", "L2", "L3", "L4"];
44291
44593
  theoryCommand = new Command28("theory").description("Request more theory on the current lesson (escape hatch)").option("--focus <text>", "Optional focus area in PT-BR (max 500 chars)").option("--level <L0|L1|L2|L3|L4>", "Override current exercise level (otherwise auto-detect)").option("--lesson <uuid>", "Override lesson ID (defaults to current workspace lesson)").option("--json", "Output structured JSON").action(async (opts) => {
@@ -44352,6 +44654,12 @@ var init_theory = __esm({
44352
44654
  output(formatMoreTheory(data), { json: false });
44353
44655
  }
44354
44656
  } catch (err) {
44657
+ if (isInsufficientCreditsError(err)) {
44658
+ const friendly = getErrors().insufficientCredits;
44659
+ if (opts.json) jsonError("insufficient_credits", { message: friendly });
44660
+ error(friendly);
44661
+ return;
44662
+ }
44355
44663
  const msg = err instanceof Error ? err.message : String(err);
44356
44664
  if (opts.json) jsonError("unexpected_error", { message: msg });
44357
44665
  error(msg);