@contextos/cli 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +648 -21
  2. package/package.json +40 -40
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command as Command14 } from "commander";
5
+ import { join as join7 } from "path";
6
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, appendFileSync } from "fs";
5
7
 
6
8
  // src/commands/init.ts
7
9
  import { Command } from "commander";
@@ -835,18 +837,36 @@ function parseValue(value) {
835
837
  import { Command as Command10 } from "commander";
836
838
  import chalk10 from "chalk";
837
839
  import ora9 from "ora";
838
- import { readFileSync as readFileSync3, readdirSync } from "fs";
839
- import { join as join4, relative } from "path";
840
+ import { readdirSync } from "fs";
841
+ import { readFile } from "fs/promises";
842
+ import { join as join4, relative, resolve, normalize } from "path";
840
843
  import {
841
844
  RLMEngine,
842
845
  createGeminiClient as createGeminiClient3,
843
846
  mergeFilesToContext
844
847
  } from "@contextos/core";
845
- function collectFiles(dir, extensions = [".ts", ".js", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"], maxFiles = 100) {
848
+ function validatePath(userPath, projectRoot) {
849
+ const resolved = resolve(projectRoot, userPath);
850
+ const normalized = normalize(resolved);
851
+ const rootNormalized = normalize(projectRoot);
852
+ if (!normalized.startsWith(rootNormalized)) {
853
+ throw new Error(
854
+ `Invalid path: "${userPath}" escapes project boundaries.
855
+ Path must be within: ${rootNormalized}`
856
+ );
857
+ }
858
+ return normalized;
859
+ }
860
+ async function collectFiles(dir, extensions = [".ts", ".js", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"], maxFiles = 100, maxDepth = 20) {
846
861
  const files = [];
847
- function walk(currentDir) {
862
+ async function walk(currentDir, depth) {
863
+ if (depth > maxDepth) {
864
+ console.warn(`Maximum depth (${maxDepth}) reached at ${currentDir}. Skipping deeper directories.`);
865
+ return;
866
+ }
848
867
  if (files.length >= maxFiles) return;
849
868
  const entries = readdirSync(currentDir, { withFileTypes: true });
869
+ const filePromises = [];
850
870
  for (const entry of entries) {
851
871
  if (files.length >= maxFiles) break;
852
872
  const fullPath = join4(currentDir, entry.name);
@@ -854,32 +874,43 @@ function collectFiles(dir, extensions = [".ts", ".js", ".tsx", ".jsx", ".py", ".
854
874
  if (["node_modules", ".git", "dist", "build", ".next", "__pycache__", "venv"].includes(entry.name)) {
855
875
  continue;
856
876
  }
857
- walk(fullPath);
877
+ await walk(fullPath, depth + 1);
858
878
  } else if (entry.isFile()) {
859
879
  const ext = entry.name.substring(entry.name.lastIndexOf("."));
860
880
  if (extensions.includes(ext)) {
861
- try {
862
- const content = readFileSync3(fullPath, "utf-8");
863
- files.push({
864
- path: relative(dir, fullPath),
865
- content
866
- });
867
- } catch {
881
+ const promise = (async () => {
882
+ try {
883
+ const content = await readFile(fullPath, "utf-8");
884
+ files.push({
885
+ path: relative(dir, fullPath),
886
+ content
887
+ });
888
+ } catch {
889
+ }
890
+ })();
891
+ filePromises.push(promise);
892
+ if (filePromises.length >= 10) {
893
+ await Promise.all(filePromises);
894
+ filePromises.length = 0;
868
895
  }
869
896
  }
870
897
  }
871
898
  }
899
+ if (filePromises.length > 0) {
900
+ await Promise.all(filePromises);
901
+ }
872
902
  }
873
- walk(dir);
903
+ await walk(dir, 0);
874
904
  return files;
875
905
  }
876
- var analyzeCommand = new Command10("analyze").description("RLM-powered deep analysis of your codebase").argument("<goal>", 'Analysis goal or question (e.g., "Find all API endpoints")').option("-d, --depth <number>", "Maximum recursion depth for RLM", "3").option("-b, --budget <tokens>", "Token budget for analysis", "50000").option("-p, --path <path>", "Path to analyze (default: current directory)", ".").option("--max-files <number>", "Maximum files to include", "100").option("--verbose", "Show detailed execution trace").action(async (goal, options) => {
906
+ var analyzeCommand = new Command10("analyze").description("RLM-powered deep analysis of your codebase").argument("<goal>", 'Analysis goal or question (e.g., "Find all API endpoints")').option("-d, --depth <number>", "Maximum recursion depth for RLM", "3").option("-b, --budget <tokens>", "Token budget for analysis", "50000").option("-p, --path <path>", "Path to analyze (default: current directory)", ".").option("--max-files <number>", "Maximum files to include", "100").option("--max-depth <number>", "Maximum directory depth to traverse", "20").option("--verbose", "Show detailed execution trace").action(async (goal, options) => {
877
907
  console.log(chalk10.blue.bold("\n\u{1F50D} RLM Analysis Engine\n"));
878
908
  console.log(chalk10.gray("Goal: ") + chalk10.white(goal));
879
909
  console.log();
880
910
  const spinner = ora9("Collecting codebase files...").start();
881
911
  try {
882
- const files = collectFiles(options.path, void 0, parseInt(options.maxFiles));
912
+ const safePath = validatePath(options.path, process.cwd());
913
+ const files = await collectFiles(safePath, void 0, parseInt(options.maxFiles), parseInt(options.maxDepth));
883
914
  spinner.text = `Found ${files.length} files. Building context...`;
884
915
  if (files.length === 0) {
885
916
  spinner.fail("No source files found");
@@ -1079,7 +1110,16 @@ Respond in JSON format:
1079
1110
  try {
1080
1111
  const jsonMatch = result.answer.match(/\{[\s\S]*\}/);
1081
1112
  if (jsonMatch) {
1082
- plan = JSON.parse(jsonMatch[0]);
1113
+ try {
1114
+ plan = JSON.parse(jsonMatch[0]);
1115
+ } catch (parseError) {
1116
+ spinner.warn("Failed to parse refactoring plan JSON");
1117
+ console.log();
1118
+ console.log(chalk11.blue.bold("Analysis Result:"));
1119
+ console.log(result.answer);
1120
+ console.log();
1121
+ process.exit(0);
1122
+ }
1083
1123
  } else {
1084
1124
  throw new Error("No JSON found in response");
1085
1125
  }
@@ -1149,13 +1189,22 @@ Respond in JSON format:
1149
1189
  import { Command as Command12 } from "commander";
1150
1190
  import chalk12 from "chalk";
1151
1191
  import ora11 from "ora";
1152
- import { readFileSync as readFileSync4, existsSync as existsSync2, statSync as statSync2 } from "fs";
1153
- import { join as join5, basename } from "path";
1192
+ import { readFileSync as readFileSync3, existsSync as existsSync2, statSync as statSync2 } from "fs";
1193
+ import { join as join5, basename, resolve as resolve2, normalize as normalize2 } from "path";
1154
1194
  import {
1155
1195
  RLMEngine as RLMEngine3,
1156
1196
  createGeminiClient as createGeminiClient5,
1157
1197
  mergeFilesToContext as mergeFilesToContext3
1158
1198
  } from "@contextos/core";
1199
+ function validatePath2(userPath, projectRoot) {
1200
+ const resolved = resolve2(projectRoot, userPath);
1201
+ const normalized = normalize2(resolved);
1202
+ const rootNormalized = normalize2(projectRoot);
1203
+ if (!normalized.startsWith(rootNormalized)) {
1204
+ throw new Error(`Path traversal detected: "${userPath}" escapes project boundaries`);
1205
+ }
1206
+ return normalized;
1207
+ }
1159
1208
  var explainCommand = new Command12("explain").description("Get AI-powered explanation of code").argument("<target>", "File path, function name, or module to explain").option("-d, --depth <number>", "Include dependencies up to depth", "1").option("-f, --format <format>", "Output format: text, markdown, json", "markdown").option("--no-examples", "Skip usage examples in explanation").option("--technical", "Include technical implementation details").action(async (target, options) => {
1160
1209
  console.log(chalk12.blue.bold("\n\u{1F4DA} ContextOS Code Explainer\n"));
1161
1210
  const spinner = ora11("Analyzing target...").start();
@@ -1163,15 +1212,26 @@ var explainCommand = new Command12("explain").description("Get AI-powered explan
1163
1212
  let targetContent = "";
1164
1213
  let targetPath = "";
1165
1214
  let contextFiles = [];
1215
+ let safePath;
1216
+ try {
1217
+ safePath = validatePath2(target, process.cwd());
1218
+ } catch (error) {
1219
+ if (error instanceof Error && error.message.includes("Path traversal")) {
1220
+ spinner.fail("Invalid path");
1221
+ console.log(chalk12.red("Error:"), error.message);
1222
+ process.exit(1);
1223
+ }
1224
+ throw error;
1225
+ }
1166
1226
  const possiblePath = join5(process.cwd(), target);
1167
1227
  if (existsSync2(possiblePath) && statSync2(possiblePath).isFile()) {
1168
1228
  targetPath = target;
1169
- targetContent = readFileSync4(possiblePath, "utf-8");
1229
+ targetContent = readFileSync3(possiblePath, "utf-8");
1170
1230
  contextFiles.push({ path: target, content: targetContent });
1171
1231
  spinner.text = `Found file: ${target}`;
1172
- } else if (existsSync2(target) && statSync2(target).isFile()) {
1232
+ } else if (existsSync2(safePath) && statSync2(safePath).isFile()) {
1173
1233
  targetPath = target;
1174
- targetContent = readFileSync4(target, "utf-8");
1234
+ targetContent = readFileSync3(safePath, "utf-8");
1175
1235
  contextFiles.push({ path: basename(target), content: targetContent });
1176
1236
  spinner.text = `Found file: ${target}`;
1177
1237
  } else {
@@ -1331,7 +1391,571 @@ var traceCommand = new Command13("trace").description("Trace function call chain
1331
1391
  }
1332
1392
  });
1333
1393
 
1394
+ // src/commands/plugin.ts
1395
+ import chalk14 from "chalk";
1396
+ import ora13 from "ora";
1397
+ import inquirer3 from "inquirer";
1398
+ import { createPluginManager, createPluginRegistry } from "@contextos/core";
1399
+ function registerPluginCommand(program2) {
1400
+ const plugin = program2.command("plugin").description("Manage ContextOS plugins");
1401
+ plugin.command("list").description("List installed plugins").option("-a, --all", "Include disabled plugins").option("--remote", "Show available plugins from registry").action(async (options) => {
1402
+ const cwd = process.cwd();
1403
+ const registry = createPluginRegistry(cwd);
1404
+ if (options.remote) {
1405
+ console.log(chalk14.cyan("\n\u{1F4E6} Featured Plugins:\n"));
1406
+ const featured = await registry.getFeatured();
1407
+ for (const plugin2 of featured) {
1408
+ const installed = registry.isInstalled(plugin2.name);
1409
+ const status = installed ? chalk14.green("\u2713 installed") : "";
1410
+ console.log(` ${chalk14.bold(plugin2.name)} ${chalk14.gray(`v${plugin2.version}`)} ${status}`);
1411
+ console.log(` ${plugin2.description}`);
1412
+ console.log(` ${chalk14.gray(`Downloads: ${plugin2.downloads}`)}
1413
+ `);
1414
+ }
1415
+ return;
1416
+ }
1417
+ const plugins = registry.listLocal();
1418
+ if (plugins.length === 0) {
1419
+ console.log(chalk14.yellow("\nNo plugins installed."));
1420
+ console.log(chalk14.gray("Run `ctx plugin list --remote` to see available plugins.\n"));
1421
+ return;
1422
+ }
1423
+ console.log(chalk14.cyan("\n\u{1F50C} Installed Plugins:\n"));
1424
+ for (const plugin2 of plugins) {
1425
+ if (!options.all && !plugin2.enabled) continue;
1426
+ const status = plugin2.enabled ? chalk14.green("\u25CF enabled") : chalk14.gray("\u25CB disabled");
1427
+ console.log(` ${status} ${chalk14.bold(plugin2.name)} ${chalk14.gray(`v${plugin2.version}`)}`);
1428
+ console.log(` ${plugin2.description || "No description"}`);
1429
+ console.log(` ${chalk14.gray(plugin2.path)}
1430
+ `);
1431
+ }
1432
+ });
1433
+ plugin.command("install <source>").description("Install a plugin").option("-l, --local", "Install from local path").option("-f, --force", "Force reinstall").action(async (source, options) => {
1434
+ const spinner = ora13("Installing plugin...").start();
1435
+ try {
1436
+ const cwd = process.cwd();
1437
+ const manager = createPluginManager(cwd);
1438
+ const plugin2 = await manager.install(source, {
1439
+ local: options.local,
1440
+ force: options.force
1441
+ });
1442
+ spinner.succeed(`Installed ${chalk14.bold(plugin2.name)} v${plugin2.version}`);
1443
+ } catch (error) {
1444
+ spinner.fail(`Failed to install plugin`);
1445
+ console.error(chalk14.red(error instanceof Error ? error.message : String(error)));
1446
+ process.exit(1);
1447
+ }
1448
+ });
1449
+ plugin.command("remove <name>").alias("uninstall").description("Remove a plugin").option("-f, --force", "Skip confirmation").action(async (name, options) => {
1450
+ const cwd = process.cwd();
1451
+ const registry = createPluginRegistry(cwd);
1452
+ const localPlugin = registry.getLocal(name);
1453
+ if (!localPlugin) {
1454
+ console.log(chalk14.red(`Plugin not found: ${name}`));
1455
+ process.exit(1);
1456
+ }
1457
+ if (!options.force) {
1458
+ const { confirm } = await inquirer3.prompt([
1459
+ {
1460
+ type: "confirm",
1461
+ name: "confirm",
1462
+ message: `Remove plugin ${chalk14.bold(name)}?`,
1463
+ default: false
1464
+ }
1465
+ ]);
1466
+ if (!confirm) {
1467
+ console.log(chalk14.gray("Cancelled."));
1468
+ return;
1469
+ }
1470
+ }
1471
+ const spinner = ora13("Removing plugin...").start();
1472
+ try {
1473
+ const manager = createPluginManager(cwd);
1474
+ await manager.uninstall(name);
1475
+ spinner.succeed(`Removed ${chalk14.bold(name)}`);
1476
+ } catch (error) {
1477
+ spinner.fail(`Failed to remove plugin`);
1478
+ console.error(chalk14.red(error instanceof Error ? error.message : String(error)));
1479
+ process.exit(1);
1480
+ }
1481
+ });
1482
+ plugin.command("enable <name>").description("Enable a disabled plugin").action(async (name) => {
1483
+ const cwd = process.cwd();
1484
+ const manager = createPluginManager(cwd);
1485
+ await manager.loadAll();
1486
+ const success = await manager.enablePlugin(name);
1487
+ if (success) {
1488
+ console.log(chalk14.green(`\u2713 Enabled ${chalk14.bold(name)}`));
1489
+ } else {
1490
+ console.log(chalk14.red(`Plugin not found: ${name}`));
1491
+ process.exit(1);
1492
+ }
1493
+ });
1494
+ plugin.command("disable <name>").description("Disable a plugin").action(async (name) => {
1495
+ const cwd = process.cwd();
1496
+ const manager = createPluginManager(cwd);
1497
+ await manager.loadAll();
1498
+ const success = await manager.disablePlugin(name);
1499
+ if (success) {
1500
+ console.log(chalk14.green(`\u2713 Disabled ${chalk14.bold(name)}`));
1501
+ } else {
1502
+ console.log(chalk14.red(`Plugin not found: ${name}`));
1503
+ process.exit(1);
1504
+ }
1505
+ });
1506
+ plugin.command("create <name>").description("Create a new plugin scaffold").option("-d, --description <desc>", "Plugin description").option("-a, --author <author>", "Plugin author").option("--with-commands", "Include command examples").action(async (name, options) => {
1507
+ const cwd = process.cwd();
1508
+ const answers = await inquirer3.prompt([
1509
+ {
1510
+ type: "input",
1511
+ name: "description",
1512
+ message: "Plugin description:",
1513
+ default: options.description || `A ContextOS plugin`,
1514
+ when: !options.description
1515
+ },
1516
+ {
1517
+ type: "input",
1518
+ name: "author",
1519
+ message: "Author:",
1520
+ default: options.author || process.env.USER || "Anonymous",
1521
+ when: !options.author
1522
+ },
1523
+ {
1524
+ type: "checkbox",
1525
+ name: "hooks",
1526
+ message: "Select hooks to implement:",
1527
+ choices: [
1528
+ { name: "onBeforeContextBuild", value: "onBeforeContextBuild" },
1529
+ { name: "onAfterContextBuild", value: "onAfterContextBuild" },
1530
+ { name: "onBeforeIndex", value: "onBeforeIndex" },
1531
+ { name: "onAfterIndex", value: "onAfterIndex" },
1532
+ { name: "onBeforeAnalyze", value: "onBeforeAnalyze" },
1533
+ { name: "onAfterAnalyze", value: "onAfterAnalyze" },
1534
+ { name: "fileFilter", value: "fileFilter" },
1535
+ { name: "rankingBoost", value: "rankingBoost" }
1536
+ ],
1537
+ default: ["onAfterContextBuild"]
1538
+ }
1539
+ ]);
1540
+ const template = {
1541
+ name,
1542
+ description: options.description || answers.description,
1543
+ author: options.author || answers.author,
1544
+ hooks: answers.hooks,
1545
+ withCommands: options.withCommands || false
1546
+ };
1547
+ const spinner = ora13("Creating plugin scaffold...").start();
1548
+ try {
1549
+ const manager = createPluginManager(cwd);
1550
+ const pluginPath = manager.createPluginScaffold(template);
1551
+ spinner.succeed(`Created plugin at ${chalk14.cyan(pluginPath)}`);
1552
+ console.log(`
1553
+ ${chalk14.gray("Next steps:")}`);
1554
+ console.log(` 1. Edit ${chalk14.cyan(`${pluginPath}/index.js`)}`);
1555
+ console.log(` 2. Run ${chalk14.cyan(`ctx plugin enable ${name}`)}`);
1556
+ console.log(` 3. Test your plugin!
1557
+ `);
1558
+ } catch (error) {
1559
+ spinner.fail(`Failed to create plugin`);
1560
+ console.error(chalk14.red(error instanceof Error ? error.message : String(error)));
1561
+ process.exit(1);
1562
+ }
1563
+ });
1564
+ plugin.command("run <plugin> <command> [args...]").description("Run a custom command from a plugin").action(async (pluginName, command, args) => {
1565
+ const cwd = process.cwd();
1566
+ const manager = createPluginManager(cwd);
1567
+ await manager.loadAll();
1568
+ const pluginState = manager.get(pluginName);
1569
+ if (!pluginState) {
1570
+ console.log(chalk14.red(`Plugin not found: ${pluginName}`));
1571
+ process.exit(1);
1572
+ }
1573
+ const cmd = pluginState.instance.commands?.[command];
1574
+ if (!cmd) {
1575
+ console.log(chalk14.red(`Command not found: ${command}`));
1576
+ console.log(chalk14.gray("Available commands:"));
1577
+ const commands = Object.keys(pluginState.instance.commands || {});
1578
+ if (commands.length === 0) {
1579
+ console.log(chalk14.gray(" (none)"));
1580
+ } else {
1581
+ for (const c of commands) {
1582
+ console.log(` ${c}`);
1583
+ }
1584
+ }
1585
+ process.exit(1);
1586
+ }
1587
+ try {
1588
+ const context = {
1589
+ projectRoot: cwd,
1590
+ configDir: `${cwd}/.contextos`,
1591
+ log: {
1592
+ debug: console.debug,
1593
+ info: console.info,
1594
+ warn: console.warn,
1595
+ error: console.error
1596
+ },
1597
+ query: async () => ({ files: [], context: "" }),
1598
+ readFile: async () => "",
1599
+ getDependencies: async () => [],
1600
+ storage: {
1601
+ get: () => void 0,
1602
+ set: () => {
1603
+ },
1604
+ delete: () => false
1605
+ }
1606
+ };
1607
+ await cmd.handler(args, context);
1608
+ } catch (error) {
1609
+ console.error(chalk14.red(`Command failed: ${error instanceof Error ? error.message : String(error)}`));
1610
+ process.exit(1);
1611
+ }
1612
+ });
1613
+ }
1614
+
1615
+ // src/commands/finetune.ts
1616
+ import chalk15 from "chalk";
1617
+ import ora14 from "ora";
1618
+ import {
1619
+ createTrainingDataCollector,
1620
+ createDatasetFormatter
1621
+ } from "@contextos/core";
1622
+ function registerFinetuneCommand(program2) {
1623
+ const finetune = program2.command("finetune").description("Training data management for model fine-tuning");
1624
+ finetune.command("export").description("Export training data to file").option("-o, --output <path>", "Output file path", "./training-data.jsonl").option("-f, --format <format>", "Output format (jsonl, openai, anthropic, csv)", "jsonl").option("-m, --max <count>", "Maximum examples to export", parseInt).option("-r, --min-rating <rating>", "Minimum rating (good, neutral, bad)").option("-l, --language <lang>", "Filter by language").option("--shuffle", "Shuffle examples").option("--split <ratio>", "Train/validation split ratio", parseFloat).action(async (options) => {
1625
+ const cwd = process.cwd();
1626
+ const spinner = ora14("Exporting training data...").start();
1627
+ try {
1628
+ const collector = createTrainingDataCollector(cwd);
1629
+ const formatter = createDatasetFormatter();
1630
+ const examples = collector.getAll();
1631
+ if (examples.length === 0) {
1632
+ spinner.warn("No training data found");
1633
+ console.log(chalk15.gray("\nTraining data is collected automatically when you use ctx goal/build commands."));
1634
+ return;
1635
+ }
1636
+ const format = {
1637
+ format: options.format
1638
+ };
1639
+ const config = {
1640
+ maxExamples: options.max,
1641
+ minRating: options.minRating,
1642
+ languageFilter: options.language,
1643
+ shuffle: options.shuffle,
1644
+ validationSplit: options.split
1645
+ };
1646
+ if (options.split) {
1647
+ const outputDir = options.output.replace(/\.[^.]+$/, "");
1648
+ const result = await formatter.exportWithSplit(
1649
+ examples,
1650
+ outputDir,
1651
+ format,
1652
+ config
1653
+ );
1654
+ spinner.succeed("Export complete");
1655
+ console.log(`
1656
+ Train: ${chalk15.cyan(result.train)} examples \u2192 ${outputDir}/train.jsonl`);
1657
+ console.log(` Validation: ${chalk15.cyan(result.validation)} examples \u2192 ${outputDir}/validation.jsonl`);
1658
+ } else {
1659
+ const result = await formatter.export(
1660
+ examples,
1661
+ options.output,
1662
+ format,
1663
+ config
1664
+ );
1665
+ spinner.succeed(`Exported ${chalk15.cyan(result.exported)} examples to ${chalk15.cyan(result.path)}`);
1666
+ }
1667
+ } catch (error) {
1668
+ spinner.fail("Export failed");
1669
+ console.error(chalk15.red(error instanceof Error ? error.message : String(error)));
1670
+ process.exit(1);
1671
+ }
1672
+ });
1673
+ finetune.command("validate <file>").description("Validate a training data file").action(async (file) => {
1674
+ const spinner = ora14("Validating dataset...").start();
1675
+ try {
1676
+ const formatter = createDatasetFormatter();
1677
+ const result = await formatter.validate(file);
1678
+ if (result.valid) {
1679
+ spinner.succeed("Dataset is valid");
1680
+ } else {
1681
+ spinner.fail("Dataset has errors");
1682
+ }
1683
+ console.log(`
1684
+ ${chalk15.bold("Statistics:")}`);
1685
+ console.log(` Total examples: ${chalk15.cyan(result.stats.totalExamples)}`);
1686
+ console.log(` Avg tokens: ${chalk15.cyan(result.stats.avgTokenCount)}`);
1687
+ console.log(` Avg files/example: ${chalk15.cyan(result.stats.avgFilesPerExample)}`);
1688
+ console.log(`
1689
+ ${chalk15.bold("Rating Distribution:")}`);
1690
+ console.log(` ${chalk15.green("Good")}: ${result.stats.ratingDistribution.good}`);
1691
+ console.log(` ${chalk15.yellow("Neutral")}: ${result.stats.ratingDistribution.neutral}`);
1692
+ console.log(` ${chalk15.red("Bad")}: ${result.stats.ratingDistribution.bad}`);
1693
+ console.log(` ${chalk15.gray("Unrated")}: ${result.stats.ratingDistribution.unrated}`);
1694
+ if (Object.keys(result.stats.languageDistribution).length > 0) {
1695
+ console.log(`
1696
+ ${chalk15.bold("Languages:")}`);
1697
+ for (const [lang, count] of Object.entries(result.stats.languageDistribution)) {
1698
+ console.log(` ${lang}: ${count}`);
1699
+ }
1700
+ }
1701
+ if (result.errors.length > 0) {
1702
+ console.log(`
1703
+ ${chalk15.red.bold("Errors:")}`);
1704
+ for (const error of result.errors.slice(0, 10)) {
1705
+ console.log(` Line ${error.line}: ${error.message}`);
1706
+ }
1707
+ if (result.errors.length > 10) {
1708
+ console.log(chalk15.gray(` ... and ${result.errors.length - 10} more`));
1709
+ }
1710
+ }
1711
+ if (result.warnings.length > 0) {
1712
+ console.log(`
1713
+ ${chalk15.yellow.bold("Warnings:")}`);
1714
+ for (const warning of result.warnings) {
1715
+ console.log(` \u26A0 ${warning}`);
1716
+ }
1717
+ }
1718
+ } catch (error) {
1719
+ spinner.fail("Validation failed");
1720
+ console.error(chalk15.red(error instanceof Error ? error.message : String(error)));
1721
+ process.exit(1);
1722
+ }
1723
+ });
1724
+ finetune.command("stats").description("Show training data statistics").action(async () => {
1725
+ const cwd = process.cwd();
1726
+ const collector = createTrainingDataCollector(cwd);
1727
+ const stats = collector.getStats();
1728
+ console.log(chalk15.cyan.bold("\n\u{1F4CA} Training Data Statistics\n"));
1729
+ if (stats.totalExamples === 0) {
1730
+ console.log(chalk15.yellow("No training data collected yet."));
1731
+ console.log(chalk15.gray("\nTraining data is collected when you use ctx goal/build commands.\n"));
1732
+ return;
1733
+ }
1734
+ console.log(`${chalk15.bold("Total Examples:")} ${stats.totalExamples}`);
1735
+ console.log(`${chalk15.bold("Avg Token Count:")} ${stats.avgTokenCount}`);
1736
+ console.log(`${chalk15.bold("Avg Files/Example:")} ${stats.avgFilesPerExample}`);
1737
+ console.log(`
1738
+ ${chalk15.bold("Rating Distribution:")}`);
1739
+ const total = stats.totalExamples;
1740
+ const pct = (n) => (n / total * 100).toFixed(1);
1741
+ console.log(` ${chalk15.green("\u25CF")} Good: ${stats.ratingDistribution.good} (${pct(stats.ratingDistribution.good)}%)`);
1742
+ console.log(` ${chalk15.yellow("\u25CF")} Neutral: ${stats.ratingDistribution.neutral} (${pct(stats.ratingDistribution.neutral)}%)`);
1743
+ console.log(` ${chalk15.red("\u25CF")} Bad: ${stats.ratingDistribution.bad} (${pct(stats.ratingDistribution.bad)}%)`);
1744
+ console.log(` ${chalk15.gray("\u25CB")} Unrated: ${stats.ratingDistribution.unrated} (${pct(stats.ratingDistribution.unrated)}%)`);
1745
+ console.log(`
1746
+ ${chalk15.bold("Languages:")}`);
1747
+ for (const [lang, count] of Object.entries(stats.languageDistribution)) {
1748
+ console.log(` ${lang}: ${count} (${pct(count)}%)`);
1749
+ }
1750
+ console.log(`
1751
+ ${chalk15.bold("Date Range:")}`);
1752
+ console.log(` From: ${stats.dateRange.earliest.toLocaleDateString()}`);
1753
+ console.log(` To: ${stats.dateRange.latest.toLocaleDateString()}`);
1754
+ console.log();
1755
+ });
1756
+ finetune.command("feedback <id> <rating>").description("Add feedback to a training example (good/bad/neutral)").option("-c, --comment <comment>", "Optional comment").action(async (id, rating, options) => {
1757
+ const validRatings = ["good", "bad", "neutral"];
1758
+ if (!validRatings.includes(rating)) {
1759
+ console.error(chalk15.red(`Invalid rating. Use: ${validRatings.join(", ")}`));
1760
+ process.exit(1);
1761
+ }
1762
+ const cwd = process.cwd();
1763
+ const collector = createTrainingDataCollector(cwd);
1764
+ const success = collector.addFeedback(
1765
+ id,
1766
+ rating,
1767
+ options.comment
1768
+ );
1769
+ if (success) {
1770
+ console.log(chalk15.green(`\u2713 Feedback added for ${id}`));
1771
+ } else {
1772
+ console.error(chalk15.red(`Example not found: ${id}`));
1773
+ process.exit(1);
1774
+ }
1775
+ });
1776
+ finetune.command("recent").description("Show recent training examples").option("-n, --limit <count>", "Number of examples", parseInt, 5).action(async (options) => {
1777
+ const cwd = process.cwd();
1778
+ const collector = createTrainingDataCollector(cwd);
1779
+ const recent = collector.getRecent(options.limit);
1780
+ if (recent.length === 0) {
1781
+ console.log(chalk15.yellow("\nNo training data collected yet.\n"));
1782
+ return;
1783
+ }
1784
+ console.log(chalk15.cyan.bold("\n\u{1F4CB} Recent Training Examples\n"));
1785
+ for (const example of recent) {
1786
+ const rating = example.feedback?.rating;
1787
+ const ratingIcon = rating === "good" ? "\u{1F44D}" : rating === "bad" ? "\u{1F44E}" : "\u2022";
1788
+ const date = new Date(example.meta.timestamp).toLocaleString();
1789
+ console.log(`${chalk15.bold(example.id)} ${ratingIcon}`);
1790
+ console.log(` Goal: ${chalk15.white(example.goal.substring(0, 60))}${example.goal.length > 60 ? "..." : ""}`);
1791
+ console.log(` Files: ${example.selectedFiles.length} | Tokens: ${example.tokenCount}`);
1792
+ console.log(` ${chalk15.gray(date)}
1793
+ `);
1794
+ }
1795
+ });
1796
+ }
1797
+
1798
+ // src/commands/cloud.ts
1799
+ import chalk16 from "chalk";
1800
+ import ora15 from "ora";
1801
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
1802
+ import { homedir } from "os";
1803
+ import { join as join6 } from "path";
1804
+ var CLOUD_CONFIG_PATH = join6(homedir(), ".contextos", "cloud.json");
1805
+ var DEFAULT_CLOUD_URL = "https://api.contextos.dev";
1806
+ function loadCloudConfig() {
1807
+ if (existsSync3(CLOUD_CONFIG_PATH)) {
1808
+ try {
1809
+ return JSON.parse(readFileSync4(CLOUD_CONFIG_PATH, "utf-8"));
1810
+ } catch {
1811
+ return { cloudUrl: DEFAULT_CLOUD_URL };
1812
+ }
1813
+ }
1814
+ return { cloudUrl: DEFAULT_CLOUD_URL };
1815
+ }
1816
+ function saveCloudConfig(config) {
1817
+ const dir = join6(CLOUD_CONFIG_PATH, "..");
1818
+ if (!existsSync3(dir)) {
1819
+ mkdirSync2(dir, { recursive: true });
1820
+ }
1821
+ writeFileSync4(CLOUD_CONFIG_PATH, JSON.stringify(config, null, 2));
1822
+ }
1823
+ function registerCloudCommands(program2) {
1824
+ program2.command("login").description("Login to ContextOS Cloud").option("--api-key <key>", "API key").option("--url <url>", "Cloud server URL", DEFAULT_CLOUD_URL).action(async (options) => {
1825
+ console.log(chalk16.cyan.bold("\n\u{1F510} ContextOS Cloud Login\n"));
1826
+ let apiKey = options.apiKey;
1827
+ if (!apiKey) {
1828
+ const { default: inquirer4 } = await import("inquirer");
1829
+ const answers = await inquirer4.prompt([
1830
+ {
1831
+ type: "input",
1832
+ name: "apiKey",
1833
+ message: "Enter your API key:",
1834
+ validate: (input) => input.startsWith("ctx_") || "API key must start with ctx_"
1835
+ }
1836
+ ]);
1837
+ apiKey = answers.apiKey;
1838
+ }
1839
+ const spinner = ora15("Validating API key...").start();
1840
+ try {
1841
+ const response = await fetch(`${options.url}/health`, {
1842
+ headers: { "X-API-Key": apiKey }
1843
+ });
1844
+ if (!response.ok) {
1845
+ spinner.fail("Invalid API key or server error");
1846
+ return;
1847
+ }
1848
+ spinner.succeed("API key validated");
1849
+ const config = {
1850
+ apiKey,
1851
+ cloudUrl: options.url,
1852
+ userId: `user_${apiKey.slice(4, 12)}`,
1853
+ tier: "free"
1854
+ };
1855
+ saveCloudConfig(config);
1856
+ console.log(chalk16.green("\n\u2705 Logged in successfully!"));
1857
+ console.log(chalk16.gray(`Config saved to: ${CLOUD_CONFIG_PATH}
1858
+ `));
1859
+ } catch (error) {
1860
+ spinner.fail(error instanceof Error ? error.message : "Connection failed");
1861
+ }
1862
+ });
1863
+ program2.command("logout").description("Logout from ContextOS Cloud").action(() => {
1864
+ const config = loadCloudConfig();
1865
+ config.apiKey = void 0;
1866
+ config.userId = void 0;
1867
+ saveCloudConfig(config);
1868
+ console.log(chalk16.green("\n\u2705 Logged out successfully\n"));
1869
+ });
1870
+ program2.command("connect").description("Connect your IDEs to ContextOS Cloud").option("--all", "Configure all detected IDEs").option("--ide <name>", "Configure specific IDE").action(async (options) => {
1871
+ const config = loadCloudConfig();
1872
+ if (!config.apiKey) {
1873
+ console.log(chalk16.red("\n\u274C Not logged in. Run `ctx login` first.\n"));
1874
+ return;
1875
+ }
1876
+ console.log(chalk16.cyan.bold("\n\u{1F50C} Connecting IDEs to ContextOS Cloud\n"));
1877
+ const { detectIDEs } = await import("@contextos/setup").catch(() => ({
1878
+ detectIDEs: async () => []
1879
+ }));
1880
+ const ides = await detectIDEs();
1881
+ if (ides.length === 0) {
1882
+ console.log(chalk16.yellow("No supported IDEs detected.\n"));
1883
+ return;
1884
+ }
1885
+ console.log(chalk16.gray("This will configure your IDEs to use the cloud MCP server."));
1886
+ console.log(chalk16.gray(`Cloud URL: ${config.cloudUrl}
1887
+ `));
1888
+ for (const ide of ides) {
1889
+ if (options.ide && ide.id !== options.ide) continue;
1890
+ console.log(`${chalk16.cyan("\u2192")} ${ide.name}`);
1891
+ }
1892
+ console.log(chalk16.green("\n\u2705 IDEs connected to cloud!\n"));
1893
+ });
1894
+ program2.command("cloud").description("Show ContextOS Cloud status").action(async () => {
1895
+ const config = loadCloudConfig();
1896
+ console.log(chalk16.cyan.bold("\n\u2601\uFE0F ContextOS Cloud Status\n"));
1897
+ if (!config.apiKey) {
1898
+ console.log(chalk16.yellow("Not logged in"));
1899
+ console.log(chalk16.gray("\nRun `ctx login` to connect to ContextOS Cloud.\n"));
1900
+ return;
1901
+ }
1902
+ console.log(`${chalk16.gray("User ID:")} ${config.userId}`);
1903
+ console.log(`${chalk16.gray("Tier:")} ${config.tier}`);
1904
+ console.log(`${chalk16.gray("Cloud URL:")} ${config.cloudUrl}`);
1905
+ const spinner = ora15("Checking connection...").start();
1906
+ try {
1907
+ const response = await fetch(`${config.cloudUrl}/health`, {
1908
+ headers: { "X-API-Key": config.apiKey }
1909
+ });
1910
+ if (response.ok) {
1911
+ const data = await response.json();
1912
+ spinner.succeed(`Connected (v${data.version})`);
1913
+ } else {
1914
+ spinner.warn("Connection issues");
1915
+ }
1916
+ } catch {
1917
+ spinner.fail("Cannot reach cloud server");
1918
+ }
1919
+ console.log();
1920
+ });
1921
+ }
1922
+
1334
1923
  // src/index.ts
1924
+ var crashLogDir = join7(process.cwd(), ".contextos");
1925
+ if (!existsSync4(crashLogDir)) {
1926
+ mkdirSync3(crashLogDir, { recursive: true });
1927
+ }
1928
+ var crashLogPath = join7(crashLogDir, "crash.log");
1929
+ function logCrash(message, data) {
1930
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1931
+ const logEntry = `[${timestamp}] ${message}${data ? `
1932
+ ${data}
1933
+
1934
+ ` : "\n\n"}`;
1935
+ console.error(logEntry);
1936
+ appendFileSync(crashLogPath, logEntry, "utf-8");
1937
+ }
1938
+ process.on("uncaughtException", (error) => {
1939
+ console.error("\u274C UNCAUGHT EXCEPTION:", error.message);
1940
+ console.error("Stack:", error.stack);
1941
+ if (process.env.NODE_ENV === "production") {
1942
+ logCrash(`UNCAUGHT EXCEPTION: ${error.message}`, error.stack);
1943
+ }
1944
+ setTimeout(() => process.exit(1), 1e3);
1945
+ });
1946
+ process.on("unhandledRejection", (reason) => {
1947
+ console.error("\u274C UNHANDLED REJECTION:", reason);
1948
+ if (process.env.NODE_ENV === "production") {
1949
+ const reasonStr = reason instanceof Error ? reason.stack : String(reason);
1950
+ logCrash("UNHANDLED REJECTION", reasonStr);
1951
+ }
1952
+ });
1953
+ process.on("multipleResolves", (type, promise) => {
1954
+ console.warn("\u26A0\uFE0F MULTIPLE RESOLVES:", type, promise);
1955
+ });
1956
+ process.on("warning", (warning) => {
1957
+ console.warn("\u26A0\uFE0F PROCESS WARNING:", warning.name, warning.message);
1958
+ });
1335
1959
  var program = new Command14();
1336
1960
  program.name("ctx").description("ContextOS - The Context Server Protocol for AI Coding").version("0.1.0").option("-v, --verbose", "Enable verbose output");
1337
1961
  program.addCommand(initCommand);
@@ -1347,4 +1971,7 @@ program.addCommand(analyzeCommand);
1347
1971
  program.addCommand(refactorCommand);
1348
1972
  program.addCommand(explainCommand);
1349
1973
  program.addCommand(traceCommand);
1974
+ registerPluginCommand(program);
1975
+ registerFinetuneCommand(program);
1976
+ registerCloudCommands(program);
1350
1977
  program.parse();
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
1
  {
2
- "name": "@contextos/cli",
3
- "version": "0.1.0",
4
- "description": "CLI for ContextOS - The Context Server Protocol for AI Coding",
5
- "type": "module",
6
- "bin": {
7
- "ctx": "./dist/index.js"
8
- },
9
- "main": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "scripts": {
12
- "build": "tsup src/index.ts --format esm --clean",
13
- "dev": "tsup src/index.ts --format cjs --dts --watch",
14
- "start": "node dist/index.js",
15
- "test": "vitest run",
16
- "lint": "eslint src/",
17
- "clean": "rimraf dist"
18
- },
19
- "dependencies": {
20
- "@contextos/core": "workspace:*",
21
- "chalk": "^5.3.0",
22
- "clipboardy": "^4.0.0",
23
- "commander": "^12.0.0",
24
- "glob": "^10.3.0",
25
- "inquirer": "^9.2.0",
26
- "ora": "^8.0.0",
27
- "yaml": "^2.4.0"
28
- },
29
- "devDependencies": {
30
- "@types/inquirer": "^9.0.7",
31
- "@types/node": "^20.11.0",
32
- "eslint": "^8.57.0",
33
- "rimraf": "^5.0.5",
34
- "tsup": "^8.0.2",
35
- "typescript": "^5.4.0",
36
- "vitest": "^1.4.0"
37
- },
38
- "files": [
39
- "dist"
40
- ],
41
- "license": "MIT"
2
+ "name": "@contextos/cli",
3
+ "version": "0.2.2",
4
+ "description": "CLI for ContextOS - The Context Server Protocol for AI Coding",
5
+ "type": "module",
6
+ "bin": {
7
+ "ctx": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "dependencies": {
12
+ "chalk": "^5.3.0",
13
+ "clipboardy": "^4.0.0",
14
+ "commander": "^12.0.0",
15
+ "glob": "^10.3.0",
16
+ "inquirer": "^9.2.0",
17
+ "ora": "^8.0.0",
18
+ "yaml": "^2.4.0",
19
+ "@contextos/core": "0.2.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/inquirer": "^9.0.7",
23
+ "@types/node": "^20.11.0",
24
+ "eslint": "^8.57.0",
25
+ "rimraf": "^5.0.5",
26
+ "tsup": "^8.0.2",
27
+ "typescript": "^5.4.0",
28
+ "vitest": "^1.4.0"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "build": "tsup src/index.ts --format esm --clean",
36
+ "dev": "tsup src/index.ts --format cjs --dts --watch",
37
+ "start": "node dist/index.js",
38
+ "test": "vitest run",
39
+ "lint": "eslint src/",
40
+ "clean": "rimraf dist"
41
+ }
42
42
  }