@tostudy-ai/cli 0.18.20 → 0.18.23

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,29 @@ 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 identityOf(secrets) {
1329
+ if (secrets.userId) return { userId: secrets.userId, sessionId: secrets.sessionId };
1330
+ try {
1331
+ const claims = JSON.parse(
1332
+ Buffer.from(secrets.accessToken.split(".")[1] ?? "", "base64url").toString("utf-8")
1333
+ );
1334
+ if (typeof claims.userId !== "string") return null;
1335
+ return {
1336
+ userId: claims.userId,
1337
+ ...typeof claims.sid === "string" ? { sessionId: claims.sid } : {}
1338
+ };
1339
+ } catch {
1340
+ return null;
1341
+ }
1342
+ }
1343
+ function belongsTo(secrets, owner) {
1344
+ if (!owner) return true;
1345
+ const identity = identityOf(secrets);
1346
+ if (!identity) return null;
1347
+ if (identity.userId !== owner.userId) return false;
1348
+ if (identity.sessionId && owner.sessionId && identity.sessionId !== owner.sessionId) return false;
1349
+ return true;
1350
+ }
1324
1351
  function getStoreKey(configDir) {
1325
1352
  return path2.resolve(configDir ?? path2.join(os2.homedir(), ".tostudy"));
1326
1353
  }
@@ -1337,8 +1364,13 @@ async function hasCommand(command) {
1337
1364
  }
1338
1365
  async function saveFallback(secrets, configDir) {
1339
1366
  const fallbackPath = getFallbackPath(configDir);
1340
- fs.mkdirSync(path2.dirname(fallbackPath), { recursive: true });
1341
- await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), "utf-8");
1367
+ const dir = path2.dirname(fallbackPath);
1368
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
1369
+ await chmod(dir, 448);
1370
+ await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), {
1371
+ encoding: "utf-8",
1372
+ mode: 384
1373
+ });
1342
1374
  await chmod(fallbackPath, 384);
1343
1375
  }
1344
1376
  async function loadFallback(configDir) {
@@ -1367,11 +1399,10 @@ async function saveSessionSecrets(secrets, configDir) {
1367
1399
  payload,
1368
1400
  keychain
1369
1401
  ]);
1370
- return;
1371
1402
  } catch {
1372
- await saveFallback(secrets, configDir);
1373
- return;
1374
1403
  }
1404
+ await saveFallback(secrets, configDir);
1405
+ return;
1375
1406
  }
1376
1407
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
1377
1408
  await execFileAsync(
@@ -1392,9 +1423,17 @@ async function saveSessionSecrets(secrets, configDir) {
1392
1423
  }
1393
1424
  await saveFallback(secrets, configDir);
1394
1425
  }
1395
- async function loadStoredSessionSecrets(configDir) {
1426
+ async function loadStoredSessionSecrets(configDir, owner) {
1396
1427
  const account = getStoreKey(configDir);
1397
1428
  if (process.platform === "darwin") {
1429
+ let legacyFile = null;
1430
+ try {
1431
+ const fromFile = await loadFallback(configDir);
1432
+ const verdict = fromFile && belongsTo(fromFile, owner);
1433
+ if (fromFile && verdict === true) return fromFile;
1434
+ if (fromFile && verdict === null) legacyFile = fromFile;
1435
+ } catch {
1436
+ }
1398
1437
  try {
1399
1438
  const { stdout: stdout2 } = await execFileAsync("security", [
1400
1439
  "find-generic-password",
@@ -1404,9 +1443,10 @@ async function loadStoredSessionSecrets(configDir) {
1404
1443
  account,
1405
1444
  "-w"
1406
1445
  ]);
1407
- return JSON.parse(stdout2.trim());
1446
+ const fromKeychain = JSON.parse(stdout2.trim());
1447
+ return belongsTo(fromKeychain, owner) === false ? legacyFile : fromKeychain;
1408
1448
  } catch {
1409
- return loadFallback(configDir);
1449
+ return legacyFile;
1410
1450
  }
1411
1451
  }
1412
1452
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
@@ -1438,7 +1478,6 @@ async function deleteStoredSessionSecrets(configDir) {
1438
1478
  ]);
1439
1479
  } catch {
1440
1480
  }
1441
- return;
1442
1481
  }
1443
1482
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
1444
1483
  }
