@tostudy-ai/cli 0.18.19 → 0.18.22

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/main.js CHANGED
@@ -1024,6 +1024,8 @@ var init_errors_pt_br = __esm({
1024
1024
  dailyAiLimit: "\u274C Voc\xEA atingiu o teto di\xE1rio de gastos com IA da sua conta. N\xE3o \xE9 falta de cr\xE9ditos: o teto reinicia \xE0 meia-noite (UTC). Para um teto maior, fa\xE7a upgrade do plano em https://tostudy.ai/student/plan.\n",
1025
1025
  workspaceRefusedAtHome: "N\xE3o d\xE1 para configurar o estudo direto na sua pasta pessoal.\nCrie uma pasta para o curso e entre nela antes de continuar:\n\n mkdir ~/meus-cursos && cd ~/meus-cursos\n",
1026
1026
  courseArchived: "Este curso foi arquivado e est\xE1 dispon\xEDvel somente para leitura.\nUse `tostudy lesson` para revisar o conte\xFAdo j\xE1 estudado.\n",
1027
+ notAuthenticated: "N\xE3o autenticado. Rode: tostudy login\n",
1028
+ sessionSecretsUnreachable: "Voc\xEA j\xE1 entrou nesta m\xE1quina, mas a chave guardada n\xE3o p\xF4de ser lida daqui.\nIsso acontece quando o comando roda dentro de um assistente de c\xF3digo no macOS: o Chaveiro do sistema n\xE3o existe l\xE1 dentro.\nRode `tostudy login` uma vez num terminal comum. A partir desta vers\xE3o o acesso passa a valer tamb\xE9m aqui.\n",
1027
1029
  noActiveCourse: "Nenhum curso ativo nesta pasta.\nVeja seus cursos com `tostudy courses` e ative um com `tostudy select <n>`.\n",
1028
1030
  studyWorkspaceHint: "Dica: rode `tostudy select` desta pasta para us\xE1-la como workspace, ou `tostudy workspace setup` para criar em ~/study/. Depois rode `tostudy export` para gravar o exerc\xEDcio e os crit\xE9rios de valida\xE7\xE3o.\n",
1029
1031
  setupCommandDescription: "Atalho antigo. A pasta de estudo \xE9 workspace setup; o MCP \xE9 setup mcp",
@@ -1136,6 +1138,8 @@ var init_errors_en_us = __esm({
1136
1138
  dailyAiLimit: "\u274C You have reached your account's daily AI spending limit. This is not an empty wallet: the limit resets at midnight (UTC). For a higher limit, upgrade your plan at https://tostudy.ai/student/plan.\n",
1137
1139
  workspaceRefusedAtHome: "Study can't be set up directly in your home folder.\nCreate a folder for the course and switch into it first:\n\n mkdir ~/my-courses && cd ~/my-courses\n",
1138
1140
  courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
1141
+ notAuthenticated: "Not authenticated. Run: tostudy login\n",
1142
+ sessionSecretsUnreachable: "You are already logged in on this machine, but the stored key could not be read from here.\nThis happens when the command runs inside a coding assistant on macOS: the system keychain does not exist in there.\nRun `tostudy login` once in a normal terminal. From this version on, that login also works in here.\n",
1139
1143
  noActiveCourse: "No active course in this folder.\nList your courses with `tostudy courses` and activate one with `tostudy select <n>`.\n",
1140
1144
  studyWorkspaceHint: "Tip: run `tostudy select` in this folder to use it as the workspace, or `tostudy workspace setup` to create one in ~/study/. Then run `tostudy export` to save the exercise and the validation criteria on this computer.\n",
1141
1145
  setupCommandDescription: "Old shortcut. The study folder is workspace setup; MCP is setup mcp",
@@ -1321,6 +1325,13 @@ import path2 from "node:path";
1321
1325
  import { chmod, readFile, rm, writeFile } from "node:fs/promises";
1322
1326
  import { execFile } from "node:child_process";
1323
1327
  import { promisify } from "node:util";
1328
+ function fileBelongsTo(secrets, owner) {
1329
+ if (!owner) return true;
1330
+ if (!secrets.userId) return false;
1331
+ if (secrets.userId !== owner.userId) return false;
1332
+ if (secrets.sessionId && owner.sessionId && secrets.sessionId !== owner.sessionId) return false;
1333
+ return true;
1334
+ }
1324
1335
  function getStoreKey(configDir) {
1325
1336
  return path2.resolve(configDir ?? path2.join(os2.homedir(), ".tostudy"));
1326
1337
  }
@@ -1337,8 +1348,13 @@ async function hasCommand(command) {
1337
1348
  }
1338
1349
  async function saveFallback(secrets, configDir) {
1339
1350
  const fallbackPath = getFallbackPath(configDir);
1340
- fs.mkdirSync(path2.dirname(fallbackPath), { recursive: true });
1341
- await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), "utf-8");
1351
+ const dir = path2.dirname(fallbackPath);
1352
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
1353
+ await chmod(dir, 448);
1354
+ await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), {
1355
+ encoding: "utf-8",
1356
+ mode: 384
1357
+ });
1342
1358
  await chmod(fallbackPath, 384);
1343
1359
  }
