clawflowbang 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # ClawFlow local environment
2
+ DEBUG=
3
+ TZ=Asia/Bangkok
4
+ OPENCLAW_URL=http://localhost:18789
5
+
6
+ # Optional skill credentials
7
+ BINANCE_API_KEY=
8
+ BINANCE_SECRET_KEY=
9
+ FB_ACCESS_TOKEN=
10
+ TWITTER_API_KEY=
11
+ TWITTER_API_SECRET=
12
+ DISCORD_WEBHOOK=
13
+ GOOGLE_CREDENTIALS=
14
+ NOTION_TOKEN=
@@ -0,0 +1,46 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read
12
+ packages: write
13
+ id-token: write
14
+
15
+ jobs:
16
+ publish:
17
+ name: Publish package
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Use Node.js
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '18'
28
+ registry-url: 'https://registry.npmjs.org'
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Publish to npm (using NPM_TOKEN)
34
+ if: secrets.NPM_TOKEN != ''
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37
+ run: |
38
+ echo "Publishing with NPM_TOKEN..."
39
+ npm publish --access public
40
+
41
+ - name: Publish to npm (OIDC / Trusted Publisher)
42
+ if: secrets.NPM_TOKEN == ''
43
+ run: |
44
+ echo "No NPM_TOKEN found. To publish without a stored token, set up a Trusted Publisher in npm and enable OIDC."
45
+ echo "See README or npm docs for steps to configure Trusted Publisher."
46
+ exit 1
@@ -0,0 +1,31 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main, develop]
8
+
9
+ jobs:
10
+ test:
11
+ name: Run tests
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Use Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '18'
22
+ cache: 'npm'
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Run tests
28
+ run: npm test
29
+
30
+ - name: Run linter
31
+ run: npm run lint
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `ClawFlow` is a CLI wrapper for OpenClaw that installs skill bundles and wires cron jobs in one flow.
4
4
 
5
5
  Published package: `clawflowbang`
6
- Primary command: `clawflow` (aliases: `clawflowhub`, `cfh`)
6
+ Primary command: `clawflow` (alias: `cfh`)
7
7
 
8
8
  ## What It Does
9
9
 
@@ -61,7 +61,7 @@ clawflow remove <package>
61
61
 
62
62
  clawflow cron-list
63
63
  clawflow cron-add <skill> --schedule "*/5 * * * *"
64
- clawflow cron-edit <id> --every 15m
64
+ clawflow cron-edit <id> --every 15m --description "updated job"
65
65
  clawflow cron-remove <id>
66
66
  ```
67
67
 
@@ -78,6 +78,8 @@ Examples:
78
78
  ```bash
79
79
  clawflow cron-add crypto-price --every 15m
80
80
  clawflow cron-edit <job-id> --schedule "@daily"
