@tostudy-ai/cli 0.16.0 → 0.16.2

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
@@ -41,8 +41,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  mod
42
42
  ));
43
43
 
44
- // ../../node_modules/@parisgroup-ai/logger/dist/sanitize-D8bjOQig.mjs
44
+ // ../../node_modules/@parisgroup-ai/logger/dist/sanitize-4AyoT9ic.mjs
45
45
  function isSensitiveKey(key) {
46
+ if (matchesSensitiveField(key)) return true;
47
+ return matchesPluralOfSensitiveField(key);
48
+ }
49
+ function matchesPluralOfSensitiveField(key) {
50
+ const lower = key.toLowerCase();
51
+ if (lower.endsWith("es") && matchesSensitiveField(key.slice(0, -2))) return true;
52
+ if (lower.endsWith("s") && matchesSensitiveField(key.slice(0, -1))) return true;
53
+ return false;
54
+ }
55
+ function matchesSensitiveField(key) {
46
56
  const lowerKey = key.toLowerCase();
47
57
  return SENSITIVE_FIELDS.some((field) => {
48
58
  const lowerField = field.toLowerCase();
@@ -106,7 +116,8 @@ function sanitizeRecord(obj, ctx) {
106
116
  const sanitized = { ...obj };
107
117
  for (const [key, value] of Object.entries(sanitized)) {
108
118
  const isAdditionalKey = ctx.additionalSensitiveKeys?.some((k) => key.toLowerCase() === k.toLowerCase());
109
- if (isSensitiveKey(key) || isAdditionalKey) {
119
+ const pluralCountExemption = typeof value === "number" && !isAdditionalKey && !matchesSensitiveField(key) && matchesPluralOfSensitiveField(key);
120
+ if ((isSensitiveKey(key) || isAdditionalKey) && !pluralCountExemption) {
110
121
  sanitized[key] = "[REDACTED]";
111
122
  continue;
112
123
  }
@@ -136,8 +147,8 @@ function sanitizeEntryData(obj, additionalSensitiveKeys, includeStack) {
136
147
  });
137
148
  }
138
149
  var SENSITIVE_FIELDS;
139
- var init_sanitize_D8bjOQig = __esm({
140
- "../../node_modules/@parisgroup-ai/logger/dist/sanitize-D8bjOQig.mjs"() {
150
+ var init_sanitize_4AyoT9ic = __esm({
151
+ "../../node_modules/@parisgroup-ai/logger/dist/sanitize-4AyoT9ic.mjs"() {
141
152
  SENSITIVE_FIELDS = [
142
153
  "password",
143
154
  "senha",
@@ -268,7 +279,7 @@ var require_picocolors = __commonJS({
268
279
  }
269
280
  });
270
281
 
271
- // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-MYys2STJ.mjs
282
+ // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-DOZPgsDx.mjs
272
283
  function formatJson(entry) {
273
284
  const { timestamp, level, message, service, environment, context, data, error: error49 } = entry;
274
285
  const output3 = {
@@ -327,6 +338,35 @@ function formatPretty(entry) {
327
338
  }
328
339
  return output3;
329
340
  }
341
+ function filterStrings(value, safe, visited) {
342
+ if (typeof value === "string") return safe(value);
343
+ if (value === null || typeof value !== "object") return value;
344
+ if (visited.has(value)) return value;
345
+ visited.add(value);
346
+ if (Array.isArray(value)) return value.map((item) => filterStrings(item, safe, visited));
347
+ const proto = Object.getPrototypeOf(value);
348
+ if (proto !== Object.prototype && proto !== null) return value;
349
+ const out = {};
350
+ for (const [key, item] of Object.entries(value)) out[key] = filterStrings(item, safe, visited);
351
+ return out;
352
+ }
353
+ function applyPiiFilterToEntry(entry, filter) {
354
+ const safe = (text) => {
355
+ try {
356
+ return filter(text);
357
+ } catch {
358
+ return "[PII_FILTER_ERROR]";
359
+ }
360
+ };
361
+ const visited = /* @__PURE__ */ new WeakSet();
362
+ return {
363
+ ...entry,
364
+ message: safe(entry.message),
365
+ context: filterStrings(entry.context, safe, visited),
366
+ data: entry.data ? filterStrings(entry.data, safe, visited) : entry.data,
367
+ error: entry.error ? filterStrings(entry.error, safe, visited) : entry.error
368
+ };
369
+ }
330
370
  function extractErrorInfo(error49, includeStack, visited = /* @__PURE__ */ new WeakSet()) {
331
371
  const info = {
332
372
  name: error49.name,
@@ -343,9 +383,9 @@ function extractErrorInfo(error49, includeStack, visited = /* @__PURE__ */ new W
343
383
  return info;
344
384
  }
345
385
  var import_picocolors, LOG_LEVELS, LEVEL_COLORS, LEVEL_WIDTH, BaseLogger;
346
- var init_logger_base_MYys2STJ = __esm({
347
- "../../node_modules/@parisgroup-ai/logger/dist/logger.base-MYys2STJ.mjs"() {
348
- init_sanitize_D8bjOQig();
386
+ var init_logger_base_DOZPgsDx = __esm({
387
+ "../../node_modules/@parisgroup-ai/logger/dist/logger.base-DOZPgsDx.mjs"() {
388
+ init_sanitize_4AyoT9ic();
349
389
  import_picocolors = __toESM(require_picocolors(), 1);
350
390
  LOG_LEVELS = {
351
391
  trace: 0,
@@ -375,6 +415,7 @@ var init_logger_base_MYys2STJ = __esm({
375
415
  shouldSanitize;
376
416
  sensitiveFields;
377
417
  includeCaller;
418
+ piiFilter;
378
419
  constructor(options, defaults) {
379
420
  this.level = options.level ?? defaults.level;
380
421
  this.format = options.format ?? defaults.format;
@@ -386,6 +427,7 @@ var init_logger_base_MYys2STJ = __esm({
386
427
  this.shouldSanitize = options.sanitize ?? true;
387
428
  this.sensitiveFields = options.sensitiveFields;
388
429
  this.includeCaller = options.includeCaller ?? "errors";
430
+ this.piiFilter = options.piiFilter;
389
431
  }
390
432
  /**
391
433
  * Get caller information for error logging (Node.js only).
@@ -401,10 +443,11 @@ var init_logger_base_MYys2STJ = __esm({
401
443
  return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
402
444
  }
403
445
  buildEntry(level, message, data, error49) {
404
- const context = {
446
+ const merged = {
405
447
  ...this.getLogContext() ?? {},
406
448
  ...this.additionalContext
407
449
  };
450
+ const context = this.shouldSanitize ? sanitizeEntryData(merged, this.sensitiveFields, !this.isProd()) : merged;
408
451
  if (this.module) context.module = this.module;
409
452
  if (this.includeCaller === true || this.includeCaller === "errors" && (level === "error" || level === "fatal")) {
410
453
  const caller = this.getCaller();
@@ -447,7 +490,8 @@ var init_logger_base_MYys2STJ = __esm({
447
490
  if (!this.shouldLog(level)) return;
448
491
  const normalizedData = this.normalizeData(data);
449
492
  const errorInfo = errorObj ? extractErrorInfo(errorObj, !this.isProd()) : void 0;
450
- const entry = this.buildEntry(level, message, normalizedData, errorInfo);
493
+ const built = this.buildEntry(level, message, normalizedData, errorInfo);
494
+ const entry = this.shouldSanitize && this.piiFilter ? applyPiiFilterToEntry(built, this.piiFilter) : built;
451
495
  const formatted = this.format === "json" ? formatJson(entry) : formatPretty(entry);
452
496
  for (const transport of this.transports) transport.log(entry, formatted);
453
497
  this.flush();
@@ -609,8 +653,8 @@ function createLogger(module, options) {
609
653
  var asyncLocalStorage, ConsoleTransport, FileTransport, HttpTransport, SINGLETON_KEY, Logger, logger;
610
654
  var init_dist = __esm({
611
655
  "../../node_modules/@parisgroup-ai/logger/dist/index.mjs"() {
612
- init_logger_base_MYys2STJ();
613
- init_sanitize_D8bjOQig();
656
+ init_logger_base_DOZPgsDx();
657
+ init_sanitize_4AyoT9ic();
614
658
  asyncLocalStorage = new AsyncLocalStorage();
615
659
  ConsoleTransport = class {
616
660
  log(entry, formatted) {
@@ -768,7 +812,8 @@ var init_dist = __esm({
768
812
  environment: this.environment,
769
813
  sanitize: this.shouldSanitize,
770
814
  sensitiveFields: this.sensitiveFields,
771
- includeCaller: this.includeCaller
815
+ includeCaller: this.includeCaller,
816
+ piiFilter: this.piiFilter
772
817
  });
773
818
  child.additionalContext = {
774
819
  ...this.additionalContext,
@@ -836,9 +881,15 @@ async function cliApiFetch(url2, token, init) {
836
881
  ...init?.headers
837
882
  }
838
883
  });
839
- const body = await response.json();
884
+ const body = await response.json().catch(() => ({}));
840
885
  if (!response.ok) {
841
- throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
886
+ const code = typeof body.error === "string" && body.error.length > 0 ? body.error : void 0;
887
+ const friendly = typeof body.message === "string" && body.message.length > 0 ? body.message : void 0;
888
+ throw new CliApiError(
889
+ friendly ?? code ?? `API error ${response.status}`,
890
+ response.status,
891
+ code
892
+ );
842
893
  }
843
894
  return body;
844
895
  }
@@ -973,12 +1024,14 @@ var init_http = __esm({
973
1024
  "../../packages/tostudy-core/src/adapters/http.ts"() {
974
1025
  "use strict";
975
1026
  CliApiError = class extends Error {
976
- constructor(message, status) {
1027
+ constructor(message, status, code) {
977
1028
  super(message);
978
1029
  this.status = status;
1030
+ this.code = code;
979
1031
  this.name = "CliApiError";
980
1032
  }
981
1033
  status;
1034
+ code;
982
1035
  };
983
1036
  }
984
1037
  });
@@ -1251,8 +1304,68 @@ var init_session_store = __esm({
1251
1304
  }
1252
1305
  });
1253
1306
 
1307
+ // ../../packages/tostudy-core/src/slug.ts
1308
+ function slugify(text, maxLength = 60) {
1309
+ return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1310
+ }
1311
+ var init_slug = __esm({
1312
+ "../../packages/tostudy-core/src/slug.ts"() {
1313
+ "use strict";
1314
+ }
1315
+ });
1316
+
1317
+ // ../../packages/tostudy-core/src/lessons/workspace-tokens.ts
1318
+ function resolveWorkspaceTokensInText(text, paths) {
1319
+ return text.replace(WORKSPACE_TOKEN, () => paths.workspace).replace(VAULT_TOKEN, () => paths.vault);
1320
+ }
1321
+ function resolveWorkspaceTokens(value, paths) {
1322
+ if (typeof value === "string") {
1323
+ return resolveWorkspaceTokensInText(value, paths);
1324
+ }
1325
+ if (Array.isArray(value)) {
1326
+ return value.map((item) => resolveWorkspaceTokens(item, paths));
1327
+ }
1328
+ if (value !== null && typeof value === "object") {
1329
+ const out = {};
1330
+ for (const [key, item] of Object.entries(value)) {
1331
+ out[key] = resolveWorkspaceTokens(item, paths);
1332
+ }
1333
+ return out;
1334
+ }
1335
+ return value;
1336
+ }
1337
+ var WORKSPACE_TOKEN, VAULT_TOKEN;
1338
+ var init_workspace_tokens = __esm({
1339
+ "../../packages/tostudy-core/src/lessons/workspace-tokens.ts"() {
1340
+ "use strict";
1341
+ init_slug();
1342
+ WORKSPACE_TOKEN = /\$WORKSPACE(?![A-Za-z0-9_])/g;
1343
+ VAULT_TOKEN = /\$VAULT(?![A-Za-z0-9_])/g;
1344
+ }
1345
+ });
1346
+
1254
1347
  // src/output/formatter.ts
1348
+ var formatter_exports = {};
1349
+ __export(formatter_exports, {
1350
+ error: () => error,
1351
+ filterExerciseContent: () => filterExerciseContent,
1352
+ formatCourseList: () => formatCourseList,
1353
+ formatHint: () => formatHint,
1354
+ formatLesson: () => formatLesson,
1355
+ formatModuleStart: () => formatModuleStart,
1356
+ formatMoreTheory: () => formatMoreTheory,
1357
+ formatProgress: () => formatProgress,
1358
+ formatValidation: () => formatValidation,
1359
+ jsonError: () => jsonError,
1360
+ output: () => output,
1361
+ progressBar: () => progressBar,
1362
+ setWorkspaceTokenPaths: () => setWorkspaceTokenPaths
1363
+ });
1364
+ function setWorkspaceTokenPaths(paths) {
1365
+ workspaceTokenPaths = paths;
1366
+ }
1255
1367
  function output(data, opts) {
1368
+ if (workspaceTokenPaths) data = resolveWorkspaceTokens(data, workspaceTokenPaths);
1256
1369
  if (opts.json) {
1257
1370
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
1258
1371
  } else if (typeof data === "string") {
@@ -1494,10 +1607,12 @@ function formatMoreTheory(data) {
1494
1607
  lines.push("", "\u2192 tostudy lesson voltar ao exerc\xEDcio atual");
1495
1608
  return lines.join("\n");
1496
1609
  }
1497
- var TIER_HEADERS, UNEVALUATED_CRITERION_SENTINEL;
1610
+ var workspaceTokenPaths, TIER_HEADERS, UNEVALUATED_CRITERION_SENTINEL;
1498
1611
  var init_formatter = __esm({
1499
1612
  "src/output/formatter.ts"() {
1500
1613
  "use strict";
1614
+ init_workspace_tokens();
1615
+ workspaceTokenPaths = null;
1501
1616
  TIER_HEADERS = [/^## Prática Guiada/im, /^## Prática Semi-?[Gg]uiada/im, /^## Desafio/im];
1502
1617
  UNEVALUATED_CRITERION_SENTINEL = "Crit\xE9rio n\xE3o avaliado explicitamente; tratado como n\xE3o atendido.";
1503
1618
  }
@@ -1925,16 +2040,6 @@ var init_instruction_template_v3 = __esm({
1925
2040
  }
1926
2041
  });
1927
2042
 
1928
- // ../../packages/tostudy-core/src/slug.ts
1929
- function slugify(text, maxLength = 60) {
1930
- return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1931
- }
1932
- var init_slug = __esm({
1933
- "../../packages/tostudy-core/src/slug.ts"() {
1934
- "use strict";
1935
- }
1936
- });
1937
-
1938
2043
  // src/workspace/runtime-registry.ts
1939
2044
  var runtime_registry_exports = {};
1940
2045
  __export(runtime_registry_exports, {
@@ -2638,6 +2743,15 @@ var init_course_drift = __esm({
2638
2743
  });
2639
2744
 
2640
2745
  // src/onboarding/course-state.ts
2746
+ var course_state_exports = {};
2747
+ __export(course_state_exports, {
2748
+ checkCourseDrift: () => checkCourseDrift,
2749
+ clearAllCourseOnboarding: () => clearAllCourseOnboarding,
2750
+ getCourseOnboardingState: () => getCourseOnboardingState,
2751
+ saveCourseLearnerProfile: () => saveCourseLearnerProfile,
2752
+ setCourseWorkspacePath: () => setCourseWorkspacePath,
2753
+ setLastInitCourseId: () => setLastInitCourseId
2754
+ });
2641
2755
  import fs6 from "node:fs";
2642
2756
  async function getCourseOnboardingState(courseId, configDir) {
2643
2757
  const state = readCourseOnboardingState(configDir);
@@ -2734,17 +2848,21 @@ function resolveLocale() {
2734
2848
  function getErrors(locale) {
2735
2849
  return BUNDLES[locale ?? resolveLocale()];
2736
2850
  }
2851
+ function resolveErrorCode(err) {
2852
+ if (err && typeof err === "object" && "code" in err) {
2853
+ const code = err.code;
2854
+ if (typeof code === "string" && code.length > 0) return code;
2855
+ }
2856
+ return err instanceof Error ? err.message : String(err);
2857
+ }
2737
2858
  function isInsufficientCreditsError(err) {
2738
- const msg = err instanceof Error ? err.message : String(err);
2739
- return msg.includes("INSUFFICIENT_CREDITS");
2859
+ return resolveErrorCode(err).includes("INSUFFICIENT_CREDITS");
2740
2860
  }
2741
2861
  function isCourseArchivedError(err) {
2742
- const msg = err instanceof Error ? err.message : String(err);
2743
- return msg.includes("COURSE_ARCHIVED");
2862
+ return resolveErrorCode(err).includes("COURSE_ARCHIVED");
2744
2863
  }
2745
2864
  function isEnrollmentNotEntitledError(err) {
2746
- const msg = err instanceof Error ? err.message : String(err);
2747
- return msg.includes("ENROLLMENT_NOT_ENTITLED");
2865
+ return resolveErrorCode(err).includes("ENROLLMENT_NOT_ENTITLED");
2748
2866
  }
2749
2867
  var BUNDLES, _cachedLocale;
2750
2868
  var init_errors = __esm({
@@ -2777,6 +2895,121 @@ var init_logout = __esm({
2777
2895
  }
2778
2896
  });
2779
2897
 
2898
+ // src/workspace/resolve.ts
2899
+ var resolve_exports = {};
2900
+ __export(resolve_exports, {
2901
+ cliWorkspacePaths: () => cliWorkspacePaths,
2902
+ courseSlug: () => courseSlug,
2903
+ findCwdWorkspaceUpwards: () => findCwdWorkspaceUpwards,
2904
+ findExistingVault: () => findExistingVault,
2905
+ isCwdWorkspace: () => isCwdWorkspace,
2906
+ resolveActiveCourseFromWorkspace: () => resolveActiveCourseFromWorkspace,
2907
+ resolveActiveCourseWithOverride: () => resolveActiveCourseWithOverride,
2908
+ resolveCwdWorkspacePath: () => resolveCwdWorkspacePath,
2909
+ resolveEffectiveWorkspace: () => resolveEffectiveWorkspace,
2910
+ resolveVaultPath: () => resolveVaultPath,
2911
+ resolveWorkspace: () => resolveWorkspace
2912
+ });
2913
+ import fs7 from "node:fs/promises";
2914
+ import path9 from "node:path";
2915
+ import os6 from "node:os";
2916
+ function courseSlug(title) {
2917
+ return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2918
+ }
2919
+ async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
2920
+ const slug = courseSlug(courseTitle);
2921
+ const candidate = path9.join(basePath, slug);
2922
+ try {
2923
+ await fs7.access(path9.join(candidate, ".ana-config.json"));
2924
+ return { found: true, workspacePath: candidate, source: "default" };
2925
+ } catch {
2926
+ return { found: false, workspacePath: null };
2927
+ }
2928
+ }
2929
+ async function isCwdWorkspace(cwd = process.cwd()) {
2930
+ return await resolveCwdWorkspacePath(cwd) !== null;
2931
+ }
2932
+ async function resolveCwdWorkspacePath(cwd = process.cwd()) {
2933
+ const tostudyDir = path9.join(cwd, ".tostudy");
2934
+ try {
2935
+ const stat = await fs7.stat(tostudyDir);
2936
+ if (stat.isDirectory()) return tostudyDir;
2937
+ } catch {
2938
+ }
2939
+ try {
2940
+ await fs7.access(path9.join(cwd, ".ana-config.json"));
2941
+ return cwd;
2942
+ } catch {
2943
+ return null;
2944
+ }
2945
+ }
2946
+ async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
2947
+ const home = path9.resolve(homeDir);
2948
+ let current = path9.resolve(startCwd);
2949
+ while (true) {
2950
+ if (current === home) return null;
2951
+ const direct = await resolveCwdWorkspacePath(current);
2952
+ if (direct) return direct;
2953
+ const parent = path9.dirname(current);
2954
+ if (parent === current) return null;
2955
+ current = parent;
2956
+ }
2957
+ }
2958
+ function resolveVaultPath(workspacePath, slug) {
2959
+ const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
2960
+ return path9.join(base, `vault-${slug}`);
2961
+ }
2962
+ function cliWorkspacePaths(workspacePath, courseTitle) {
2963
+ const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
2964
+ return { workspace: base, vault: resolveVaultPath(workspacePath, courseSlug(courseTitle)) };
2965
+ }
2966
+ async function findExistingVault(workspacePath, slug) {
2967
+ const slugged = resolveVaultPath(workspacePath, slug);
2968
+ try {
2969
+ await fs7.access(path9.join(slugged, ".ana-vault.json"));
2970
+ return slugged;
2971
+ } catch {
2972
+ }
2973
+ const legacy = path9.join(workspacePath, "vault");
2974
+ try {
2975
+ await fs7.access(path9.join(legacy, ".ana-vault.json"));
2976
+ return legacy;
2977
+ } catch {
2978
+ return null;
2979
+ }
2980
+ }
2981
+ async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os6.homedir()) {
2982
+ const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
2983
+ if (cwdWorkspace) {
2984
+ return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
2985
+ }
2986
+ if (storedPath) {
2987
+ try {
2988
+ await fs7.access(path9.join(storedPath, ".ana-config.json"));
2989
+ return { found: true, workspacePath: storedPath, source: "stored" };
2990
+ } catch {
2991
+ }
2992
+ }
2993
+ const result = await resolveWorkspace(courseTitle, defaultBasePath);
2994
+ if (result.found) {
2995
+ return { ...result, source: "default" };
2996
+ }
2997
+ return { found: false, workspacePath: null };
2998
+ }
2999
+ async function resolveActiveCourseFromWorkspace(options = {}) {
3000
+ const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3001
+ const result = await findWorkspaceState2(options.cwd, options.homeDir);
3002
+ return result?.state ?? null;
3003
+ }
3004
+ var DEFAULT_BASE, resolveActiveCourseWithOverride;
3005
+ var init_resolve = __esm({
3006
+ "src/workspace/resolve.ts"() {
3007
+ "use strict";
3008
+ DEFAULT_BASE = path9.join(os6.homedir(), "study");
3009
+ resolveActiveCourseWithOverride = resolveActiveCourseFromWorkspace;
3010
+ }
3011
+ });
3012
+
2780
3013
  // src/auth/guards.ts
2781
3014
  async function refreshCliSession(session, configDir) {
2782
3015
  try {
@@ -2823,10 +3056,32 @@ async function requireSession(configDir) {
2823
3056
  async function requireActiveCourse() {
2824
3057
  const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
2825
3058
  const result = await findWorkspaceState2();
2826
- if (result) return result.state;
3059
+ if (result) {
3060
+ await primeWorkspaceTokenPaths(result.state);
3061
+ return result.state;
3062
+ }
2827
3063
  process.stderr.write("Erro: Nenhum workspace configurado. Rode: tostudy setup\n");
2828
3064
  process.exit(3);
2829
3065
  }
3066
+ async function primeWorkspaceTokenPaths(state) {
3067
+ try {
3068
+ const [
3069
+ { setWorkspaceTokenPaths: setWorkspaceTokenPaths2 },
3070
+ { resolveEffectiveWorkspace: resolveEffectiveWorkspace2, cliWorkspacePaths: cliWorkspacePaths2 },
3071
+ onboarding
3072
+ ] = await Promise.all([
3073
+ Promise.resolve().then(() => (init_formatter(), formatter_exports)),
3074
+ Promise.resolve().then(() => (init_resolve(), resolve_exports)),
3075
+ Promise.resolve().then(() => (init_course_state(), course_state_exports))
3076
+ ]);
3077
+ const stored = await onboarding.getCourseOnboardingState(state.courseId);
3078
+ const ws = await resolveEffectiveWorkspace2(state.courseTitle, stored?.workspacePath);
3079
+ setWorkspaceTokenPaths2(
3080
+ ws.found && ws.workspacePath ? cliWorkspacePaths2(ws.workspacePath, state.courseTitle) : null
3081
+ );
3082
+ } catch {
3083
+ }
3084
+ }
2830
3085
  var init_guards = __esm({
2831
3086
  "src/auth/guards.ts"() {
2832
3087
  "use strict";
@@ -2942,7 +3197,7 @@ var CLI_VERSION;
2942
3197
  var init_version = __esm({
2943
3198
  "src/version.ts"() {
2944
3199
  "use strict";
2945
- CLI_VERSION = true ? "0.16.0" : "0.7.1";
3200
+ CLI_VERSION = true ? "0.16.2" : "0.7.1";
2946
3201
  }
2947
3202
  });
2948
3203
 
@@ -2956,13 +3211,13 @@ __export(update_checker_exports, {
2956
3211
  readCache: () => readCache,
2957
3212
  writeCache: () => writeCache
2958
3213
  });
2959
- import fs7 from "node:fs";
2960
- import path9 from "node:path";
3214
+ import fs8 from "node:fs";
3215
+ import path10 from "node:path";
2961
3216
  function readCache(configDir) {
2962
3217
  try {
2963
- const p = path9.join(configDir ?? getConfigDir(), CACHE_FILE);
2964
- if (!fs7.existsSync(p)) return null;
2965
- return JSON.parse(fs7.readFileSync(p, "utf-8"));
3218
+ const p = path10.join(configDir ?? getConfigDir(), CACHE_FILE);
3219
+ if (!fs8.existsSync(p)) return null;
3220
+ return JSON.parse(fs8.readFileSync(p, "utf-8"));
2966
3221
  } catch {
2967
3222
  return null;
2968
3223
  }
@@ -2970,8 +3225,8 @@ function readCache(configDir) {
2970
3225
  function writeCache(cache, configDir) {
2971
3226
  try {
2972
3227
  const dir = configDir ?? getConfigDir();
2973
- fs7.mkdirSync(dir, { recursive: true });
2974
- fs7.writeFileSync(path9.join(dir, CACHE_FILE), JSON.stringify(cache));
3228
+ fs8.mkdirSync(dir, { recursive: true });
3229
+ fs8.writeFileSync(path10.join(dir, CACHE_FILE), JSON.stringify(cache));
2975
3230
  } catch {
2976
3231
  }
2977
3232
  }
@@ -3041,17 +3296,17 @@ var init_update_checker = __esm({
3041
3296
  });
3042
3297
 
3043
3298
  // src/learner-brief/cache.ts
3044
- import fs8 from "node:fs";
3045
- import path10 from "node:path";
3299
+ import fs9 from "node:fs";
3300
+ import path11 from "node:path";
3046
3301
  function resolveCachePath(configDir) {
3047
- return path10.join(configDir, "student-brief.json");
3302
+ return path11.join(configDir, "student-brief.json");
3048
3303
  }
3049
3304
  async function readBriefCache(configDir) {
3050
3305
  const dir = configDir ?? getConfigDir();
3051
3306
  const p = resolveCachePath(dir);
3052
- if (!fs8.existsSync(p)) return null;
3307
+ if (!fs9.existsSync(p)) return null;
3053
3308
  try {
3054
- const raw = fs8.readFileSync(p, "utf-8");
3309
+ const raw = fs9.readFileSync(p, "utf-8");
3055
3310
  return JSON.parse(raw);
3056
3311
  } catch {
3057
3312
  return null;
@@ -3059,8 +3314,8 @@ async function readBriefCache(configDir) {
3059
3314
  }
3060
3315
  async function writeBriefCache(configDir, cache) {
3061
3316
  const dir = configDir ?? getConfigDir();
3062
- fs8.mkdirSync(dir, { recursive: true });
3063
- fs8.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
3317
+ fs9.mkdirSync(dir, { recursive: true });
3318
+ fs9.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
3064
3319
  }
3065
3320
  var init_cache = __esm({
3066
3321
  "src/learner-brief/cache.ts"() {
@@ -3070,23 +3325,23 @@ var init_cache = __esm({
3070
3325
  });
3071
3326
 
3072
3327
  // src/tutor-persona/cache.ts
3073
- import fs9 from "node:fs";
3074
- import path11 from "node:path";
3328
+ import fs10 from "node:fs";
3329
+ import path12 from "node:path";
3075
3330
  function resolveCachePath2(configDir) {
3076
- return path11.join(configDir, "tutor-personalities.json");
3331
+ return path12.join(configDir, "tutor-personalities.json");
3077
3332
  }
3078
3333
  function readAll(configDir) {
3079
3334
  const p = resolveCachePath2(configDir);
3080
- if (!fs9.existsSync(p)) return {};
3335
+ if (!fs10.existsSync(p)) return {};
3081
3336
  try {
3082
- return JSON.parse(fs9.readFileSync(p, "utf-8"));
3337
+ return JSON.parse(fs10.readFileSync(p, "utf-8"));
3083
3338
  } catch {
3084
3339
  return {};
3085
3340
  }
3086
3341
  }
3087
3342
  function writeAll(configDir, cache) {
3088
- fs9.mkdirSync(configDir, { recursive: true });
3089
- fs9.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
3343
+ fs10.mkdirSync(configDir, { recursive: true });
3344
+ fs10.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
3090
3345
  }
3091
3346
  async function readPersonaCacheForCourse(configDir, courseId) {
3092
3347
  const dir = configDir ?? getConfigDir();
@@ -3356,97 +3611,6 @@ var init_courses2 = __esm({
3356
3611
  }
3357
3612
  });
3358
3613
 
3359
- // src/workspace/resolve.ts
3360
- import fs10 from "node:fs/promises";
3361
- import path12 from "node:path";
3362
- import os6 from "node:os";
3363
- function courseSlug(title) {
3364
- return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
3365
- }
3366
- async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
3367
- const slug = courseSlug(courseTitle);
3368
- const candidate = path12.join(basePath, slug);
3369
- try {
3370
- await fs10.access(path12.join(candidate, ".ana-config.json"));
3371
- return { found: true, workspacePath: candidate, source: "default" };
3372
- } catch {
3373
- return { found: false, workspacePath: null };
3374
- }
3375
- }
3376
- async function isCwdWorkspace(cwd = process.cwd()) {
3377
- return await resolveCwdWorkspacePath(cwd) !== null;
3378
- }
3379
- async function resolveCwdWorkspacePath(cwd = process.cwd()) {
3380
- const tostudyDir = path12.join(cwd, ".tostudy");
3381
- try {
3382
- const stat = await fs10.stat(tostudyDir);
3383
- if (stat.isDirectory()) return tostudyDir;
3384
- } catch {
3385
- }
3386
- try {
3387
- await fs10.access(path12.join(cwd, ".ana-config.json"));
3388
- return cwd;
3389
- } catch {
3390
- return null;
3391
- }
3392
- }
3393
- async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
3394
- const home = path12.resolve(homeDir);
3395
- let current = path12.resolve(startCwd);
3396
- while (true) {
3397
- if (current === home) return null;
3398
- const direct = await resolveCwdWorkspacePath(current);
3399
- if (direct) return direct;
3400
- const parent = path12.dirname(current);
3401
- if (parent === current) return null;
3402
- current = parent;
3403
- }
3404
- }
3405
- function resolveVaultPath(workspacePath, slug) {
3406
- const base = path12.basename(workspacePath) === ".tostudy" ? path12.dirname(workspacePath) : workspacePath;
3407
- return path12.join(base, `vault-${slug}`);
3408
- }
3409
- async function findExistingVault(workspacePath, slug) {
3410
- const slugged = resolveVaultPath(workspacePath, slug);
3411
- try {
3412
- await fs10.access(path12.join(slugged, ".ana-vault.json"));
3413
- return slugged;
3414
- } catch {
3415
- }
3416
- const legacy = path12.join(workspacePath, "vault");
3417
- try {
3418
- await fs10.access(path12.join(legacy, ".ana-vault.json"));
3419
- return legacy;
3420
- } catch {
3421
- return null;
3422
- }
3423
- }
3424
- async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os6.homedir()) {
3425
- const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
3426
- if (cwdWorkspace) {
3427
- return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
3428
- }
3429
- if (storedPath) {
3430
- try {
3431
- await fs10.access(path12.join(storedPath, ".ana-config.json"));
3432
- return { found: true, workspacePath: storedPath, source: "stored" };
3433
- } catch {
3434
- }
3435
- }
3436
- const result = await resolveWorkspace(courseTitle, defaultBasePath);
3437
- if (result.found) {
3438
- return { ...result, source: "default" };
3439
- }
3440
- return { found: false, workspacePath: null };
3441
- }
3442
- var DEFAULT_BASE;
3443
- var init_resolve = __esm({
3444
- "src/workspace/resolve.ts"() {
3445
- "use strict";
3446
- DEFAULT_BASE = path12.join(os6.homedir(), "study");
3447
- }
3448
- });
3449
-
3450
3614
  // src/onboarding/status.ts
3451
3615
  async function getCourseOnboardingStatus(activeCourse, configDir, cwd = process.cwd()) {
3452
3616
  const onboardingState = await getCourseOnboardingState(activeCourse.courseId, configDir);
@@ -4130,6 +4294,7 @@ var init_lessons = __esm({
4130
4294
  init_get_hint();
4131
4295
  init_start_module();
4132
4296
  init_start_next_module();
4297
+ init_workspace_tokens();
4133
4298
  }
4134
4299
  });
4135
4300
 
@@ -4163,7 +4328,7 @@ async function runStart(opts, deps = defaultDeps3) {
4163
4328
  `);
4164
4329
  }
4165
4330
  } catch (err) {
4166
- if (err instanceof CliApiError && err.status === 403 && err.message === "CHANNEL_MISMATCH") {
4331
+ if (err instanceof CliApiError && err.status === 403 && (err.code ?? err.message) === "CHANNEL_MISMATCH") {
4167
4332
  process.stderr.write("Este curso n\xE3o est\xE1 dispon\xEDvel via CLI.\n");
4168
4333
  process.stderr.write("Acesse tostudy.ai para estudar este curso pelo ChatStudy.\n");
4169
4334
  process.exit(1);
@@ -4251,7 +4416,7 @@ var init_start_next = __esm({
4251
4416
  output(formatModuleStart(moduleData), { json: false });
4252
4417
  }
4253
4418
  } catch (err) {
4254
- if (err instanceof CliApiError && err.message === "COURSE_COMPLETE") {
4419
+ if (err instanceof CliApiError && (err.code ?? err.message) === "COURSE_COMPLETE") {
4255
4420
  process.stderr.write("\u{1F389} Parab\xE9ns! Curso conclu\xEDdo!\n");
4256
4421
  process.stderr.write("\u2192 tostudy courses para ver outros cursos\n");
4257
4422
  process.exit(0);
@@ -4324,26 +4489,37 @@ var init_next = __esm({
4324
4489
  process.exit(1);
4325
4490
  }
4326
4491
  if (err instanceof CliApiError) {
4327
- if (err.status === 403 && err.message === "CHANNEL_MISMATCH") {
4492
+ const code = err.code ?? err.message;
4493
+ if (err.status === 403 && code === "CHANNEL_MISMATCH") {
4328
4494
  process.stderr.write("Este curso n\xE3o est\xE1 dispon\xEDvel via CLI.\n");
4329
4495
  process.stderr.write("Acesse tostudy.ai para estudar este curso pelo ChatStudy.\n");
4330
4496
  process.exit(1);
4331
4497
  }
4332
- if (err.message === "MODULE_COMPLETE") {
4498
+ if (code === "MODULE_COMPLETE") {
4333
4499
  process.stderr.write("\u2713 M\xF3dulo conclu\xEDdo!\n");
4334
4500
  process.stderr.write("\u2192 tostudy start-next para iniciar o pr\xF3ximo m\xF3dulo\n");
4335
4501
  process.exit(0);
4336
4502
  }
4337
- if (err.message === "COURSE_COMPLETE") {
4503
+ if (code === "COURSE_COMPLETE") {
4338
4504
  process.stderr.write("\u{1F389} Parab\xE9ns! Curso conclu\xEDdo!\n");
4339
4505
  process.stderr.write("\u2192 tostudy courses para ver outros cursos\n");
4340
4506
  process.exit(0);
4341
4507
  }
4342
- if (err.message === "NO_ACTIVE_LESSON") {
4508
+ if (code === "EXERCISE_NOT_PASSED") {
4509
+ process.stderr.write("Voc\xEA ainda n\xE3o foi aprovado no exerc\xEDcio desta li\xE7\xE3o.\n");
4510
+ process.stderr.write("\u2192 tostudy validate para enviar sua solu\xE7\xE3o\n");
4511
+ process.exit(1);
4512
+ }
4513
+ if (code === "NO_ACTIVE_LESSON") {
4343
4514
  process.stderr.write("Nenhuma li\xE7\xE3o em progresso.\n");
4344
4515
  process.stderr.write("\u2192 tostudy start-next para iniciar o pr\xF3ximo m\xF3dulo\n");
4345
4516
  process.exit(0);
4346
4517
  }
4518
+ if (err.status === 409) {
4519
+ process.stderr.write(`${err.message}
4520
+ `);
4521
+ process.exit(1);
4522
+ }
4347
4523
  }
4348
4524
  const msg = err instanceof Error ? err.message : String(err);
4349
4525
  if (opts.json) jsonError(msg);
@@ -4363,12 +4539,6 @@ function adjustTimeEstimate(type, baseMinutes) {
4363
4539
  function isCheckpoint(title) {
4364
4540
  return /^checkpoint/i.test(title.trim());
4365
4541
  }
4366
- function replacePlaceholders(text, workspacePath, courseTitle) {
4367
- const base = path13.basename(workspacePath) === ".tostudy" ? path13.dirname(workspacePath) : workspacePath;
4368
- const slug = courseSlug(courseTitle);
4369
- const vaultPath = resolveVaultPath(workspacePath, slug);
4370
- return text.replace(/\$WORKSPACE(?![A-Za-z0-9_])/g, () => base).replace(/\$VAULT(?![A-Za-z0-9_])/g, () => vaultPath);
4371
- }
4372
4542
  function formatLessonContent(data, workspacePath, courseTitle) {
4373
4543
  const time3 = adjustTimeEstimate(data.type, data.estimatedTimeMinutes);
4374
4544
  const isPractical = data.type === "exercise" || isCheckpoint(data.title);
@@ -4420,6 +4590,7 @@ var init_lesson = __esm({
4420
4590
  init_course_state();
4421
4591
  init_formatter();
4422
4592
  init_resolve();
4593
+ init_workspace_tokens();
4423
4594
  init_errors();
4424
4595
  logger13 = createLogger("cli:lesson");
4425
4596
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").option(
@@ -4455,22 +4626,6 @@ var init_lesson = __esm({
4455
4626
  activeCourse.courseTitle,
4456
4627
  onboardingState?.workspacePath
4457
4628
  );
4458
- if (ws.found && ws.workspacePath) {
4459
- if (content.content) {
4460
- content.content = replacePlaceholders(
4461
- content.content,
4462
- ws.workspacePath,
4463
- activeCourse.courseTitle
4464
- );
4465
- }
4466
- if (content.acceptanceCriteria) {
4467
- content.acceptanceCriteria = replacePlaceholders(
4468
- content.acceptanceCriteria,
4469
- ws.workspacePath,
4470
- activeCourse.courseTitle
4471
- );
4472
- }
4473
- }
4474
4629
  if (opts.json) {
4475
4630
  output(content, { json: true });
4476
4631
  } else {
@@ -4611,6 +4766,11 @@ var init_hint = __esm({
4611
4766
  else process.stderr.write(friendly);
4612
4767
  process.exit(1);
4613
4768
  }
4769
+ if (err instanceof CliApiError && (err.code ?? err.message) === "NO_ACTIVE_LESSON") {
4770
+ process.stderr.write("Nenhuma li\xE7\xE3o em progresso.\n");
4771
+ process.stderr.write("\u2192 tostudy start-next para iniciar o pr\xF3ximo m\xF3dulo\n");
4772
+ process.exit(0);
4773
+ }
4614
4774
  if (isInsufficientCreditsError(err)) {
4615
4775
  const friendly = getErrors().insufficientCredits;
4616
4776
  if (opts.json) jsonError("insufficient_credits", { message: friendly });
@@ -21666,7 +21826,12 @@ var init_theory = __esm({
21666
21826
  });
21667
21827
  if (!res.ok) {
21668
21828
  const body = await res.json().catch(() => ({}));
21669
- const msg = body.error ?? `Falha ao pedir teoria (${res.status})`;
21829
+ if (body.error === "INSUFFICIENT_CREDITS" || (body.message ?? "").includes("INSUFFICIENT_CREDITS")) {
21830
+ const friendly = getErrors().insufficientCredits;
21831
+ if (opts.json) jsonError("insufficient_credits", { message: friendly });
21832
+ error(friendly);
21833
+ }
21834
+ const msg = body.error ?? body.message ?? `Falha ao pedir teoria (${res.status})`;
21670
21835
  if (opts.json) jsonError("request_failed", { message: msg, code: res.status });
21671
21836
  error(msg);
21672
21837
  }