@przeprogramowani/10x-cli 1.26.5 → 1.28.0

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/README.md CHANGED
@@ -173,6 +173,7 @@ remain visible. Full lesson downloads and other commands remain available below.
173
173
  | `10x list` | Browse modules and lessons in your course |
174
174
  | `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
175
175
  | `10x sync` | Bulk-download / refresh lessons and report what changed upstream |
176
+ | `10x changelog` | Show 10x-toolkit release notes since your last sync |
176
177
  | `10x doctor` | Diagnose auth, API connectivity, and local config |
177
178
  | `10x bench` | Live top-10 AI model leaderboard from [10xbench.ai](https://10xbench.ai) — no login needed |
178
179
  | `10x bench-kit <action>` | Create (`init`) and update (`update`) a company benchmark instance from the [10x-bench-kit](https://github.com/przeprogramowani/10x-bench-kit) template |
@@ -278,6 +279,44 @@ all for skills and prompts. Config templates are create-only. Course rules retai
278
279
  conflict is reported, not a failure), `1` if any lesson failed to fetch. The full
279
280
  report is still emitted on a partial failure.
280
281
 
282
+ ### `10x changelog`
283
+
284
+ `10x changelog` shows the **10x-toolkit release notes** — which course skills,
285
+ prompts, rules and config templates changed in each toolkit release — since your
286
+ project's baseline. It describes toolkit releases, not your files: to see what a
287
+ sync would actually download or update in this project, run `10x sync --dry-run`.
288
+ It needs a login and never writes anything.
289
+
290
+ The baseline is recorded per project: after a successful `10x sync` (not
291
+ `--dry-run`, no failed lessons) or a full, unfiltered `10x get`, the CLI stores the newest
292
+ toolkit release version in the project manifest (`toolkit.version`). If the
293
+ backend cannot answer, nothing is recorded and the sync/get result is unchanged.
294
+ Without a recorded version, `10x changelog` falls back to the date of the last
295
+ lesson applied in the project; in a project with no manifest it shows the latest 5
296
+ releases and suggests running `10x sync`.
297
+
298
+ | Flag | Description |
299
+ |------|-------------|
300
+ | `--since <ref>` | Ignore the baseline: `vX.Y.Z` lists releases **after** that version; `YYYY-MM-DD` lists releases **from** the start of that UTC day |
301
+ | `--limit <n>` | Maximum number of releases, 1-100 (default 20; 5 without a baseline) |
302
+
303
+ ```bash
304
+ # What changed in the toolkit since this project's last sync
305
+ 10x changelog
306
+
307
+ # Browse history regardless of the baseline
308
+ 10x changelog --since v2.55.0
309
+ 10x changelog --since 2026-10-01 --limit 50
310
+ ```
311
+
312
+ Releases are listed newest first as `vX.Y.Z — <date>` followed by their notes, or
313
+ `No toolkit changes since <baseline>.` Exit code is `0` whether or not there are
314
+ new releases; an invalid `--since`/`--limit` exits `2`, a missing login `3`. Against
315
+ a backend that does not serve the changelog yet the command fails with
316
+ `changelog_unsupported` (exit `1`); `get` and `sync` keep working. JSON output
317
+ (`--json` or piped) is `{ baseline: { source, version?, date? }, newEntries,
318
+ entries }`, where `source` is `flag`, `sync`, `applied` or `none`.
319
+
281
320
  ### `10x bench`
282
321
 
283
322
  Shows the [10xBench](https://10xbench.ai) leaderboard — the top 10 AI models
package/dist/index.mjs CHANGED
@@ -3183,7 +3183,7 @@ var cac = (name = "") => new CAC(name);
3183
3183
  // package.json
3184
3184
  var package_default = {
3185
3185
  name: "@przeprogramowani/10x-cli",
3186
- version: "1.26.5",
3186
+ version: "1.28.0",
3187
3187
  description: "Open-source CLI for 10xDevs course content",
3188
3188
  repository: {
3189
3189
  type: "git",
@@ -3402,6 +3402,7 @@ var ERROR_CODE_MESSAGES = {
3402
3402
  module_locked: "This module is not available yet.",
3403
3403
  signing_failed: "Could not sign the download URL. Try again in a moment.",
3404
3404
  invalid_json: "The server received a malformed request.",
3405
+ changelog_unsupported: "The backend does not support the toolkit changelog yet.",
3405
3406
  unauthorized: "You are not signed in. Run `10x auth` first.",
3406
3407
  no_membership: "No active course membership was found for this email.",
3407
3408
  invalid_refresh_token: "Your session has expired. Run `10x auth` again.",
@@ -3922,6 +3923,62 @@ async function fetchCourses(token) {
3922
3923
  return { ok: false, status: 0, code: "discovery_invalid", error: "Invalid course discovery response; no course was selected." };
3923
3924
  return result;
3924
3925
  }
3926
+ var CHANGELOG_STATUSES = ["added", "modified", "removed", "renamed"];
3927
+ var CHANGELOG_ARTIFACT_KINDS = ["skills", "prompts", "rules", "configTemplates"];
3928
+ function isChangelogEntry(value) {
3929
+ if (!value || typeof value !== "object" || Array.isArray(value))
3930
+ return false;
3931
+ const e2 = value;
3932
+ if (e2["schemaVersion"] !== 1)
3933
+ return false;
3934
+ if (typeof e2["version"] !== "string" || !e2["version"])
3935
+ return false;
3936
+ if (e2["previousVersion"] !== null && typeof e2["previousVersion"] !== "string")
3937
+ return false;
3938
+ if (typeof e2["releasedAt"] !== "string" || typeof e2["markdown"] !== "string")
3939
+ return false;
3940
+ if (e2["model"] !== null && typeof e2["model"] !== "string")
3941
+ return false;
3942
+ if (!Array.isArray(e2["highlights"]) || !e2["highlights"].every((h2) => typeof h2 === "string"))
3943
+ return false;
3944
+ const artifacts = e2["artifacts"];
3945
+ if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts))
3946
+ return false;
3947
+ return CHANGELOG_ARTIFACT_KINDS.every((kind) => {
3948
+ const list = artifacts[kind];
3949
+ return Array.isArray(list) && list.every((item) => {
3950
+ if (!item || typeof item !== "object")
3951
+ return false;
3952
+ const change = item;
3953
+ return typeof change["name"] === "string" && typeof change["status"] === "string" && CHANGELOG_STATUSES.includes(change["status"]) && (change["summary"] === null || typeof change["summary"] === "string");
3954
+ });
3955
+ });
3956
+ }
3957
+ function validateChangelogResponse(value) {
3958
+ if (!value || typeof value !== "object")
3959
+ return false;
3960
+ const entries = value["entries"];
3961
+ return Array.isArray(entries) && entries.every(isChangelogEntry);
3962
+ }
3963
+ async function fetchChangelog(token, query = {}) {
3964
+ const params = new URLSearchParams;
3965
+ if (query.since !== undefined)
3966
+ params.set("since", query.since);
3967
+ if (query.sinceDate !== undefined)
3968
+ params.set("sinceDate", query.sinceDate);
3969
+ if (query.limit !== undefined)
3970
+ params.set("limit", String(query.limit));
3971
+ const qs = params.toString();
3972
+ const result = await apiGet(`/api/changelog${qs ? `?${qs}` : ""}`, { token });
3973
+ if (!result.ok) {
3974
+ if (result.status === 404)
3975
+ return { ...result, code: "changelog_unsupported", error: "The backend does not support the toolkit changelog yet." };
3976
+ return result;
3977
+ }
3978
+ if (!validateChangelogResponse(result.data))
3979
+ return { ok: false, status: 0, code: "changelog_invalid", error: "Invalid toolkit changelog response." };
3980
+ return result;
3981
+ }
3925
3982
  function catalogRelease(catalog) {
3926
3983
  if (catalog.course !== "10xdevs4")
3927
3984
  return;
@@ -4953,8 +5010,24 @@ function readManifest(dir) {
4953
5010
  }
4954
5011
  if (!isManifest(parsed))
4955
5012
  return null;
5013
+ if (parsed.toolkit !== undefined && !isToolkitBaseline(parsed.toolkit))
5014
+ delete parsed.toolkit;
4956
5015
  return parsed;
4957
5016
  }
5017
+ function isToolkitBaseline(value) {
5018
+ if (!value || typeof value !== "object" || Array.isArray(value))
5019
+ return false;
5020
+ const v3 = value;
5021
+ return typeof v3["version"] === "string" && v3["version"].length > 0 && typeof v3["recordedAt"] === "string";
5022
+ }
5023
+ function recordToolkitVersion(dir, version, now) {
5024
+ const manifest = readManifest(dir);
5025
+ if (!manifest)
5026
+ return false;
5027
+ manifest.toolkit = { version, recordedAt: now.toISOString() };
5028
+ writeManifest(dir, manifest);
5029
+ return true;
5030
+ }
4958
5031
  function writeManifest(dir, manifest) {
4959
5032
  assertProjectFilePath(dirname2(dir), join3(dir, MANIFEST_FILENAME));
4960
5033
  assertProjectFilePath(dirname2(dir), join3(dir, `${MANIFEST_FILENAME}.tmp`));
@@ -5682,9 +5755,167 @@ var defaultDeps = {
5682
5755
  now: () => new Date
5683
5756
  };
5684
5757
 
5758
+ // src/commands/changelog.ts
5759
+ import { join as join6 } from "node:path";
5760
+ var DEFAULT_CHANGELOG_LIMIT = 20;
5761
+ var NO_BASELINE_LIMIT = 5;
5762
+ var MAX_LIMIT = 100;
5763
+ var VERSION_RE = /^v\d+\.\d+\.\d+$/;
5764
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
5765
+ function registerChangelogCommand(cli) {
5766
+ cli.command("changelog", "Show 10x-toolkit changes since your last sync").option("--since <ref>", "Show changes after a version (vX.Y.Z) or from a date (YYYY-MM-DD)").option("--limit <n>", `Maximum number of entries, 1-${MAX_LIMIT} (default: ${DEFAULT_CHANGELOG_LIMIT})`).action(async (options) => {
5767
+ const ctx = resolveContext(options);
5768
+ await runChangelog(ctx, options);
5769
+ });
5770
+ }
5771
+ async function runChangelog(ctx, options) {
5772
+ const flagBaseline = options.since === undefined ? null : parseSinceFlag(ctx, options.since);
5773
+ const explicitLimit = options.limit === undefined ? undefined : parseLimitFlag(ctx, options.limit);
5774
+ const auth = await requireAuth(ctx);
5775
+ const baseline = flagBaseline ?? resolveManifestBaseline(ctx, process.cwd());
5776
+ const limit = explicitLimit ?? (baseline.source === "none" ? NO_BASELINE_LIMIT : DEFAULT_CHANGELOG_LIMIT);
5777
+ const query = { limit };
5778
+ if (baseline.version !== undefined)
5779
+ query.since = baseline.version;
5780
+ else if (baseline.date !== undefined)
5781
+ query.sinceDate = baseline.date;
5782
+ verbose(ctx, `fetching changelog ${JSON.stringify(query)}`);
5783
+ const result = await fetchChangelog(auth.access_token, query);
5784
+ if (!result.ok)
5785
+ handleChangelogError(ctx, result.status, result.code, result.error);
5786
+ render(ctx, baseline, result.data.entries);
5787
+ }
5788
+ function parseSinceFlag(ctx, raw) {
5789
+ const value = typeof raw === "string" ? raw.trim() : "";
5790
+ if (VERSION_RE.test(value))
5791
+ return { source: "flag", version: value };
5792
+ if (isCalendarDate(value))
5793
+ return { source: "flag", date: value };
5794
+ outputError(ctx, "invalid_since", `'${typeof raw === "string" ? raw : String(raw)}' is not a valid --since value.`, ExitCodes.USAGE, "Pass a toolkit version like 'v2.55.0' or a date like '2026-10-01'.");
5795
+ }
5796
+ function isCalendarDate(value) {
5797
+ const match = DATE_RE.exec(value);
5798
+ if (!match)
5799
+ return false;
5800
+ const [, y3, m3, d2] = match;
5801
+ const year = Number(y3);
5802
+ const month = Number(m3);
5803
+ const day = Number(d2);
5804
+ const date = new Date(Date.UTC(year, month - 1, day));
5805
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
5806
+ }
5807
+ function parseLimitFlag(ctx, raw) {
5808
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
5809
+ if (/^\d+$/.test(text)) {
5810
+ const n = Number(text);
5811
+ if (n >= 1 && n <= MAX_LIMIT)
5812
+ return n;
5813
+ }
5814
+ outputError(ctx, "invalid_limit", `'${typeof raw === "string" || typeof raw === "number" ? raw : String(raw)}' is not a valid --limit value.`, ExitCodes.USAGE, `Pass a whole number between 1 and ${MAX_LIMIT}.`);
5815
+ }
5816
+ function findProjectManifest(projectRoot) {
5817
+ const configured = getToolProfile(readToolConfig()?.tool ?? DEFAULT_TOOL) ?? PROFILES[DEFAULT_TOOL];
5818
+ const candidates = [configured, ...Object.values(PROFILES).filter((p2) => p2.toolId !== configured.toolId)];
5819
+ for (const profile of candidates) {
5820
+ const manifest = readManifest(join6(projectRoot, profile.manifestDir));
5821
+ if (manifest)
5822
+ return { profile, manifest };
5823
+ }
5824
+ return null;
5825
+ }
5826
+ function resolveManifestBaseline(ctx, projectRoot) {
5827
+ const found = findProjectManifest(projectRoot);
5828
+ if (!found) {
5829
+ verbose(ctx, "no project manifest found");
5830
+ return { source: "none" };
5831
+ }
5832
+ const { profile, manifest } = found;
5833
+ verbose(ctx, `using manifest in ${profile.manifestDir}/`);
5834
+ if (manifest.toolkit)
5835
+ return { source: "sync", version: manifest.toolkit.version };
5836
+ const applied = newestAppliedAt(manifest);
5837
+ if (applied)
5838
+ return { source: "applied", date: applied };
5839
+ return { source: "none" };
5840
+ }
5841
+ function newestAppliedAt(manifest) {
5842
+ let newest;
5843
+ for (const lesson of Object.values(manifest.lessons ?? {})) {
5844
+ const t = Date.parse(lesson.appliedAt);
5845
+ if (Number.isFinite(t) && (newest === undefined || t > newest))
5846
+ newest = t;
5847
+ }
5848
+ if (newest === undefined) {
5849
+ const t = Date.parse(manifest.lastApplied);
5850
+ if (Number.isFinite(t))
5851
+ newest = t;
5852
+ }
5853
+ return newest === undefined ? undefined : new Date(newest).toISOString();
5854
+ }
5855
+ function handleChangelogError(ctx, status, code, error) {
5856
+ if (code === "changelog_unsupported") {
5857
+ outputError(ctx, code, "The 10x-toolkit API does not support the toolkit changelog yet.", ExitCodes.ERROR, "The changelog endpoint has not been deployed yet. Try again later; '10x sync' still updates your files.");
5858
+ }
5859
+ if (status === 403) {
5860
+ outputError(ctx, "course_access_denied", "Your account has no active course access, so there is no toolkit changelog to show.", ExitCodes.FORBIDDEN);
5861
+ }
5862
+ if (status === 401) {
5863
+ outputError(ctx, "auth_required", "Your session is no longer valid.", ExitCodes.AUTH_REQUIRED, "Run '10x auth' to log in again.");
5864
+ }
5865
+ if (status === 0 && code === "network_error") {
5866
+ outputError(ctx, "network_error", "Could not reach the 10x-toolkit API.", ExitCodes.ERROR, "Check your internet connection and run '10x changelog' again.");
5867
+ }
5868
+ outputError(ctx, code || "changelog_failed", error || "Failed to load the toolkit changelog.", ExitCodes.ERROR);
5869
+ }
5870
+ function describeBaseline(baseline) {
5871
+ return baseline.version ?? baseline.date ?? "";
5872
+ }
5873
+ function describeSource(baseline) {
5874
+ switch (baseline.source) {
5875
+ case "flag":
5876
+ return "from --since";
5877
+ case "sync":
5878
+ return "recorded by your last 10x sync";
5879
+ case "applied":
5880
+ return "last lesson applied in this project";
5881
+ case "none":
5882
+ return "no baseline recorded";
5883
+ }
5884
+ }
5885
+ function render(ctx, baseline, entries) {
5886
+ if (ctx.json) {
5887
+ output(ctx, "", { baseline, newEntries: entries.length, entries });
5888
+ return;
5889
+ }
5890
+ const lines = [];
5891
+ if (baseline.source === "none") {
5892
+ lines.push(`Latest toolkit changes (${describeSource(baseline)})`);
5893
+ } else {
5894
+ lines.push(`Toolkit changes since ${describeBaseline(baseline)} (${describeSource(baseline)})`);
5895
+ }
5896
+ lines.push("");
5897
+ if (entries.length === 0) {
5898
+ lines.push(baseline.source === "none" ? "No toolkit changes published yet." : `No toolkit changes since ${describeBaseline(baseline)}.`);
5899
+ } else {
5900
+ entries.forEach((entry, index) => {
5901
+ if (index > 0)
5902
+ lines.push("");
5903
+ lines.push(`${entry.version} — ${entry.releasedAt.slice(0, 10)}`);
5904
+ lines.push("");
5905
+ lines.push(entry.markdown.trimEnd());
5906
+ });
5907
+ }
5908
+ if (baseline.source === "none") {
5909
+ lines.push("");
5910
+ lines.push("Run 10x sync to record your baseline.");
5911
+ }
5912
+ output(ctx, lines.join(`
5913
+ `), undefined);
5914
+ }
5915
+
5685
5916
  // src/commands/doctor.ts
5686
5917
  import { existsSync as existsSync5, accessSync, constants } from "node:fs";
5687
- import { join as join6 } from "node:path";
5918
+ import { join as join7 } from "node:path";
5688
5919
 
5689
5920
  // src/lib/sentinel-migration.ts
5690
5921
  var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
@@ -6085,7 +6316,7 @@ function checkToolDirectory() {
6085
6316
  const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
6086
6317
  const profile = getToolProfile(toolId) ?? PROFILES[DEFAULT_TOOL];
6087
6318
  const dirName = profile.manifestDir;
6088
- const toolDir = join6(cwd, dirName);
6319
+ const toolDir = join7(cwd, dirName);
6089
6320
  if (!existsSync5(toolDir)) {
6090
6321
  return {
6091
6322
  name: "tool-dir",
@@ -6118,7 +6349,7 @@ function checkToolDirectory() {
6118
6349
  }
6119
6350
 
6120
6351
  // src/commands/get.ts
6121
- import { join as join10 } from "node:path";
6352
+ import { join as join11 } from "node:path";
6122
6353
 
6123
6354
  // src/lib/conflict-prompt.ts
6124
6355
  init_dist2();
@@ -6207,7 +6438,7 @@ init_dist2();
6207
6438
 
6208
6439
  // src/lib/tool-switch.ts
6209
6440
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync5, statSync, chmodSync as chmodSync3, openSync, closeSync } from "node:fs";
6210
- import { dirname as dirname5, join as join9 } from "node:path";
6441
+ import { dirname as dirname5, join as join10 } from "node:path";
6211
6442
  import { randomUUID as randomUUID3 } from "node:crypto";
6212
6443
 
6213
6444
  // src/lib/managed-removal.ts
@@ -6260,13 +6491,13 @@ function pruneEmptyParents(root, file, boundary) {
6260
6491
 
6261
6492
  // src/lib/managed-rules.ts
6262
6493
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
6263
- import { join as join7 } from "node:path";
6494
+ import { join as join8 } from "node:path";
6264
6495
  function otherRulesOwners(root, profile) {
6265
6496
  const owners = [];
6266
6497
  for (const candidate of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
6267
6498
  if (candidate.manifestDir === profile.manifestDir || candidate.rulesFile !== profile.rulesFile)
6268
6499
  continue;
6269
- const manifest = readManifest(join7(root, candidate.manifestDir));
6500
+ const manifest = readManifest(join8(root, candidate.manifestDir));
6270
6501
  if (!manifest)
6271
6502
  continue;
6272
6503
  if (manifest.managedRules || !manifest.lessons || Object.values(manifest.lessons).some((lesson) => lesson.representation?.courseRules !== false))
@@ -6282,7 +6513,7 @@ function inspectExisting(content, begin, end) {
6282
6513
  }
6283
6514
  }
6284
6515
  function planManagedRules(root, profile, body, enabled, baseline) {
6285
- const path = join7(root, profile.rulesFile);
6516
+ const path = join8(root, profile.rulesFile);
6286
6517
  assertProjectFilePath(root, path);
6287
6518
  const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
6288
6519
  const current = inspectExisting(existing, profile.sentinelBegin, profile.sentinelEnd);
@@ -6328,7 +6559,7 @@ import {
6328
6559
  readFileSync as readFileSync7,
6329
6560
  writeFileSync as writeFileSync4
6330
6561
  } from "node:fs";
6331
- import { dirname as dirname4, join as join8 } from "node:path";
6562
+ import { dirname as dirname4, join as join9 } from "node:path";
6332
6563
  var CLI_VERSION = package_default.version;
6333
6564
  async function applyBundle(bundle, projectRoot, options) {
6334
6565
  const dryRun = options.dryRun === true;
@@ -6337,7 +6568,7 @@ async function applyBundle(bundle, projectRoot, options) {
6337
6568
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
6338
6569
  const applyCourseRules = options.applyCourseRules !== false;
6339
6570
  const plan = planBundle(bundle, projectRoot, { course, profile, applyCourseRules });
6340
- const manifestDir = join8(projectRoot, profile.manifestDir);
6571
+ const manifestDir = join9(projectRoot, profile.manifestDir);
6341
6572
  const previous = readManifest(manifestDir);
6342
6573
  const ledger = previous ? structuredClone(previous) : {
6343
6574
  package: CLI_PACKAGE_NAME,
@@ -6454,22 +6685,22 @@ async function applyBundle(bundle, projectRoot, options) {
6454
6685
  let rulesDelivered = !rules.isConflict;
6455
6686
  let rulesBackup;
6456
6687
  if (rules.isConflict && !rules.blocked && !dryRun) {
6457
- const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join8(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
6688
+ const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join9(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
6458
6689
  if (resolution !== "skip") {
6459
6690
  rulesDelivered = true;
6460
6691
  rulesAction = resolution === "save_user" ? "conflict_saved_user" : "conflict_overwritten";
6461
6692
  if (resolution === "save_user") {
6462
- rulesBackup = buildUserBackupPath(join8(projectRoot, profile.rulesFile));
6693
+ rulesBackup = buildUserBackupPath(join9(projectRoot, profile.rulesFile));
6463
6694
  assertProjectFilePath(projectRoot, rulesBackup);
6464
- assertProjectFilePath(projectRoot, join8(projectRoot, profile.rulesFile));
6465
- copyFileSync(join8(projectRoot, profile.rulesFile), rulesBackup);
6695
+ assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
6696
+ copyFileSync(join9(projectRoot, profile.rulesFile), rulesBackup);
6466
6697
  }
6467
6698
  }
6468
6699
  }
6469
6700
  if (rulesDelivered) {
6470
6701
  if (!dryRun && rulesAction !== "unchanged") {
6471
- assertProjectFilePath(projectRoot, join8(projectRoot, profile.rulesFile));
6472
- const path = join8(projectRoot, profile.rulesFile);
6702
+ assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
6703
+ const path = join9(projectRoot, profile.rulesFile);
6473
6704
  if (!existsSync8(path) || readFileSync7(path, "utf8") !== rules.content)
6474
6705
  writeFileAt(path, rules.content);
6475
6706
  }
@@ -6546,14 +6777,14 @@ function planBundle(bundle, projectRoot, options) {
6546
6777
  assertSafeName(prompt.name, "prompt");
6547
6778
  for (const config of bundle.configs)
6548
6779
  assertSafeName(config.name, "config");
6549
- const manifestDir = join8(projectRoot, profile.manifestDir);
6550
- assertProjectFilePath(projectRoot, join8(manifestDir, MANIFEST_FILENAME));
6780
+ const manifestDir = join9(projectRoot, profile.manifestDir);
6781
+ assertProjectFilePath(projectRoot, join9(manifestDir, MANIFEST_FILENAME));
6551
6782
  const prevManifest = readManifest(manifestDir);
6552
6783
  const skills = bundle.skills.map((skill) => {
6553
- const skillDir = join8(projectRoot, profile.skillDir(skill.name));
6784
+ const skillDir = join9(projectRoot, profile.skillDir(skill.name));
6554
6785
  const prevEntry = prevManifest?.files.skills[skill.name];
6555
6786
  const files = skill.files.map((file) => {
6556
- const target = join8(skillDir, file.path);
6787
+ const target = join9(skillDir, file.path);
6557
6788
  assertProjectFilePath(projectRoot, target);
6558
6789
  const storedHash = prevEntry?.contentHashes?.[file.path];
6559
6790
  const { action, isConflict } = computeFileAction(target, file.content, storedHash);
@@ -6568,7 +6799,7 @@ function planBundle(bundle, projectRoot, options) {
6568
6799
  return { name: skill.name, files };
6569
6800
  });
6570
6801
  const prompts = bundle.prompts.map((prompt) => {
6571
- const target = join8(projectRoot, profile.promptPath(prompt.name));
6802
+ const target = join9(projectRoot, profile.promptPath(prompt.name));
6572
6803
  assertProjectFilePath(projectRoot, target);
6573
6804
  const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
6574
6805
  const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
@@ -6580,12 +6811,12 @@ function planBundle(bundle, projectRoot, options) {
6580
6811
  upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
6581
6812
  };
6582
6813
  });
6583
- const rulesFilePath = join8(projectRoot, profile.rulesFile);
6814
+ const rulesFilePath = join9(projectRoot, profile.rulesFile);
6584
6815
  const rulesPlan = planManagedRules(projectRoot, profile, bundle.rules.length ? bundle.rules.map((r2) => r2.content.trim()).join(`