1344
1360
  async function loadFallback(configDir) {
@@ -1367,11 +1383,10 @@ async function saveSessionSecrets(secrets, configDir) {
1367
1383
  payload,
1368
1384
  keychain
1369
1385
  ]);
1370
- return;
1371
1386
  } catch {
1372
- await saveFallback(secrets, configDir);
1373
- return;
1374
1387
  }
1388
+ await saveFallback(secrets, configDir);
1389
+ return;
1375
1390
  }
1376
1391
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
1377
1392
  await execFileAsync(
@@ -1392,9 +1407,18 @@ async function saveSessionSecrets(secrets, configDir) {
1392
1407
  }
1393
1408
  await saveFallback(secrets, configDir);
1394
1409
  }
1395
- async function loadStoredSessionSecrets(configDir) {
1410
+ async function loadStoredSessionSecrets(configDir, owner) {
1396
1411
  const account = getStoreKey(configDir);
1397
1412
  if (process.platform === "darwin") {
1413
+ let legacyFile = null;
1414
+ try {
1415
+ const fromFile = await loadFallback(configDir);
1416
+ if (fromFile && fileBelongsTo(fromFile, owner)) {
1417
+ return fromFile;
1418
+ }
1419
+ if (fromFile && !fromFile.userId) legacyFile = fromFile;
1420
+ } catch {
1421
+ }
1398
1422
  try {
1399
1423
  const { stdout: stdout2 } = await execFileAsync("security", [
1400
1424
  "find-generic-password",
@@ -1406,7 +1430,7 @@ async function loadStoredSessionSecrets(configDir) {
1406
1430
  ]);
1407
1431
  return JSON.parse(stdout2.trim());
1408
1432
  } catch {
1409
- return loadFallback(configDir);
1433
+ return legacyFile;
1410
1434
  }
1411
1435
  }
1412
1436
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
@@ -1438,7 +1462,6 @@ async function deleteStoredSessionSecrets(configDir) {
1438
1462
  ]);
1439
1463
  } catch {
1440
1464
  }
1441
- return;
1442
1465
  }
1443
1466
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
1444
1467
  }
@@ -1461,7 +1484,9 @@ async function saveSession(session, configDir) {
1461
1484
  await saveSessionSecrets(
1462
1485
  {
1463
1486
  accessToken: session.token,
1464
- refreshToken: session.refreshToken
1487
+ refreshToken: session.refreshToken,
1488
+ userId: session.userId,
1489
+ ...session.sessionId ? { sessionId: session.sessionId } : {}
1465
1490
  },
1466
1491
  dir
1467
1492
  );
@@ -1478,12 +1503,18 @@ async function saveSession(session, configDir) {
1478
1503
  mode: 384
1479
1504
  });
1480
1505
  }
