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.js CHANGED
@@ -178,8 +178,8 @@ var init_types = __esm({
178
178
  };
179
179
  FileSystemError = class extends CLIError {
180
180
  code = "FILESYSTEM_ERROR";
181
- constructor(message, path24) {
182
- super(path24 ? `${message}: ${path24}` : message);
181
+ constructor(message, path25) {
182
+ super(path25 ? `${message}: ${path25}` : message);
183
183
  this.suggestions = [
184
184
  "Check file/directory permissions",
185
185
  "Verify path exists and is accessible",
@@ -1854,8 +1854,8 @@ ${helpText}
1854
1854
  }
1855
1855
  }
1856
1856
  if (options.cwd) {
1857
- const fs23 = await import('fs-extra');
1858
- if (!await fs23.pathExists(options.cwd)) {
1857
+ const fs24 = await import('fs-extra');
1858
+ if (!await fs24.pathExists(options.cwd)) {
1859
1859
  throw new ValidationError(
1860
1860
  `Working directory does not exist: ${options.cwd}`,
1861
1861
  ["Verify the path exists", "Use absolute paths to avoid ambiguity"]
@@ -3682,31 +3682,103 @@ function parseResetTime(message) {
3682
3682
  }
3683
3683
  return { resetTime, timezone };
3684
3684
  }
3685
+ function parseCodexResetTime(message) {
3686
+ 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;
3687
+ const match = message.match(resetPattern);
3688
+ if (!match) {
3689
+ return null;
3690
+ }
3691
+ const monthStr = match[1];
3692
+ const day = parseInt(match[2], 10);
3693
+ const year = parseInt(match[3], 10);
3694
+ let hours = parseInt(match[4], 10);
3695
+ const minutes = parseInt(match[5], 10);
3696
+ const ampm = match[6].toUpperCase();
3697
+ const MONTH_MAP = {
3698
+ "jan": 0,
3699
+ "january": 0,
3700
+ "feb": 1,
3701
+ "february": 1,
3702
+ "mar": 2,
3703
+ "march": 2,
3704
+ "apr": 3,
3705
+ "april": 3,
3706
+ "may": 4,
3707
+ "jun": 5,
3708
+ "june": 5,
3709
+ "jul": 6,
3710
+ "july": 6,
3711
+ "aug": 7,
3712
+ "august": 7,
3713
+ "sep": 8,
3714
+ "september": 8,
3715
+ "oct": 9,
3716
+ "october": 9,
3717
+ "nov": 10,
3718
+ "november": 10,
3719
+ "dec": 11,
3720
+ "december": 11
3721
+ };
3722
+ const month = MONTH_MAP[monthStr.toLowerCase()];
3723
+ if (month === void 0) {
3724
+ return null;
3725
+ }
3726
+ if (ampm === "PM" && hours !== 12) {
3727
+ hours += 12;
3728
+ } else if (ampm === "AM" && hours === 12) {
3729
+ hours = 0;
3730
+ }
3731
+ const resetTime = new Date(year, month, day, hours, minutes, 0, 0);
3732
+ const now = /* @__PURE__ */ new Date();
3733
+ if (resetTime.getTime() <= now.getTime()) {
3734
+ resetTime.setTime(resetTime.getTime() + 24 * 60 * 60 * 1e3);
3735
+ }
3736
+ return { resetTime };
3737
+ }
3685
3738
  function detectQuotaLimit(message) {
3686
3739
  if (!message || typeof message !== "string") {
3687
3740
  return { detected: false };
3688
3741
  }
3689
- const quotaPattern = /you'?ve hit your limit/i;
3690
- if (!quotaPattern.test(message)) {
3742
+ const claudePattern = /you'?ve hit your limit/i;
3743
+ const codexPattern = /you'?ve hit your usage limit/i;
3744
+ const isClaudeQuota = claudePattern.test(message) && !codexPattern.test(message);
3745
+ const isCodexQuota = codexPattern.test(message);
3746
+ if (!isClaudeQuota && !isCodexQuota) {
3691
3747
  return { detected: false };
3692
3748
  }
3693
- const parsed = parseResetTime(message);
3694
- if (parsed) {
3749
+ const source = isCodexQuota ? "codex" : "claude";
3750
+ const parsedClaude = parseResetTime(message);
3751
+ if (parsedClaude) {
3695
3752
  const now = /* @__PURE__ */ new Date();
3696
- const sleepDurationMs = Math.max(0, parsed.resetTime.getTime() - now.getTime());
3753
+ const sleepDurationMs = Math.max(0, parsedClaude.resetTime.getTime() - now.getTime());
3697
3754
  return {
3698
3755
  detected: true,
3699
- resetTime: parsed.resetTime,
3756
+ resetTime: parsedClaude.resetTime,
3700
3757
  sleepDurationMs,
3701
- timezone: parsed.timezone,
3702
- originalMessage: message
3758
+ timezone: parsedClaude.timezone,
3759
+ originalMessage: message,
3760
+ source
3761
+ };
3762
+ }
3763
+ const parsedCodex = parseCodexResetTime(message);
3764
+ if (parsedCodex) {
3765
+ const now = /* @__PURE__ */ new Date();
3766
+ const sleepDurationMs = Math.max(0, parsedCodex.resetTime.getTime() - now.getTime());
3767
+ return {
3768
+ detected: true,
3769
+ resetTime: parsedCodex.resetTime,
3770
+ sleepDurationMs,
3771
+ timezone: "local",
3772
+ originalMessage: message,
3773
+ source
3703
3774
  };
3704
3775
  }
3705
3776
  return {
3706
3777
  detected: true,
3707
3778
  sleepDurationMs: 5 * 60 * 1e3,
3708
3779
  // 5 minutes default
3709
- originalMessage: message
3780
+ originalMessage: message,
3781
+ source
3710
3782
  };
3711
3783
  }
3712
3784
  function formatDuration(ms) {
@@ -4231,8 +4303,68 @@ var init_shell_backend = __esm({
4231
4303
  metadata
4232
4304
  };
4233
4305
  }
4306
+ if (subagentType === "codex") {
4307
+ const codexQuotaMessage = this.extractCodexQuotaMessage(result.output, result.error);
4308
+ if (codexQuotaMessage) {
4309
+ const quotaLimitInfo = detectQuotaLimit(codexQuotaMessage);
4310
+ if (quotaLimitInfo.detected) {
4311
+ const metadata = {
4312
+ structuredOutput: true,
4313
+ contentType: "application/json",
4314
+ rawOutput: result.output,
4315
+ quotaLimitInfo
4316
+ };
4317
+ const structuredPayload = {
4318
+ type: "result",
4319
+ subtype: "error",
4320
+ is_error: true,
4321
+ result: codexQuotaMessage,
4322
+ error: codexQuotaMessage,
4323
+ exit_code: result.exitCode,
4324
+ duration_ms: result.duration,
4325
+ quota_limit: quotaLimitInfo
4326
+ };
4327
+ return {
4328
+ content: JSON.stringify(structuredPayload),
4329
+ metadata
4330
+ };
4331
+ }
4332
+ }
4333
+ }
4234
4334
  return { content: result.output, metadata: result.metadata };
4235
4335
  }
4336
+ /**
4337
+ * Extract quota limit message from Codex stream output
4338
+ * Codex outputs JSON events like:
4339
+ * {"type": "error", "message": "You've hit your usage limit..."}
4340
+ * {"type": "turn.failed", "error": {"message": "You've hit your usage limit..."}}
4341
+ */
4342
+ extractCodexQuotaMessage(output, stderr) {
4343
+ const sources = [output, stderr].filter(Boolean);
4344
+ for (const source of sources) {
4345
+ const lines = source.split("\n").map((l) => l.trim()).filter(Boolean);
4346
+ for (const line of lines) {
4347
+ try {
4348
+ const parsed = JSON.parse(line);
4349
+ if (parsed?.type === "error" && parsed?.message) {
4350
+ if (/you'?ve hit your usage limit/i.test(parsed.message)) {
4351
+ return parsed.message;
4352
+ }
4353
+ }
4354
+ if (parsed?.type === "turn.failed" && parsed?.error?.message) {
4355
+ if (/you'?ve hit your usage limit/i.test(parsed.error.message)) {
4356
+ return parsed.error.message;
4357
+ }
4358
+ }
4359
+ } catch {
4360
+ if (/you'?ve hit your usage limit/i.test(line)) {
4361
+ return line;
4362
+ }
4363
+ }
4364
+ }
4365
+ }
4366
+ return null;
4367
+ }
4236
4368
  /**
4237
4369
  * Extract the last valid JSON object from a script's stdout to use as a structured payload fallback.
4238
4370
  */
@@ -5212,8 +5344,9 @@ var init_engine = __esm({
5212
5344
  hour12: true,
5213
5345
  timeZoneName: "short"
5214
5346
  }) : "unknown";
5347
+ const sourceLabel = quotaInfo.source === "codex" ? "Codex" : "Claude";
5215
5348
  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`);
5216
- engineLogger.info(`\u2551 Claude Quota Limit Reached \u2551`);
5349
+ engineLogger.info(`\u2551 ${sourceLabel} Quota Limit Reached${" ".repeat(44 - sourceLabel.length - " Quota Limit Reached".length)}\u2551`);
5217
5350
  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`);
5218
5351
  engineLogger.info(`\u2551 Quota resets at: ${resetTimeStr2.padEnd(44)}\u2551`);
5219
5352
  engineLogger.info(`\u2551 Behavior: raise (exit immediately) \u2551`);
@@ -5223,7 +5356,7 @@ var init_engine = __esm({
5223
5356
  engineLogger.info(`\u2551 Or in config.json: { "onHourlyLimit": "wait" } \u2551`);
5224
5357
  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`);
5225
5358
  this.emit("quota-limit:raise", { context, quotaInfo });
5226
- throw new Error(`Claude quota limit reached. Quota resets at ${resetTimeStr2}. Use --on-hourly-limit wait to auto-retry.`);
5359
+ throw new Error(`${sourceLabel} quota limit reached. Quota resets at ${resetTimeStr2}. Use --on-hourly-limit wait to auto-retry.`);
5227
5360
  }
5228
5361
  const waitTimeMs = quotaInfo.sleepDurationMs;
5229
5362
  const maxWaitTimeMs = 12 * 60 * 60 * 1e3;
@@ -5239,8 +5372,9 @@ var init_engine = __esm({
5239
5372
  timeZoneName: "short"
5240
5373
  }) : "unknown";
5241
5374
  const durationStr = formatDuration(waitTimeMs);
5375
+ const waitSourceLabel = quotaInfo.source === "codex" ? "Codex" : "Claude";
5242
5376
  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`);
5243
- engineLogger.info(`\u2551 Claude Quota Limit Reached \u2551`);
5377
+ engineLogger.info(`\u2551 ${waitSourceLabel} Quota Limit Reached${" ".repeat(44 - waitSourceLabel.length - " Quota Limit Reached".length)}\u2551`);
5244
5378
  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`);
5245
5379
  engineLogger.info(`\u2551 Quota resets at: ${resetTimeStr.padEnd(44)}\u2551`);
5246
5380
  engineLogger.info(`\u2551 Sleeping for: ${durationStr.padEnd(44)}\u2551`);
@@ -12699,12 +12833,12 @@ function safeConsoleOutput(message, options = {}) {
12699
12833
  const capabilities = getTUICapabilities();
12700
12834
  let output = message;
12701
12835
  if (capabilities.hasColors && (color || bold)) {
12702
- const chalk24 = __require("chalk");
12703
- if (color && chalk24[color]) {
12704
- output = chalk24[color](output);
12836
+ const chalk25 = __require("chalk");
12837
+ if (color && chalk25[color]) {
12838
+ output = chalk25[color](output);
12705
12839
  }
12706
12840
  if (bold) {
12707
- output = chalk24.bold(output);
12841
+ output = chalk25.bold(output);
12708
12842
  }
12709
12843
  }
12710
12844
  console[type](output);
@@ -13040,8 +13174,45 @@ var init_tui = __esm({
13040
13174
  var main_exports = {};
13041
13175
  __export(main_exports, {
13042
13176
  createMainCommand: () => createMainCommand,
13177
+ getDefaultModelForSubagent: () => getDefaultModelForSubagent,
13178
+ isModelCompatibleWithSubagent: () => isModelCompatibleWithSubagent,
13043
13179
  mainCommandHandler: () => mainCommandHandler
13044
13180
  });
13181
+ function getDefaultModelForSubagent(subagent) {
13182
+ const modelDefaults = {
13183
+ claude: ":sonnet",
13184
+ codex: ":codex",
13185
+ // Expands to gpt-5.3-codex in codex.py
13186
+ gemini: ":pro",
13187
+ // Expands to gemini-2.5-pro in gemini.py
13188
+ cursor: "auto"
13189
+ };
13190
+ return modelDefaults[subagent] || modelDefaults.claude;
13191
+ }
13192
+ function isModelCompatibleWithSubagent(model, subagent) {
13193
+ if (!model.startsWith(":")) {
13194
+ return true;
13195
+ }
13196
+ const claudeShorthands = [":sonnet", ":haiku", ":opus"];
13197
+ const codexShorthands = [":codex", ":codex-mini", ":gpt-5", ":mini"];
13198
+ const geminiShorthands = [":pro", ":flash"];
13199
+ const isClaudeModel = claudeShorthands.includes(model) || model.startsWith(":claude");
13200
+ const isCodexModel = codexShorthands.includes(model) || model.startsWith(":gpt");
13201
+ const isGeminiModel = geminiShorthands.includes(model) || model.startsWith(":gemini");
13202
+ switch (subagent) {
13203
+ case "claude":
13204
+ return isClaudeModel || !isCodexModel && !isGeminiModel;
13205
+ case "codex":
13206
+ return isCodexModel || !isClaudeModel && !isGeminiModel;
13207
+ case "gemini":
13208
+ return isGeminiModel || !isClaudeModel && !isCodexModel;
13209
+ case "cursor":
13210
+ return true;
13211
+ // Cursor accepts any model
13212
+ default:
13213
+ return true;
13214
+ }
13215
+ }
13045
13216
  async function mainCommandHandler(args, options, command) {
13046
13217
  try {
13047
13218
  const validSubagents = ["claude", "cursor", "codex", "gemini"];
@@ -13093,13 +13264,15 @@ async function mainCommandHandler(args, options, command) {
13093
13264
  ["Use -1 for unlimited iterations", "Use positive integers like 1, 5, or 10", "Example: -i 5"]
13094
13265
  );
13095
13266
  }
13267
+ const configModelIsValid = config.defaultModel && config.defaultSubagent === options.subagent && isModelCompatibleWithSubagent(config.defaultModel, options.subagent);
13268
+ const resolvedModel = options.model || (configModelIsValid ? config.defaultModel : void 0) || getDefaultModelForSubagent(options.subagent);
13096
13269
  const executionRequest = createExecutionRequest({
13097
13270
  instruction,
13098
13271
  subagent: options.subagent,
13099
13272
  backend: selectedBackend,
13100
13273
  workingDirectory: config.workingDirectory,
13101
13274
  maxIterations: options.maxIterations ?? config.defaultMaxIterations,
13102
- model: options.model || config.defaultModel,
13275
+ model: resolvedModel,
13103
13276
  agents: options.agents,
13104
13277
  tools: options.tools,
13105
13278
  allowedTools: options.allowedTools,
@@ -13621,6 +13794,289 @@ var init_main = __esm({
13621
13794
  }
13622
13795
  });
13623
13796
 
13797
+ // src/utils/skill-installer.ts
13798
+ var skill_installer_exports = {};
13799
+ __export(skill_installer_exports, {
13800
+ SkillInstaller: () => SkillInstaller
13801
+ });
13802
+ var SkillInstaller;
13803
+ var init_skill_installer = __esm({
13804
+ "src/utils/skill-installer.ts"() {
13805
+ init_version();
13806
+ SkillInstaller = class {
13807
+ /**
13808
+ * Skill groups define which template folders map to which project directories.
13809
+ * New agents can be added here without changing any other logic.
13810
+ */
13811
+ static SKILL_GROUPS = [
13812
+ { name: "codex", destDir: ".agents/skills" },
13813
+ { name: "claude", destDir: ".claude/skills" }
13814
+ ];
13815
+ /**
13816
+ * Get the templates skills directory from the package
13817
+ */
13818
+ static getPackageSkillsDir() {
13819
+ const __dirname2 = path3__namespace.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.js', document.baseURI).href))));
13820
+ const candidates = [
13821
+ path3__namespace.join(__dirname2, "..", "..", "templates", "skills"),
13822
+ // dist (production)
13823
+ path3__namespace.join(__dirname2, "..", "templates", "skills")
13824
+ // src (development)
13825
+ ];
13826
+ for (const skillsPath of candidates) {
13827
+ if (fs3__default.default.existsSync(skillsPath)) {
13828
+ return skillsPath;
13829
+ }
13830
+ }
13831
+ if (process.env.JUNO_CODE_DEBUG === "1") {
13832
+ console.error("[DEBUG] SkillInstaller: Could not find templates/skills directory");
13833
+ console.error("[DEBUG] Tried:", candidates);
13834
+ }
13835
+ return null;
13836
+ }
13837
+ /**
13838
+ * Get list of skill files in a specific skill group template directory.
13839
+ * Returns paths relative to the group directory.
13840
+ */
13841
+ static async getSkillFiles(groupDir) {
13842
+ if (!await fs3__default.default.pathExists(groupDir)) {
13843
+ return [];
13844
+ }
13845
+ const files = [];
13846
+ const walk = async (dir, prefix) => {
13847
+ const entries = await fs3__default.default.readdir(dir, { withFileTypes: true });
13848
+ for (const entry of entries) {
13849
+ if (entry.name.startsWith(".") || entry.name === "__pycache__") {
13850
+ continue;
13851
+ }
13852
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
13853
+ if (entry.isDirectory()) {
13854
+ await walk(path3__namespace.join(dir, entry.name), relPath);
13855
+ } else {
13856
+ files.push(relPath);
13857
+ }
13858
+ }
13859
+ };
13860
+ await walk(groupDir, "");
13861
+ return files;
13862
+ }
13863
+ /**
13864
+ * Install skills for a single skill group.
13865
+ * Only copies skill files, does NOT delete or modify any other files in the destination.
13866
+ *
13867
+ * @param projectDir - The project root directory
13868
+ * @param group - The skill group to install
13869
+ * @param silent - If true, suppresses console output
13870
+ * @param force - If true, overwrite even if content is identical
13871
+ * @returns number of files installed or updated
13872
+ */
13873
+ static async installGroup(projectDir, group, silent = true, force = false) {
13874
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13875
+ const packageSkillsDir = this.getPackageSkillsDir();
13876
+ if (!packageSkillsDir) {
13877
+ if (debug) {
13878
+ console.error("[DEBUG] SkillInstaller: Package skills directory not found");
13879
+ }
13880
+ return 0;
13881
+ }
13882
+ const sourceGroupDir = path3__namespace.join(packageSkillsDir, group.name);
13883
+ const destGroupDir = path3__namespace.join(projectDir, group.destDir);
13884
+ const skillFiles = await this.getSkillFiles(sourceGroupDir);
13885
+ if (skillFiles.length === 0) {
13886
+ if (debug) {
13887
+ console.error(`[DEBUG] SkillInstaller: No skill files found for group '${group.name}'`);
13888
+ }
13889
+ return 0;
13890
+ }
13891
+ await fs3__default.default.ensureDir(destGroupDir);
13892
+ let installed = 0;
13893
+ for (const relFile of skillFiles) {
13894
+ const srcPath = path3__namespace.join(sourceGroupDir, relFile);
13895
+ const destPath = path3__namespace.join(destGroupDir, relFile);
13896
+ const destParent = path3__namespace.dirname(destPath);
13897
+ await fs3__default.default.ensureDir(destParent);
13898
+ let shouldCopy = force;
13899
+ if (!shouldCopy) {
13900
+ if (!await fs3__default.default.pathExists(destPath)) {
13901
+ shouldCopy = true;
13902
+ } else {
13903
+ const [srcContent, destContent] = await Promise.all([
13904
+ fs3__default.default.readFile(srcPath, "utf-8"),
13905
+ fs3__default.default.readFile(destPath, "utf-8")
13906
+ ]);
13907
+ if (srcContent !== destContent) {
13908
+ shouldCopy = true;
13909
+ }
13910
+ }
13911
+ }
13912
+ if (shouldCopy) {
13913
+ await fs3__default.default.copy(srcPath, destPath, { overwrite: true });
13914
+ if (relFile.endsWith(".sh") || relFile.endsWith(".py")) {
13915
+ await fs3__default.default.chmod(destPath, 493);
13916
+ }
13917
+ installed++;
13918
+ if (debug) {
13919
+ console.error(`[DEBUG] SkillInstaller: Installed ${group.name}/${relFile} -> ${destPath}`);
13920
+ }
13921
+ }
13922
+ }
13923
+ if (installed > 0 && !silent) {
13924
+ console.log(`\u2713 Installed ${installed} skill file(s) for ${group.name} -> ${group.destDir}`);
13925
+ }
13926
+ return installed;
13927
+ }
13928
+ /**
13929
+ * Install skills for all skill groups.
13930
+ * This copies skill files to the appropriate project directories while
13931
+ * preserving any existing files the user may have added.
13932
+ *
13933
+ * @param projectDir - The project root directory
13934
+ * @param silent - If true, suppresses console output
13935
+ * @param force - If true, overwrite even if content matches
13936
+ * @returns true if any skill files were installed or updated
13937
+ */
13938
+ static async install(projectDir, silent = false, force = false) {
13939
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13940
+ let totalInstalled = 0;
13941
+ for (const group of this.SKILL_GROUPS) {
13942
+ try {
13943
+ const count = await this.installGroup(projectDir, group, silent, force);
13944
+ totalInstalled += count;
13945
+ } catch (error) {
13946
+ if (debug) {
13947
+ console.error(`[DEBUG] SkillInstaller: Error installing group '${group.name}':`, error);
13948
+ }
13949
+ if (!silent) {
13950
+ console.error(`\u26A0\uFE0F Failed to install skills for ${group.name}: ${error instanceof Error ? error.message : String(error)}`);
13951
+ }
13952
+ }
13953
+ }
13954
+ if (totalInstalled > 0 && !silent) {
13955
+ console.log(`\u2713 Total: ${totalInstalled} skill file(s) installed/updated`);
13956
+ }
13957
+ return totalInstalled > 0;
13958
+ }
13959
+ /**
13960
+ * Auto-update skills on CLI startup.
13961
+ * Only installs/updates if the project is initialized (.juno_task exists).
13962
+ * Silently does nothing if no skill files are bundled or project is not initialized.
13963
+ *
13964
+ * @param projectDir - The project root directory
13965
+ * @param force - If true, force reinstall all skills
13966
+ * @returns true if any updates occurred
13967
+ */
13968
+ static async autoUpdate(projectDir, force = false) {
13969
+ try {
13970
+ const debug = process.env.JUNO_CODE_DEBUG === "1";
13971
+ const junoTaskDir = path3__namespace.join(projectDir, ".juno_task");
13972
+ if (!await fs3__default.default.pathExists(junoTaskDir)) {
13973
+ return false;
13974
+ }
13975
+ if (debug) {
13976
+ console.error(`[DEBUG] SkillInstaller: Auto-updating skills (force=${force})`);
13977
+ }
13978
+ const updated = await this.install(projectDir, true, force);
13979
+ if (updated && debug) {
13980
+ console.error("[DEBUG] SkillInstaller: Skills auto-updated successfully");
13981
+ }
13982
+ return updated;
13983
+ } catch (error) {
13984
+ if (process.env.JUNO_CODE_DEBUG === "1") {
13985
+ console.error("[DEBUG] SkillInstaller: autoUpdate error:", error instanceof Error ? error.message : String(error));
13986
+ }
13987
+ return false;
13988
+ }
13989
+ }
13990
+ /**
13991
+ * Check if any skills need to be installed or updated.
13992
+ *
13993
+ * @param projectDir - The project root directory
13994
+ * @returns true if any skills are missing or outdated
13995
+ */
13996
+ static async needsUpdate(projectDir) {
13997
+ try {
13998
+ const junoTaskDir = path3__namespace.join(projectDir, ".juno_task");
13999
+ if (!await fs3__default.default.pathExists(junoTaskDir)) {
14000
+ return false;
14001
+ }
14002
+ const packageSkillsDir = this.getPackageSkillsDir();
14003
+ if (!packageSkillsDir) {
14004
+ return false;
14005
+ }
14006
+ for (const group of this.SKILL_GROUPS) {
14007
+ const sourceGroupDir = path3__namespace.join(packageSkillsDir, group.name);
14008
+ const destGroupDir = path3__namespace.join(projectDir, group.destDir);
14009
+ const skillFiles = await this.getSkillFiles(sourceGroupDir);
14010
+ for (const relFile of skillFiles) {
14011
+ const srcPath = path3__namespace.join(sourceGroupDir, relFile);
14012
+ const destPath = path3__namespace.join(destGroupDir, relFile);
14013
+ if (!await fs3__default.default.pathExists(destPath)) {
14014
+ return true;
14015
+ }
14016
+ const [srcContent, destContent] = await Promise.all([
14017
+ fs3__default.default.readFile(srcPath, "utf-8"),
14018
+ fs3__default.default.readFile(destPath, "utf-8")
14019
+ ]);
14020
+ if (srcContent !== destContent) {
14021
+ return true;
14022
+ }
14023
+ }
14024
+ }
14025
+ return false;
14026
+ } catch {
14027
+ return false;
14028
+ }
14029
+ }
14030
+ /**
14031
+ * List all skill groups and their installation status.
14032
+ *
14033
+ * @param projectDir - The project root directory
14034
+ * @returns Array of skill group status objects
14035
+ */
14036
+ static async listSkillGroups(projectDir) {
14037
+ const packageSkillsDir = this.getPackageSkillsDir();
14038
+ const results = [];
14039
+ for (const group of this.SKILL_GROUPS) {
14040
+ const sourceGroupDir = packageSkillsDir ? path3__namespace.join(packageSkillsDir, group.name) : "";
14041
+ const destGroupDir = path3__namespace.join(projectDir, group.destDir);
14042
+ const skillFiles = packageSkillsDir ? await this.getSkillFiles(sourceGroupDir) : [];
14043
+ const files = [];
14044
+ for (const relFile of skillFiles) {
14045
+ const srcPath = path3__namespace.join(sourceGroupDir, relFile);
14046
+ const destPath = path3__namespace.join(destGroupDir, relFile);
14047
+ const installed = await fs3__default.default.pathExists(destPath);
14048
+ let upToDate = false;
14049
+ if (installed) {
14050
+ try {
14051
+ const [srcContent, destContent] = await Promise.all([
14052
+ fs3__default.default.readFile(srcPath, "utf-8"),
14053
+ fs3__default.default.readFile(destPath, "utf-8")
14054
+ ]);
14055
+ upToDate = srcContent === destContent;
14056
+ } catch {
14057
+ upToDate = false;
14058
+ }
14059
+ }
14060
+ files.push({ name: relFile, installed, upToDate });
14061
+ }
14062
+ results.push({
14063
+ name: group.name,
14064
+ destDir: group.destDir,
14065
+ files
14066
+ });
14067
+ }
14068
+ return results;
14069
+ }
14070
+ /**
14071
+ * Get the list of skill group configurations.
14072
+ */
14073
+ static getSkillGroups() {
14074
+ return [...this.SKILL_GROUPS];
14075
+ }
14076
+ };
14077
+ }
14078
+ });
14079
+
13624
14080
  // src/utils/script-installer.ts
13625
14081
  var script_installer_exports = {};
13626
14082
  __export(script_installer_exports, {
@@ -13664,8 +14120,11 @@ var init_script_installer = __esm({
13664
14120
  "github.py",
13665
14121
  // Unified GitHub integration (fetch, respond, sync)
13666
14122
  // Claude Code hooks (stored in hooks/ subdirectory)
13667
- "hooks/session_counter.sh"
14123
+ "hooks/session_counter.sh",
13668
14124
  // Session message counter hook for warning about long sessions
14125
+ // Log scanning utility
14126
+ "log_scanner.sh"
14127
+ // Scans log files for errors/exceptions and creates kanban bug reports
13669
14128
  ];
13670
14129
  /**
13671
14130
  * Get the templates scripts directory from the package
@@ -16798,8 +17257,10 @@ ${variables.EDITOR ? `using ${variables.EDITOR} as primary AI subagent` : ""}
16798
17257
  getDefaultModelForSubagent(subagent) {
16799
17258
  const modelDefaults = {
16800
17259
  claude: ":sonnet",
16801
- codex: "gpt-5",
16802
- gemini: "gemini-2.5-pro",
17260
+ codex: ":codex",
17261
+ // Expands to gpt-5.3-codex in codex.py
17262
+ gemini: ":pro",
17263
+ // Expands to gemini-2.5-pro in gemini.py
16803
17264
  cursor: "auto"
16804
17265
  };
16805
17266
  return modelDefaults[subagent] || modelDefaults.claude;
@@ -19149,8 +19610,8 @@ async function compactConfigFile(filePath, options = {}) {
19149
19610
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19150
19611
  const ext = path3__namespace.extname(filePath);
19151
19612
  const basename11 = path3__namespace.basename(filePath, ext);
19152
- const dirname13 = path3__namespace.dirname(filePath);
19153
- backupPath = path3__namespace.join(dirname13, `${basename11}.backup.${timestamp}${ext}`);
19613
+ const dirname14 = path3__namespace.dirname(filePath);
19614
+ backupPath = path3__namespace.join(dirname14, `${basename11}.backup.${timestamp}${ext}`);
19154
19615
  await fs3__default.default.writeFile(backupPath, originalContent, "utf-8");
19155
19616
  }
19156
19617
  const compactionAnalysis = analyzeMarkdownStructure(originalContent);
@@ -21616,7 +22077,7 @@ var LogViewer = ({
21616
22077
  init_types();
21617
22078
  async function exportLogs(logger2, filepath, options) {
21618
22079
  try {
21619
- const fs23 = await import('fs-extra');
22080
+ const fs24 = await import('fs-extra');
21620
22081
  let entries = logger2.getRecentEntries(options.tail || 1e3);
21621
22082
  if (options.level) {
21622
22083
  const level = LogLevel[options.level.toUpperCase()];
@@ -21646,7 +22107,7 @@ async function exportLogs(logger2, filepath, options) {
21646
22107
  },
21647
22108
  entries
21648
22109
  };
21649
- await fs23.writeFile(filepath, JSON.stringify(exportData, null, 2));
22110
+ await fs24.writeFile(filepath, JSON.stringify(exportData, null, 2));
21650
22111
  console.log(chalk15__default.default.green(`\u2705 Exported ${entries.length} log entries to: ${filepath}`));
21651
22112
  } catch (error) {
21652
22113
  console.error(chalk15__default.default.red(`\u274C Failed to export logs: ${error}`));
@@ -23536,6 +23997,125 @@ Location: ${ServiceInstaller.getServicesDir()}`));
23536
23997
  return servicesCmd;
23537
23998
  }
23538
23999
 
24000
+ // src/cli/commands/skills.ts
24001
+ init_version();
24002
+ init_skill_installer();
24003
+ function createSkillsCommand() {
24004
+ const skillsCmd = new commander.Command("skills").description("Manage agent skill files").addHelpText("after", `
24005
+ Examples:
24006
+ $ juno-code skills install Install skill files to project directories
24007
+ $ juno-code skills install --force Force reinstall all skill files
24008
+ $ juno-code skills list List skill groups and their files
24009
+ $ juno-code skills status Check installation status
24010
+
24011
+ Skill files are copied from the juno-code package into the project:
24012
+ - Codex skills -> .agents/skills/
24013
+ - Claude skills -> .claude/skills/
24014
+
24015
+ Skills are installed for ALL agents regardless of which subagent is selected.
24016
+ Existing files in the destination directories are preserved.
24017
+ `);
24018
+ 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) => {
24019
+ try {
24020
+ const projectDir = process.cwd();
24021
+ if (!options.force) {
24022
+ const needsUpdate = await SkillInstaller.needsUpdate(projectDir);
24023
+ if (!needsUpdate) {
24024
+ console.log(chalk15__default.default.yellow("\u26A0 All skill files are up-to-date"));
24025
+ console.log(chalk15__default.default.dim(" Use --force to reinstall"));
24026
+ return;
24027
+ }
24028
+ }
24029
+ console.log(chalk15__default.default.blue("Installing skill files..."));
24030
+ const installed = await SkillInstaller.install(projectDir, false, options.force);
24031
+ if (installed) {
24032
+ console.log(chalk15__default.default.green("\n\u2713 Skill files installed successfully"));
24033
+ } else {
24034
+ console.log(chalk15__default.default.yellow("\u26A0 No skill files to install (templates may be empty)"));
24035
+ }
24036
+ } catch (error) {
24037
+ console.error(chalk15__default.default.red("\u2717 Installation failed:"));
24038
+ console.error(chalk15__default.default.red(error instanceof Error ? error.message : String(error)));
24039
+ process.exit(1);
24040
+ }
24041
+ });
24042
+ skillsCmd.command("list").alias("ls").description("List skill groups and their files").action(async () => {
24043
+ try {
24044
+ const projectDir = process.cwd();
24045
+ const groups = await SkillInstaller.listSkillGroups(projectDir);
24046
+ if (groups.length === 0) {
24047
+ console.log(chalk15__default.default.yellow("\u26A0 No skill groups configured"));
24048
+ return;
24049
+ }
24050
+ let hasAnyFiles = false;
24051
+ for (const group of groups) {
24052
+ console.log(chalk15__default.default.blue.bold(`
24053
+ ${group.name} skills -> ${group.destDir}/`));
24054
+ if (group.files.length === 0) {
24055
+ console.log(chalk15__default.default.dim(" (no skill files bundled yet)"));
24056
+ continue;
24057
+ }
24058
+ hasAnyFiles = true;
24059
+ for (const file of group.files) {
24060
+ const statusIcon = !file.installed ? chalk15__default.default.red("\u2717") : file.upToDate ? chalk15__default.default.green("\u2713") : chalk15__default.default.yellow("\u21BB");
24061
+ const statusLabel = !file.installed ? chalk15__default.default.dim("not installed") : file.upToDate ? chalk15__default.default.dim("up-to-date") : chalk15__default.default.yellow("outdated");
24062
+ console.log(` ${statusIcon} ${file.name} ${statusLabel}`);
24063
+ }
24064
+ }
24065
+ if (!hasAnyFiles) {
24066
+ console.log(chalk15__default.default.dim("\nNo skill files bundled yet. Add files to src/templates/skills/<agent>/ to bundle skills."));
24067
+ }
24068
+ } catch (error) {
24069
+ console.error(chalk15__default.default.red("\u2717 Failed to list skills:"));
24070
+ console.error(chalk15__default.default.red(error instanceof Error ? error.message : String(error)));
24071
+ process.exit(1);
24072
+ }
24073
+ });
24074
+ skillsCmd.command("status").description("Check skill installation status").action(async () => {
24075
+ try {
24076
+ const projectDir = process.cwd();
24077
+ const groups = await SkillInstaller.listSkillGroups(projectDir);
24078
+ const skillGroups = SkillInstaller.getSkillGroups();
24079
+ console.log(chalk15__default.default.blue("Skills Status:\n"));
24080
+ console.log(chalk15__default.default.dim(" Skill groups:"));
24081
+ for (const sg of skillGroups) {
24082
+ console.log(chalk15__default.default.dim(` ${sg.name} -> ${sg.destDir}/`));
24083
+ }
24084
+ const needsUpdate = await SkillInstaller.needsUpdate(projectDir);
24085
+ console.log(`
24086
+ ${needsUpdate ? chalk15__default.default.yellow("\u26A0 Updates available") : chalk15__default.default.green("\u2713 All skills up-to-date")}`);
24087
+ let totalFiles = 0;
24088
+ let installedFiles = 0;
24089
+ let outdatedFiles = 0;
24090
+ for (const group of groups) {
24091
+ for (const file of group.files) {
24092
+ totalFiles++;
24093
+ if (file.installed) {
24094
+ installedFiles++;
24095
+ if (!file.upToDate) {
24096
+ outdatedFiles++;
24097
+ }
24098
+ }
24099
+ }
24100
+ }
24101
+ if (totalFiles > 0) {
24102
+ console.log(chalk15__default.default.dim(`
24103
+ Files: ${installedFiles}/${totalFiles} installed, ${outdatedFiles} outdated`));
24104
+ } else {
24105
+ console.log(chalk15__default.default.dim("\n No skill files bundled yet"));
24106
+ }
24107
+ if (needsUpdate) {
24108
+ console.log(chalk15__default.default.dim("\n Run: juno-code skills install"));
24109
+ }
24110
+ } catch (error) {
24111
+ console.error(chalk15__default.default.red("\u2717 Failed to check status:"));
24112
+ console.error(chalk15__default.default.red(error instanceof Error ? error.message : String(error)));
24113
+ process.exit(1);
24114
+ }
24115
+ });
24116
+ return skillsCmd;
24117
+ }
24118
+
23539
24119
  // src/cli/commands/completion.ts
23540
24120
  init_version();
23541
24121
 
@@ -24801,10 +25381,10 @@ function setupMainCommand(program) {
24801
25381
  const allOptions2 = { ...definedGlobalOptions, ...options };
24802
25382
  if (allOptions2.tilCompletion || allOptions2.untilCompletion || allOptions2.runUntilCompletion || allOptions2.tillComplete) {
24803
25383
  const { spawn: spawn4 } = await import('child_process');
24804
- const path24 = await import('path');
24805
- const fs23 = await import('fs-extra');
24806
- const scriptPath = path24.join(process.cwd(), ".juno_task", "scripts", "run_until_completion.sh");
24807
- if (!await fs23.pathExists(scriptPath)) {
25384
+ const path25 = await import('path');
25385
+ const fs24 = await import('fs-extra');
25386
+ const scriptPath = path25.join(process.cwd(), ".juno_task", "scripts", "run_until_completion.sh");
25387
+ if (!await fs24.pathExists(scriptPath)) {
24808
25388
  console.error(chalk15__default.default.red.bold("\n\u274C Error: run_until_completion.sh not found"));
24809
25389
  console.error(chalk15__default.default.red(` Expected location: ${scriptPath}`));
24810
25390
  console.error(chalk15__default.default.yellow('\n\u{1F4A1} Suggestion: Run "juno-code init" to initialize the project'));
@@ -24855,11 +25435,11 @@ function setupMainCommand(program) {
24855
25435
  return;
24856
25436
  }
24857
25437
  if (!globalOptions.subagent && !options.prompt && !options.interactive && !options.interactivePrompt) {
24858
- const fs23 = await import('fs-extra');
24859
- const path24 = await import('path');
25438
+ const fs24 = await import('fs-extra');
25439
+ const path25 = await import('path');
24860
25440
  const cwd2 = process.cwd();
24861
- const junoTaskDir = path24.join(cwd2, ".juno_task");
24862
- if (await fs23.pathExists(junoTaskDir)) {
25441
+ const junoTaskDir = path25.join(cwd2, ".juno_task");
25442
+ if (await fs24.pathExists(junoTaskDir)) {
24863
25443
  console.log(chalk15__default.default.blue.bold("\u{1F3AF} Juno Code - Auto-detected Initialized Project\n"));
24864
25444
  try {
24865
25445
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -24876,12 +25456,12 @@ function setupMainCommand(program) {
24876
25456
  allOptions2.subagent = config.defaultSubagent;
24877
25457
  console.log(chalk15__default.default.gray(`\u{1F916} Using configured subagent: ${chalk15__default.default.cyan(config.defaultSubagent)}`));
24878
25458
  }
24879
- const promptFile = path24.join(junoTaskDir, "prompt.md");
24880
- if (!allOptions2.prompt && await fs23.pathExists(promptFile)) {
25459
+ const promptFile = path25.join(junoTaskDir, "prompt.md");
25460
+ if (!allOptions2.prompt && await fs24.pathExists(promptFile)) {
24881
25461
  allOptions2.prompt = promptFile;
24882
25462
  console.log(chalk15__default.default.gray(`\u{1F4C4} Using default prompt: ${chalk15__default.default.cyan(".juno_task/prompt.md")}`));
24883
25463
  }
24884
- if (allOptions2.subagent && (allOptions2.prompt || await fs23.pathExists(promptFile))) {
25464
+ if (allOptions2.subagent && (allOptions2.prompt || await fs24.pathExists(promptFile))) {
24885
25465
  console.log(chalk15__default.default.green("\u2713 Auto-detected project configuration\n"));
24886
25466
  const { mainCommandHandler: mainCommandHandler3 } = await Promise.resolve().then(() => (init_main(), main_exports));
24887
25467
  await mainCommandHandler3([], allOptions2, command);
@@ -25063,6 +25643,23 @@ async function main() {
25063
25643
  console.error("[DEBUG] Script auto-update failed:", error instanceof Error ? error.message : String(error));
25064
25644
  }
25065
25645
  }
25646
+ try {
25647
+ const { SkillInstaller: SkillInstaller2 } = await Promise.resolve().then(() => (init_skill_installer(), skill_installer_exports));
25648
+ if (isForceUpdate) {
25649
+ console.log(chalk15__default.default.blue("\u{1F504} Force updating agent skill files..."));
25650
+ await SkillInstaller2.autoUpdate(process.cwd(), true);
25651
+ console.log(chalk15__default.default.green("\u2713 Agent skill files updated"));
25652
+ } else {
25653
+ const updated = await SkillInstaller2.autoUpdate(process.cwd());
25654
+ if (updated && (process.argv.includes("--verbose") || process.argv.includes("-v") || process.env.JUNO_CODE_DEBUG === "1")) {
25655
+ console.error("[DEBUG] Agent skill files auto-updated");
25656
+ }
25657
+ }
25658
+ } catch (error) {
25659
+ if (process.env.JUNO_CODE_DEBUG === "1") {
25660
+ console.error("[DEBUG] Skill auto-update failed:", error instanceof Error ? error.message : String(error));
25661
+ }
25662
+ }
25066
25663
  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");
25067
25664
  setupGlobalOptions(program);
25068
25665
  const isVerbose = process.argv.includes("--verbose") || process.argv.includes("-v");
@@ -25070,10 +25667,10 @@ async function main() {
25070
25667
  const isHelpOrVersion = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("--version") || process.argv.includes("-V");
25071
25668
  const hasNoArguments = process.argv.length <= 2;
25072
25669
  const isInitCommand = process.argv.includes("init");
25073
- const fs23 = await import('fs-extra');
25074
- const path24 = await import('path');
25075
- const junoTaskDir = path24.join(process.cwd(), ".juno_task");
25076
- const isInitialized = await fs23.pathExists(junoTaskDir);
25670
+ const fs24 = await import('fs-extra');
25671
+ const path25 = await import('path');
25672
+ const junoTaskDir = path25.join(process.cwd(), ".juno_task");
25673
+ const isInitialized = await fs24.pathExists(junoTaskDir);
25077
25674
  if (!isHelpOrVersion && !hasNoArguments && !isInitCommand && isInitialized) {
25078
25675
  try {
25079
25676
  const { validateStartupConfigs: validateStartupConfigs2 } = await Promise.resolve().then(() => (init_startup_validation(), startup_validation_exports));
@@ -25100,6 +25697,7 @@ async function main() {
25100
25697
  configureHelpCommand(program);
25101
25698
  setupConfigCommand(program);
25102
25699
  program.addCommand(createServicesCommand());
25700
+ program.addCommand(createSkillsCommand());
25103
25701
  setupCompletion(program);
25104
25702
  setupAliases(program);
25105
25703
  setupMainCommand(program);