6585
6816
 
6586
6817
  `) : undefined, applyCourseRules, prevManifest?.managedRules);
6587
6818
  const configs = bundle.configs.map((config) => {
6588
- const target = join8(projectRoot, profile.configPath(config.name));
6819
+ const target = join9(projectRoot, profile.configPath(config.name));
6589
6820
  assertProjectFilePath(projectRoot, target);
6590
6821
  const action = existsSync8(target) ? "skipped" : "created";
6591
6822
  return {
@@ -6598,8 +6829,8 @@ function planBundle(bundle, projectRoot, options) {
6598
6829
  });
6599
6830
  const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
6600
6831
  const targets = [
6601
- join8(manifestDir, MANIFEST_FILENAME),
6602
- join8(manifestDir, `${MANIFEST_FILENAME}.tmp`),
6832
+ join9(manifestDir, MANIFEST_FILENAME),
6833
+ join9(manifestDir, `${MANIFEST_FILENAME}.tmp`),
6603
6834
  rulesFilePath,
6604
6835
  buildUserBackupPath(rulesFilePath),
6605
6836
  ...skills.flatMap((skill) => skill.files.flatMap((file) => [file.path, buildUserBackupPath(file.path)])),
@@ -6692,7 +6923,7 @@ function computeRemovals(previous, bundle, profile, root) {
6692
6923
  throw new Error("Unsafe skill path in manifest");
6693
6924
  if (bundle.skills.some((entry) => entry.name === name && entry.files.some((entry2) => entry2.path === file)))
6694
6925
  continue;
6695
- out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path: join8(root, profile.skillDir(name), file), storedHash: previous.files.skills[name]?.contentHashes?.[file], protected: others.some((entry) => entry.skills[name]?.files.includes(file)), pruneRoot: join8(root, profile.skillDir(name)) }));
6926
+ out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path: join9(root, profile.skillDir(name), file), storedHash: previous.files.skills[name]?.contentHashes?.[file], protected: others.some((entry) => entry.skills[name]?.files.includes(file)), pruneRoot: join9(root, profile.skillDir(name)) }));
6696
6927
  }
6697
6928
  }
6698
6929
  for (const kind of ["prompts", "configs"])
@@ -6701,7 +6932,7 @@ function computeRemovals(previous, bundle, profile, root) {
6701
6932
  throw new Error("Unsafe file name in manifest");
6702
6933
  if ((kind === "prompts" ? bundle.prompts.map((p2) => `${p2.name}.md`) : bundle.configs.map((c2) => c2.name)).includes(name))
6703
6934
  continue;
6704
- const path = join8(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
6935
+ const path = join9(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
6705
6936
  out[kind].push(planManagedRemoval(root, { name, path, storedHash: kind === "prompts" ? previous.files.promptHashes?.[name] : previous.files.configHashes?.[name], protected: others.some((entry) => entry[kind].includes(name)), config: kind === "configs" }));
6706
6937
  }
6707
6938
  return out;
@@ -6711,10 +6942,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
6711
6942
  for (const profile of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
6712
6943
  if (profile.toolId === currentProfile.toolId)
6713
6944
  continue;
6714
- const manifestPath = join8(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
6945
+ const manifestPath = join9(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
6715
6946
  if (!existsSync8(manifestPath))
6716
6947
  continue;
6717
- const manifest = readManifest(join8(projectRoot, profile.manifestDir));
6948
+ const manifest = readManifest(join9(projectRoot, profile.manifestDir));
6718
6949
  if (!manifest)
6719
6950
  continue;
6720
6951
  out.push({ profile, manifestPath, manifest });
@@ -6789,7 +7020,7 @@ function entries(manifest) {
6789
7020
  function entryPath(root, profile, entry) {
6790
7021
  if (!isSafeName(entry.name) || entry.file !== undefined && !isSafeSkillFilePath(entry.file))
6791
7022
  throw new Error("unsafe name or path in manifest");
6792
- return join9(root, entry.kind === "skills" ? join9(profile.skillDir(entry.name), entry.file) : entry.kind === "prompts" ? profile.promptPath(entry.name.replace(/\.md$/, "")) : profile.configPath(entry.name));
7023
+ return join10(root, entry.kind === "skills" ? join10(profile.skillDir(entry.name), entry.file) : entry.kind === "prompts" ? profile.promptPath(entry.name.replace(/\.md$/, "")) : profile.configPath(entry.name));
6793
7024
  }
6794
7025
  function owns(lesson, entry) {
6795
7026
  return entry.kind === "skills" ? !!lesson.skills[entry.name]?.files.includes(entry.file) : lesson[entry.kind].includes(entry.name);
@@ -6841,19 +7072,19 @@ function record(summary, entry) {
6841
7072
  }
6842
7073
  function persistSource(root, profile, source) {
6843
7074
  rebuildManifestFiles(source);
6844
- const dir = join9(root, profile.manifestDir);
7075
+ const dir = join10(root, profile.manifestDir);
6845
7076
  if (entries(source).length || source.managedRules) {
6846
7077
  writeManifest(dir, source);
6847
7078
  return;
6848
7079
  }
6849
- const path = join9(dir, MANIFEST_FILENAME);
7080
+ const path = join10(dir, MANIFEST_FILENAME);
6850
7081
  assertProjectFilePath(root, path);
6851
7082
  rmSync6(path, { force: true });
6852
7083
  pruneEmptyParents(root, path, dir);
6853
7084
  }
6854
7085
  function commitSourceRules(root, profile, source, update) {
6855
- const rulesPath = join9(root, profile.rulesFile);
6856
- const manifestPath = join9(root, profile.manifestDir, MANIFEST_FILENAME);
7086
+ const rulesPath = join10(root, profile.rulesFile);
7087
+ const manifestPath = join10(root, profile.manifestDir, MANIFEST_FILENAME);
6857
7088
  assertProjectFilePath(root, rulesPath);
6858
7089
  assertProjectFilePath(root, manifestPath);
6859
7090
  const rulesBefore = existsSync9(rulesPath) ? readFileSync8(rulesPath) : undefined;
@@ -6899,16 +7130,16 @@ function copyForTransfer(root, from, to, bytes) {
6899
7130
  function preflightProfilePaths(root, orphan, destination) {
6900
7131
  assertProjectFilePath(root, orphan.manifestPath);
6901
7132
  for (const profile of destination ? [orphan.profile, destination] : [orphan.profile]) {
6902
- assertProjectPath(root, join9(root, profile.manifestDir));
7133
+ assertProjectPath(root, join10(root, profile.manifestDir));
6903
7134
  for (const file of [MANIFEST_FILENAME, `${MANIFEST_FILENAME}.tmp`])
6904
- assertProjectFilePath(root, join9(root, profile.manifestDir, file));
6905
- assertProjectFilePath(root, join9(root, profile.rulesFile));
7135
+ assertProjectFilePath(root, join10(root, profile.manifestDir, file));
7136
+ assertProjectFilePath(root, join10(root, profile.rulesFile));
6906
7137
  for (const entry of entries(orphan.manifest)) {
6907
7138
  const path = entryPath(root, profile, entry);
6908
7139
  assertProjectFilePath(root, path);
6909
7140
  assertProjectFilePath(root, `${path}.tmp`);
6910
7141
  }
6911
- const rulesPath = join9(root, profile.rulesFile);
7142
+ const rulesPath = join10(root, profile.rulesFile);
6912
7143
  if (existsSync9(rulesPath))
6913
7144
  inspectRulesBlock(readFileSync8(rulesPath, "utf8"), profile.sentinelBegin, profile.sentinelEnd);
6914
7145
  }
@@ -6923,7 +7154,7 @@ function begin(root, orphan, destination) {
6923
7154
  const source = structuredClone(orphan.manifest);
6924
7155
  source.course = course;
6925
7156
  source.lessons = seedLessons(source);
6926
- const rulesPath = join9(root, orphan.profile.rulesFile);
7157
+ const rulesPath = join10(root, orphan.profile.rulesFile);
6927
7158
  if (!source.managedRules && existsSync9(rulesPath) && inspectRulesBlock(readFileSync8(rulesPath, "utf8"), orphan.profile.sentinelBegin, orphan.profile.sentinelEnd)) {
6928
7159
  source.managedRules = { path: orphan.profile.rulesFile, begin: orphan.profile.sentinelBegin, end: orphan.profile.sentinelEnd };
6929
7160
  }
@@ -6934,7 +7165,7 @@ function migrateArtifacts(root, orphan, profile) {
6934
7165
  if (!existsSync9(orphan.manifestPath))
6935
7166
  return summary;
6936
7167
  const source = begin(root, orphan, profile);
6937
- const destination = readManifest(join9(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
7168
+ const destination = readManifest(join10(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
6938
7169
  destination.lessons = seedLessons(destination);
6939
7170
  destination.course = source.course;
6940
7171
  destination.tool = profile.toolId;
@@ -6954,7 +7185,7 @@ function migrateArtifacts(root, orphan, profile) {
6954
7185
  if (!existsSync9(to))
6955
7186
  copyForTransfer(root, from, to, bytes);
6956
7187
  acquire(destination, source, entry);
6957
- writeManifest(join9(root, profile.manifestDir), destination);
7188
+ writeManifest(join10(root, profile.manifestDir), destination);
6958
7189
  record(summary, entry);
6959
7190
  try {
6960
7191
  assertProjectFilePath(root, from);
@@ -6977,7 +7208,7 @@ function migrateArtifacts(root, orphan, profile) {
6977
7208
  }
6978
7209
  throw error;
6979
7210
  }
6980
- pruneEmptyParents(root, from, join9(root, orphan.profile.manifestDir));
7211
+ pruneEmptyParents(root, from, join10(root, orphan.profile.manifestDir));
6981
7212
  }
6982
7213
  commitSourceRules(root, orphan.profile, source, () => {
6983
7214
  transferRules(root, orphan.profile, profile, source, destination, summary);
@@ -6988,7 +7219,7 @@ function transferRules(root, old, profile, source, destination, summary) {
6988
7219
  const metadata = source.managedRules;
6989
7220
  if (!metadata)
6990
7221
  return;
6991
- const from = join9(root, old.rulesFile);
7222
+ const from = join10(root, old.rulesFile);
6992
7223
  if (!existsSync9(from)) {
6993
7224
  delete source.managedRules;
6994
7225
  return;
@@ -7009,24 +7240,24 @@ function transferRules(root, old, profile, source, destination, summary) {
7009
7240
  return;
7010
7241
  }
7011
7242
  destination.managedRules = { ...metadata };
7012
- writeManifest(join9(root, profile.manifestDir), destination);
7243
+ writeManifest(join10(root, profile.manifestDir), destination);
7013
7244
  if (!entries(source).length)
7014
7245
  delete source.managedRules;
7015
7246
  return;
7016
7247
  }
7017
7248
  const plan = planManagedRules(root, profile, block.body, true, destination.managedRules);
7018
7249
  if (plan.isConflict) {
7019
- summary.skipped.push({ path: join9(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
7250
+ summary.skipped.push({ path: join10(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
7020
7251
  return;
7021
7252
  }
7022
- const to = join9(root, profile.rulesFile);
7253
+ const to = join10(root, profile.rulesFile);
7023
7254
  if (plan.action !== "unchanged") {
7024
7255
  assertProjectFilePath(root, to);
7025
7256
  mkdirSync5(dirname5(to), { recursive: true });
7026
7257
  writeFileSync5(to, plan.content);
7027
7258
  }
7028
7259
  destination.managedRules = plan.next;
7029
- writeManifest(join9(root, profile.manifestDir), destination);
7260
+ writeManifest(join10(root, profile.manifestDir), destination);
7030
7261
  if (entries(source).length)
7031
7262
  return;
7032
7263
  if (!otherRulesOwners(root, old).length) {
@@ -7044,7 +7275,7 @@ function deleteArtifacts(root, orphan) {
7044
7275
  const source = begin(root, orphan);
7045
7276
  for (const entry of entries(source)) {
7046
7277
  const path = entryPath(root, orphan.profile, entry);
7047
- const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join9(root, orphan.profile.manifestDir) });
7278
+ const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join10(root, orphan.profile.manifestDir) });
7048
7279
  const result = executeManagedRemoval(root, plan);
7049
7280
  if (result.action === "removed")
7050
7281
  record(summary, entry);
@@ -7055,7 +7286,7 @@ function deleteArtifacts(root, orphan) {
7055
7286
  }
7056
7287
  commitSourceRules(root, orphan.profile, source, () => {
7057
7288
  if (source.managedRules) {
7058
- const path = join9(root, orphan.profile.rulesFile);
7289
+ const path = join10(root, orphan.profile.rulesFile);
7059
7290
  const plan = planManagedRules(root, orphan.profile, undefined, false, source.managedRules);
7060
7291
  if (plan.isConflict)
7061
7292
  summary.skipped.push({ path, reason: "managed rules require explicit resolution: missing baseline or local edits" });
@@ -7204,6 +7435,28 @@ function printMigrationSummary(summary, newProfile) {
7204
7435
  `));