81
+ clawflow cron-edit <job-id> --params '{"symbols":["BTC","ETH"]}'
82
+ clawflow cron-remove <job-id>
81
83
  ```
82
84
 
83
85
  ## Skill Install Fallback (ClawHub -> Git)
@@ -1,7 +1,9 @@
1
- #!/usr/bin/env node
2
-
3
- const { program } = require('commander');
4
- const pkg = require('../package.json');
1
+ #!/usr/bin/env node
2
+
3
+ require('dotenv').config();
4
+
5
+ const { program } = require('commander');
6
+ const pkg = require('../package.json');
5
7
 
6
8
  // Import TUI
7
9
  const TUI = require('../src/core/TerminalUI');
@@ -35,7 +37,7 @@ program
35
37
  .option('--openclaw-bin <path>', 'Path to openclaw CLI binary')
36
38
  .option('--clawhub-bin <path>', 'Path to clawhub CLI binary')
37
39
  .option('--no-cron', 'Install skills without creating cronjobs')
38
- .option('--dry-run', 'Show what would be done without installing');
40
+ .option('--dry-run', 'Show what would be done without installing')
39
41
  .action(installCommand);
40
42
 
41
43
  // List command
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "clawflowbang",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Skill + Cron Installer for OpenClaw - Install skills and configure them for immediate use",
5
5
  "main": "src/index.js",
6
- "bin": {
7
- "clawflow": "bin/clawflowhub.js",
8
- "clawflowhub": "bin/clawflowhub.js",
9
- "cfh": "bin/clawflowhub.js"
10
- },
6
+ "bin": {
7
+ "clawflow": "bin/clawflowhub.js",
8
+ "cfh": "bin/clawflowhub.js"
9
+ },
11
10
  "scripts": {
12
11
  "start": "node bin/clawflowhub.js",
13
12
  "test": "jest",
@@ -29,6 +28,7 @@
29
28
  "boxen": "^8.0.1",
30
29
  "chalk": "^4.1.2",
31
30
  "commander": "^11.0.0",
31
+ "dotenv": "^16.6.1",
32
32
  "fs-extra": "^11.1.1",
33
33
  "gradient-string": "^3.0.0",
34
34
  "inquirer": "^8.2.6",
@@ -5,6 +5,7 @@
5
5
  const chalk = require('chalk');
6
6
  const ClawFlow = require('../index');
7
7
  const { normalizeCronExpression } = require('../core/CronFormat');
8
+ const YAML = require('yaml');
8
9
 
9
10
  function parseJsonParams(params) {
10
11
  if (params === undefined || params === null || params === '') {
@@ -15,7 +16,41 @@ function parseJsonParams(params) {
15
16
  return params;
16
17
  }
17
18
 
18
- return JSON.parse(params);
19
+ const raw = String(params).trim();
20
+ const unquoted =
21
+ (raw.startsWith("'") && raw.endsWith("'")) || (raw.startsWith('"') && raw.endsWith('"'))
22
+ ? raw.slice(1, -1).trim()
23
+ : raw;
24
+
25
+ const candidates = [raw, unquoted];
26
+
27
+ for (const candidate of candidates) {
28
+ if (!candidate) continue;
29
+ try {
30
+ const parsed = JSON.parse(candidate);
31
+ if (parsed && typeof parsed === 'object') {
32
+ return parsed;
33
+ }
34
+ } catch (_error) {
35
+ // Continue to YAML fallback
36
+ }
37
+ }
38
+
39
+ for (const candidate of candidates) {
40
+ if (!candidate) continue;
41
+ try {
42
+ const parsed = YAML.parse(candidate);
43
+ if (parsed && typeof parsed === 'object') {
44
+ return parsed;
45
+ }
46
+ } catch (_error) {
47
+ // Continue
48
+ }
49
+ }
50
+
51
+ throw new Error(
52
+ 'รูปแบบ --params ไม่ถูกต้อง (ใช้ JSON object เช่น {"message":"hello"} หรือ YAML object เช่น message: hello)',
53
+ );
19
54
  }
20
55
 
21
56
  function resolveScheduleInput(options) {
@@ -65,9 +65,9 @@ module.exports = async function initCommand(options) {
65
65
  // สร้าง config
66
66
  const config = {
67
67
  version: '1.0.0',
68
- openclaw: {
69
- baseUrl: 'http://localhost:3000',
70
- apiKey: null,
68
+ openclaw: {
69
+ baseUrl: 'http://localhost:18789',
70
+ apiKey: null,
71
71
  cliBin: answers.openclawBin,
72
72
  clawhubBin: answers.clawhubBin,
73
73
  workspacePath: path.join(cwd, '.openclaw'),
@@ -6,13 +6,13 @@ const fs = require('fs-extra');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
8
 
9
- class ConfigManager {
10
- constructor(configPath = null, options = {}) {
11
- this.configPath = configPath || this.getDefaultConfigPath();
12
- this.overrides = options;
13
- this.config = null;
14
- this.ensureDirectories();
15
- }
9
+ class ConfigManager {
10
+ constructor(configPath = null, options = {}) {
11
+ this.configPath = configPath || this.getDefaultConfigPath();
12
+ this.overrides = options;
13
+ this.config = null;
14
+ this.ensureDirectories();
15
+ }
16
16
 
17
17
  /**
18
18
  * ได้รับ default config path ตาม OS
@@ -32,55 +32,55 @@ class ConfigManager {
32
32
  /**
33
33
  * สร้างโฟลเดอร์ที่จำเป็น
34
34
  */
35
- ensureDirectories() {
36
- const configFile = path.join(this.configPath, 'config.json');
37
- const defaultConfig = this.getDefaultConfig();
38
- const existingConfig = fs.existsSync(configFile) ? fs.readJsonSync(configFile) : {};
39
- const mergedConfig = this.applyOpenClawOverrides({
40
- ...defaultConfig,
41
- ...existingConfig,
42
- openclaw: {
43
- ...defaultConfig.openclaw,
44
- ...(existingConfig.openclaw || {}),
45
- },
46
- });
47
-
48
- const dirs = [
49
- this.configPath,
50
- path.dirname(mergedConfig.openclaw.skillsPath),
51
- mergedConfig.openclaw.skillsPath,
52
- path.dirname(mergedConfig.openclaw.cronJobsFile),
53
- path.join(this.configPath, 'logs'),
54
- path.join(this.configPath, 'packages'),
55
- ];
56
-
57
- dirs.forEach((dir) => {
58
- fs.ensureDirSync(dir);
59
- });
60
-
61
- if (!fs.existsSync(configFile) || JSON.stringify(existingConfig) !== JSON.stringify(mergedConfig)) {
62
- fs.writeJsonSync(configFile, mergedConfig, { spaces: 2 });
63
- }
64
- }
35
+ ensureDirectories() {
36
+ const configFile = path.join(this.configPath, 'config.json');
37
+ const defaultConfig = this.getDefaultConfig();
38
+ const existingConfig = fs.existsSync(configFile) ? fs.readJsonSync(configFile) : {};
39
+ const mergedConfig = this.applyOpenClawOverrides({
40
+ ...defaultConfig,
41
+ ...existingConfig,
42
+ openclaw: {
43
+ ...defaultConfig.openclaw,
44
+ ...(existingConfig.openclaw || {}),
45
+ },
46
+ });
47
+
48
+ const dirs = [
49
+ this.configPath,
50
+ path.dirname(mergedConfig.openclaw.skillsPath),
51
+ mergedConfig.openclaw.skillsPath,
52
+ path.dirname(mergedConfig.openclaw.cronJobsFile),
53
+ path.join(this.configPath, 'logs'),
54
+ path.join(this.configPath, 'packages'),
55
+ ];
56
+
57
+ dirs.forEach((dir) => {
58
+ fs.ensureDirSync(dir);
59
+ });
60
+
61
+ if (!fs.existsSync(configFile) || JSON.stringify(existingConfig) !== JSON.stringify(mergedConfig)) {
62
+ fs.writeJsonSync(configFile, mergedConfig, { spaces: 2 });
63
+ }
64
+ }
65
65
 
66
66
  /**
67
67
  * Config เริ่มต้น
68
68
  */
69
- getDefaultConfig() {
70
- const openclawHome = path.join(os.homedir(), '.openclaw');
71
- const workspacePath = path.join(openclawHome, 'workspace');
72
-
73
- return {
74
- version: '1.0.0',
75
- openclaw: {
76
- baseUrl: 'http://localhost:3000',
77
- apiKey: null,
78
- cliBin: 'openclaw',
79
- clawhubBin: 'clawhub',
80
- workspacePath,
81
- skillsPath: path.join(workspacePath, 'skills'),
82
- cronJobsFile: path.join(openclawHome, 'cron', 'jobs.json'),
83
- },
69
+ getDefaultConfig() {
70
+ const openclawHome = path.join(os.homedir(), '.openclaw');
71
+ const workspacePath = path.join(openclawHome, 'workspace');
72
+
73
+ return {
74
+ version: '1.0.0',
75
+ openclaw: {
76
+ baseUrl: 'http://localhost:18789',
77
+ apiKey: null,
78
+ cliBin: 'openclaw',
79
+ clawhubBin: 'clawhub',
80
+ workspacePath,
81
+ skillsPath: path.join(workspacePath, 'skills'),
82
+ cronJobsFile: path.join(openclawHome, 'cron', 'jobs.json'),
83
+ },
84
84
  registry: {
85
85
  url: 'https://registry.clawflowhub.dev',
86
86
  cacheExpiry: 3600, // วินาที
@@ -93,34 +93,34 @@ class ConfigManager {
93
93
  installed: {},
94
94
  crons: [],
95
95
  lastUpdate: null,
96
- };
97
- }
98
-
99
- applyOpenClawOverrides(config) {
100
- return {
101
- ...config,
102
- openclaw: {
103
- ...(config.openclaw || {}),
104
- ...(this.overrides.skillsPath ? { skillsPath: this.overrides.skillsPath } : {}),
105
- ...(this.overrides.cronJobsFile ? { cronJobsFile: this.overrides.cronJobsFile } : {}),
106
- ...(this.overrides.openclawBin ? { cliBin: this.overrides.openclawBin } : {}),
107
- ...(this.overrides.clawhubBin ? { clawhubBin: this.overrides.clawhubBin } : {}),
108
- },
109
- };
110
- }
96
+ };
97
+ }
98
+
99
+ applyOpenClawOverrides(config) {
100
+ return {
101
+ ...config,
102
+ openclaw: {
103
+ ...(config.openclaw || {}),
104
+ ...(this.overrides.skillsPath ? { skillsPath: this.overrides.skillsPath } : {}),
105
+ ...(this.overrides.cronJobsFile ? { cronJobsFile: this.overrides.cronJobsFile } : {}),
106
+ ...(this.overrides.openclawBin ? { cliBin: this.overrides.openclawBin } : {}),
107
+ ...(this.overrides.clawhubBin ? { clawhubBin: this.overrides.clawhubBin } : {}),
108
+ },
109
+ };
110
+ }
111
111
 
112
112
  /**
113
113
  * โหลด config
114
114
  */
115
- loadConfig() {
116
- const configFile = path.join(this.configPath, 'config.json');
117
- if (fs.existsSync(configFile)) {
118
- this.config = fs.readJsonSync(configFile);
119
- } else {
120
- this.config = this.applyOpenClawOverrides(this.getDefaultConfig());
121
- }
122
- return this.config;
123
- }
115
+ loadConfig() {
116
+ const configFile = path.join(this.configPath, 'config.json');
117
+ if (fs.existsSync(configFile)) {
118
+ this.config = fs.readJsonSync(configFile);
119
+ } else {
120
+ this.config = this.applyOpenClawOverrides(this.getDefaultConfig());
121
+ }
122
+ return this.config;
123
+ }
124
124
 
125
125
  /**
126
126
  * บันทึก config
@@ -172,25 +172,25 @@ class ConfigManager {
172
172
  /**
173
173
  * ดึง path ของ skills
174
174
  */
175
- getSkillsPath() {
176
- const config = this.getConfig();
177
- return config.openclaw?.skillsPath || path.join(this.configPath, 'skills');
178
- }
175
+ getSkillsPath() {
176
+ const config = this.getConfig();
177
+ return config.openclaw?.skillsPath || path.join(this.configPath, 'skills');
178
+ }
179
179
 
180
180
  /**
181
181
  * ดึง path ของ crons
182
182
  */
183
- getCronsPath() {
184
- return path.dirname(this.getCronJobsFilePath());
185
- }
186
-
187
- /**
188
- * ดึง path ของ cron jobs file
189
- */
190
- getCronJobsFilePath() {
191
- const config = this.getConfig();
192
- return config.openclaw?.cronJobsFile || path.join(this.configPath, 'crons', 'jobs.json');
193
- }
183
+ getCronsPath() {
184
+ return path.dirname(this.getCronJobsFilePath());
185
+ }
186
+
187
+ /**
188
+ * ดึง path ของ cron jobs file
189
+ */
190
+ getCronJobsFilePath() {
191
+ const config = this.getConfig();
192
+ return config.openclaw?.cronJobsFile || path.join(this.configPath, 'crons', 'jobs.json');
193
+ }
194
194
 
195
195
  /**
196
196
  * ดึง path ของ logs
@@ -235,16 +235,16 @@ class ConfigManager {
235
235
  /**
236
236
  * เพิ่ม cronjob
237
237
  */
238
- addCron(cronInfo) {
239
- const config = this.getConfig();
240
- if (!config.crons) {
241
- config.crons = [];
242
- }
243
- config.crons.push({
244
- id: cronInfo.id || Date.now().toString(),
245
- ...cronInfo,
246
- createdAt: new Date().toISOString(),
247
- });
238
+ addCron(cronInfo) {
239
+ const config = this.getConfig();
240
+ if (!config.crons) {
241
+ config.crons = [];
242
+ }
243
+ config.crons.push({
244
+ id: cronInfo.id || Date.now().toString(),
245
+ ...cronInfo,
246
+ createdAt: new Date().toISOString(),
247
+ });
248
248
  this.saveConfig();
249
249
  return config.crons[config.crons.length - 1];
250
250
  }
@@ -252,37 +252,37 @@ class ConfigManager {
252
252
  /**
253
253
  * ลบ cronjob
254
254
  */
255
- removeCron(cronId) {
256
- const config = this.getConfig();
257
- if (config.crons) {
258
- config.crons = config.crons.filter(cron => cron.id !== cronId);
259
- this.saveConfig();
260
- }
261
- }
262
-
263
- /**
264
- * แก้ไข cronjob
265
- */
266
- updateCron(cronId, patch = {}) {
267
- const config = this.getConfig();
268
- if (!config.crons) {
269
- return null;
270
- }
271
-
272
- const idx = config.crons.findIndex((c) => c.id === cronId);
273
- if (idx === -1) {
274
- return null;
275
- }
276
-
277
- config.crons[idx] = {
278
- ...config.crons[idx],
279
- ...patch,
280
- updatedAt: new Date().toISOString(),
281
- };
282
- this.saveConfig();
283
-
284
- return config.crons[idx];
285
- }
255
+ removeCron(cronId) {
256
+ const config = this.getConfig();
257
+ if (config.crons) {
258
+ config.crons = config.crons.filter(cron => cron.id !== cronId);
259
+ this.saveConfig();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * แก้ไข cronjob
265
+ */
266
+ updateCron(cronId, patch = {}) {
267
+ const config = this.getConfig();
268
+ if (!config.crons) {
269
+ return null;
270
+ }
271
+
272
+ const idx = config.crons.findIndex((c) => c.id === cronId);
273
+ if (idx === -1) {
274
+ return null;
275
+ }
276
+
277
+ config.crons[idx] = {
278
+ ...config.crons[idx],
279
+ ...patch,
280
+ updatedAt: new Date().toISOString(),
281
+ };
282
+ this.saveConfig();
283
+
284
+ return config.crons[idx];
285
+ }
286
286
 
287
287
  /**
288
288
  * ดึงรายการ cronjobs
@@ -3,46 +3,46 @@
3
3
  */
4
4
 
5
5
  const fs = require('fs-extra');
6
- const path = require('path');
7
- const cron = require('node-cron');
8
- const chalk = require('chalk');
9
- const OpenClawCLI = require('./OpenClawCLI');
10
- const { normalizeCronExpression } = require('./CronFormat');
11
-
12
- class CronManager {
13
- constructor(configManager) {
14
- this.configManager = configManager;
15
- this.tasks = new Map();
16
- this.jobsFile = configManager.getCronJobsFilePath();
17
- this.openclawCLI = new OpenClawCLI(configManager);
18
- this.useOpenClawCron = this.openclawCLI.hasOpenClaw();
19
-
20
- if (!this.useOpenClawCron) {
21
- this.ensureJobsFile();
22
- }
23
- }
6
+ const path = require('path');
7
+ const cron = require('node-cron');
8
+ const chalk = require('chalk');
9
+ const OpenClawCLI = require('./OpenClawCLI');
10
+ const { normalizeCronExpression } = require('./CronFormat');
11
+
12
+ class CronManager {
13
+ constructor(configManager) {
14
+ this.configManager = configManager;
15
+ this.tasks = new Map();
16
+ this.jobsFile = configManager.getCronJobsFilePath();
17
+ this.openclawCLI = new OpenClawCLI(configManager);
18
+ this.useOpenClawCron = this.openclawCLI.hasOpenClaw();
19
+
20
+ if (!this.useOpenClawCron) {
21
+ this.ensureJobsFile();
22
+ }
23
+ }
24
24
 
25
25
  /**
26
26
  * สร้างไฟล์ jobs ถ้ายังไม่มี
27
27
  */
28
- ensureJobsFile() {
29
- fs.ensureDirSync(path.dirname(this.jobsFile));
30
- if (!fs.existsSync(this.jobsFile)) {
31
- fs.writeJsonSync(this.jobsFile, { jobs: [] }, { spaces: 2 });
32
- }
33
- }
28
+ ensureJobsFile() {
29
+ fs.ensureDirSync(path.dirname(this.jobsFile));
30
+ if (!fs.existsSync(this.jobsFile)) {
31
+ fs.writeJsonSync(this.jobsFile, { jobs: [] }, { spaces: 2 });
32
+ }
33
+ }
34
34
 
35
35
  /**
36
36
  * อ่านรายการ jobs
37
37
  */
38
- getJobs() {
39
- if (!fs.existsSync(this.jobsFile)) {
40
- return [];
41
- }
42
-
43
- const data = fs.readJsonSync(this.jobsFile);
44
- return data.jobs || [];
45
- }
38
+ getJobs() {
39
+ if (!fs.existsSync(this.jobsFile)) {
40
+ return [];
41
+ }
42
+
43
+ const data = fs.readJsonSync(this.jobsFile);
44
+ return data.jobs || [];
45
+ }
46
46
 
47
47
  /**
48
48
  * บันทึกรายการ jobs
@@ -54,76 +54,76 @@ class CronManager {
54
54
  /**
55
55
  * เพิ่ม cronjob
56
56
  */
57
- async add(skillName, schedule, params = {}, description = '') {
58
- const normalizedSchedule = normalizeCronExpression(schedule);
59
-
60
- if (this.useOpenClawCron) {
61
- const uniqueName = `cfh:${skillName}:${Date.now()}`;
62
- const message = `Run skill "${skillName}" with params: ${JSON.stringify(params)}`;
63
- const created = await this.openclawCLI.addCronJob({
64
- name: uniqueName,
65
- description: description || `Run ${skillName}`,
66
- schedule: normalizedSchedule,
67
- message,
68
- });
69
-
70
- let jobId = created.jobId;
71
- if (!jobId) {
72
- const jobs = await this.openclawCLI.listCronJobs();
73
- const matched = jobs.find((job) => job.name === uniqueName);
74
- jobId = matched?.id || null;
75
- }
76
-
77
- if (!jobId) {
78
- throw new Error('สร้าง cronjob ผ่าน openclaw สำเร็จ แต่ไม่สามารถระบุ job id ได้');
79
- }
80
-
81
- this.configManager.addCron({
82
- id: jobId,
83
- skill: skillName,
84
- schedule: normalizedSchedule,
85
- description: description || `Run ${skillName}`,
86
- });
87
-
88
- console.log(chalk.green(`✓ เพิ่ม cronjob (openclaw): ${skillName} (${normalizedSchedule})`));
89
-
90
- return {
91
- id: jobId,
92
- skill: skillName,
93
- schedule: normalizedSchedule,
94
- params,
95
- description: description || `Run ${skillName}`,
96
- enabled: true,
97
- };
98
- }
99
-
100
- const jobs = this.getJobs();
101
- const jobId = `cron_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
57
+ async add(skillName, schedule, params = {}, description = '') {
58
+ const normalizedSchedule = normalizeCronExpression(schedule);
59
+
60
+ if (this.useOpenClawCron) {
61
+ const uniqueName = `cfh:${skillName}:${Date.now()}`;
62
+ const message = `Run skill ${skillName} with params: ${JSON.stringify(params)}`;
63
+ const created = await this.openclawCLI.addCronJob({
64
+ name: uniqueName,
65
+ description: description || `Run ${skillName}`,
66
+ schedule: normalizedSchedule,
67
+ message,
68
+ });
69
+
70
+ let jobId = created.jobId;
71
+ if (!jobId) {
72
+ const jobs = await this.openclawCLI.listCronJobs();
73
+ const matched = jobs.find((job) => job.name === uniqueName);
74
+ jobId = matched?.id || null;
75
+ }
76
+
77
+ if (!jobId) {
78
+ throw new Error('สร้าง cronjob ผ่าน openclaw สำเร็จ แต่ไม่สามารถระบุ job id ได้');
79
+ }
80
+
81
+ this.configManager.addCron({
82
+ id: jobId,
83
+ skill: skillName,
84
+ schedule: normalizedSchedule,
85
+ description: description || `Run ${skillName}`,
86
+ });
87
+
88
+ console.log(chalk.green(`✓ เพิ่ม cronjob (openclaw): ${skillName} (${normalizedSchedule})`));
89
+
90
+ return {
91
+ id: jobId,
92
+ skill: skillName,
93
+ schedule: normalizedSchedule,
94
+ params,
95
+ description: description || `Run ${skillName}`,
96
+ enabled: true,
97
+ };
98
+ }
99
+
100
+ const jobs = this.getJobs();
101
+ const jobId = `cron_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
102
102
 
103
103
  const job = {
104
- id: jobId,
105
- skill: skillName,
106
- schedule: normalizedSchedule,
107
- params,
108
- description: description || `Run ${skillName}`,
109
- enabled: true,
110
- createdAt: new Date().toISOString(),
111
- lastRun: null,
112
- nextRun: this.getNextRun(normalizedSchedule),
113
- runCount: 0,
114
- errorCount: 0,
115
- };
104
+ id: jobId,
105
+ skill: skillName,
106
+ schedule: normalizedSchedule,
107
+ params,
108
+ description: description || `Run ${skillName}`,
109
+ enabled: true,
110
+ createdAt: new Date().toISOString(),
111
+ lastRun: null,
112
+ nextRun: this.getNextRun(normalizedSchedule),
113
+ runCount: 0,
114
+ errorCount: 0,
115
+ };
116
116
 
117
117
  jobs.push(job);
118
118
  this.saveJobs(jobs);
119
119
 
120
120
  // บันทึกลง config ด้วย
121
121
  this.configManager.addCron({
122
- id: jobId,
123
- skill: skillName,
124
- schedule: normalizedSchedule,
125
- description,
126
- });
122
+ id: jobId,
123
+ skill: skillName,
124
+ schedule: normalizedSchedule,
125
+ description,
126
+ });
127
127
 
128
128
  // สร้าง cron script
129
129
  this.createCronScript(job);
@@ -133,89 +133,89 @@ class CronManager {
133
133
  this.startJob(job);
134
134
  }
135
135
 
136
- console.log(chalk.green(`✓ เพิ่ม cronjob: ${skillName} (${normalizedSchedule})`));
137
-
138
- return job;
139
- }
140
-
141
- /**
142
- * แก้ไข cronjob
143
- */
144
- async edit(jobId, updates = {}) {
145
- const normalized = { ...updates };
146
-
147
- if (updates.schedule) {
148
- normalized.schedule = normalizeCronExpression(updates.schedule);
149
- }
150
-
151
- if (this.useOpenClawCron) {
152
- const tracked = this.configManager.getCrons().find((c) => c.id === jobId);
153
- const message =
154
- Object.prototype.hasOwnProperty.call(updates, 'params') && tracked?.skill
155
- ? `Run skill "${tracked.skill}" with params: ${JSON.stringify(updates.params || {})}`
156
- : undefined;
157
-
158
- if (Object.prototype.hasOwnProperty.call(updates, 'params') && !tracked?.skill) {
159
- throw new Error('แก้ไข params ไม่ได้ เพราะไม่พบ mapping skill ของ cron นี้ใน config');
160
- }
161
-
162
- await this.openclawCLI.editCronJob(jobId, {
163
- schedule: normalized.schedule,
164
- description: normalized.description,
165
- message,
166
- });
167
-
168
- this.configManager.updateCron(jobId, {
169
- ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
170
- ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
171
- ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
172
- });
173
-
174
- return { id: jobId, ...normalized };
175
- }
176
-
177
- const jobs = this.getJobs();
178
- const job = jobs.find((j) => j.id === jobId);
179
-
180
- if (!job) {
181
- throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
182
- }
183
-
184
- if (normalized.schedule) {
185
- job.schedule = normalized.schedule;
186
- job.nextRun = this.getNextRun(normalized.schedule);
187
- }
188
-
189
- if (typeof normalized.description === 'string') {
190
- job.description = normalized.description;
191
- }
192
-
193
- if (Object.prototype.hasOwnProperty.call(updates, 'params')) {
194
- job.params = updates.params || {};
195
- }
196
-
197
- this.saveJobs(jobs);
198
- this.configManager.updateCron(jobId, {
199
- ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
200
- ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
201
- ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
202
- });
203
-
204
- return job;
205
- }
136
+ console.log(chalk.green(`✓ เพิ่ม cronjob: ${skillName} (${normalizedSchedule})`));
137
+
138
+ return job;
139
+ }
140
+
141
+ /**
142
+ * แก้ไข cronjob
143
+ */
144
+ async edit(jobId, updates = {}) {
145
+ const normalized = { ...updates };
146
+
147
+ if (updates.schedule) {
148
+ normalized.schedule = normalizeCronExpression(updates.schedule);
149
+ }
150
+
151
+ if (this.useOpenClawCron) {
152
+ const tracked = this.configManager.getCrons().find((c) => c.id === jobId);
153
+ const message =
154
+ Object.prototype.hasOwnProperty.call(updates, 'params') && tracked?.skill
155
+ ? `Run skill ${tracked.skill} with params: ${JSON.stringify(updates.params || {})}`
156
+ : undefined;
157
+
158
+ if (Object.prototype.hasOwnProperty.call(updates, 'params') && !tracked?.skill) {
159
+ throw new Error('แก้ไข params ไม่ได้ เพราะไม่พบ mapping skill ของ cron นี้ใน config');
160
+ }
161
+
162
+ await this.openclawCLI.editCronJob(jobId, {
163
+ schedule: normalized.schedule,
164
+ description: normalized.description,
165
+ message,
166
+ });
167
+
168
+ this.configManager.updateCron(jobId, {
169
+ ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
170
+ ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
171
+ ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
172
+ });
173
+
174
+ return { id: jobId, ...normalized };
175
+ }
176
+
177
+ const jobs = this.getJobs();
178
+ const job = jobs.find((j) => j.id === jobId);
179
+
180
+ if (!job) {
181
+ throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
182
+ }
183
+
184
+ if (normalized.schedule) {
185
+ job.schedule = normalized.schedule;
186
+ job.nextRun = this.getNextRun(normalized.schedule);
187
+ }
188
+
189
+ if (typeof normalized.description === 'string') {
190
+ job.description = normalized.description;
191
+ }
192
+
193
+ if (Object.prototype.hasOwnProperty.call(updates, 'params')) {
194
+ job.params = updates.params || {};
195
+ }
196
+
197
+ this.saveJobs(jobs);
198
+ this.configManager.updateCron(jobId, {
199
+ ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
200
+ ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
201
+ ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
202
+ });
203
+
204
+ return job;
205
+ }
206
206
 
207
207
  /**
208
208
  * ลบ cronjob
209
209
  */
210
- async remove(jobId) {
211
- if (this.useOpenClawCron) {
212
- await this.openclawCLI.removeCronJob(jobId);
213
- this.configManager.removeCron(jobId);
214
- console.log(chalk.green(`✓ ลบ cronjob (openclaw): ${jobId}`));
215
- return { success: true, removed: { id: jobId } };
216
- }
217
-
218
- let jobs = this.getJobs();
210
+ async remove(jobId) {
211
+ if (this.useOpenClawCron) {
212
+ await this.openclawCLI.removeCronJob(jobId);
213
+ this.configManager.removeCron(jobId);
214
+ console.log(chalk.green(`✓ ลบ cronjob (openclaw): ${jobId}`));
215
+ return { success: true, removed: { id: jobId } };
216
+ }
217
+
218
+ let jobs = this.getJobs();
219
219
  const job = jobs.find(j => j.id === jobId);
220
220
 
221
221
  if (!job) {
@@ -243,12 +243,12 @@ class CronManager {
243
243
  /**
244
244
  * แสดงรายการ cronjobs
245
245
  */
246
- list() {
247
- if (this.useOpenClawCron) {
248
- return this.listViaOpenClaw();
249
- }
250
-
251
- const jobs = this.getJobs();
246
+ list() {
247
+ if (this.useOpenClawCron) {
248
+ return this.listViaOpenClaw();
249
+ }
250
+
251
+ const jobs = this.getJobs();
252
252
 
253
253
  if (jobs.length === 0) {
254
254
  console.log(chalk.gray('ไม่มี cronjob ที่ตั้งไว้'));
@@ -265,30 +265,30 @@ class CronManager {
265
265
  nextRun: job.nextRun,
266
266
  runCount: job.runCount,
267
267
  }));
268
- }
269
-
270
- /**
271
- * แสดงรายการ cronjobs ผ่าน OpenClaw CLI
272
- */
273
- async listViaOpenClaw() {
274
- const jobs = await this.openclawCLI.listCronJobs();
275
-
276
- if (!jobs || jobs.length === 0) {
277
- console.log(chalk.gray('ไม่มี cronjob ที่ตั้งไว้'));
278
- return [];
279
- }
280
-
281
- return jobs.map((job) => ({
282
- id: job.id,
283
- skill: job.name || 'unknown',
284
- schedule: job.schedule?.expr || job.schedule?.kind || '-',
285
- description: job.description || '',
286
- enabled: job.enabled !== false,
287
- lastRun: job.lastRunAt || null,
288
- nextRun: job.nextRunAt || null,
289
- runCount: job.runCount || 0,
290
- }));
291
- }
268
+ }
269
+
270
+ /**
271
+ * แสดงรายการ cronjobs ผ่าน OpenClaw CLI
272
+ */
273
+ async listViaOpenClaw() {
274
+ const jobs = await this.openclawCLI.listCronJobs();
275
+
276
+ if (!jobs || jobs.length === 0) {
277
+ console.log(chalk.gray('ไม่มี cronjob ที่ตั้งไว้'));
278
+ return [];
279
+ }
280
+
281
+ return jobs.map((job) => ({
282
+ id: job.id,
283
+ skill: job.name || 'unknown',
284
+ schedule: job.schedule?.expr || job.schedule?.kind || '-',
285
+ description: job.description || '',
286
+ enabled: job.enabled !== false,
287
+ lastRun: job.lastRunAt || null,
288
+ nextRun: job.nextRunAt || null,
289
+ runCount: job.runCount || 0,
290
+ }));
291
+ }
292
292
 
293
293
  /**
294
294
  * เริ่มต้น job
@@ -311,16 +311,16 @@ class CronManager {
311
311
  /**
312
312
  * หยุด job
313
313
  */
314
- stopJob(jobId) {
315
- const task = this.tasks.get(jobId);
316
- if (task) {
317
- task.stop();
318
- if (typeof task.destroy === 'function') {
319
- task.destroy();
320
- }
321
- this.tasks.delete(jobId);
322
- }
323
- }
314
+ stopJob(jobId) {
315
+ const task = this.tasks.get(jobId);
316
+ if (task) {
317
+ task.stop();
318
+ if (typeof task.destroy === 'function') {
319
+ task.destroy();
320
+ }
321
+ this.tasks.delete(jobId);
322
+ }
323
+ }
324
324
 
325
325
  /**
326
326
  * รัน job
@@ -362,10 +362,10 @@ class CronManager {
362
362
  /**
363
363
  * รัน skill
364
364
  */
365
- async runSkill(skillName, params) {
366
- // ในโลกจริงจะเรียก OpenClaw API
367
- // const response = await axios.post(`${baseUrl}/api/skills/execute`, {
368
- // name: skillName,
365
+ async runSkill(skillName, params) {
366
+ // ในโลกจริงจะเรียก OpenClaw API
367
+ // const response = await axios.post(`${baseUrl}/api/skills/execute`, {
368
+ // name: skillName,
369
369
  // params,
370
370
  // });
371
371
 
@@ -392,7 +392,7 @@ async function run() {
392
392
  console.log(\`[\${new Date().toISOString()}] Running \${skill}...\`);
393
393
 
394
394
  // Call OpenClaw API
395
- const baseUrl = process.env.OPENCLAW_URL || 'http://localhost:3000';
395
+ const baseUrl = process.env.OPENCLAW_URL || 'http://localhost:18789';
396
396
  const response = await axios.post(\`\${baseUrl}/api/skills/execute\`, {
397
397
  name: skill,
398
398
  params: { ...config, ...params },
@@ -435,7 +435,7 @@ run();
435
435
  /**
436
436
  * คำนวณเวลารันครั้งถัดไป
437
437
  */
438
- getNextRun(_schedule) {
438
+ getNextRun(_schedule) {
439
439
  // ง่ายๆ แค่ return null ตอนนี้
440
440
  // ในอนาคตอาจใช้ library คำนวณจริง
441
441
  return null;
@@ -444,13 +444,13 @@ run();
444
444
  /**
445
445
  * เปิด/ปิด job
446
446
  */
447
- toggleJob(jobId, enabled) {
448
- if (this.useOpenClawCron) {
449
- throw new Error('โหมด openclaw cron: ให้ใช้คำสั่ง "openclaw cron enable|disable <id>"');
450
- }
451
-
452
- const jobs = this.getJobs();
453
- const job = jobs.find(j => j.id === jobId);
447
+ toggleJob(jobId, enabled) {
448
+ if (this.useOpenClawCron) {
449
+ throw new Error('โหมด openclaw cron: ให้ใช้คำสั่ง "openclaw cron enable|disable <id>"');
450
+ }
451
+
452
+ const jobs = this.getJobs();
453
+ const job = jobs.find(j => j.id === jobId);
454
454
 
455
455
  if (!job) {
456
456
  throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
@@ -473,13 +473,13 @@ run();
473
473
  /**
474
474
  * เริ่มต้นระบบ cron (เรียกตอน start)
475
475
  */
476
- startAll() {
477
- if (this.useOpenClawCron) {
478
- console.log(chalk.green('✓ ใช้ openclaw cron scheduler (ไม่ต้อง start local tasks)'));
479
- return;
480
- }
481
-
482
- const jobs = this.getJobs().filter(j => j.enabled);
476
+ startAll() {
477
+ if (this.useOpenClawCron) {
478
+ console.log(chalk.green('✓ ใช้ openclaw cron scheduler (ไม่ต้อง start local tasks)'));
479
+ return;
480
+ }
481
+
482
+ const jobs = this.getJobs().filter(j => j.enabled);
483
483
 
484
484
  for (const job of jobs) {
485
485
  this.startJob(job);
@@ -491,18 +491,18 @@ run();
491
491
  /**
492
492
  * หยุดระบบ cron ทั้งหมด
493
493
  */
494
- stopAll() {
495
- if (this.useOpenClawCron) {
496
- console.log(chalk.yellow('⏹️ openclaw cron scheduler ยังทำงานภายนอก process นี้'));
497
- return;
498
- }
499
-
500
- for (const [, task] of this.tasks) {
501
- task.stop();
502
- if (typeof task.destroy === 'function') {
503
- task.destroy();
504
- }
505
- }
494
+ stopAll() {
495
+ if (this.useOpenClawCron) {
496
+ console.log(chalk.yellow('⏹️ openclaw cron scheduler ยังทำงานภายนอก process นี้'));
497
+ return;
498
+ }
499
+
500
+ for (const [, task] of this.tasks) {
501
+ task.stop();
502
+ if (typeof task.destroy === 'function') {
503
+ task.destroy();
504
+ }
505
+ }
506
506
  this.tasks.clear();
507
507
 
508
508
  console.log(chalk.yellow('⏹️ หยุดระบบ cron'));
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { promisify } = require('util');
4
- const { execFile, spawnSync } = require('child_process');
4
+ const { execFile, spawn, spawnSync } = require('child_process');
5
5
 
6
6
  const execFileAsync = promisify(execFile);
7
7
 
@@ -33,22 +33,67 @@ class OpenClawCLI {
33
33
  }
34
34
 
35
35
  commandExists(command, args = ['--help']) {
36
- const result = spawnSync(command, args, {
37
- encoding: 'utf8',
38
- stdio: 'pipe',
39
- windowsHide: true,
40
- });
36
+ try {
37
+ const result = spawnSync(command, args, {
38
+ encoding: 'utf8',
39
+ stdio: 'pipe',
40
+ windowsHide: true,
41
+ shell: true,
42
+ });
41
43
 
42
- return result.status === 0;
44
+ return result.status === 0;
45
+ } catch (e) {
46
+ return false;
47
+ }
43
48
  }
44
49
 
45
50
  async run(command, args, options = {}) {
51
+ if (process.platform === 'win32') {
52
+ return new Promise((resolve, reject) => {
53
+ const comspec = process.env.ComSpec || 'cmd.exe';
54
+
55
+ const child = spawn(comspec, ['/d', '/s', '/c', command, ...args], {
56
+ cwd: options.cwd,
57
+ env: process.env,
58
+ windowsHide: true,
59
+ shell: false,
60
+ });
61
+
62
+ let stdout = '';
63
+ let stderr = '';
64
+
65
+ child.stdout.on('data', (chunk) => {
66
+ stdout += String(chunk);
67
+ });
68
+
69
+ child.stderr.on('data', (chunk) => {
70
+ stderr += String(chunk);
71
+ });
72
+
73
+ child.on('error', (error) => {
74
+ reject(new Error(`${command} ${args.join(' ')} ล้มเหลว: ${error.message}`));
75
+ });
76
+
77
+ child.on('close', (code) => {
78
+ const out = stdout.trim();
79
+ const err = stderr.trim();
80
+ if (code === 0) {
81
+ resolve({ stdout: out, stderr: err });
82
+ return;
83
+ }
84
+ const detail = err || out || `exit code ${code}`;
85
+ reject(new Error(`${command} ${args.join(' ')} ล้มเหลว: ${detail}`));
86
+ });
87
+ });
88
+ }
89
+
46
90
  try {
47
91
  const { stdout, stderr } = await execFileAsync(command, args, {
48
92
  cwd: options.cwd,
49
93
  env: process.env,
50
94
  maxBuffer: 1024 * 1024 * 4,
51
95
  windowsHide: true,
96
+ shell: true,
52
97
  });
53
98
  return {
54
99
  stdout: (stdout || '').trim(),
@@ -58,8 +58,9 @@ const boxStyles = {
58
58
  },
59
59
  };
60
60
 
61
- // ASCII Art for logo - Cool Retro Style
62
- const asciiLogo = `
61
+ /* eslint-disable no-useless-escape */
62
+ // ASCII Art for logo - Cool Retro Style
63
+ const asciiLogo = `
63
64
  ________ ___ ________ ___ __ ________ ___ ________ ___ __
64
65
  |\ ____\|\ \ |\ __ \|\ \ |\ \|\ _____\\ \ |\ __ \|\ \ |\ \
65
66
  \ \ \___|\ \ \ \ \ \|\ \ \ \ \ \ \ \ \__/\ \ \ \ \ \|\ \ \ \ \ \ \
@@ -70,8 +71,9 @@ const asciiLogo = `
70
71
 
71
72
 
72
73
 
73
- ClawFlow - Skill + Cron Installer for OpenClaw
74
- `;
74
+ ClawFlow - Skill + Cron Installer for OpenClaw
75
+ `;
76
+ /* eslint-enable no-useless-escape */
75
77
 
76
78
  // Mini logo for sub-headers
77
79
  const miniLogo = `
@@ -178,7 +180,7 @@ function createProgressBar(total, current, width = 30) {
178
180
  /**
179
181
  * Print main banner
180
182
  */
181
- function printBanner(pkgVersion = '1.0.0') {
183
+ function printBanner(_pkgVersion = '1.0.0') {
182
184
  const banner = gradientText(asciiLogo, colors.primary, colors.secondary);
183
185
 
184
186
  console.log(chalk.bgBlack(banner));