@@ -1461,7 +1500,9 @@ async function saveSession(session, configDir) {
1461
1500
  await saveSessionSecrets(
1462
1501
  {
1463
1502
  accessToken: session.token,
1464
- refreshToken: session.refreshToken
1503
+ refreshToken: session.refreshToken,
1504
+ userId: session.userId,
1505
+ ...session.sessionId ? { sessionId: session.sessionId } : {}
1465
1506
  },
1466
1507
  dir
1467
1508
  );
@@ -1478,12 +1519,18 @@ async function saveSession(session, configDir) {
1478
1519
  mode: 384
1479
1520
  });
1480
1521
  }
1522
+ function hasSessionConfig(configDir) {
1523
+ return fs2.existsSync(path3.join(getConfigDir(configDir), "config.json"));
1524
+ }
1481
1525
  async function getSession(configDir) {
1482
1526
  const dir = getConfigDir(configDir);
1483
1527
  const p = path3.join(dir, "config.json");
1484
1528
  if (!fs2.existsSync(p)) return null;
1485
1529
  const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
1486
- const secrets = await loadStoredSessionSecrets(dir);
1530
+ const secrets = await loadStoredSessionSecrets(dir, {
1531
+ userId: stored.userId,
1532
+ ...stored.sessionId ? { sessionId: stored.sessionId } : {}
1533
+ });
1487
1534
  if (!secrets) return null;
1488
1535
  return {
1489
1536
  token: secrets.accessToken,
@@ -3132,7 +3179,10 @@ async function refreshCliSession(session, configDir) {
3132
3179
  async function requireSession(configDir) {
3133
3180
  const session = await getSession(configDir);
3134
3181
  if (!session) {
3135
- process.stderr.write("Erro: N\xE3o autenticado. Rode: tostudy login\n");
3182
+ const errors = getErrors();
3183
+ process.stderr.write(
3184
+ `Erro: ${hasSessionConfig(configDir) ? errors.sessionSecretsUnreachable : errors.notAuthenticated}`
3185
+ );
3136
3186
  process.exit(2);
3137
3187
  }
3138
3188
  const expiresAt = new Date(session.expiresAt);
@@ -3756,7 +3806,7 @@ import { spawn } from "node:child_process";
3756
3806
  import { hostname } from "node:os";
3757
3807
 
3758
3808
  // src/version.ts
3759
- var CLI_VERSION = true ? "0.18.20" : "0.7.1";
3809
+ var CLI_VERSION = true ? "0.18.23" : "0.7.1";
3760
3810
 
3761
3811
  // src/installer/mcp-setup.ts
3762
3812
  function safeServerError(text2) {
@@ -21633,35 +21683,33 @@ var authoredManifestSchema = external_exports.strictObject({
21633
21683
  })
21634
21684
  ).min(1).max(AUTHORED_LIMITS.maxModules)
21635
21685
  });
21636
- var authoredModule = external_exports.strictObject({
21637
- ...moduleFields,
21638
- lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
21639
- }).superRefine((module, ctx) => {
21640
- const types = module.lessons.map((lesson) => lesson.type);
21686
+ function moduleCeremonyIssues(types) {
21687
+ const issues = [];
21641
21688
  const last = types.length - 1;
21642
21689
  if (types[0] !== "intro") {
21643
- ctx.addIssue({
21644
- code: "custom",
21645
- path: ["lessons", 0, "type"],
21646
- message: "the first lesson of a module must be an intro"
21647
- });
21690
+ issues.push({ lesson: 0, message: "the first lesson of a module must be an intro" });
21648
21691
  }
21649
21692
  if (types[last] !== "checkpoint") {
21650
- ctx.addIssue({
21651
- code: "custom",
21652
- path: ["lessons", last, "type"],
21653
- message: "the last lesson of a module must be a checkpoint"
21654
- });
21693
+ issues.push({ lesson: last, message: "the last lesson of a module must be a checkpoint" });
21655
21694
  }
21656
21695
  for (const required2 of ["concept", "exercise", "realworld"]) {
21657
21696
  if (!types.includes(required2)) {
21658
- ctx.addIssue({
21659
- code: "custom",
21660
- path: ["lessons"],
21661
- message: `a module needs at least one ${required2} lesson`
21662
- });
21697
+ issues.push({ message: `a module needs at least one ${required2} lesson` });
21663
21698
  }
21664
21699
  }
21700
+ return issues;
21701
+ }
21702
+ var authoredModule = external_exports.strictObject({
21703
+ ...moduleFields,
21704
+ lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
21705
+ }).superRefine((module, ctx) => {
21706
+ for (const issue2 of moduleCeremonyIssues(module.lessons.map((lesson) => lesson.type))) {
21707
+ ctx.addIssue({
21708
+ code: "custom",
21709
+ path: issue2.lesson === void 0 ? ["lessons"] : ["lessons", issue2.lesson, "type"],
21710
+ message: issue2.message
21711
+ });
21712
+ }
21665
21713
  });
21666
21714
  var authoredCourseSchema = external_exports.strictObject({
21667
21715
  schemaVersion: external_exports.literal(1),
@@ -21711,26 +21759,66 @@ function readLesson(lessonPath2, files, issues) {
21711
21759
  }
21712
21760
  return { ...lesson, teachingContent: sidecar };
21713
21761
  }
21762
+ function looseLessonPaths(manifest) {
21763
+ const modules = manifest?.modules;
21764
+ if (!Array.isArray(modules)) return [];
21765
+ return modules.map((module) => {
21766
+ const paths = module?.lessons;
21767
+ return Array.isArray(paths) ? paths.filter((entry) => lessonPath.safeParse(entry).success) : [];
21768
+ });
21769
+ }
21770
+ function checkLesson(path29, files, issues) {
21771
+ const lesson = readLesson(path29, files, issues);
21772
+ if (lesson === null) return null;
21773
+ const parsed = authoredLessonSchema.safeParse(lesson);
21774
+ if (!parsed.success) {
21775
+ for (const issue2 of parsed.error.issues) {
21776
+ issues.push({ path: path29, message: describeIssue(issue2.path, issue2.message), severity: "error" });
21777
+ }
21778
+ }
21779
+ return lesson;
21780
+ }
21781
+ function lessonTypeOf(lesson) {
21782
+ const type = lesson?.type;
21783
+ return LESSON_TYPES.includes(type) ? type : null;
21784
+ }
21714
21785
  function assembleAuthoredCourse(manifest, files) {
21786
+ const issues = [];
21715
21787
  const parsedManifest = authoredManifestSchema.safeParse(manifest);
21716
21788
  if (!parsedManifest.success) {
21717
- return {
21718
- issues: parsedManifest.error.issues.map((issue2) => ({
21789
+ for (const issue2 of parsedManifest.error.issues) {
21790
+ issues.push({
21719
21791
  path: MANIFEST_FILE,
21720
21792
  message: describeIssue(issue2.path, issue2.message),
21721
21793
  severity: "error"
21722
- }))
21723
- };
21794
+ });
21795
+ }
21724
21796
  }
21797
+ const lessonPaths = parsedManifest.success ? parsedManifest.data.modules.map((module) => module.lessons) : looseLessonPaths(manifest);
21798
+ const lessons = lessonPaths.map(
21799
+ (paths) => paths.map((lessonPath2) => checkLesson(lessonPath2, files, issues))
21800
+ );
21801
+ lessons.forEach((moduleLessons, moduleIndex) => {
21802
+ const types = moduleLessons.map(lessonTypeOf);
21803
+ if (types.length === 0 || types.some((type) => type === null)) return;
21804
+ for (const issue2 of moduleCeremonyIssues(types)) {
21805
+ const file2 = issue2.lesson === void 0 ? void 0 : lessonPaths[moduleIndex]?.[issue2.lesson];
21806
+ issues.push({
21807
+ path: file2 ?? MANIFEST_FILE,
21808
+ message: file2 ? describeIssue(["type"], issue2.message) : describeIssue(["modules", moduleIndex, "lessons"], issue2.message),
21809
+ severity: "error"
21810
+ });
21811
+ }
21812
+ });
21813
+ if (!parsedManifest.success) return { issues };
21725
21814
  const { courseId, course, modules } = parsedManifest.data;
21726
- const issues = [];
21727
- const assembledModules = modules.map((module) => ({
21815
+ if (issues.length > 0) return { courseId, issues };
21816
+ const assembledModules = modules.map((module, moduleIndex) => ({
21728
21817
  title: module.title,
21729
21818
  ...module.description !== void 0 ? { description: module.description } : {},
21730
21819
  ...module.objectives !== void 0 ? { objectives: module.objectives } : {},
21731
- lessons: module.lessons.map((lessonPath2) => readLesson(lessonPath2, files, issues))
21820
+ lessons: lessons[moduleIndex] ?? []
21732
21821
  }));
21733
- if (issues.length > 0) return { courseId, issues };
21734
21822
  const parsed = authoredCourseSchema.safeParse({
21735
21823
  schemaVersion: 1,
21736
21824
  course,