7205
7436
  }
7206
7437
 
7438
+ // src/lib/toolkit-baseline.ts
7439
+ async function recordLatestToolkitVersion(ctx, token, manifestDir, now = () => new Date) {
7440
+ try {
7441
+ if (!readManifest(manifestDir))
7442
+ return;
7443
+ const result = await fetchChangelog(token, { limit: 1 });
7444
+ if (!result.ok) {
7445
+ verbose(ctx, `toolkit version not recorded: ${result.code}`);
7446
+ return;
7447
+ }
7448
+ const latest = result.data.entries[0];
7449
+ if (!latest) {
7450
+ verbose(ctx, "toolkit version not recorded: no changelog entries");
7451
+ return;
7452
+ }
7453
+ if (recordToolkitVersion(manifestDir, latest.version, now()))
7454
+ verbose(ctx, `recorded toolkit ${latest.version}`);
7455
+ } catch (error) {
7456
+ verbose(ctx, `toolkit version not recorded: ${error instanceof Error ? error.message : String(error)}`);
7457
+ }
7458
+ }
7459
+
7207
7460
  // src/commands/get.ts
7208
7461
  var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
7209
7462
  function isArtifactType(s) {
@@ -7277,7 +7530,7 @@ async function runGet(ctx, ref, options) {
7277
7530
  const isFiltered = options.type !== undefined;
7278
7531
  const bundle = filterBundle(ctx, result.data, options);
7279
7532
  const isTTY = process.stdout.isTTY === true;
7280
- const prevManifest = readManifest(join10(process.cwd(), profile.manifestDir));
7533
+ const prevManifest = readManifest(join11(process.cwd(), profile.manifestDir));
7281
7534
  if (prevManifest && prevManifest.manifestVersion === 2) {
7282
7535
  showUpgradeNotice(isTTY);
7283
7536
  }
@@ -7302,6 +7555,9 @@ async function runGet(ctx, ref, options) {
7302
7555
  } catch (error) {
7303
7556
  reportCourseError(ctx, error);
7304
7557
  }
7558
+ if (!dryRun && !isFiltered) {
7559
+ await recordLatestToolkitVersion(ctx, auth.access_token, join11(process.cwd(), profile.manifestDir));
7560
+ }
7305
7561
  renderGetResult(ctx, bundle, writeResult, dryRun, profile, {
7306
7562
  course,
7307
7563
  selectionReason: selection.reason,
@@ -7534,7 +7790,7 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta) {
7534
7790
 
7535
7791
  // src/commands/helpers.ts
7536
7792
  import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
7537
- import { dirname as dirname6, join as join11, resolve as resolve3 } from "node:path";
7793
+ import { dirname as dirname6, join as join12, resolve as resolve3 } from "node:path";
7538
7794
 
7539
7795
  // skills/10x-cli-setup/SKILL.md
7540
7796
  var SKILL_default = `---
@@ -8023,7 +8279,7 @@ their own evidence; do not imply a Windows or translated-content walkthrough ran
8023
8279
  // skills/10x-cli-guide/SKILL.md
8024
8280
  var SKILL_default2 = `---
8025
8281
  name: 10x-cli-guide
8026
- description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
8282
+ description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
8027
8283
  ---
8028
8284
 
8029
8285
  # 10x-cli: download, use, update
@@ -8239,6 +8495,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
8239
8495
  templates remain create-only. Cleanup preserves modified/untracked files and
8240
8496
  files owned elsewhere; do not manually sweep a skill directory after sync.
8241
8497
 
8498
+ To see what changed in the course toolkit itself, use \`changelog\`. This command
8499
+ is **unreleased**: check the actual runner's \`changelog --help\` first.
8500
+
8501
+ \`\`\`bash
8502
+ 10x_cli changelog # toolkit releases since this project's last sync
8503
+ 10x_cli changelog --since v2.55.0 # releases after a version
8504
+ 10x_cli changelog --since 2026-10-01 # releases from that UTC day on
8505
+ \`\`\`
8506
+
8507
+ It lists toolkit release notes (skills, prompts, rules, config templates per
8508
+ release), not this project's files: \`sync --dry-run\` remains the authority for
8509
+ what would be downloaded. A successful sync (or full unfiltered get) records the
8510
+ newest toolkit version in the project manifest as the baseline; without it the
8511
+ command falls back to the last applied lesson's date, or shows the latest 5
8512
+ releases. "No toolkit changes" with exit 0 is normal. It never writes files.
8513
+
8242
8514
  Three updates are independent: changing the npm/binary version updates the CLI;
8243
8515
  repeating pinned public \`skills add\` with a deliberately chosen new retained SHA
8244
8516
  updates a public helper; CLI sync updates CLI-owned course skills. It does not
@@ -8299,6 +8571,7 @@ visible. Doctor exit 78 can coexist with outer JSON \`status: "ok"\`.
8299
8571
  | Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
8300
8572
  | Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
8301
8573
  | Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
8574
+ | \`changelog_unsupported\` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
8302
8575
  | Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
8303
8576
  | Missing \`10x-idea-check\` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
8304
8577
  | Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |
@@ -8662,7 +8935,7 @@ function registerHelpersCommand(cli) {
8662
8935
  const root = process.cwd();
8663
8936
  try {
8664
8937
  const files = Object.entries(BUNDLED_HELPERS).flatMap(([name, tree]) => Object.entries(tree).map(([file, content]) => {
8665
- const path = join11(profile.skillDir(name), file);
8938
+ const path = join12(profile.skillDir(name), file);
8666
8939
  const absolute = resolve3(root, path);
8667
8940
  assertProjectFilePath(root, absolute);
8668
8941
  const state = !existsSync10(absolute) ? "missing" : readFileSync9(absolute).equals(Buffer.from(content)) ? "unchanged" : "conflict";
@@ -8841,11 +9114,11 @@ function renderModuleDetail(ctx, module, course, reason) {
8841
9114
  }
8842
9115
 
8843
9116
  // src/commands/sync.ts
8844
- import { join as join13 } from "node:path";
9117
+ import { join as join14 } from "node:path";
8845
9118
 
8846
9119
  // src/lib/sync-freshness.ts
8847
9120
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
8848
- import { join as join12 } from "node:path";
9121
+ import { join as join13 } from "node:path";
8849
9122
  function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
8850
9123
  const lesson = manifest?.lessons?.[id];
8851
9124
  const representation = lesson?.representation;
@@ -8860,16 +9133,16 @@ function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
8860
9133
  if (!isSafeName(name))
8861
9134
  return false;
8862
9135
  for (const file of skill.files)
8863
- if (!isSafeSkillFilePath(file) || !matches(join12(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
9136
+ if (!isSafeSkillFilePath(file) || !matches(join13(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
8864
9137
  return false;
8865
9138
  }
8866
9139
  for (const name of lesson.prompts)
8867
- if (!isSafeName(name) || !matches(join12(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
9140
+ if (!isSafeName(name) || !matches(join13(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
8868
9141
  return false;
8869
9142
  for (const name of lesson.configs)
8870
- if (!isSafeName(name) || !matches(join12(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
9143
+ if (!isSafeName(name) || !matches(join13(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
8871
9144
  return false;
8872
- const rulesPath = join12(root, profile.rulesFile);
9145
+ const rulesPath = join13(root, profile.rulesFile);
8873
9146
  assertProjectFilePath(root, rulesPath);
8874
9147
  const existing = existsSync11(rulesPath) ? readFileSync10(rulesPath, "utf8") : "";
8875
9148
  const block = inspectRulesBlock(existing, profile.sentinelBegin, profile.sentinelEnd);
@@ -8930,7 +9203,7 @@ async function runSync(ctx, options) {
8930
9203
  }
8931
9204
  return true;
8932
9205
  });
8933
- const manifest = readManifest(join13(process.cwd(), profile.manifestDir));
9206
+ const manifest = readManifest(join14(process.cwd(), profile.manifestDir));
8934
9207
  const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : manifest ? [manifest.lessonId] : []);
8935
9208
  let targets = unlocked;
8936
9209
  if (moduleFilter !== undefined)
@@ -8973,6 +9246,9 @@ async function runSync(ctx, options) {
8973
9246
  } finally {
8974
9247
  process.removeListener("SIGINT", onSigint);
8975
9248
  }
9249
+ if (!dryRun && !controller.signal.aborted && !outcomes.some((o) => o.status === "errored")) {
9250
+ await recordLatestToolkitVersion(ctx, auth.access_token, join14(process.cwd(), profile.manifestDir));
9251
+ }
8976
9252
  renderReport(ctx, profile, {
8977
9253
  course,
8978
9254
  selectionReason: selection.reason,
@@ -8988,7 +9264,7 @@ async function runSync(ctx, options) {
8988
9264
  }
8989
9265
  }
8990
9266
  async function syncLesson(ctx, lesson, opts) {
8991
- const currentManifest = readManifest(join13(process.cwd(), opts.profile.manifestDir));
9267
+ const currentManifest = readManifest(join14(process.cwd(), opts.profile.manifestDir));
8992
9268
  const stored = currentManifest?.lessons?.[lesson.lessonId]?.catalogContentHash;
8993
9269
  if (!opts.force && lesson.contentHash !== undefined && stored !== undefined && lesson.contentHash === stored && isLessonFresh(process.cwd(), currentManifest, lesson.lessonId, opts.profile, opts.lang, opts.applyCourseRules)) {
8994
9270
  verbose(ctx, `${lesson.lessonId}: upstream unchanged (digest match) — skipping fetch`);
@@ -9339,6 +9615,7 @@ registerGetCommand(cli);
9339
9615
  registerHelpersCommand(cli);
9340
9616
  registerListCommand(cli);
9341
9617
  registerSyncCommand(cli);
9618
+ registerChangelogCommand(cli);
9342
9619
  registerDoctorCommand(cli);
9343
9620
  registerBenchCommand(cli);
9344
9621
  registerBenchKitCommand(cli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.26.5",
3
+ "version": "1.28.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 10x-cli-guide
3
- description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
3
+ description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
4
4
  ---
5
5
 
6
6
  # 10x-cli: download, use, update
@@ -216,6 +216,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
216
216
  templates remain create-only. Cleanup preserves modified/untracked files and
217
217
  files owned elsewhere; do not manually sweep a skill directory after sync.
218
218
 
219
+ To see what changed in the course toolkit itself, use `changelog`. This command
220
+ is **unreleased**: check the actual runner's `changelog --help` first.
221
+
222
+ ```bash
223
+ 10x_cli changelog # toolkit releases since this project's last sync
224
+ 10x_cli changelog --since v2.55.0 # releases after a version
225
+ 10x_cli changelog --since 2026-10-01 # releases from that UTC day on
226
+ ```
227
+
228
+ It lists toolkit release notes (skills, prompts, rules, config templates per
229
+ release), not this project's files: `sync --dry-run` remains the authority for
230
+ what would be downloaded. A successful sync (or full unfiltered get) records the
231
+ newest toolkit version in the project manifest as the baseline; without it the
232
+ command falls back to the last applied lesson's date, or shows the latest 5
233
+ releases. "No toolkit changes" with exit 0 is normal. It never writes files.
234
+
219
235
  Three updates are independent: changing the npm/binary version updates the CLI;
220
236
  repeating pinned public `skills add` with a deliberately chosen new retained SHA
221
237
  updates a public helper; CLI sync updates CLI-owned course skills. It does not
@@ -276,6 +292,7 @@ visible. Doctor exit 78 can coexist with outer JSON `status: "ok"`.
276
292
  | Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
277
293
  | Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
278
294
  | Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
295
+ | `changelog_unsupported` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
279
296
  | Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
280
297
  | Missing `10x-idea-check` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
281
298
  | Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |