juno-code 1.0.45 → 1.0.46

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/bin/cli.mjs CHANGED
@@ -143,8 +143,8 @@ var init_types = __esm({
143
143
  };
144
144
  FileSystemError = class extends CLIError {
145
145
  code = "FILESYSTEM_ERROR";
146
- constructor(message, path24) {
147
- super(path24 ? `${message}: ${path24}` : message);
146
+ constructor(message, path25) {
147
+ super(path25 ? `${message}: ${path25}` : message);
148
148
  this.suggestions = [
149
149
  "Check file/directory permissions",
150
150
  "Verify path exists and is accessible",
@@ -1819,8 +1819,8 @@ ${helpText}
1819
1819
  }
1820
1820
  }
1821
1821
  if (options.cwd) {
1822
- const fs23 = await import('fs-extra');
1823
- if (!await fs23.pathExists(options.cwd)) {
1822
+ const fs24 = await import('fs-extra');
1823
+ if (!await fs24.pathExists(options.cwd)) {
1824
1824
  throw new ValidationError(
1825
1825
  `Working directory does not exist: ${options.cwd}`,
1826
1826
  ["Verify the path exists", "Use absolute paths to avoid ambiguity"]
@@ -3647,31 +3647,103 @@ function parseResetTime(message) {
3647
3647
  }
3648
3648
  return { resetTime, timezone };
3649
3649
  }
3650
+ function parseCodexResetTime(message) {
3651
+ const resetPattern = /try again at\s+(\w+)\s+(\d{1,2})(?:st|nd|rd|th)?,?\s*(\d{4})\s+(\d{1,2}):(\d{2})\s*(AM|PM)/i;
3652
+ const match = message.match(resetPattern);
3653
+ if (!match) {
3654
+ return null;
3655
+ }
3656
+ const monthStr = match[1];
3657
+ const day = parseInt(match[2], 10);
3658
+ const year = parseInt(match[3], 10);
3659
+ let hours = parseInt(match[4], 10);
3660
+ const minutes = parseInt(match[5], 10);
3661
+ const ampm = match[6].toUpperCase();
3662
+ const MONTH_MAP = {
3663
+ "jan": 0,
3664
+ "january": 0,
3665
+ "feb": 1,
3666
+ "february": 1,
3667
+ "mar": 2,
3668
+ "march": 2,
3669
+ "apr": 3,
3670
+ "april": 3,
3671
+ "may": 4,
3672
+ "jun": 5,
3673
+ "june": 5,
3674
+ "jul": 6,
3675
+ "july": 6,
3676
+ "aug": 7,
3677
+ "august": 7,
3678
+ "sep": 8,
3679
+ "september": 8,
3680
+ "oct": 9,
3681
+ "october": 9,
3682
+ "nov": 10,
3683
+ "november": 10,
3684
+ "dec": 11,
3685
+ "december": 11
3686
+ };
3687
+ const month = MONTH_MAP[monthStr.toLowerCase()];
3688
+ if (month === void 0) {
3689
+ return null;
3690
+ }
3691
+ if (ampm === "PM" && hours !== 12) {
3692
+ hours += 12;
3693
+ } else if (ampm === "AM" && hours === 12) {
3694
+ hours = 0;
3695
+ }
3696
+ const resetTime = new Date(year, month, day, hours, minutes, 0, 0);
3697
+ const now = /* @__PURE__ */ new Date();
3698
+ if (resetTime.getTime() <= now.getTime()) {
3699
+ resetTime.setTime(resetTime.getTime() + 24 * 60 * 60 * 1e3);
3700
+ }
3701
+ return { resetTime };
3702
+ }
3650
3703
  function detectQuotaLimit(message) {
3651
3704
  if (!message || typeof message !== "string") {
3652
3705
  return { detected: false };
3653
3706
  }
3654
- const quotaPattern = /you'?ve hit your limit/i;
3655
- if (!quotaPattern.test(message)) {
3707
+ const claudePattern = /you'?ve hit your limit/i;
3708
+ const codexPattern = /you'?ve hit your usage limit/i;
3709
+ const isClaudeQuota = claudePattern.test(message) && !codexPattern.test(message);
3710
+ const isCodexQuota = codexPattern.test(message);
3711
+ if (!isClaudeQuota && !isCodexQuota) {
3656
3712
  return { detected: false };
3657
3713
  }
3658
- const parsed = parseResetTime(message);
3659
- if (parsed) {
3714
+ const source = isCodexQuota ? "codex" : "claude";
3715
+ const parsedClaude = parseResetTime(message);
3716
+ if (parsedClaude) {
3660
3717
  const now = /* @__PURE__ */ new Date();
3661
- const sleepDurationMs = Math.max(0, parsed.resetTime.getTime() - now.getTime());
3718
+ const sleepDurationMs = Math.max(0, parsedClaude.resetTime.getTime() - now.getTime());
3662
3719
  return {
3663
3720
  detected: true,
3664
- resetTime: parsed.resetTime,
3721
+ resetTime: parsedClaude.resetTime,
3665
3722
  sleepDurationMs,
3666
- timezone: parsed.timezone,
3667
- originalMessage: message
3723
+ timezone: parsedClaude.timezone,
3724
+ originalMessage: message,
3725
+ source
3726
+ };
3727
+ }
3728
+ const parsedCodex = parseCodexResetTime(message);
3729
+ if (parsedCodex) {
3730
+ const now = /* @__PURE__ */ new Date();
3731
+ const sleepDurationMs = Math.max(0, parsedCodex.resetTime.getTime() - now.getTime());
3732
+ return {
3733
+ detected: true,
3734
+ resetTime: parsedCodex.resetTime,
3735
+ sleepDurationMs,
3736
+ timezone: "local",
3737
+ originalMessage: message,
3738
+ source
3668
3739
  };
3669
3740
  }
3670
3741
  return {
3671
3742
  detected: true,
3672
3743
  sleepDurationMs: 5 * 60 * 1e3,
3673
3744
  // 5 minutes default
3674
- originalMessage: message
3745
+ originalMessage: message,
3746
+ source
3675
3747
  };
3676
3748
  }
3677
3749
  function formatDuration(ms) {
@@ -4196,8 +4268,68 @@ var init_shell_backend = __esm({
4196
4268
  metadata
4197
4269
  };
4198
4270
  }
4271
+ if (subagentType === "codex") {
4272
+ const codexQuotaMessage = this.extractCodexQuotaMessage(result.output, result.error);
4273
+ if (codexQuotaMessage) {
4274
+ const quotaLimitInfo = detectQuotaLimit(codexQuotaMessage);
4275
+ if (quotaLimitInfo.detected) {
4276
+ const metadata = {
4277
+ structuredOutput: true,
4278
+ contentType: "application/json",
4279
+ rawOutput: result.output,
4280
+ quotaLimitInfo
4281
+ };
4282
+ const structuredPayload = {
4283
+ type: "result",
4284
+ subtype: "error",
4285
+ is_error: true,
4286
+ result: codexQuotaMessage,
4287
+ error: codexQuotaMessage,
4288
+ exit_code: result.exitCode,
4289
+ duration_ms: result.duration,
4290
+ quota_limit: quotaLimitInfo
4291
+ };
4292
+ return {
4293
+ content: JSON.stringify(structuredPayload),
4294
+ metadata
4295
+ };
4296
+ }
4297
+ }
4298
+ }
4199
4299
  return { content: result.output, metadata: result.metadata };
4200
4300
  }
4301
+ /**
4302
+ * Extract quota limit message from Codex stream output
4303
+ * Codex outputs JSON events like:
4304
+ * {"type": "error", "message": "You've hit your usage limit..."}
4305
+ * {"type": "turn.failed", "error": {"message": "You've hit your usage limit..."}}
4306
+ */
4307
+ extractCodexQuotaMessage(output, stderr) {
4308
+ const sources = [output, stderr].filter(Boolean);
4309
+ for (const source of sources) {
4310
+ const lines = source.split("\n").map((l) => l.trim()).filter(Boolean);
4311
+ for (const line of lines) {
4312
+ try {
4313
+ const parsed = JSON.parse(line);
4314
+ if (parsed?.type === "error" && parsed?.message) {
4315
+ if (/you'?ve hit your usage limit/i.test(parsed.message)) {
4316
+ return parsed.message;
4317
+ }
4318
+ }
4319
+ if (parsed?.type === "turn.failed" && parsed?.error?.message) {
4320
+ if (/you'?ve hit your usage limit/i.test(parsed.error.message)) {
4321
+ return parsed.error.message;
4322
+ }
4323
+ }
4324
+ } catch {
4325
+ if (/you'?ve hit your usage limit/i.test(line)) {
4326
+ return line;
4327
+ }
4328
+ }
4329
+ }
4330
+ }
4331
+ return null;
4332
+ }
4201
4333
  /**
4202
4334
  * Extract the last valid JSON object from a script's stdout to use as a structured payload fallback.
4203
4335
  */
@@ -5177,8 +5309,9 @@ var init_engine = __esm({
5177
5309
  hour12: true,
5178
5310
  timeZoneName: "short"
5179
5311
  }) : "unknown";
5312
+ const sourceLabel = quotaInfo.source === "codex" ? "Codex" : "Claude";
5180
5313
  engineLogger.info(`\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
5181
- engineLogger.info(`\u2551 Claude Quota Limit Reached \u2551`);
5314
+ engineLogger.info(`\u2551 ${sourceLabel} Quota Limit Reached${" ".repeat(44 - sourceLabel.length - " Quota Limit Reached".length)}\u2551`);
5182
5315
  engineLogger.info(`\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
5183
5316
  engineLogger.info(`\u2551 Quota resets at: ${resetTimeStr2.padEnd(44)}\u2551`);
5184
5317
  engineLogger.info(`\u2551 Behavior: raise (exit immediately) \u2551`);
@@ -5188,7 +5321,7 @@ var init_engine = __esm({
5188
5321
  engineLogger.info(`\u2551 Or in config.json: { "onHourlyLimit": "wait" } \u2551`);
5189
5322
  engineLogger.info(`\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
5190
5323
  this.emit("quota-limit:raise", { context, quotaInfo });
5191
- throw new Error(`Claude quota limit reached. Quota resets at ${resetTimeStr2}. Use --on-hourly-limit wait to auto-retry.`);
5324
+ throw new Error(`${sourceLabel} quota limit reached. Quota resets at ${resetTimeStr2}. Use --on-hourly-limit wait to auto-retry.`);
5192
5325
  }
5193
5326
  const waitTimeMs = quotaInfo.sleepDurationMs;
5194
5327
  const maxWaitTimeMs = 12 * 60 * 60 * 1e3;
@@ -5204,8 +5337,9 @@ var init_engine = __esm({
5204
5337
  timeZoneName: "short"
5205
5338
  }) : "unknown";
5206
5339
  const durationStr = formatDuration(waitTimeMs);
5340
+ const waitSourceLabel = quotaInfo.source === "codex" ? "Codex" : "Claude";
5207
5341
  engineLogger.info(`\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
5208
- engineLogger.info(`\u2551 Claude Quota Limit Reached \u2551`);
5342
+ engineLogger.info(`\u2551 ${waitSourceLabel} Quota Limit Reached${" ".repeat(44 - waitSourceLabel.length - " Quota Limit Reached".length)}\u2551`);
5209
5343
  engineLogger.info(`\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
5210
5344
  engineLogger.info(`\u2551 Quota resets at: ${resetTimeStr.padEnd(44)}\u2551`);
5211
5345
  engineLogger.info(`\u2551 Sleeping for: ${durationStr.padEnd(44)}\u2551`);
@@ -12664,12 +12798,12 @@ function safeConsoleOutput(message, options = {}) {
12664
12798
  const capabilities = getTUICapabilities();
12665
12799
  let output = message;
12666
12800
  if (capabilities.hasColors && (color || bold)) {
12667
- const chalk24 = __require("chalk");
12668
- if (color && chalk24[color]) {
12669
- output = chalk24[color](output);
12801
+ const chalk25 = __require("chalk");
12802
+ if (color && chalk25[color]) {
12803
+ output = chalk25[color](output);
12670
12804
  }
12671
12805
  if (bold) {
12672
- output = chalk24.bold(output);
12806
+ output = chalk25.bold(output);
12673
12807
  }
12674
12808
  }
12675
12809
  console[type](output);
@@ -13005,8 +13139,45 @@ var init_tui = __esm({
13005
13139
  var main_exports = {};
13006
13140
  __export(main_exports, {
13007
13141
  createMainCommand: () => createMainCommand,
13142
+ getDefaultModelForSubagent: () => getDefaultModelForSubagent,
13143
+ isModelCompatibleWithSubagent: () => isModelCompatibleWithSubagent,
13008
13144
  mainCommandHandler: () => mainCommandHandler
13009
13145
  });
13146
+ function getDefaultModelForSubagent(subagent) {
13147
+ const modelDefaults = {
13148
+ claude: ":sonnet",
13149
+ codex: ":codex",
13150
+ // Expands to gpt-5.3-codex in codex.py
13151
+ gemini: ":pro",
13152
+ // Expands to gemini-2.5-pro in gemini.py
13153
+ cursor: "auto"
13154
+ };
13155
+ return modelDefaults[subagent] || modelDefaults.claude;
13156
+ }
13157
+ function isModelCompatibleWithSubagent(model, subagent) {
13158
+ if (!model.startsWith(":")) {
13159
+ return true;
13160
+ }
13161
+ const claudeShorthands = [":sonnet", ":haiku", ":opus"];
13162
+ const codexShorthands = [":codex", ":codex-mini", ":gpt-5", ":mini"];
13163
+ const geminiShorthands = [":pro", ":flash"];
13164
+ const isClaudeModel = claudeShorthands.includes(model) || model.startsWith(":claude");
13165
+ const isCodexModel = codexShorthands.includes(model) || model.startsWith(":gpt");
13166
+ const isGeminiModel = geminiShorthands.includes(model) || model.startsWith(":gemini");
13167
+ switch (subagent) {
13168
+ case "claude":
13169
+ return isClaudeModel || !isCodexModel && !isGeminiModel;
13170
+ case "codex":
13171
+ return isCodexModel || !isClaudeModel && !isGeminiModel;
13172
+ case "gemini":
13173
+ return isGeminiModel || !isClaudeModel && !isCodexModel;
13174
+ case "cursor":
13175
+ return true;
13176
+ // Cursor accepts any model
13177
+ default:
13178
+ return true;
13179
+ }
13180
+ }
13010
13181
  async function mainCommandHandler(args, options, command) {
13011
13182
  try {
13012
13183
  const validSubagents = ["claude", "cursor", "codex", "gemini"];
@@ -13058,13 +13229,15 @@ async function mainCommandHandler(args, options, command) {
13058
13229
  ["Use -1 for unlimited iterations", "Use positive integers like 1, 5, or 10", "Example: -i 5"]
13059
13230
  );
13060
13231
  }
13232
+ const configModelIsValid = config.defaultModel && config.defaultSubagent === options.subagent && isModelCompatibleWithSubagent(config.defaultModel, options.subagent);
13233
+ const resolvedModel = options.model || (configModelIsValid ? config.defaultModel : void 0) || getDefaultModelForSubagent(options.subagent);
13061
13234
  const executionRequest = createExecutionRequest({
13062
13235
  instruction,
13063
13236
  subagent: options.subagent,
13064
13237
  backend: selectedBackend,
13065
13238
  workingDirectory: config.workingDirectory,
13066
13239
  maxIterations: options.maxIterations ?? config.defaultMaxIterations,
13067
- model: options.model || config.defaultModel,
13240
+ model: resolvedModel,
13068
13241
  agents: options.agents,
13069
13242
  tools: options.tools,
13070
13243
  allowedTools: options.allowedTools,
@@ -13586,6 +13759,289 @@ var init_main = __esm({
13586
13759
  }
13587
13760
  });
13588
13761
 
13762
+ // src/utils/skill-installer.ts
13763
+ var skill_installer_exports = {};
13764
+ __export(skill_installer_exports, {
13765
+ SkillInstaller: () => SkillInstaller
13766
+ });
13767
+ var SkillInstaller;
13768
+ var init_skill_installer = __esm({
13769
+ "src/utils/skill-installer.ts"() {
13770
+ init_version();
13771
+ SkillInstaller = class {
13772
+ /**
13773
+ * Skill groups define which template folders map to which project directories.
13774
+ * New agents can be added here without changing any other logic.
13775
+ */
13776
+ static SKILL_GROUPS = [
13777
+ { name: "codex", destDir: ".agents/skills" },
13778
+ { name: "claude", destDir: ".claude/skills" }
13779
+ ];
13780
+ /**
13781
+ * Get the templates skills directory from the package
13782
+ */
13783
+ static getPackageSkillsDir() {
13784
+ const __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
13785
+ const candidates = [
13786
+ path3.join(__dirname2, "..", "..", "templates", "skills"),
13787
+ // dist (production)
13788
+ path3.join(__dirname2, "..", "templates", "skills")
13789
+ // src (development)
13790
+ ];
13791
+ for (const skillsPath of candidates) {
13792
+ if (fs3.existsSync(skillsPath)) {
13793
+ return skillsPath;
13794
+ }
13795
+ }
13796
+ if (process.env.JUNO_CODE_DEBUG === "1") {
13797
+ console.error("[DEBUG] SkillInstaller: Could not find templates/skills directory");
13798
+ console.error("[DEBUG] Tried:", candidates);
13799
+ }
13800
+ return null;
13801
+ }
13802
+ /**
13803
+ * Get list of skill files in a specific skill group template directory.
13804
+ * Returns paths relative to the group directory.
13805
+ */
13806
+ static async getSkillFiles(groupDir) {
13807
+ if (!await fs3.pathExists(groupDir)) {
13808
+ return [];
13809
+ }
13810
+ const files = [];
13811
+ const walk = async (dir, prefix) => {
13812
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
13813
+ for (const entry of entries) {
13814
+ if (entry.name.startsWith(".") || entry.name === "__pycache__") {
13815
+ continue;
13816
+ }
13817
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
13818
+ if (entry.isDirectory()) {
13819
+ await walk(path3.join(dir, entry.name), relPath);
13820
+ } else {
13821
+ files.push(relPath);
13822
+ }
13823
+ }
13824
+ };
13825
+ await walk(groupDir, "");
13826
+ return files;
13827
+ }
13828
+ /**
13829
+ * Install skills for a single skill group.
13830
+ * Only copies skill files, does NOT delete or modify any other files in the destination.
13831
+ *
13832
+ * @param projectDir - The project root directory
13833
+ * @param group - The skill group to install
13834
+ * @param silent - If true, suppresses console output
13835
+ * @param force - If true, overwrite even if content is identical
13836
+ * @returns number of files installed or updated
13837
+ */
13838
+ static async installGroup(projectDir, group, silent = true, force = false) {
13839
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13840
+ const packageSkillsDir = this.getPackageSkillsDir();
13841
+ if (!packageSkillsDir) {
13842
+ if (debug) {
13843
+ console.error("[DEBUG] SkillInstaller: Package skills directory not found");
13844
+ }
13845
+ return 0;
13846
+ }
13847
+ const sourceGroupDir = path3.join(packageSkillsDir, group.name);
13848
+ const destGroupDir = path3.join(projectDir, group.destDir);
13849
+ const skillFiles = await this.getSkillFiles(sourceGroupDir);
13850
+ if (skillFiles.length === 0) {
13851
+ if (debug) {
13852
+ console.error(`[DEBUG] SkillInstaller: No skill files found for group '${group.name}'`);
13853
+ }
13854
+ return 0;
13855
+ }
13856
+ await fs3.ensureDir(destGroupDir);
13857
+ let installed = 0;
13858
+ for (const relFile of skillFiles) {
13859
+ const srcPath = path3.join(sourceGroupDir, relFile);
13860
+ const destPath = path3.join(destGroupDir, relFile);
13861
+ const destParent = path3.dirname(destPath);
13862
+ await fs3.ensureDir(destParent);
13863
+ let shouldCopy = force;
13864
+ if (!shouldCopy) {
13865
+ if (!await fs3.pathExists(destPath)) {
13866
+ shouldCopy = true;
13867
+ } else {
13868
+ const [srcContent, destContent] = await Promise.all([
13869
+ fs3.readFile(srcPath, "utf-8"),
13870
+ fs3.readFile(destPath, "utf-8")
13871
+ ]);
13872
+ if (srcContent !== destContent) {
13873
+ shouldCopy = true;
13874
+ }
13875
+ }
13876
+ }
13877
+ if (shouldCopy) {
13878
+ await fs3.copy(srcPath, destPath, { overwrite: true });
13879
+ if (relFile.endsWith(".sh") || relFile.endsWith(".py")) {
13880
+ await fs3.chmod(destPath, 493);
13881
+ }
13882
+ installed++;
13883
+ if (debug) {
13884
+ console.error(`[DEBUG] SkillInstaller: Installed ${group.name}/${relFile} -> ${destPath}`);
13885
+ }
13886
+ }
13887
+ }
13888
+ if (installed > 0 && !silent) {
13889
+ console.log(`\u2713 Installed ${installed} skill file(s) for ${group.name} -> ${group.destDir}`);
13890
+ }
13891
+ return installed;
13892
+ }
13893
+ /**
13894
+ * Install skills for all skill groups.
13895
+ * This copies skill files to the appropriate project directories while
13896
+ * preserving any existing files the user may have added.
13897
+ *
13898
+ * @param projectDir - The project root directory
13899
+ * @param silent - If true, suppresses console output
13900
+ * @param force - If true, overwrite even if content matches
13901
+ * @returns true if any skill files were installed or updated
13902
+ */
13903
+ static async install(projectDir, silent = false, force = false) {
13904
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13905
+ let totalInstalled = 0;
13906
+ for (const group of this.SKILL_GROUPS) {
13907
+ try {
13908
+ const count = await this.installGroup(projectDir, group, silent, force);
13909
+ totalInstalled += count;
13910
+ } catch (error) {
13911
+ if (debug) {
13912
+ console.error(`[DEBUG] SkillInstaller: Error installing group '${group.name}':`, error);
13913
+ }
13914
+ if (!silent) {
13915
+ console.error(`\u26A0\uFE0F Failed to install skills for ${group.name}: ${error instanceof Error ? error.message : String(error)}`);
13916
+ }
13917
+ }
13918
+ }
13919
+ if (totalInstalled > 0 && !silent) {
13920
+ console.log(`\u2713 Total: ${totalInstalled} skill file(s) installed/updated`);
13921
+ }
13922
+ return totalInstalled > 0;
13923
+ }
13924
+ /**
13925
+ * Auto-update skills on CLI startup.
13926
+ * Only installs/updates if the project is initialized (.juno_task exists).
13927
+ * Silently does nothing if no skill files are bundled or project is not initialized.
13928
+ *
13929
+ * @param projectDir - The project root directory
13930
+ * @param force - If true, force reinstall all skills
13931
+ * @returns true if any updates occurred
13932
+ */
13933
+ static async autoUpdate(projectDir, force = false) {
13934
+ try {
13935
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13936
+ const junoTaskDir = path3.join(projectDir, ".juno_task");
13937
+ if (!await fs3.pathExists(junoTaskDir)) {
13938
+ return false;
13939
+ }
13940
+ if (debug) {
13941
+ console.error(`[DEBUG] SkillInstaller: Auto-updating skills (force=${force})`);
13942
+ }
13943
+ const updated = await this.install(projectDir, true, force);
13944
+ if (updated && debug) {
13945
+ console.error("[DEBUG] SkillInstaller: Skills auto-updated successfully");
13946
+ }
13947
+ return updated;
13948
+ } catch (error) {
13949
+ if (process.env.JUNO_CODE_DEBUG === "1") {
13950
+ console.error("[DEBUG] SkillInstaller: autoUpdate error:", error instanceof Error ? error.message : String(error));
13951
+ }
13952
+ return false;
13953
+ }
13954
+ }
13955
+ /**
13956
+ * Check if any skills need to be installed or updated.
13957
+ *
13958
+ * @param projectDir - The project root directory
13959
+ * @returns true if any skills are missing or outdated
13960
+ */
13961
+ static async needsUpdate(projectDir) {
13962
+ try {
13963
+ const junoTaskDir = path3.join(projectDir, ".juno_task");
13964
+ if (!await fs3.pathExists(junoTaskDir)) {
13965
+ return false;
13966
+ }
13967
+ const packageSkillsDir = this.getPackageSkillsDir();
13968
+ if (!packageSkillsDir) {
13969
+ return false;
13970
+ }
13971
+ for (const group of this.SKILL_GROUPS) {
13972
+ const sourceGroupDir = path3.join(packageSkillsDir, group.name);
13973
+ const destGroupDir = path3.join(projectDir, group.destDir);
13974
+ const skillFiles = await this.getSkillFiles(sourceGroupDir);
13975
+ for (const relFile of skillFiles) {
13976
+ const srcPath = path3.join(sourceGroupDir, relFile);
13977
+ const destPath = path3.join(destGroupDir, relFile);
13978
+ if (!await fs3.pathExists(destPath)) {
13979
+ return true;
13980
+ }
13981
+ const [srcContent, destContent] = await Promise.all([
13982
+ fs3.readFile(srcPath, "utf-8"),
13983
+ fs3.readFile(destPath, "utf-8")
13984
+ ]);
13985
+ if (srcContent !== destContent) {
13986
+ return true;
13987
+ }
13988
+ }
13989
+ }
13990
+ return false;
13991
+ } catch {
13992
+ return false;
13993
+ }
13994
+ }
13995
+ /**
13996
+ * List all skill groups and their installation status.
13997
+ *
13998
+ * @param projectDir - The project root directory
13999
+ * @returns Array of skill group status objects
14000
+ */
14001
+ static async listSkillGroups(projectDir) {
14002
+ const packageSkillsDir = this.getPackageSkillsDir();
14003
+ const results = [];
14004
+ for (const group of this.SKILL_GROUPS) {
14005
+ const sourceGroupDir = packageSkillsDir ? path3.join(packageSkillsDir, group.name) : "";
14006
+ const destGroupDir = path3.join(projectDir, group.destDir);
14007
+ const skillFiles = packageSkillsDir ? await this.getSkillFiles(sourceGroupDir) : [];
14008
+ const files = [];
14009
+ for (const relFile of skillFiles) {
14010
+ const srcPath = path3.join(sourceGroupDir, relFile);
14011
+ const destPath = path3.join(destGroupDir, relFile);
14012
+ const installed = await fs3.pathExists(destPath);
14013
+ let upToDate = false;
14014
+ if (installed) {
14015
+ try {
14016
+ const [srcContent, destContent] = await Promise.all([
14017
+ fs3.readFile(srcPath, "utf-8"),
14018
+ fs3.readFile(destPath, "utf-8")
14019
+ ]);
14020
+ upToDate = srcContent === destContent;
14021
+ } catch {
14022
+ upToDate = false;
14023
+ }
14024
+ }
14025
+ files.push({ name: relFile, installed, upToDate });
14026
+ }
14027
+ results.push({
14028
+ name: group.name,
14029
+ destDir: group.destDir,
14030
+ files
14031
+ });
14032
+ }
14033
+ return results;
14034
+ }
14035
+ /**
14036
+ * Get the list of skill group configurations.
14037
+ */
14038
+ static getSkillGroups() {
14039
+ return [...this.SKILL_GROUPS];
14040
+ }
14041
+ };
14042
+ }
14043
+ });
14044
+
13589
14045
  // src/utils/script-installer.ts
13590
14046
  var script_installer_exports = {};
13591
14047
  __export(script_installer_exports, {
@@ -13629,8 +14085,11 @@ var init_script_installer = __esm({
13629
14085
  "github.py",
13630
14086
  // Unified GitHub integration (fetch, respond, sync)
13631
14087
  // Claude Code hooks (stored in hooks/ subdirectory)
13632
- "hooks/session_counter.sh"
14088
+ "hooks/session_counter.sh",
13633
14089
  // Session message counter hook for warning about long sessions
14090
+ // Log scanning utility
14091
+ "log_scanner.sh"
14092
+ // Scans log files for errors/exceptions and creates kanban bug reports
13634
14093
  ];
13635
14094
  /**
13636
14095
  * Get the templates scripts directory from the package
@@ -16763,8 +17222,10 @@ ${variables.EDITOR ? `using ${variables.EDITOR} as primary AI subagent` : ""}
16763
17222
  getDefaultModelForSubagent(subagent) {
16764
17223
  const modelDefaults = {
16765
17224
  claude: ":sonnet",
16766
- codex: "gpt-5",
16767
- gemini: "gemini-2.5-pro",
17225
+ codex: ":codex",
17226
+ // Expands to gpt-5.3-codex in codex.py
17227
+ gemini: ":pro",
17228
+ // Expands to gemini-2.5-pro in gemini.py
16768
17229
  cursor: "auto"
16769
17230
  };
16770
17231
  return modelDefaults[subagent] || modelDefaults.claude;
@@ -19114,8 +19575,8 @@ async function compactConfigFile(filePath, options = {}) {
19114
19575
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19115
19576
  const ext = path3.extname(filePath);
19116
19577
  const basename11 = path3.basename(filePath, ext);
19117
- const dirname13 = path3.dirname(filePath);
19118
- backupPath = path3.join(dirname13, `${basename11}.backup.${timestamp}${ext}`);
19578
+ const dirname14 = path3.dirname(filePath);
19579
+ backupPath = path3.join(dirname14, `${basename11}.backup.${timestamp}${ext}`);
19119
19580
  await fs3.writeFile(backupPath, originalContent, "utf-8");
19120
19581
  }
19121
19582
  const compactionAnalysis = analyzeMarkdownStructure(originalContent);
@@ -21581,7 +22042,7 @@ var LogViewer = ({
21581
22042
  init_types();
21582
22043
  async function exportLogs(logger2, filepath, options) {
21583
22044
  try {
21584
- const fs23 = await import('fs-extra');
22045
+ const fs24 = await import('fs-extra');
21585
22046
  let entries = logger2.getRecentEntries(options.tail || 1e3);
21586
22047
  if (options.level) {
21587
22048
  const level = LogLevel[options.level.toUpperCase()];
@@ -21611,7 +22072,7 @@ async function exportLogs(logger2, filepath, options) {
21611
22072
  },
21612
22073
  entries
21613
22074
  };
21614
- await fs23.writeFile(filepath, JSON.stringify(exportData, null, 2));
22075
+ await fs24.writeFile(filepath, JSON.stringify(exportData, null, 2));
21615
22076
  console.log(chalk15.green(`\u2705 Exported ${entries.length} log entries to: ${filepath}`));
21616
22077
  } catch (error) {
21617
22078
  console.error(chalk15.red(`\u274C Failed to export logs: ${error}`));
@@ -23501,6 +23962,125 @@ Location: ${ServiceInstaller.getServicesDir()}`));
23501
23962
  return servicesCmd;
23502
23963
  }
23503
23964
 
23965
+ // src/cli/commands/skills.ts
23966
+ init_version();
23967
+ init_skill_installer();
23968
+ function createSkillsCommand() {
23969
+ const skillsCmd = new Command("skills").description("Manage agent skill files").addHelpText("after", `
23970
+ Examples:
23971
+ $ juno-code skills install Install skill files to project directories
23972
+ $ juno-code skills install --force Force reinstall all skill files
23973
+ $ juno-code skills list List skill groups and their files
23974
+ $ juno-code skills status Check installation status
23975
+
23976
+ Skill files are copied from the juno-code package into the project:
23977
+ - Codex skills -> .agents/skills/
23978
+ - Claude skills -> .claude/skills/
23979
+
23980
+ Skills are installed for ALL agents regardless of which subagent is selected.
23981
+ Existing files in the destination directories are preserved.
23982
+ `);
23983
+ skillsCmd.command("install").description("Install skill files to project directories").option("-f, --force", "Force reinstall even if files are up-to-date").action(async (options) => {
23984
+ try {
23985
+ const projectDir = process.cwd();
23986
+ if (!options.force) {
23987
+ const needsUpdate = await SkillInstaller.needsUpdate(projectDir);
23988
+ if (!needsUpdate) {
23989
+ console.log(chalk15.yellow("\u26A0 All skill files are up-to-date"));
23990
+ console.log(chalk15.dim(" Use --force to reinstall"));
23991
+ return;
23992
+ }
23993
+ }
23994
+ console.log(chalk15.blue("Installing skill files..."));
23995
+ const installed = await SkillInstaller.install(projectDir, false, options.force);
23996
+ if (installed) {
23997
+ console.log(chalk15.green("\n\u2713 Skill files installed successfully"));
23998
+ } else {
23999
+ console.log(chalk15.yellow("\u26A0 No skill files to install (templates may be empty)"));
24000
+ }
24001
+ } catch (error) {
24002
+ console.error(chalk15.red("\u2717 Installation failed:"));
24003
+ console.error(chalk15.red(error instanceof Error ? error.message : String(error)));
24004
+ process.exit(1);
24005
+ }
24006
+ });
24007
+ skillsCmd.command("list").alias("ls").description("List skill groups and their files").action(async () => {
24008
+ try {
24009
+ const projectDir = process.cwd();
24010
+ const groups = await SkillInstaller.listSkillGroups(projectDir);
24011
+ if (groups.length === 0) {
24012
+ console.log(chalk15.yellow("\u26A0 No skill groups configured"));
24013
+ return;
24014
+ }
24015
+ let hasAnyFiles = false;
24016
+ for (const group of groups) {
24017
+ console.log(chalk15.blue.bold(`
24018
+ ${group.name} skills -> ${group.destDir}/`));
24019
+ if (group.files.length === 0) {
24020
+ console.log(chalk15.dim(" (no skill files bundled yet)"));
24021
+ continue;
24022
+ }
24023
+ hasAnyFiles = true;
24024
+ for (const file of group.files) {
24025
+ const statusIcon = !file.installed ? chalk15.red("\u2717") : file.upToDate ? chalk15.green("\u2713") : chalk15.yellow("\u21BB");
24026
+ const statusLabel = !file.installed ? chalk15.dim("not installed") : file.upToDate ? chalk15.dim("up-to-date") : chalk15.yellow("outdated");
24027
+ console.log(` ${statusIcon} ${file.name} ${statusLabel}`);
24028
+ }
24029
+ }
24030
+ if (!hasAnyFiles) {
24031
+ console.log(chalk15.dim("\nNo skill files bundled yet. Add files to src/templates/skills/<agent>/ to bundle skills."));
24032
+ }
24033
+ } catch (error) {
24034
+ console.error(chalk15.red("\u2717 Failed to list skills:"));
24035
+ console.error(chalk15.red(error instanceof Error ? error.message : String(error)));
24036
+ process.exit(1);
24037
+ }
24038
+ });
24039
+ skillsCmd.command("status").description("Check skill installation status").action(async () => {
24040
+ try {
24041
+ const projectDir = process.cwd();
24042
+ const groups = await SkillInstaller.listSkillGroups(projectDir);
24043
+ const skillGroups = SkillInstaller.getSkillGroups();
24044
+ console.log(chalk15.blue("Skills Status:\n"));
24045
+ console.log(chalk15.dim(" Skill groups:"));
24046
+ for (const sg of skillGroups) {
24047
+ console.log(chalk15.dim(` ${sg.name} -> ${sg.destDir}/`));
24048
+ }
24049
+ const needsUpdate = await SkillInstaller.needsUpdate(projectDir);
24050
+ console.log(`
24051
+ ${needsUpdate ? chalk15.yellow("\u26A0 Updates available") : chalk15.green("\u2713 All skills up-to-date")}`);
24052
+ let totalFiles = 0;
24053
+ let installedFiles = 0;
24054
+ let outdatedFiles = 0;
24055
+ for (const group of groups) {
24056
+ for (const file of group.files) {
24057
+ totalFiles++;
24058
+ if (file.installed) {
24059
+ installedFiles++;
24060
+ if (!file.upToDate) {
24061
+ outdatedFiles++;
24062
+ }
24063
+ }
24064
+ }
24065
+ }
24066
+ if (totalFiles > 0) {
24067
+ console.log(chalk15.dim(`
24068
+ Files: ${installedFiles}/${totalFiles} installed, ${outdatedFiles} outdated`));
24069
+ } else {
24070
+ console.log(chalk15.dim("\n No skill files bundled yet"));
24071
+ }
24072
+ if (needsUpdate) {
24073
+ console.log(chalk15.dim("\n Run: juno-code skills install"));
24074
+ }
24075
+ } catch (error) {
24076
+ console.error(chalk15.red("\u2717 Failed to check status:"));
24077
+ console.error(chalk15.red(error instanceof Error ? error.message : String(error)));
24078
+ process.exit(1);
24079
+ }
24080
+ });
24081
+ return skillsCmd;
24082
+ }
24083
+
23504
24084
  // src/cli/commands/completion.ts
23505
24085
  init_version();
23506
24086
 
@@ -24766,10 +25346,10 @@ function setupMainCommand(program) {
24766
25346
  const allOptions2 = { ...definedGlobalOptions, ...options };
24767
25347
  if (allOptions2.tilCompletion || allOptions2.untilCompletion || allOptions2.runUntilCompletion || allOptions2.tillComplete) {
24768
25348
  const { spawn: spawn4 } = await import('child_process');
24769
- const path24 = await import('path');
24770
- const fs23 = await import('fs-extra');
24771
- const scriptPath = path24.join(process.cwd(), ".juno_task", "scripts", "run_until_completion.sh");
24772
- if (!await fs23.pathExists(scriptPath)) {
25349
+ const path25 = await import('path');
25350
+ const fs24 = await import('fs-extra');
25351
+ const scriptPath = path25.join(process.cwd(), ".juno_task", "scripts", "run_until_completion.sh");
25352
+ if (!await fs24.pathExists(scriptPath)) {
24773
25353
  console.error(chalk15.red.bold("\n\u274C Error: run_until_completion.sh not found"));
24774
25354
  console.error(chalk15.red(` Expected location: ${scriptPath}`));
24775
25355
  console.error(chalk15.yellow('\n\u{1F4A1} Suggestion: Run "juno-code init" to initialize the project'));
@@ -24820,11 +25400,11 @@ function setupMainCommand(program) {
24820
25400
  return;
24821
25401
  }
24822
25402
  if (!globalOptions.subagent && !options.prompt && !options.interactive && !options.interactivePrompt) {
24823
- const fs23 = await import('fs-extra');
24824
- const path24 = await import('path');
25403
+ const fs24 = await import('fs-extra');
25404
+ const path25 = await import('path');
24825
25405
  const cwd2 = process.cwd();
24826
- const junoTaskDir = path24.join(cwd2, ".juno_task");
24827
- if (await fs23.pathExists(junoTaskDir)) {
25406
+ const junoTaskDir = path25.join(cwd2, ".juno_task");
25407
+ if (await fs24.pathExists(junoTaskDir)) {
24828
25408
  console.log(chalk15.blue.bold("\u{1F3AF} Juno Code - Auto-detected Initialized Project\n"));
24829
25409
  try {
24830
25410
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -24841,12 +25421,12 @@ function setupMainCommand(program) {
24841
25421
  allOptions2.subagent = config.defaultSubagent;
24842
25422
  console.log(chalk15.gray(`\u{1F916} Using configured subagent: ${chalk15.cyan(config.defaultSubagent)}`));
24843
25423
  }
24844
- const promptFile = path24.join(junoTaskDir, "prompt.md");
24845
- if (!allOptions2.prompt && await fs23.pathExists(promptFile)) {
25424
+ const promptFile = path25.join(junoTaskDir, "prompt.md");
25425
+ if (!allOptions2.prompt && await fs24.pathExists(promptFile)) {
24846
25426
  allOptions2.prompt = promptFile;
24847
25427
  console.log(chalk15.gray(`\u{1F4C4} Using default prompt: ${chalk15.cyan(".juno_task/prompt.md")}`));
24848
25428
  }
24849
- if (allOptions2.subagent && (allOptions2.prompt || await fs23.pathExists(promptFile))) {
25429
+ if (allOptions2.subagent && (allOptions2.prompt || await fs24.pathExists(promptFile))) {
24850
25430
  console.log(chalk15.green("\u2713 Auto-detected project configuration\n"));
24851
25431
  const { mainCommandHandler: mainCommandHandler3 } = await Promise.resolve().then(() => (init_main(), main_exports));
24852
25432
  await mainCommandHandler3([], allOptions2, command);
@@ -25028,6 +25608,23 @@ async function main() {
25028
25608
  console.error("[DEBUG] Script auto-update failed:", error instanceof Error ? error.message : String(error));
25029
25609
  }
25030
25610
  }
25611
+ try {
25612
+ const { SkillInstaller: SkillInstaller2 } = await Promise.resolve().then(() => (init_skill_installer(), skill_installer_exports));
25613
+ if (isForceUpdate) {
25614
+ console.log(chalk15.blue("\u{1F504} Force updating agent skill files..."));
25615
+ await SkillInstaller2.autoUpdate(process.cwd(), true);
25616
+ console.log(chalk15.green("\u2713 Agent skill files updated"));
25617
+ } else {
25618
+ const updated = await SkillInstaller2.autoUpdate(process.cwd());
25619
+ if (updated && (process.argv.includes("--verbose") || process.argv.includes("-v") || process.env.JUNO_CODE_DEBUG === "1")) {
25620
+ console.error("[DEBUG] Agent skill files auto-updated");
25621
+ }
25622
+ }
25623
+ } catch (error) {
25624
+ if (process.env.JUNO_CODE_DEBUG === "1") {
25625
+ console.error("[DEBUG] Skill auto-update failed:", error instanceof Error ? error.message : String(error));
25626
+ }
25627
+ }
25031
25628
  program.name("juno-code").description("TypeScript implementation of juno-code CLI tool for AI subagent orchestration").version(VERSION, "-V, --version", "Display version information").helpOption("-h, --help", "Display help information");
25032
25629
  setupGlobalOptions(program);
25033
25630
  const isVerbose = process.argv.includes("--verbose") || process.argv.includes("-v");
@@ -25035,10 +25632,10 @@ async function main() {
25035
25632
  const isHelpOrVersion = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("--version") || process.argv.includes("-V");
25036
25633
  const hasNoArguments = process.argv.length <= 2;
25037
25634
  const isInitCommand = process.argv.includes("init");
25038
- const fs23 = await import('fs-extra');
25039
- const path24 = await import('path');
25040
- const junoTaskDir = path24.join(process.cwd(), ".juno_task");
25041
- const isInitialized = await fs23.pathExists(junoTaskDir);
25635
+ const fs24 = await import('fs-extra');
25636
+ const path25 = await import('path');
25637
+ const junoTaskDir = path25.join(process.cwd(), ".juno_task");
25638
+ const isInitialized = await fs24.pathExists(junoTaskDir);
25042
25639
  if (!isHelpOrVersion && !hasNoArguments && !isInitCommand && isInitialized) {
25043
25640
  try {
25044
25641
  const { validateStartupConfigs: validateStartupConfigs2 } = await Promise.resolve().then(() => (init_startup_validation(), startup_validation_exports));
@@ -25065,6 +25662,7 @@ async function main() {
25065
25662
  configureHelpCommand(program);
25066
25663
  setupConfigCommand(program);
25067
25664
  program.addCommand(createServicesCommand());
25665
+ program.addCommand(createSkillsCommand());
25068
25666
  setupCompletion(program);
25069
25667
  setupAliases(program);
25070
25668
  setupMainCommand(program);