ccem 2.5.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.
- package/dist/index.js +425 -217
- 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
|
|
11
|
-
import * as
|
|
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-
|
|
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
|
|
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(
|
|
455
|
-
var CCEM_DIR = path2.join(
|
|
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((
|
|
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((
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
1361
|
+
resolve3({ action: "select", name: envNames[selectedIndex] });
|
|
1351
1362
|
return;
|
|
1352
1363
|
}
|
|
1353
1364
|
if (char === "e" || char === "E") {
|
|
1354
1365
|
cleanup();
|
|
1355
|
-
|
|
1366
|
+
resolve3({ action: "edit", name: envNames[selectedIndex] });
|
|
1356
1367
|
return;
|
|
1357
1368
|
}
|
|
1358
1369
|
if (char === "r" || char === "R") {
|
|
1359
1370
|
cleanup();
|
|
1360
|
-
|
|
1371
|
+
resolve3({ action: "rename", name: envNames[selectedIndex] });
|
|
1361
1372
|
return;
|
|
1362
1373
|
}
|
|
1363
1374
|
if (char === "c" || char === "C") {
|
|
1364
1375
|
cleanup();
|
|
1365
|
-
|
|
1376
|
+
resolve3({ action: "copy", name: envNames[selectedIndex] });
|
|
1366
1377
|
return;
|
|
1367
1378
|
}
|
|
1368
1379
|
if (char === "d" || char === "D") {
|
|
1369
1380
|
cleanup();
|
|
1370
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1800
|
-
|
|
1801
|
-
args.push("--allowedTools",
|
|
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
|
-
|
|
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((
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2637
|
+
resolve3({ type: "custom" });
|
|
2626
2638
|
} else {
|
|
2627
|
-
|
|
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
|
-
|
|
2646
|
+
resolve3({ type: "cancelled" });
|
|
2635
2647
|
}
|
|
2636
2648
|
}
|
|
2637
2649
|
)
|
|
2638
2650
|
);
|
|
2639
2651
|
waitUntilExit().then(() => {
|
|
2640
2652
|
if (!resolved) {
|
|
2641
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2830
|
+
Replace the placeholder values with real values. The CLI owns validation, ID generation, defaults, and persistence.
|
|
2858
2831
|
|
|
2859
|
-
|
|
2832
|
+
Confirm creation by reading back the structured task list:
|
|
2860
2833
|
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
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
|
-
|
|
2838
|
+
## Listing Tasks
|
|
2870
2839
|
|
|
2871
|
-
|
|
2840
|
+
\`\`\`bash
|
|
2841
|
+
ccem cron list
|
|
2842
|
+
\`\`\`
|
|
2872
2843
|
|
|
2873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2852
|
+
\`\`\`bash
|
|
2853
|
+
ccem cron delete "TASK_ID_OR_EXACT_NAME"
|
|
2854
|
+
\`\`\`
|
|
2892
2855
|
|
|
2893
2856
|
## Safety Rules
|
|
2894
2857
|
|
|
2895
|
-
-
|
|
2896
|
-
- Use
|
|
2897
|
-
-
|
|
2898
|
-
-
|
|
2899
|
-
-
|
|
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 =
|
|
2905
|
-
var pkgPath =
|
|
2906
|
-
var pkg = JSON.parse(
|
|
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 (!
|
|
3111
|
+
if (!fs10.existsSync(legacyConfigPath)) {
|
|
2971
3112
|
return registries;
|
|
2972
3113
|
}
|
|
2973
3114
|
try {
|
|
2974
|
-
const legacyRaw = JSON.parse(
|
|
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 = () =>
|
|
3169
|
-
var getRuntimeStateFilePath = () =>
|
|
3309
|
+
var getSessionsFilePath = () => path8.join(getCcemConfigDir(), "sessions.json");
|
|
3310
|
+
var getRuntimeStateFilePath = () => path8.join(getCcemConfigDir(), "runtime-state.json");
|
|
3170
3311
|
var parseJsonFile = (filePath) => {
|
|
3171
|
-
if (!
|
|
3312
|
+
if (!fs10.existsSync(filePath)) {
|
|
3172
3313
|
return null;
|
|
3173
3314
|
}
|
|
3174
3315
|
try {
|
|
3175
|
-
return JSON.parse(
|
|
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((
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
3516
|
-
const legacyDir =
|
|
3723
|
+
fs10.unlinkSync(legacyConfigPath);
|
|
3724
|
+
const legacyDir = path8.dirname(legacyConfigPath);
|
|
3517
3725
|
try {
|
|
3518
|
-
|
|
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 =
|
|
3530
|
-
const targetPath =
|
|
3531
|
-
if (!
|
|
3532
|
-
|
|
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 (
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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.
|
|
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": "
|
|
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
|
+
}
|