ccem 2.4.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +425 -217
  2. package/package.json +20 -15
package/dist/index.js CHANGED
@@ -7,11 +7,11 @@ import inquirer from "inquirer";
7
7
  import chalk7 from "chalk";
8
8
  import Table3 from "cli-table3";
9
9
  import { spawn as spawn3 } from "child_process";
10
- import * as fs9 from "fs";
11
- import * as path7 from "path";
10
+ import * as fs10 from "fs";
11
+ import * as path8 from "path";
12
12
  import { fileURLToPath as fileURLToPath2 } from "url";
13
13
 
14
- // ../../packages/core/dist/chunk-25EB45SM.js
14
+ // ../../packages/core/dist/chunk-DFS6BUXP.js
15
15
  var TIER_MODEL_ALIASES = /* @__PURE__ */ new Set(["opus", "sonnet", "haiku"]);
16
16
  function normalizeEnvConfig(envConfig, defaultRuntimeModel = "opus") {
17
17
  const hasTierDefaults = Boolean(envConfig.ANTHROPIC_DEFAULT_OPUS_MODEL) || Boolean(envConfig.ANTHROPIC_DEFAULT_SONNET_MODEL) || Boolean(envConfig.ANTHROPIC_DEFAULT_HAIKU_MODEL);
@@ -151,16 +151,16 @@ var PERMISSION_PRESETS = {
151
151
  permissionMode: "bypassPermissions",
152
152
  permissions: {
153
153
  allow: [
154
- "Bash(*)",
155
- "Read(*)",
156
- "Edit(*)",
157
- "Write(*)",
158
- "WebFetch(*)",
159
- "WebSearch(*)",
160
- "Glob(*)",
161
- "Grep(*)",
162
- "LSP(*)",
163
- "NotebookEdit(*)"
154
+ "Bash",
155
+ "Read",
156
+ "Edit",
157
+ "Write",
158
+ "WebFetch",
159
+ "WebSearch",
160
+ "Glob",
161
+ "Grep",
162
+ "LSP",
163
+ "NotebookEdit"
164
164
  ],
165
165
  deny: []
166
166
  }
@@ -171,13 +171,13 @@ var PERMISSION_PRESETS = {
171
171
  permissionMode: "acceptEdits",
172
172
  permissions: {
173
173
  allow: [
174
- "Read(*)",
175
- "Edit(*)",
176
- "Write(*)",
177
- "Glob(*)",
178
- "Grep(*)",
179
- "LSP(*)",
180
- "NotebookEdit(*)",
174
+ "Read",
175
+ "Edit",
176
+ "Write",
177
+ "Glob",
178
+ "Grep",
179
+ "LSP",
180
+ "NotebookEdit",
181
181
  "Bash(npm:*)",
182
182
  "Bash(pnpm:*)",
183
183
  "Bash(yarn:*)",
@@ -229,10 +229,10 @@ var PERMISSION_PRESETS = {
229
229
  permissionMode: "plan",
230
230
  permissions: {
231
231
  allow: [
232
- "Read(*)",
233
- "Glob(*)",
234
- "Grep(*)",
235
- "LSP(*)",
232
+ "Read",
233
+ "Glob",
234
+ "Grep",
235
+ "LSP",
236
236
  "Bash(git status:*)",
237
237
  "Bash(git log:*)",
238
238
  "Bash(git diff:*)",
@@ -273,10 +273,10 @@ var PERMISSION_PRESETS = {
273
273
  permissionMode: "default",
274
274
  permissions: {
275
275
  allow: [
276
- "Read(*)",
277
- "Glob(*)",
278
- "Grep(*)",
279
- "LSP(*)",
276
+ "Read",
277
+ "Glob",
278
+ "Grep",
279
+ "LSP",
280
280
  "Bash(git status:*)",
281
281
  "Bash(git log:*)",
282
282
  "Bash(git diff:*)",
@@ -314,12 +314,12 @@ var PERMISSION_PRESETS = {
314
314
  permissionMode: "default",
315
315
  permissions: {
316
316
  allow: [
317
- "Read(*)",
318
- "Edit(*)",
319
- "Write(*)",
320
- "Glob(*)",
321
- "Grep(*)",
322
- "LSP(*)",
317
+ "Read",
318
+ "Edit",
319
+ "Write",
320
+ "Glob",
321
+ "Grep",
322
+ "LSP",
323
323
  "Bash(npm:*)",
324
324
  "Bash(pnpm:*)",
325
325
  "Bash(yarn:*)",
@@ -352,10 +352,10 @@ var PERMISSION_PRESETS = {
352
352
  permissionMode: "plan",
353
353
  permissions: {
354
354
  allow: [
355
- "Read(*)",
356
- "Glob(*)",
357
- "Grep(*)",
358
- "LSP(*)",
355
+ "Read",
356
+ "Glob",
357
+ "Grep",
358
+ "LSP",
359
359
  "Bash(git log:*)",
360
360
  "Bash(git blame:*)",
361
361
  "Bash(git show:*)",
@@ -381,6 +381,16 @@ var PERMISSION_PRESETS = {
381
381
  }
382
382
  }
383
383
  };
384
+ var LEGACY_ALLOW_ALL_RULE = /^([A-Za-z][A-Za-z0-9_]*?)\(\*\)$/;
385
+ var normalizePermissionAllowRule = (rule) => {
386
+ const trimmed = rule.trim();
387
+ const match = trimmed.match(LEGACY_ALLOW_ALL_RULE);
388
+ return match ? match[1] : trimmed;
389
+ };
390
+ var normalizePermissionAllowRules = (rules) => {
391
+ const normalized = rules.map(normalizePermissionAllowRule).filter((rule) => rule.length > 0);
392
+ return [...new Set(normalized)];
393
+ };
384
394
  var getPermissionModeNames = () => {
385
395
  return Object.keys(PERMISSION_PRESETS);
386
396
  };
@@ -388,6 +398,7 @@ var getPermissionModeNames = () => {
388
398
  // ../../packages/core/dist/index.js
389
399
  import crypto from "crypto";
390
400
  import fs from "fs";
401
+ import os from "os";
391
402
  import path from "path";
392
403
  var ALGORITHM = "aes-256-cbc";
393
404
  var SECRET_KEY = crypto.scryptSync("claude-code-env-manager-secret", "salt", 32);
@@ -415,7 +426,7 @@ var decrypt = (text) => {
415
426
  }
416
427
  };
417
428
  var getHomeDir = () => {
418
- return process.env.HOME || process.env.USERPROFILE || "";
429
+ return process.env.HOME || process.env.USERPROFILE || os.homedir() || "";
419
430
  };
420
431
  var getCcemConfigDir = () => {
421
432
  return path.join(getHomeDir(), ".ccem");
@@ -446,13 +457,13 @@ import Table from "cli-table3";
446
457
  import * as fs2 from "fs";
447
458
  import * as fsPromises from "fs/promises";
448
459
  import * as path2 from "path";
449
- import * as os from "os";
460
+ import * as os2 from "os";
450
461
  import * as readline from "readline";
451
462
  import { fileURLToPath } from "url";
452
463
  var __filename = fileURLToPath(import.meta.url);
453
464
  var __dirname = path2.dirname(__filename);
454
- var CLAUDE_PROJECTS_DIR = path2.join(os.homedir(), ".claude", "projects");
455
- var CCEM_DIR = path2.join(os.homedir(), ".ccem");
465
+ var CLAUDE_PROJECTS_DIR = path2.join(os2.homedir(), ".claude", "projects");
466
+ var CCEM_DIR = path2.join(os2.homedir(), ".ccem");
456
467
  var CACHE_VERSION = 1;
457
468
  var getCachePath = () => path2.join(CCEM_DIR, "usage-cache.json");
458
469
  var getPricesPath = () => path2.join(CCEM_DIR, "model-prices.json");
@@ -667,7 +678,7 @@ async function parseJSONLFileAsync(filePath, prices, signal) {
667
678
  throw new Error("Aborted");
668
679
  }
669
680
  if (lineCount % 1e3 === 0) {
670
- await new Promise((resolve2) => setTimeout(resolve2, 0));
681
+ await new Promise((resolve3) => setTimeout(resolve3, 0));
671
682
  }
672
683
  }
673
684
  if (!line.trim()) continue;
@@ -756,7 +767,7 @@ async function getUsageStats(signal) {
756
767
  fileStats = cachedFile.stats;
757
768
  } else {
758
769
  fileStats = await parseJSONLFileAsync(file, prices, signal);
759
- await new Promise((resolve2) => setTimeout(resolve2, 0));
770
+ await new Promise((resolve3) => setTimeout(resolve3, 0));
760
771
  }
761
772
  return {
762
773
  file,
@@ -935,10 +946,10 @@ var renderLogoWithEnvPanel = (envName, env, defaultMode) => {
935
946
  const parsed = new URL(url);
936
947
  const protocol = parsed.protocol + "//";
937
948
  const host = parsed.host;
938
- const path8 = parsed.pathname + parsed.search;
949
+ const path9 = parsed.pathname + parsed.search;
939
950
  const hostStart = host.slice(0, 8);
940
951
  const hostEnd = host.slice(-4);
941
- const pathPart = path8.length > 10 ? path8.slice(0, 7) + "..." : path8;
952
+ const pathPart = path9.length > 10 ? path9.slice(0, 7) + "..." : path9;
942
953
  return `${protocol}${hostStart}...${hostEnd}${pathPart}`;
943
954
  } catch {
944
955
  return truncate(url, max);
@@ -1268,7 +1279,7 @@ var renderFooterHints = (hints) => {
1268
1279
  return minimal;
1269
1280
  };
1270
1281
  var selectEnvWithKeys = (registries, current) => {
1271
- return new Promise((resolve2) => {
1282
+ return new Promise((resolve3) => {
1272
1283
  const envNames = Object.keys(registries);
1273
1284
  let selectedIndex = envNames.indexOf(current);
1274
1285
  if (selectedIndex === -1) selectedIndex = 0;
@@ -1330,7 +1341,7 @@ var selectEnvWithKeys = (registries, current) => {
1330
1341
  }
1331
1342
  if (char === "\x1B" && key.length === 1) {
1332
1343
  cleanup();
1333
- resolve2({ action: "cancel" });
1344
+ resolve3({ action: "cancel" });
1334
1345
  return;
1335
1346
  }
1336
1347
  if (char === "\x1B[A" || char === "k") {
@@ -1347,27 +1358,27 @@ var selectEnvWithKeys = (registries, current) => {
1347
1358
  }
1348
1359
  if (char === "\r" || char === "\n") {
1349
1360
  cleanup();
1350
- resolve2({ action: "select", name: envNames[selectedIndex] });
1361
+ resolve3({ action: "select", name: envNames[selectedIndex] });
1351
1362
  return;
1352
1363
  }
1353
1364
  if (char === "e" || char === "E") {
1354
1365
  cleanup();
1355
- resolve2({ action: "edit", name: envNames[selectedIndex] });
1366
+ resolve3({ action: "edit", name: envNames[selectedIndex] });
1356
1367
  return;
1357
1368
  }
1358
1369
  if (char === "r" || char === "R") {
1359
1370
  cleanup();
1360
- resolve2({ action: "rename", name: envNames[selectedIndex] });
1371
+ resolve3({ action: "rename", name: envNames[selectedIndex] });
1361
1372
  return;
1362
1373
  }
1363
1374
  if (char === "c" || char === "C") {
1364
1375
  cleanup();
1365
- resolve2({ action: "copy", name: envNames[selectedIndex] });
1376
+ resolve3({ action: "copy", name: envNames[selectedIndex] });
1366
1377
  return;
1367
1378
  }
1368
1379
  if (char === "d" || char === "D") {
1369
1380
  cleanup();
1370
- resolve2({ action: "delete", name: envNames[selectedIndex] });
1381
+ resolve3({ action: "delete", name: envNames[selectedIndex] });
1371
1382
  return;
1372
1383
  }
1373
1384
  };
@@ -1383,6 +1394,7 @@ import Table2 from "cli-table3";
1383
1394
  // src/utils.ts
1384
1395
  import crypto2 from "crypto";
1385
1396
  import fs3 from "fs";
1397
+ import os3 from "os";
1386
1398
  import path3 from "path";
1387
1399
  var SECRET_KEY2 = crypto2.scryptSync("claude-code-env-manager-secret", "salt", 32);
1388
1400
  var findProjectRoot = () => {
@@ -1411,7 +1423,7 @@ var ensureClaudeDir = () => {
1411
1423
  return claudeDir;
1412
1424
  };
1413
1425
  var getHomeDir2 = () => {
1414
- return process.env.HOME || process.env.USERPROFILE || "";
1426
+ return process.env.HOME || process.env.USERPROFILE || os3.homedir() || "";
1415
1427
  };
1416
1428
  var getGlobalClaudeConfigPath = () => {
1417
1429
  return path3.join(getHomeDir2(), ".claude.json");
@@ -1437,7 +1449,7 @@ import chalk2 from "chalk";
1437
1449
  import { execFileSync } from "child_process";
1438
1450
  import fs4 from "fs";
1439
1451
  import { createRequire } from "module";
1440
- import os2 from "os";
1452
+ import os4 from "os";
1441
1453
  import path4 from "path";
1442
1454
  var DEFAULT_CONFIG_SOURCE = "ccem";
1443
1455
  var STATE_DB_FILE_NAME = "state.sqlite";
@@ -1674,7 +1686,7 @@ function getStateDbPath() {
1674
1686
  if (override) {
1675
1687
  return override;
1676
1688
  }
1677
- return path4.join(os2.homedir(), ".ccem", STATE_DB_FILE_NAME);
1689
+ return path4.join(os4.homedir(), ".ccem", STATE_DB_FILE_NAME);
1678
1690
  }
1679
1691
  function schemaSql() {
1680
1692
  return `
@@ -1700,7 +1712,7 @@ function schemaSql() {
1700
1712
  `;
1701
1713
  }
1702
1714
  function discoverClaudeJsonlPath(projectDir, startedAtMs) {
1703
- const projectsDir = path4.join(os2.homedir(), ".claude", "projects");
1715
+ const projectsDir = path4.join(os4.homedir(), ".claude", "projects");
1704
1716
  const earliestModifiedAt = startedAtMs - 15e3;
1705
1717
  for (const key of projectDirKeys(projectDir)) {
1706
1718
  const baseDir = path4.join(projectsDir, key);
@@ -1796,13 +1808,12 @@ function buildPermArgs(modeName) {
1796
1808
  const preset = PERMISSION_PRESETS[modeName];
1797
1809
  if (!preset) return [];
1798
1810
  const args = ["--permission-mode", preset.permissionMode];
1799
- if (preset.permissions.allow.length > 0) {
1800
- const quoted = preset.permissions.allow.map((t) => `"${t}"`).join(" ");
1801
- args.push("--allowedTools", quoted);
1811
+ const allowRules = normalizePermissionAllowRules(preset.permissions.allow);
1812
+ if (allowRules.length > 0) {
1813
+ args.push("--allowedTools", ...allowRules);
1802
1814
  }
1803
1815
  if (preset.permissions.deny.length > 0) {
1804
- const quoted = preset.permissions.deny.map((t) => `"${t}"`).join(" ");
1805
- args.push("--disallowedTools", quoted);
1816
+ args.push("--disallowedTools", ...preset.permissions.deny);
1806
1817
  }
1807
1818
  return args;
1808
1819
  }
@@ -1846,7 +1857,7 @@ async function launchClaude(options) {
1846
1857
  console.log(renderStarting());
1847
1858
  }
1848
1859
  const sessionsDir = ensureSessionsDir();
1849
- return new Promise((resolve2) => {
1860
+ return new Promise((resolve3) => {
1850
1861
  let provenanceTracking = null;
1851
1862
  const child = spawn("claude", args, {
1852
1863
  stdio: "inherit",
@@ -1923,9 +1934,10 @@ var writeSettings = (settingsPath, config3) => {
1923
1934
  fs6.writeFileSync(settingsPath, JSON.stringify(config3, null, 2) + "\n");
1924
1935
  };
1925
1936
  var mergePermissions = (existing, preset) => {
1926
- const existingAllow = existing.permissions?.allow || [];
1937
+ const existingAllow = normalizePermissionAllowRules(existing.permissions?.allow || []);
1927
1938
  const existingDeny = existing.permissions?.deny || [];
1928
- const mergedAllow = [.../* @__PURE__ */ new Set([...preset.allow, ...existingAllow])];
1939
+ const presetAllow = normalizePermissionAllowRules(preset.allow);
1940
+ const mergedAllow = [.../* @__PURE__ */ new Set([...presetAllow, ...existingAllow])];
1929
1941
  const mergedDeny = [.../* @__PURE__ */ new Set([...preset.deny, ...existingDeny])];
1930
1942
  return {
1931
1943
  ...existing,
@@ -2102,7 +2114,7 @@ var setupEnvSettings = () => {
2102
2114
  }
2103
2115
  };
2104
2116
  var setupMcpTool = () => {
2105
- return new Promise((resolve2) => {
2117
+ return new Promise((resolve3) => {
2106
2118
  console.log(chalk4.cyan(" \u2192 \u6B63\u5728\u6DFB\u52A0 chrome-devtools MCP \u5DE5\u5177..."));
2107
2119
  const child = spawn2("claude", [
2108
2120
  "mcp",
@@ -2127,22 +2139,22 @@ var setupMcpTool = () => {
2127
2139
  child.on("exit", (code) => {
2128
2140
  if (code === 0) {
2129
2141
  console.log(chalk4.green(" \u2713 \u5DF2\u6DFB\u52A0 chrome-devtools MCP \u5DE5\u5177"));
2130
- resolve2(true);
2142
+ resolve3(true);
2131
2143
  } else {
2132
2144
  if (stderr.includes("already exists") || stdout.includes("already exists")) {
2133
2145
  console.log(chalk4.gray(" \u2713 chrome-devtools MCP \u5DE5\u5177\u5DF2\u5B58\u5728"));
2134
- resolve2(true);
2146
+ resolve3(true);
2135
2147
  } else {
2136
2148
  console.error(chalk4.red(` \u2717 \u6DFB\u52A0 MCP \u5DE5\u5177\u5931\u8D25 (code: ${code})`));
2137
2149
  if (stderr) console.error(chalk4.gray(` ${stderr.trim()}`));
2138
- resolve2(false);
2150
+ resolve3(false);
2139
2151
  }
2140
2152
  }
2141
2153
  });
2142
2154
  child.on("error", (err) => {
2143
2155
  console.error(chalk4.red(` \u2717 \u6267\u884C claude \u547D\u4EE4\u5931\u8D25: ${err.message}`));
2144
2156
  console.log(chalk4.yellow(" \u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5 Claude Code CLI"));
2145
- resolve2(false);
2157
+ resolve3(false);
2146
2158
  });
2147
2159
  });
2148
2160
  };
@@ -2611,7 +2623,7 @@ function SkillSelector({ onSelect, onCancel }) {
2611
2623
 
2612
2624
  // src/components/index.tsx
2613
2625
  async function runSkillSelector() {
2614
- return new Promise((resolve2) => {
2626
+ return new Promise((resolve3) => {
2615
2627
  let resolved = false;
2616
2628
  const { unmount, waitUntilExit } = render(
2617
2629
  /* @__PURE__ */ React2.createElement(
@@ -2622,23 +2634,23 @@ async function runSkillSelector() {
2622
2634
  resolved = true;
2623
2635
  unmount();
2624
2636
  if (result === "custom") {
2625
- resolve2({ type: "custom" });
2637
+ resolve3({ type: "custom" });
2626
2638
  } else {
2627
- resolve2({ type: "skill", skill: result });
2639
+ resolve3({ type: "skill", skill: result });
2628
2640
  }
2629
2641
  },
2630
2642
  onCancel: () => {
2631
2643
  if (resolved) return;
2632
2644
  resolved = true;
2633
2645
  unmount();
2634
- resolve2({ type: "cancelled" });
2646
+ resolve3({ type: "cancelled" });
2635
2647
  }
2636
2648
  }
2637
2649
  )
2638
2650
  );
2639
2651
  waitUntilExit().then(() => {
2640
2652
  if (!resolved) {
2641
- resolve2({ type: "cancelled" });
2653
+ resolve3({ type: "cancelled" });
2642
2654
  }
2643
2655
  });
2644
2656
  });
@@ -2760,150 +2772,279 @@ Loaded ${results.length} environment(s) from remote:`));
2760
2772
  // src/cron-skill.ts
2761
2773
  var CCEM_CRON_SKILL_CONTENT = `# ccem-cron
2762
2774
 
2763
- Manage scheduled tasks for Claude Code via \\\`~/.ccem/cron-tasks.json\\\`. Supports creating, listing, and deleting cron tasks through conversational interaction.
2764
-
2765
- ## Storage
2766
-
2767
- All tasks are stored in \\\`~/.ccem/cron-tasks.json\\\` as a JSON array. Each task follows this schema:
2768
-
2769
- \\\`\\\`\\\`json
2770
- {
2771
- "id": "uuid-v4",
2772
- "name": "task name",
2773
- "cronExpression": "0 9 * * *",
2774
- "prompt": "Claude prompt to execute",
2775
- "workingDir": "/absolute/path",
2776
- "envName": null,
2777
- "enabled": true,
2778
- "timeoutSecs": 300,
2779
- "templateId": null,
2780
- "triggerType": "schedule",
2781
- "parentTaskId": null,
2782
- "createdAt": "ISO-8601",
2783
- "updatedAt": "ISO-8601"
2784
- }
2785
- \\\`\\\`\\\`
2775
+ Manage scheduled tasks for Claude Code/Codex through the structured \`ccem cron\` CLI. Do not edit \`~/.ccem/cron-tasks.json\` directly.
2786
2776
 
2787
2777
  ## Instructions
2788
2778
 
2789
2779
  Determine the user's intent from their message:
2780
+
2790
2781
  - **List/view**: user says "list", "show", "view", "\u67E5\u770B", "\u5217\u51FA"
2791
2782
  - **Delete/remove**: user says "delete", "remove", "\u5220\u9664", "\u79FB\u9664"
2792
2783
  - **Create**: default for anything else
2793
2784
 
2794
- ### Creating a Task
2795
-
2796
- 1. Ask the user for:
2797
- - Task name (short descriptive label)
2798
- - What they want Claude to do (natural language; derive the \\\`prompt\\\` field from this)
2799
- - When to run it (derive the \\\`cronExpression\\\`; show common examples below)
2800
- - Working directory (default: current directory via \\\`pwd\\\`)
2801
- - Timeout in seconds (default: 300)
2785
+ ## Creating A Task
2802
2786
 
2803
- 2. Show common cron patterns to help the user choose:
2787
+ If the request is specific enough, create the task directly. Ask a follow-up only when the schedule, action, or working directory is genuinely ambiguous.
2804
2788
 
2805
- \\\`\\\`\\\`
2806
- Every minute: * * * * *
2807
- Every 30 minutes: */30 * * * *
2808
- Every hour: 0 * * * *
2809
- Every day at 9am: 0 9 * * *
2810
- Every day at midnight: 0 0 * * *
2811
- Weekdays at 9am: 0 9 * * 1-5
2812
- Every Monday 8am: 0 8 * * 1
2813
- Every 1st of month: 0 0 1 * *
2814
- \\\`\\\`\\\`
2815
-
2816
- 3. Generate the task and write it:
2789
+ - Task name: derive a short descriptive label.
2790
+ - Prompt: derive a directly executable Claude/Codex instruction from the user's request.
2791
+ - Schedule: derive a standard 5-field \`cronExpression\`.
2792
+ - Working directory: default to the current directory via \`pwd\`.
2793
+ - Timeout: default to 300 seconds unless the task clearly needs longer.
2794
+ - Execution profile: use \`conservative\`, \`standard\`, or \`autonomous\` based on risk.
2817
2795
 
2818
- \\\`\\\`\\\`bash
2819
- # Generate UUID and timestamp
2820
- TASK_ID=\\$(uuidgen | tr '[:upper:]' '[:lower:]')
2821
- NOW=\\$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
2822
- WORK_DIR=\\$(pwd)
2796
+ Common cron patterns:
2823
2797
 
2824
- # Read existing file or initialize empty array
2825
- if [ -f ~/.ccem/cron-tasks.json ]; then
2826
- EXISTING=\\$(cat ~/.ccem/cron-tasks.json)
2827
- else
2828
- mkdir -p ~/.ccem
2829
- EXISTING="[]"
2830
- fi
2798
+ \`\`\`text
2799
+ Every minute: * * * * *
2800
+ Every 30 minutes: */30 * * * *
2801
+ Every hour: 0 * * * *
2802
+ Every day at 9am: 0 9 * * *
2803
+ Every day at midnight: 0 0 * * *
2804
+ Weekdays at 9am: 0 9 * * 1-5
2805
+ Every Monday 8am: 0 8 * * 1
2806
+ Every 1st of month: 0 0 1 * *
2807
+ \`\`\`
2831
2808
 
2832
- # Build new task JSON and append using python3 for safe JSON manipulation
2833
- python3 -c "
2834
- import json, sys
2835
- tasks = json.loads('''\\$EXISTING''')
2836
- tasks.append({
2837
- 'id': '\\$TASK_ID',
2838
- 'name': 'TASK_NAME_HERE',
2839
- 'cronExpression': 'CRON_EXPR_HERE',
2840
- 'prompt': 'PROMPT_HERE',
2841
- 'workingDir': '\\$WORK_DIR',
2842
- 'envName': None,
2843
- 'enabled': True,
2844
- 'timeoutSecs': TIMEOUT_HERE,
2845
- 'templateId': None,
2846
- 'triggerType': 'schedule',
2847
- 'parentTaskId': None,
2848
- 'createdAt': '\\$NOW',
2849
- 'updatedAt': '\\$NOW'
2850
- })
2851
- print(json.dumps(tasks, indent=2, ensure_ascii=False))
2852
- " > ~/.ccem/cron-tasks.json
2853
- \\\`\\\`\\\`
2809
+ Create the task with the CLI and JSON input:
2854
2810
 
2855
- Replace \\\`TASK_NAME_HERE\\\`, \\\`CRON_EXPR_HERE\\\`, \\\`PROMPT_HERE\\\`, and \\\`TIMEOUT_HERE\\\` with actual values. Escape any quotes or special characters in the prompt string properly for Python.
2811
+ \`\`\`bash
2812
+ ccem cron create --from-json - --json <<'JSON'
2813
+ {
2814
+ "name": "TASK_NAME_HERE",
2815
+ "cronExpression": "CRON_EXPR_HERE",
2816
+ "prompt": "PROMPT_HERE",
2817
+ "workingDir": "ABSOLUTE_WORKING_DIR_HERE",
2818
+ "envName": null,
2819
+ "executionProfile": "conservative",
2820
+ "maxBudgetUsd": null,
2821
+ "allowedTools": [],
2822
+ "disallowedTools": [],
2823
+ "enabled": true,
2824
+ "timeoutSecs": 300,
2825
+ "templateId": null
2826
+ }
2827
+ JSON
2828
+ \`\`\`
2856
2829
 
2857
- 4. Confirm creation by reading back the file and showing the new task.
2830
+ Replace the placeholder values with real values. The CLI owns validation, ID generation, defaults, and persistence.
2858
2831
 
2859
- ### Listing Tasks
2832
+ Confirm creation by reading back the structured task list:
2860
2833
 
2861
- \\\`\\\`\\\`bash
2862
- if [ -f ~/.ccem/cron-tasks.json ]; then
2863
- cat ~/.ccem/cron-tasks.json
2864
- else
2865
- echo "No tasks found. File ~/.ccem/cron-tasks.json does not exist."
2866
- fi
2867
- \\\`\\\`\\\`
2834
+ \`\`\`bash
2835
+ ccem cron list --json
2836
+ \`\`\`
2868
2837
 
2869
- Format the output as a readable table with columns: name, cron expression, enabled status, working directory, and creation date. If the list is empty, tell the user.
2838
+ ## Listing Tasks
2870
2839
 
2871
- ### Deleting a Task
2840
+ \`\`\`bash
2841
+ ccem cron list
2842
+ \`\`\`
2872
2843
 
2873
- 1. First list all tasks so the user can identify which to delete.
2874
- 2. Ask the user to confirm by name or ID.
2875
- 3. Remove the matching task:
2844
+ Use \`ccem cron list --json\` when you need exact fields.
2876
2845
 
2877
- \\\`\\\`\\\`bash
2878
- python3 -c "
2879
- import json
2880
- with open('\\$HOME/.ccem/cron-tasks.json') as f:
2881
- tasks = json.load(f)
2882
- tasks = [t for t in tasks if t['id'] != 'TARGET_ID' and t['name'] != 'TARGET_NAME']
2883
- with open('\\$HOME/.ccem/cron-tasks.json', 'w') as f:
2884
- json.dump(tasks, f, indent=2, ensure_ascii=False)
2885
- print(json.dumps(tasks, indent=2, ensure_ascii=False))
2886
- "
2887
- \\\`\\\`\\\`
2846
+ ## Deleting A Task
2888
2847
 
2889
- Replace \\\`TARGET_ID\\\` or \\\`TARGET_NAME\\\` with the user's selection.
2848
+ 1. List all tasks first so the user can identify which one to delete.
2849
+ 2. Ask the user to confirm by exact name or ID.
2850
+ 3. Delete through the CLI:
2890
2851
 
2891
- 4. Confirm deletion by showing the remaining tasks.
2852
+ \`\`\`bash
2853
+ ccem cron delete "TASK_ID_OR_EXACT_NAME"
2854
+ \`\`\`
2892
2855
 
2893
2856
  ## Safety Rules
2894
2857
 
2895
- - Always read the existing file before writing to avoid data loss.
2896
- - Use \\\`python3\\\` for JSON manipulation to ensure valid output (never hand-construct JSON with echo/cat).
2897
- - Create \\\`~/.ccem/\\\` directory if it does not exist.
2898
- - When the file is missing or empty, start with an empty array \\\`[]\\\`.
2899
- - Always show the user what will be written before confirming.
2858
+ - Do not directly read, write, or hand-edit \`~/.ccem/cron-tasks.json\`.
2859
+ - Use \`ccem cron create/list/delete\` as the stable contract.
2860
+ - Never construct JSON by string concatenation; pass valid JSON to \`--from-json\`.
2861
+ - After creating or deleting a task, read back with \`ccem cron list --json\` and report the actual stored task.
2862
+ - Ask for confirmation first only when the request is ambiguous or destructive.
2900
2863
  `;
2901
2864
 
2865
+ // src/cron.ts
2866
+ import crypto4 from "crypto";
2867
+ import * as fs9 from "fs";
2868
+ import * as os5 from "os";
2869
+ import * as path7 from "path";
2870
+ var CRON_TASKS_FILE_NAME = "cron-tasks.json";
2871
+ var EXECUTION_PROFILES = /* @__PURE__ */ new Set([
2872
+ "conservative",
2873
+ "standard",
2874
+ "autonomous"
2875
+ ]);
2876
+ function getCronTasksPath(baseDir = getCcemConfigDir()) {
2877
+ return path7.join(baseDir, CRON_TASKS_FILE_NAME);
2878
+ }
2879
+ function readCronTasks(tasksPath = getCronTasksPath()) {
2880
+ if (!fs9.existsSync(tasksPath)) {
2881
+ return [];
2882
+ }
2883
+ const content = fs9.readFileSync(tasksPath, "utf-8").trim();
2884
+ if (!content) {
2885
+ return [];
2886
+ }
2887
+ const parsed = JSON.parse(content);
2888
+ const tasks = Array.isArray(parsed) ? parsed : parsed.tasks;
2889
+ if (!Array.isArray(tasks)) {
2890
+ throw new Error("Invalid cron task store: expected object with tasks array");
2891
+ }
2892
+ return tasks;
2893
+ }
2894
+ function writeCronTasks(tasks, tasksPath = getCronTasksPath()) {
2895
+ if (tasksPath === getCronTasksPath()) {
2896
+ ensureCcemDir();
2897
+ } else {
2898
+ fs9.mkdirSync(path7.dirname(tasksPath), { recursive: true });
2899
+ }
2900
+ const payload = { tasks };
2901
+ const content = `${JSON.stringify(payload, null, 2)}
2902
+ `;
2903
+ const tempPath = `${tasksPath}.${process.pid}.${Date.now()}.tmp`;
2904
+ fs9.writeFileSync(tempPath, content, { encoding: "utf-8", mode: 384 });
2905
+ fs9.renameSync(tempPath, tasksPath);
2906
+ }
2907
+ function validateCronExpression(cronExpression) {
2908
+ const fields = cronExpression.trim().split(/\s+/);
2909
+ if (fields.length !== 5) {
2910
+ throw new Error("Invalid cron expression: must have exactly 5 fields (minute hour day month weekday)");
2911
+ }
2912
+ }
2913
+ function normalizeExecutionProfile(value) {
2914
+ if (!value) {
2915
+ return "conservative";
2916
+ }
2917
+ if (EXECUTION_PROFILES.has(value)) {
2918
+ return value;
2919
+ }
2920
+ return "conservative";
2921
+ }
2922
+ function parseStringList(value) {
2923
+ if (!value) {
2924
+ return [];
2925
+ }
2926
+ if (Array.isArray(value)) {
2927
+ return value.map((item) => item.trim()).filter(Boolean);
2928
+ }
2929
+ const trimmed = value.trim();
2930
+ if (!trimmed) {
2931
+ return [];
2932
+ }
2933
+ if (trimmed.startsWith("[")) {
2934
+ const parsed = JSON.parse(trimmed);
2935
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
2936
+ throw new Error("Expected a JSON array of strings");
2937
+ }
2938
+ return parsed.map((item) => item.trim()).filter(Boolean);
2939
+ }
2940
+ return trimmed.split(",").map((item) => item.trim()).filter(Boolean);
2941
+ }
2942
+ function createCronTask(input, tasksPath = getCronTasksPath()) {
2943
+ const name = input.name?.trim();
2944
+ if (!name) {
2945
+ throw new Error("Task name is required");
2946
+ }
2947
+ const cronExpression = input.cronExpression?.trim();
2948
+ if (!cronExpression) {
2949
+ throw new Error("cronExpression is required");
2950
+ }
2951
+ validateCronExpression(cronExpression);
2952
+ const prompt = input.prompt?.trim();
2953
+ if (!prompt) {
2954
+ throw new Error("Task prompt is required");
2955
+ }
2956
+ const timeoutSecs = input.timeoutSecs ?? 300;
2957
+ if (!Number.isInteger(timeoutSecs) || timeoutSecs <= 0) {
2958
+ throw new Error("timeoutSecs must be a positive integer");
2959
+ }
2960
+ const maxBudgetUsd = input.maxBudgetUsd ?? null;
2961
+ if (maxBudgetUsd !== null && (!Number.isFinite(maxBudgetUsd) || maxBudgetUsd < 0)) {
2962
+ throw new Error("maxBudgetUsd must be a non-negative number");
2963
+ }
2964
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2965
+ const task = {
2966
+ id: `cron-${Date.now()}-${crypto4.randomBytes(2).toString("hex")}`,
2967
+ name,
2968
+ cronExpression,
2969
+ prompt,
2970
+ workingDir: input.workingDir?.trim() || process.cwd(),
2971
+ envName: input.envName?.trim() || null,
2972
+ executionProfile: normalizeExecutionProfile(input.executionProfile),
2973
+ maxBudgetUsd,
2974
+ allowedTools: input.allowedTools ?? [],
2975
+ disallowedTools: input.disallowedTools ?? [],
2976
+ enabled: input.enabled ?? true,
2977
+ timeoutSecs,
2978
+ templateId: input.templateId?.trim() || null,
2979
+ triggerType: "schedule",
2980
+ parentTaskId: null,
2981
+ createdAt: now,
2982
+ updatedAt: now
2983
+ };
2984
+ const tasks = readCronTasks(tasksPath);
2985
+ tasks.push(task);
2986
+ writeCronTasks(tasks, tasksPath);
2987
+ return task;
2988
+ }
2989
+ function deleteCronTask(selector, tasksPath = getCronTasksPath()) {
2990
+ const normalizedSelector = selector.trim();
2991
+ if (!normalizedSelector) {
2992
+ throw new Error("Task id or name is required");
2993
+ }
2994
+ const tasks = readCronTasks(tasksPath);
2995
+ const matches = tasks.filter((task) => task.id === normalizedSelector || task.name === normalizedSelector);
2996
+ if (matches.length === 0) {
2997
+ throw new Error(`Cron task not found: ${normalizedSelector}`);
2998
+ }
2999
+ if (matches.length > 1) {
3000
+ throw new Error(`Cron task selector is ambiguous: ${normalizedSelector}`);
3001
+ }
3002
+ const [deleted] = matches;
3003
+ writeCronTasks(tasks.filter((task) => task.id !== deleted.id), tasksPath);
3004
+ return deleted;
3005
+ }
3006
+ function resolveJsonInput(raw) {
3007
+ if (raw === "-") {
3008
+ return fs9.readFileSync(0, "utf-8");
3009
+ }
3010
+ if (raw.startsWith("@")) {
3011
+ return fs9.readFileSync(path7.resolve(raw.slice(1)), "utf-8");
3012
+ }
3013
+ return raw;
3014
+ }
3015
+ function parseCronCreateJson(raw) {
3016
+ const parsed = JSON.parse(resolveJsonInput(raw));
3017
+ return {
3018
+ name: String(parsed.name ?? ""),
3019
+ cronExpression: String(parsed.cronExpression ?? parsed.schedule ?? ""),
3020
+ prompt: String(parsed.prompt ?? ""),
3021
+ workingDir: typeof parsed.workingDir === "string" ? parsed.workingDir : null,
3022
+ envName: typeof parsed.envName === "string" ? parsed.envName : null,
3023
+ executionProfile: typeof parsed.executionProfile === "string" ? parsed.executionProfile : null,
3024
+ maxBudgetUsd: typeof parsed.maxBudgetUsd === "number" ? parsed.maxBudgetUsd : null,
3025
+ allowedTools: Array.isArray(parsed.allowedTools) ? parsed.allowedTools.map(String) : null,
3026
+ disallowedTools: Array.isArray(parsed.disallowedTools) ? parsed.disallowedTools.map(String) : null,
3027
+ enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : null,
3028
+ timeoutSecs: typeof parsed.timeoutSecs === "number" ? parsed.timeoutSecs : null,
3029
+ templateId: typeof parsed.templateId === "string" ? parsed.templateId : null
3030
+ };
3031
+ }
3032
+ function formatCronTaskTableRows(tasks) {
3033
+ return tasks.map((task) => [
3034
+ task.name,
3035
+ task.cronExpression,
3036
+ task.enabled ? "yes" : "no",
3037
+ task.workingDir.replace(os5.homedir(), "~"),
3038
+ task.envName ?? "-",
3039
+ String(task.timeoutSecs)
3040
+ ]);
3041
+ }
3042
+
2902
3043
  // src/index.ts
2903
3044
  var __filename2 = fileURLToPath2(import.meta.url);
2904
- var __dirname2 = path7.dirname(__filename2);
2905
- var pkgPath = path7.resolve(__dirname2, "..", "package.json");
2906
- var pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3045
+ var __dirname2 = path8.dirname(__filename2);
3046
+ var pkgPath = path8.resolve(__dirname2, "..", "package.json");
3047
+ var pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
2907
3048
  var program = new Command();
2908
3049
  var DEFAULT_OFFICIAL_ENV = {
2909
3050
  ANTHROPIC_BASE_URL: "https://api.anthropic.com",
@@ -2967,11 +3108,11 @@ var recoverRegistriesFromLegacy = (registries) => {
2967
3108
  return registries;
2968
3109
  }
2969
3110
  const legacyConfigPath = getLegacyConfigPath();
2970
- if (!fs9.existsSync(legacyConfigPath)) {
3111
+ if (!fs10.existsSync(legacyConfigPath)) {
2971
3112
  return registries;
2972
3113
  }
2973
3114
  try {
2974
- const legacyRaw = JSON.parse(fs9.readFileSync(legacyConfigPath, "utf-8"));
3115
+ const legacyRaw = JSON.parse(fs10.readFileSync(legacyConfigPath, "utf-8"));
2975
3116
  const legacyRegistries = legacyRaw.registries ?? {};
2976
3117
  let changed = false;
2977
3118
  const recovered = { ...registries };
@@ -3165,14 +3306,14 @@ var switchEnvironment = async (name) => {
3165
3306
  exportCmds.forEach((cmd) => console.log(cmd));
3166
3307
  }
3167
3308
  };
3168
- var getSessionsFilePath = () => path7.join(getCcemConfigDir(), "sessions.json");
3169
- var getRuntimeStateFilePath = () => path7.join(getCcemConfigDir(), "runtime-state.json");
3309
+ var getSessionsFilePath = () => path8.join(getCcemConfigDir(), "sessions.json");
3310
+ var getRuntimeStateFilePath = () => path8.join(getCcemConfigDir(), "runtime-state.json");
3170
3311
  var parseJsonFile = (filePath) => {
3171
- if (!fs9.existsSync(filePath)) {
3312
+ if (!fs10.existsSync(filePath)) {
3172
3313
  return null;
3173
3314
  }
3174
3315
  try {
3175
- return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
3316
+ return JSON.parse(fs10.readFileSync(filePath, "utf-8"));
3176
3317
  } catch {
3177
3318
  return null;
3178
3319
  }
@@ -3234,12 +3375,12 @@ var findAttachSession = (id) => {
3234
3375
  };
3235
3376
  var attachTmuxTarget = (target) => {
3236
3377
  const args = process.env.TMUX ? ["switch-client", "-t", target] : ["attach-session", "-t", target];
3237
- return new Promise((resolve2, reject) => {
3378
+ return new Promise((resolve3, reject) => {
3238
3379
  const child = spawn3("tmux", args, { stdio: "inherit" });
3239
3380
  child.on("error", reject);
3240
3381
  child.on("exit", (code) => {
3241
3382
  if (code === 0 || code === null) {
3242
- resolve2();
3383
+ resolve3();
3243
3384
  } else {
3244
3385
  reject(new Error(`tmux exited with code ${code}`));
3245
3386
  }
@@ -3443,6 +3584,73 @@ program.command("run <command...>").description("Run a command with the current
3443
3584
  process.exit(code ?? 0);
3444
3585
  });
3445
3586
  });
3587
+ var cronCmd = program.command("cron").description("Manage CCEM cron tasks");
3588
+ cronCmd.command("list").description("List CCEM cron tasks").option("--json", "Output as JSON").action((options) => {
3589
+ try {
3590
+ const tasks = readCronTasks();
3591
+ if (options.json) {
3592
+ console.log(JSON.stringify(tasks, null, 2));
3593
+ return;
3594
+ }
3595
+ if (tasks.length === 0) {
3596
+ console.log(chalk7.yellow("No cron tasks found."));
3597
+ return;
3598
+ }
3599
+ const table = new Table3({
3600
+ head: ["Name", "Schedule", "Enabled", "Working Dir", "Env", "Timeout"]
3601
+ });
3602
+ table.push(...formatCronTaskTableRows(tasks));
3603
+ console.log(table.toString());
3604
+ } catch (err) {
3605
+ console.error(chalk7.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3606
+ process.exitCode = 1;
3607
+ }
3608
+ });
3609
+ cronCmd.command("create").description("Create a CCEM cron task").option("--from-json <json>", "Read task input from inline JSON, @file, or - for stdin").option("--name <name>", "Task name").option("--cron-expression <expr>", "5-field cron expression").option("--schedule <expr>", "Alias for --cron-expression").option("--prompt <prompt>", "Prompt to run when the task fires").option("--working-dir <dir>", "Task working directory").option("--env-name <name>", "CCEM environment name").option("--execution-profile <profile>", "conservative, standard, or autonomous").option("--max-budget-usd <amount>", "Optional max budget in USD").option("--allowed-tools <items>", "Comma-separated or JSON array of allowed tools").option("--disallowed-tools <items>", "Comma-separated or JSON array of disallowed tools").option("--timeout-secs <seconds>", "Task timeout in seconds").option("--template-id <id>", "Optional template id").option("--disabled", "Create task disabled").option("--json", "Output as JSON").action((options) => {
3610
+ try {
3611
+ const input = options.fromJson ? parseCronCreateJson(options.fromJson) : {
3612
+ name: options.name,
3613
+ cronExpression: options.cronExpression ?? options.schedule,
3614
+ prompt: options.prompt,
3615
+ workingDir: options.workingDir,
3616
+ envName: options.envName,
3617
+ executionProfile: options.executionProfile,
3618
+ maxBudgetUsd: options.maxBudgetUsd === void 0 ? null : Number(options.maxBudgetUsd),
3619
+ allowedTools: parseStringList(options.allowedTools),
3620
+ disallowedTools: parseStringList(options.disallowedTools),
3621
+ enabled: options.disabled ? false : true,
3622
+ timeoutSecs: options.timeoutSecs === void 0 ? null : Number(options.timeoutSecs),
3623
+ templateId: options.templateId
3624
+ };
3625
+ const task = createCronTask(input);
3626
+ if (options.json) {
3627
+ console.log(JSON.stringify(task, null, 2));
3628
+ return;
3629
+ }
3630
+ console.log(chalk7.green("\u2713 Cron task created"));
3631
+ console.log(chalk7.gray(` id: ${task.id}`));
3632
+ console.log(chalk7.gray(` name: ${task.name}`));
3633
+ console.log(chalk7.gray(` cronExpression: ${task.cronExpression}`));
3634
+ console.log(chalk7.gray(` workingDir: ${task.workingDir}`));
3635
+ } catch (err) {
3636
+ console.error(chalk7.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3637
+ process.exitCode = 1;
3638
+ }
3639
+ });
3640
+ cronCmd.command("delete <selector>").description("Delete a cron task by exact id or exact name").option("--json", "Output deleted task as JSON").action((selector, options) => {
3641
+ try {
3642
+ const task = deleteCronTask(selector);
3643
+ if (options.json) {
3644
+ console.log(JSON.stringify(task, null, 2));
3645
+ return;
3646
+ }
3647
+ console.log(chalk7.green(`\u2713 Deleted cron task: ${task.name}`));
3648
+ console.log(chalk7.gray(` id: ${task.id}`));
3649
+ } catch (err) {
3650
+ console.error(chalk7.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3651
+ process.exitCode = 1;
3652
+ }
3653
+ });
3446
3654
  var setupCmd = program.command("setup").description("Setup commands for permanent configurations");
3447
3655
  setupCmd.command("perms").description("\u6C38\u4E45\u914D\u7F6E\u6743\u9650\u6A21\u5F0F").option("--yolo", "\u5E94\u7528 YOLO \u6A21\u5F0F\uFF08\u5168\u90E8\u653E\u5F00\uFF09").option("--dev", "\u5E94\u7528\u5F00\u53D1\u6A21\u5F0F").option("--readonly", "\u5E94\u7528\u53EA\u8BFB\u6A21\u5F0F").option("--safe", "\u5E94\u7528\u5B89\u5168\u6A21\u5F0F").option("--ci", "\u5E94\u7528 CI/CD \u6A21\u5F0F").option("--audit", "\u5E94\u7528\u5BA1\u8BA1\u6A21\u5F0F").option("--reset", "\u91CD\u7F6E\u6743\u9650\u914D\u7F6E").action(function() {
3448
3656
  const options = this.opts();
@@ -3494,12 +3702,12 @@ setupCmd.command("migrate").description("\u8FC1\u79FB\u65E7\u7248\u914D\u7F6E\u5
3494
3702
  const newConfigPath = getCcemConfigPath();
3495
3703
  const legacyConfigPath = getLegacyConfigPath();
3496
3704
  console.log(chalk7.cyan("\n\u{1F504} \u914D\u7F6E\u8FC1\u79FB\n"));
3497
- if (!fs9.existsSync(legacyConfigPath)) {
3705
+ if (!fs10.existsSync(legacyConfigPath)) {
3498
3706
  console.log(chalk7.yellow("\u672A\u627E\u5230\u65E7\u7248\u914D\u7F6E\u6587\u4EF6"));
3499
3707
  console.log(chalk7.gray(` \u65E7\u8DEF\u5F84: ${legacyConfigPath}`));
3500
3708
  return;
3501
3709
  }
3502
- if (fs9.existsSync(newConfigPath) && !options.force) {
3710
+ if (fs10.existsSync(newConfigPath) && !options.force) {
3503
3711
  console.log(chalk7.green("\u2713 \u914D\u7F6E\u5DF2\u5728\u65B0\u8DEF\u5F84"));
3504
3712
  console.log(chalk7.gray(` \u8DEF\u5F84: ${newConfigPath}`));
3505
3713
  console.log(chalk7.gray("\n\u4F7F\u7528 --force \u5F3A\u5236\u91CD\u65B0\u8FC1\u79FB"));
@@ -3507,15 +3715,15 @@ setupCmd.command("migrate").description("\u8FC1\u79FB\u65E7\u7248\u914D\u7F6E\u5
3507
3715
  }
3508
3716
  try {
3509
3717
  ensureCcemDir();
3510
- fs9.copyFileSync(legacyConfigPath, newConfigPath);
3718
+ fs10.copyFileSync(legacyConfigPath, newConfigPath);
3511
3719
  console.log(chalk7.green("\u2713 \u914D\u7F6E\u5DF2\u8FC1\u79FB"));
3512
3720
  console.log(chalk7.gray(` \u4ECE: ${legacyConfigPath}`));
3513
3721
  console.log(chalk7.gray(` \u5230: ${newConfigPath}`));
3514
3722
  if (options.clean) {
3515
- fs9.unlinkSync(legacyConfigPath);
3516
- const legacyDir = path7.dirname(legacyConfigPath);
3723
+ fs10.unlinkSync(legacyConfigPath);
3724
+ const legacyDir = path8.dirname(legacyConfigPath);
3517
3725
  try {
3518
- fs9.rmdirSync(legacyDir);
3726
+ fs10.rmdirSync(legacyDir);
3519
3727
  } catch {
3520
3728
  }
3521
3729
  console.log(chalk7.green("\u2713 \u5DF2\u5220\u9664\u65E7\u914D\u7F6E\u6587\u4EF6"));
@@ -3526,13 +3734,13 @@ setupCmd.command("migrate").description("\u8FC1\u79FB\u65E7\u7248\u914D\u7F6E\u5
3526
3734
  });
3527
3735
  setupCmd.command("cron").description("\u5B89\u88C5 ccem-cron skill \u5230 Claude Code\uFF08~/.claude/skills/\uFF09").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u6587\u4EF6").action(async function() {
3528
3736
  const options = this.opts();
3529
- const skillDir = path7.join(process.env.HOME || "~", ".claude", "skills");
3530
- const targetPath = path7.join(skillDir, "ccem-cron.md");
3531
- if (!fs9.existsSync(skillDir)) {
3532
- fs9.mkdirSync(skillDir, { recursive: true });
3737
+ const skillDir = path8.join(getHomeDir(), ".claude", "skills");
3738
+ const targetPath = path8.join(skillDir, "ccem-cron.md");
3739
+ if (!fs10.existsSync(skillDir)) {
3740
+ fs10.mkdirSync(skillDir, { recursive: true });
3533
3741
  console.log(chalk7.gray(`\u521B\u5EFA\u76EE\u5F55: ${skillDir}`));
3534
3742
  }
3535
- if (fs9.existsSync(targetPath) && !options.force) {
3743
+ if (fs10.existsSync(targetPath) && !options.force) {
3536
3744
  const { overwrite } = await inquirer.prompt([
3537
3745
  {
3538
3746
  type: "confirm",
@@ -3546,7 +3754,7 @@ setupCmd.command("cron").description("\u5B89\u88C5 ccem-cron skill \u5230 Claude
3546
3754
  return;
3547
3755
  }
3548
3756
  }
3549
- fs9.writeFileSync(targetPath, CCEM_CRON_SKILL_CONTENT, "utf-8");
3757
+ fs10.writeFileSync(targetPath, CCEM_CRON_SKILL_CONTENT, "utf-8");
3550
3758
  console.log(chalk7.green(`\u2713 \u5DF2\u5B89\u88C5 ccem-cron skill`));
3551
3759
  console.log(chalk7.gray(` \u8DEF\u5F84: ${targetPath}`));
3552
3760
  console.log(chalk7.cyan(`
@@ -3720,11 +3928,11 @@ Editing environment '${result.name}'`));
3720
3928
  registries[result.name] = applyPromptAnswers(envToEdit, answers, true);
3721
3929
  setRegistries(registries);
3722
3930
  msg.success(`Environment '${result.name}' updated.`);
3723
- await new Promise((resolve2) => setTimeout(resolve2, 800));
3931
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3724
3932
  } else if (result.action === "rename") {
3725
3933
  if (result.name === "official") {
3726
3934
  msg.error("Cannot rename default 'official' environment.");
3727
- await new Promise((resolve2) => setTimeout(resolve2, 800));
3935
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3728
3936
  } else {
3729
3937
  const { newName } = await inquirer.prompt([
3730
3938
  {
@@ -3745,7 +3953,7 @@ Editing environment '${result.name}'`));
3745
3953
  config2.set("current", newName);
3746
3954
  }
3747
3955
  msg.success(`Environment '${result.name}' renamed to '${newName}'.`);
3748
- await new Promise((resolve2) => setTimeout(resolve2, 800));
3956
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3749
3957
  }
3750
3958
  } else if (result.action === "copy") {
3751
3959
  const { targetName } = await inquirer.prompt([
@@ -3778,11 +3986,11 @@ Editing environment '${result.name}'`));
3778
3986
  setRegistries(registries);
3779
3987
  msg.success(`Environment '${targetName}' updated.`);
3780
3988
  }
3781
- await new Promise((resolve2) => setTimeout(resolve2, 800));
3989
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3782
3990
  } else if (result.action === "delete") {
3783
3991
  if (result.name === "official") {
3784
3992
  msg.error("Cannot delete default 'official' environment.");
3785
- await new Promise((resolve2) => setTimeout(resolve2, 800));
3993
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3786
3994
  } else {
3787
3995
  const { confirm } = await inquirer.prompt([
3788
3996
  {
@@ -3802,7 +4010,7 @@ Editing environment '${result.name}'`));
3802
4010
  msg.success(`Environment '${result.name}' deleted.`);
3803
4011
  }
3804
4012
  }
3805
- await new Promise((resolve2) => setTimeout(resolve2, 800));
4013
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3806
4014
  }
3807
4015
  }
3808
4016
  } else if (action === "perm") {
@@ -3833,11 +4041,11 @@ Editing environment '${result.name}'`));
3833
4041
  if (selectedMode === "clear") {
3834
4042
  config2.set("defaultMode", null);
3835
4043
  msg.success("Default mode cleared");
3836
- await new Promise((resolve2) => setTimeout(resolve2, 800));
4044
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3837
4045
  } else if (selectedMode !== "back") {
3838
4046
  config2.set("defaultMode", selectedMode);
3839
4047
  msg.success(`Default mode set: ${PERMISSION_PRESETS[selectedMode].name}`);
3840
- await new Promise((resolve2) => setTimeout(resolve2, 800));
4048
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
3841
4049
  }
3842
4050
  } else {
3843
4051
  process.exit(0);
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "ccem",
3
- "version": "2.4.0",
3
+ "version": "2.21.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code Environment Manager",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Genuifx/claude-code-env-manager.git",
9
+ "directory": "apps/cli"
10
+ },
6
11
  "author": {
7
12
  "name": "Genuifx",
8
13
  "email": "genuifx@gmail.com",
@@ -14,7 +19,17 @@
14
19
  "scripts"
15
20
  ],
16
21
  "bin": {
17
- "ccem": "./dist/index.js"
22
+ "ccem": "dist/index.js"
23
+ },
24
+ "scripts": {
25
+ "generate-logo": "bash scripts/generate-logo.sh",
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "start": "node dist/index.js",
29
+ "postinstall": "node ./scripts/migrate.js",
30
+ "test": "vitest",
31
+ "test:run": "vitest run",
32
+ "test:coverage": "vitest run --coverage"
18
33
  },
19
34
  "dependencies": {
20
35
  "chalk": "^4.1.2",
@@ -28,6 +43,7 @@
28
43
  "terminal-image": "^4.2.0"
29
44
  },
30
45
  "devDependencies": {
46
+ "@ccem/core": "workspace:*",
31
47
  "@types/inquirer": "^9.0.7",
32
48
  "@types/node": "^20.11.24",
33
49
  "@types/react": "^19.2.9",
@@ -35,17 +51,6 @@
35
51
  "asciify-image": "^0.1.10",
36
52
  "tsup": "^8.0.2",
37
53
  "typescript": "^5.3.3",
38
- "vitest": "^4.0.18",
39
- "@ccem/core": "2.0.0"
40
- },
41
- "scripts": {
42
- "generate-logo": "bash scripts/generate-logo.sh",
43
- "build": "tsup",
44
- "dev": "tsup --watch",
45
- "start": "node dist/index.js",
46
- "postinstall": "node ./scripts/migrate.js",
47
- "test": "vitest",
48
- "test:run": "vitest run",
49
- "test:coverage": "vitest run --coverage"
54
+ "vitest": "^4.0.18"
50
55
  }
51
- }
56
+ }