1506
+ function hasSessionConfig(configDir) {
1507
+ return fs2.existsSync(path3.join(getConfigDir(configDir), "config.json"));
1508
+ }
1481
1509
  async function getSession(configDir) {
1482
1510
  const dir = getConfigDir(configDir);
1483
1511
  const p = path3.join(dir, "config.json");
1484
1512
  if (!fs2.existsSync(p)) return null;
1485
1513
  const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
1486
- const secrets = await loadStoredSessionSecrets(dir);
1514
+ const secrets = await loadStoredSessionSecrets(dir, {
1515
+ userId: stored.userId,
1516
+ ...stored.sessionId ? { sessionId: stored.sessionId } : {}
1517
+ });
1487
1518
  if (!secrets) return null;
1488
1519
  return {
1489
1520
  token: secrets.accessToken,
@@ -3132,7 +3163,10 @@ async function refreshCliSession(session, configDir) {
3132
3163
  async function requireSession(configDir) {
3133
3164
  const session = await getSession(configDir);
3134
3165
  if (!session) {
3135
- process.stderr.write("Erro: N\xE3o autenticado. Rode: tostudy login\n");
3166
+ const errors = getErrors();
3167
+ process.stderr.write(
3168
+ `Erro: ${hasSessionConfig(configDir) ? errors.sessionSecretsUnreachable : errors.notAuthenticated}`
3169
+ );
3136
3170
  process.exit(2);
3137
3171
  }
3138
3172
  const expiresAt = new Date(session.expiresAt);
@@ -3756,13 +3790,17 @@ import { spawn } from "node:child_process";
3756
3790
  import { hostname } from "node:os";
3757
3791
 
3758
3792
  // src/version.ts
3759
- var CLI_VERSION = true ? "0.18.19" : "0.7.1";
3793
+ var CLI_VERSION = true ? "0.18.22" : "0.7.1";
3760
3794
 
3761
3795
  // src/installer/mcp-setup.ts
3762
3796
  function safeServerError(text2) {
3763
3797
  if (typeof text2 !== "string" || text2.length === 0 || text2.length > 200) return null;
3764
3798
  return ["Bearer", "mcp_", "eyJ"].some((m) => text2.includes(m)) ? null : text2;
3765
3799
  }
3800
+ function mcpKeyRevokeHint(apiUrl) {
3801
+ const base = apiUrl.replace(/\/+$/, "");
3802
+ return `a chave MCP criada aparece em ${base}/settings/security/sessions (Sess\xF5es MCP) e pode ser revogada l\xE1.`;
3803
+ }
3766
3804
  async function createMcpApiKey(session, fetchImpl = fetch) {
3767
3805
  let res;
3768
3806
  try {
@@ -3842,9 +3880,7 @@ async function runSetupMcpSubcommand() {
3842
3880
  try {
3843
3881
  await runMcpSetup(session, apiKey);
3844
3882
  } catch (error49) {
3845
- console.error(
3846
- " Instala\xE7\xE3o incompleta: a chave MCP criada aparece em Configura\xE7\xF5es \u203A MCP e pode ser revogada."
3847
- );
3883
+ console.error(` Instala\xE7\xE3o incompleta: ${mcpKeyRevokeHint(session.apiUrl)}`);
3848
3884
  throw error49;
3849
3885
  }
3850
3886
  console.log("\n Setup MCP completo!");
@@ -21631,35 +21667,33 @@ var authoredManifestSchema = external_exports.strictObject({
21631
21667
  })
21632
21668
  ).min(1).max(AUTHORED_LIMITS.maxModules)
21633
21669
  });
21634
- var authoredModule = external_exports.strictObject({
21635
- ...moduleFields,
21636
- lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
21637
- }).superRefine((module, ctx) => {
21638
- const types = module.lessons.map((lesson) => lesson.type);
21670
+ function moduleCeremonyIssues(types) {
21671
+ const issues = [];
21639
21672
  const last = types.length - 1;
21640
21673
  if (types[0] !== "intro") {
21641
- ctx.addIssue({
21642
- code: "custom",
21643
- path: ["lessons", 0, "type"],
21644
- message: "the first lesson of a module must be an intro"
21645
- });
21674
+ issues.push({ lesson: 0, message: "the first lesson of a module must be an intro" });
21646
21675
  }
21647
21676
  if (types[last] !== "checkpoint") {
21648
- ctx.addIssue({
21649
- code: "custom",
21650
- path: ["lessons", last, "type"],
21651
- message: "the last lesson of a module must be a checkpoint"
21652
- });
21677
+ issues.push({ lesson: last, message: "the last lesson of a module must be a checkpoint" });
21653
21678
  }
21654
21679
  for (const required2 of ["concept", "exercise", "realworld"]) {
21655
21680
  if (!types.includes(required2)) {
21656
- ctx.addIssue({
21657
- code: "custom",
21658
- path: ["lessons"],
21659
- message: `a module needs at least one ${required2} lesson`
21660
- });
21681
+ issues.push({ message: `a module needs at least one ${required2} lesson` });
21661
21682
  }
21662
21683
  }
21684
+ return issues;
21685
+ }
21686
+ var authoredModule = external_exports.strictObject({
21687
+ ...moduleFields,
21688
+ lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
21689
+ }).superRefine((module, ctx) => {
21690
+ for (const issue2 of moduleCeremonyIssues(module.lessons.map((lesson) => lesson.type))) {
21691
+ ctx.addIssue({
21692
+ code: "custom",
21693
+ path: issue2.lesson === void 0 ? ["lessons"] : ["lessons", issue2.lesson, "type"],
21694
+ message: issue2.message
21695
+ });
21696
+ }
21663
21697
  });
21664
21698
  var authoredCourseSchema = external_exports.strictObject({
21665
21699
  schemaVersion: external_exports.literal(1),
@@ -21709,26 +21743,66 @@ function readLesson(lessonPath2, files, issues) {
21709
21743
  }
21710
21744
  return { ...lesson, teachingContent: sidecar };
21711
21745
  }
21746
+ function looseLessonPaths(manifest) {
21747
+ const modules = manifest?.modules;
21748
+ if (!Array.isArray(modules)) return [];
21749
+ return modules.map((module) => {
21750
+ const paths = module?.lessons;
21751
+ return Array.isArray(paths) ? paths.filter((entry) => lessonPath.safeParse(entry).success) : [];
21752
+ });
21753
+ }
21754
+ function checkLesson(path29, files, issues) {
21755
+ const lesson = readLesson(path29, files, issues);
21756
+ if (lesson === null) return null;
21757
+ const parsed = authoredLessonSchema.safeParse(lesson);
21758
+ if (!parsed.success) {
21759
+ for (const issue2 of parsed.error.issues) {
21760
+ issues.push({ path: path29, message: describeIssue(issue2.path, issue2.message), severity: "error" });
21761
+ }
21762
+ }
21763
+ return lesson;
21764
+ }
21765
+ function lessonTypeOf(lesson) {
21766
+ const type = lesson?.type;
21767
+ return LESSON_TYPES.includes(type) ? type : null;
21768
+ }
21712
21769
  function assembleAuthoredCourse(manifest, files) {
21770
+ const issues = [];
21713
21771
  const parsedManifest = authoredManifestSchema.safeParse(manifest);
21714
21772
  if (!parsedManifest.success) {
21715
- return {
21716
- issues: parsedManifest.error.issues.map((issue2) => ({
21773
+ for (const issue2 of parsedManifest.error.issues) {
21774
+ issues.push({
21717
21775
  path: MANIFEST_FILE,
21718
21776
  message: describeIssue(issue2.path, issue2.message),
21719
21777
  severity: "error"
21720
- }))
21721
- };
21778
+ });
21779
+ }
21722
21780
  }
21781
+ const lessonPaths = parsedManifest.success ? parsedManifest.data.modules.map((module) => module.lessons) : looseLessonPaths(manifest);
21782
+ const lessons = lessonPaths.map(
21783
+ (paths) => paths.map((lessonPath2) => checkLesson(lessonPath2, files, issues))
21784
+ );
21785
+ lessons.forEach((moduleLessons, moduleIndex) => {
21786
+ const types = moduleLessons.map(lessonTypeOf);
21787
+ if (types.length === 0 || types.some((type) => type === null)) return;
21788
+ for (const issue2 of moduleCeremonyIssues(types)) {
21789
+ const file2 = issue2.lesson === void 0 ? void 0 : lessonPaths[moduleIndex]?.[issue2.lesson];
21790
+ issues.push({
21791
+ path: file2 ?? MANIFEST_FILE,
21792
+ message: file2 ? describeIssue(["type"], issue2.message) : describeIssue(["modules", moduleIndex, "lessons"], issue2.message),
21793
+ severity: "error"
21794
+ });
21795
+ }
21796
+ });
21797
+ if (!parsedManifest.success) return { issues };
21723
21798
  const { courseId, course, modules } = parsedManifest.data;
21724
- const issues = [];
21725
- const assembledModules = modules.map((module) => ({
21799
+ if (issues.length > 0) return { courseId, issues };
21800
+ const assembledModules = modules.map((module, moduleIndex) => ({
21726
21801
  title: module.title,
21727
21802
  ...module.description !== void 0 ? { description: module.description } : {},
21728
21803
  ...module.objectives !== void 0 ? { objectives: module.objectives } : {},
21729
- lessons: module.lessons.map((lessonPath2) => readLesson(lessonPath2, files, issues))
21804
+ lessons: lessons[moduleIndex] ?? []
21730
21805
  }));
21731
- if (issues.length > 0) return { courseId, issues };
21732
21806
  const parsed = authoredCourseSchema.safeParse({
21733
21807
  schemaVersion: 1,
21734
21808
  course,