claskit 1.0.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/README.md +133 -0
- package/bin/claskit.js +523 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# claskit
|
|
2
|
+
|
|
3
|
+
Autonomous Claude Code task runner. Write task specs as Markdown, run `claskit`, watch Claude implement them.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ │
|
|
8
|
+
│ .claude/tasks/todo/ .claude/tasks/done/ │
|
|
9
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
10
|
+
│ │ feature-a.md │ │ feature-a.md │ │
|
|
11
|
+
│ │ feature-b.md │ │ │ │
|
|
12
|
+
│ └────────┬─────────┘ └────────▲─────────┘ │
|
|
13
|
+
│ │ │ │
|
|
14
|
+
│ ▼ │ │
|
|
15
|
+
│ ┌───────────┐ │ │
|
|
16
|
+
│ │ claskit │ │ │
|
|
17
|
+
│ └─────┬─────┘ │ │
|
|
18
|
+
│ │ │ │
|
|
19
|
+
│ ┌──────┴──────┐ │ │
|
|
20
|
+
│ │ │ │ │
|
|
21
|
+
│ --now --schedule implements │
|
|
22
|
+
│ │ HH:MM ⏳ │ │
|
|
23
|
+
│ │ │ │ │
|
|
24
|
+
│ └──────┬───────┘ │ │
|
|
25
|
+
│ ▼ │ │
|
|
26
|
+
│ claude --dangerously-skip-permissions │ │
|
|
27
|
+
│ reads spec → verifies → ──────────────┘ │
|
|
28
|
+
│ │
|
|
29
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js 18+
|
|
35
|
+
- [Claude Code CLI](https://claude.ai/code) installed and authenticated
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add -g claskit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm add -g claskit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
claskit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
claskit detects if the project is not set up and walks you through initialization interactively.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
claskit [flag]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Flag | What it does |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| _(none)_ | Interactive menu: pick tasks, choose when to run |
|
|
64
|
+
| `--init` | Set up `.claude/tasks/` folder structure in current project |
|
|
65
|
+
| `--now` | Skip menu, run immediately (still shows task picker + confirm) |
|
|
66
|
+
| `--test` | Create 2 sample task files in `todo/` to test the runner |
|
|
67
|
+
| `--clean-test` | Remove all test-generated files |
|
|
68
|
+
|
|
69
|
+
### Interactive menu options
|
|
70
|
+
|
|
71
|
+
- **Now** — pick tasks and launch immediately
|
|
72
|
+
- **Schedule (HH:MM)** — live countdown until target time, then launch
|
|
73
|
+
- **Exit**
|
|
74
|
+
|
|
75
|
+
### Task picker
|
|
76
|
+
|
|
77
|
+
When 2+ tasks are queued, claskit asks which to run:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Select tasks:
|
|
81
|
+
Enter numbers separated by commas, or "all"
|
|
82
|
+
[all]: 1,3
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
1. Claude reads each selected `.md` spec
|
|
88
|
+
2. Decides execution order based on dependencies noted in specs
|
|
89
|
+
3. Implements each task fully
|
|
90
|
+
4. Verifies acceptance criteria
|
|
91
|
+
5. Moves the spec from `todo/` to `done/` on success
|
|
92
|
+
6. Stops and reports if anything fails
|
|
93
|
+
|
|
94
|
+
Claude runs with `--dangerously-skip-permissions` — it will read, write, and execute commands without prompting. Only use in projects you trust.
|
|
95
|
+
|
|
96
|
+
## Task spec format
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
# Feature Title
|
|
100
|
+
|
|
101
|
+
## Task
|
|
102
|
+
|
|
103
|
+
What needs to be built.
|
|
104
|
+
|
|
105
|
+
## Acceptance Criteria
|
|
106
|
+
|
|
107
|
+
- [ ] Criterion 1
|
|
108
|
+
- [ ] Criterion 2
|
|
109
|
+
|
|
110
|
+
## Files Affected
|
|
111
|
+
|
|
112
|
+
| File | Change |
|
|
113
|
+
|------|--------|
|
|
114
|
+
| `src/foo.ts` | New component |
|
|
115
|
+
|
|
116
|
+
## Notes
|
|
117
|
+
|
|
118
|
+
Dependencies, order hints, constraints.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Save as `.claude/tasks/todo/my-feature.md`.
|
|
122
|
+
|
|
123
|
+
## Project integration
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm pkg set scripts.claskit="claskit"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then run with `npm run claskit` / `pnpm claskit` / `yarn claskit`.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/bin/claskit.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
// ANSI colors
|
|
10
|
+
const C = {
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
bold: '\x1b[1m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { name, version } = JSON.parse(
|
|
21
|
+
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const TODO_DIR = '.claude/tasks/todo';
|
|
25
|
+
const DONE_DIR = '.claude/tasks/done';
|
|
26
|
+
const TASKS_DIR = '.claude/tasks';
|
|
27
|
+
const PROJECT_NAME = path.basename(process.cwd());
|
|
28
|
+
|
|
29
|
+
const TASKS_README = `# .claude/tasks
|
|
30
|
+
|
|
31
|
+
This folder is managed by [claskit](https://github.com/phucbm/claskit) — an autonomous Claude Code task runner.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
Each \`.md\` file in \`todo/\` is a task spec. When you run \`claskit\`, Claude reads the specs,
|
|
36
|
+
implements them in order, then moves each file to \`done/\` on success.
|
|
37
|
+
|
|
38
|
+
## Folder structure
|
|
39
|
+
|
|
40
|
+
\`\`\`
|
|
41
|
+
.claude/tasks/
|
|
42
|
+
todo/ ← task specs waiting to be implemented
|
|
43
|
+
done/ ← completed task specs (moved here by claskit after success)
|
|
44
|
+
README.md ← this file
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
## Task spec format
|
|
48
|
+
|
|
49
|
+
Each \`.md\` file should contain:
|
|
50
|
+
- A \`# Title\` heading
|
|
51
|
+
- **Task** section: what to implement
|
|
52
|
+
- **Acceptance Criteria** section: checklist Claude verifies before marking done
|
|
53
|
+
- **Files Affected** section (optional): which files will change
|
|
54
|
+
- **Notes** section (optional): dependencies, order hints, constraints
|
|
55
|
+
|
|
56
|
+
## For AI assistants reading this
|
|
57
|
+
|
|
58
|
+
If you see \`.md\` files in \`todo/\`, they are pending implementation specs.
|
|
59
|
+
Do not modify them unless asked. Do not move them to \`done/\` manually.
|
|
60
|
+
claskit handles the lifecycle: read spec → implement → verify → move to done.
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
function runInit() {
|
|
64
|
+
const readmePath = path.join(TASKS_DIR, 'README.md');
|
|
65
|
+
const alreadyExists = fs.existsSync(TODO_DIR) && fs.existsSync(DONE_DIR);
|
|
66
|
+
|
|
67
|
+
fs.mkdirSync(TODO_DIR, { recursive: true });
|
|
68
|
+
fs.mkdirSync(DONE_DIR, { recursive: true });
|
|
69
|
+
fs.writeFileSync(readmePath, TASKS_README);
|
|
70
|
+
|
|
71
|
+
if (alreadyExists) {
|
|
72
|
+
console.log(`${C.yellow}Already initialized.${C.reset} Updated README.md.`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`${C.green}Initialized claskit for ${C.bold}${PROJECT_NAME}${C.reset}`);
|
|
75
|
+
console.log(` ${C.dim}Created: ${TODO_DIR}/${C.reset}`);
|
|
76
|
+
console.log(` ${C.dim}Created: ${DONE_DIR}/${C.reset}`);
|
|
77
|
+
console.log(` ${C.dim}Created: ${TASKS_DIR}/README.md${C.reset}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(`Next: add task specs to ${C.cyan}${TODO_DIR}/${C.reset}`);
|
|
80
|
+
console.log(`Then run: ${C.cyan}claskit --now${C.reset}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(`${C.dim}To try with sample tasks: claskit --test${C.reset}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const TEST_TASK_1 = `# claskit Test Task 1: Create output file
|
|
87
|
+
|
|
88
|
+
**Status:** Planned — auto-generated by claskit --test
|
|
89
|
+
|
|
90
|
+
## Task
|
|
91
|
+
|
|
92
|
+
Create the file \`.claude/tasks/claskit-test-1.txt\` with this exact content:
|
|
93
|
+
|
|
94
|
+
\`\`\`
|
|
95
|
+
claskit test task 1 complete
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Acceptance Criteria
|
|
99
|
+
|
|
100
|
+
- [ ] File \`.claude/tasks/claskit-test-1.txt\` exists
|
|
101
|
+
- [ ] File contains "claskit test task 1 complete"
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
Do NOT run any build or lint commands. Just create the file.
|
|
106
|
+
This task must complete before task 2 runs (task 2 reads this file).
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const TEST_TASK_2 = `# claskit Test Task 2: Chain verification
|
|
110
|
+
|
|
111
|
+
**Status:** Planned — auto-generated by claskit --test
|
|
112
|
+
|
|
113
|
+
## Task
|
|
114
|
+
|
|
115
|
+
Read \`.claude/tasks/claskit-test-1.txt\` and confirm it contains "claskit test task 1 complete".
|
|
116
|
+
Then create \`.claude/tasks/claskit-test-2.txt\` with this exact content:
|
|
117
|
+
|
|
118
|
+
\`\`\`
|
|
119
|
+
claskit test task 2 complete
|
|
120
|
+
task 1 output confirmed
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
## Acceptance Criteria
|
|
124
|
+
|
|
125
|
+
- [ ] \`.claude/tasks/claskit-test-1.txt\` exists and contains expected text (from task 1)
|
|
126
|
+
- [ ] \`.claude/tasks/claskit-test-2.txt\` created with correct content
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
Do NOT run any build or lint commands. Just read task 1 output and create task 2 file.
|
|
131
|
+
Depends on: claskit-test-task-1.md
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function pad(str, len) {
|
|
137
|
+
const s = String(str);
|
|
138
|
+
return s.length >= len ? s : s + ' '.repeat(len - s.length);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function box(...lines) {
|
|
142
|
+
const width = 44;
|
|
143
|
+
const inner = width - 2;
|
|
144
|
+
const top = `╔${'═'.repeat(inner)}╗`;
|
|
145
|
+
const bottom = `╚${'═'.repeat(inner)}╝`;
|
|
146
|
+
const rows = lines.map(l => `║ ${pad(l, inner - 2)}║`);
|
|
147
|
+
return [top, ...rows, bottom].join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function taskTitle(filepath) {
|
|
151
|
+
try {
|
|
152
|
+
const content = fs.readFileSync(filepath, 'utf8');
|
|
153
|
+
const m = content.match(/^#\s+(.+)/m);
|
|
154
|
+
return m ? m[1].trim() : path.basename(filepath, '.md');
|
|
155
|
+
} catch {
|
|
156
|
+
return path.basename(filepath, '.md');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fmtDuration(secs) {
|
|
161
|
+
const h = Math.floor(secs / 3600);
|
|
162
|
+
const m = Math.floor((secs % 3600) / 60);
|
|
163
|
+
const s = secs % 60;
|
|
164
|
+
if (h > 0) return `${h}h ${String(m).padStart(2,'0')}m ${String(s).padStart(2,'0')}s`;
|
|
165
|
+
if (m > 0) return `${m}m ${String(s).padStart(2,'0')}s`;
|
|
166
|
+
return `${s}s`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── countdown ──────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function countdown(totalSecs, label) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
let remaining = totalSecs;
|
|
174
|
+
|
|
175
|
+
const tick = () => {
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
`\r ${C.yellow}⏳${C.reset} ${label} — ${C.bold}${fmtDuration(remaining)}${C.reset} remaining `
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
tick();
|
|
182
|
+
const timer = setInterval(() => {
|
|
183
|
+
remaining--;
|
|
184
|
+
if (remaining <= 0) {
|
|
185
|
+
clearInterval(timer);
|
|
186
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
187
|
+
resolve();
|
|
188
|
+
} else {
|
|
189
|
+
tick();
|
|
190
|
+
}
|
|
191
|
+
}, 1000);
|
|
192
|
+
|
|
193
|
+
process.on('SIGINT', () => {
|
|
194
|
+
clearInterval(timer);
|
|
195
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
196
|
+
console.log(`\n${C.red}Cancelled.${C.reset}`);
|
|
197
|
+
exit(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── readline prompt helpers ─────────────────────────────────────────────────
|
|
203
|
+
// Single rl instance with an event-driven queue so piped stdin lines
|
|
204
|
+
// are buffered and consumed in order — even when lines arrive before ask() registers.
|
|
205
|
+
|
|
206
|
+
const _lineQueue = [];
|
|
207
|
+
let _lineWaiter = null;
|
|
208
|
+
let _rl = null;
|
|
209
|
+
|
|
210
|
+
function initRL() {
|
|
211
|
+
if (_rl) return;
|
|
212
|
+
_rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
213
|
+
_rl.on('line', line => {
|
|
214
|
+
const trimmed = line.trim();
|
|
215
|
+
if (_lineWaiter) {
|
|
216
|
+
const resolve = _lineWaiter;
|
|
217
|
+
_lineWaiter = null;
|
|
218
|
+
resolve(trimmed);
|
|
219
|
+
} else {
|
|
220
|
+
_lineQueue.push(trimmed);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function closeRL() {
|
|
226
|
+
if (_rl) { _rl.close(); _rl = null; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function exit(code = 0) {
|
|
230
|
+
closeRL();
|
|
231
|
+
process.exit(code);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ask(question) {
|
|
235
|
+
initRL();
|
|
236
|
+
process.stdout.write(question);
|
|
237
|
+
return new Promise(resolve => {
|
|
238
|
+
if (_lineQueue.length > 0) {
|
|
239
|
+
const line = _lineQueue.shift();
|
|
240
|
+
process.stdout.write(line + '\n');
|
|
241
|
+
resolve(line);
|
|
242
|
+
} else {
|
|
243
|
+
_lineWaiter = resolve;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function menu(options) {
|
|
249
|
+
options.forEach((opt, i) => {
|
|
250
|
+
console.log(` ${C.cyan}[${i + 1}]${C.reset} ${opt}`);
|
|
251
|
+
});
|
|
252
|
+
return ask(`\n${C.bold}>${C.reset} `);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── launch ──────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async function pickTasks(tasks) {
|
|
258
|
+
if (tasks.length === 1) return tasks; // nothing to pick
|
|
259
|
+
|
|
260
|
+
console.log(`${C.bold}Select tasks:${C.reset}`);
|
|
261
|
+
console.log(` ${C.dim}Enter numbers separated by commas, or "all"${C.reset}`);
|
|
262
|
+
const ans = await ask(` ${C.dim}[all]${C.reset}: `);
|
|
263
|
+
const input = ans.trim().toLowerCase();
|
|
264
|
+
|
|
265
|
+
if (!input || input === 'all') return tasks;
|
|
266
|
+
|
|
267
|
+
const indices = input.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
268
|
+
const selected = indices.filter(i => i >= 0 && i < tasks.length).map(i => tasks[i]);
|
|
269
|
+
|
|
270
|
+
if (selected.length === 0) {
|
|
271
|
+
console.log(`${C.red}No valid tasks selected.${C.reset}`);
|
|
272
|
+
exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(`${C.dim}Running ${selected.length} of ${tasks.length} task(s):${C.reset}`);
|
|
277
|
+
selected.forEach((f, i) => console.log(` ${C.cyan}${i + 1}.${C.reset} ${taskTitle(f)}`));
|
|
278
|
+
return selected;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildPrompt(selectedTasks) {
|
|
282
|
+
const fileList = selectedTasks.map(f => `- ${f.replace(/\\/g, '/')}`).join('\n');
|
|
283
|
+
return `You are an autonomous task runner for this project.
|
|
284
|
+
|
|
285
|
+
Your job:
|
|
286
|
+
1. Read ONLY the following task files (ignore other files in .claude/tasks/todo/):
|
|
287
|
+
${fileList}
|
|
288
|
+
2. Analyze each spec and decide the best execution order based on dependencies, complexity, and notes in each file
|
|
289
|
+
3. Tell me the order you chose and why (briefly)
|
|
290
|
+
4. For each task in order:
|
|
291
|
+
a. Implement it fully according to the spec
|
|
292
|
+
b. Verify all acceptance criteria listed in the spec are met
|
|
293
|
+
c. Run the project's build/lint command (check package.json for the right command)
|
|
294
|
+
d. If build/lint passes → move the .md file from .claude/tasks/todo/ to .claude/tasks/done/
|
|
295
|
+
e. If something fails → stop, report clearly what failed and why, do not proceed to next task
|
|
296
|
+
5. After all tasks are done, print a summary: what was completed, what was skipped, any issues
|
|
297
|
+
|
|
298
|
+
Do not ask for confirmation between tasks. Work autonomously until your assigned tasks are done or a blocker is hit.`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function confirmLaunch(count) {
|
|
302
|
+
console.log('');
|
|
303
|
+
console.log(` ${C.bold}Ready to run ${count} task(s) autonomously.${C.reset}`);
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(` ${C.yellow}${C.bold}⚠ WARNING: --dangerously-skip-permissions${C.reset}`);
|
|
306
|
+
console.log(` ${C.yellow}Claude will read, write, and run commands WITHOUT asking.${C.reset}`);
|
|
307
|
+
console.log(` ${C.yellow}Only use this in a project you trust and have backed up.${C.reset}`);
|
|
308
|
+
console.log('');
|
|
309
|
+
const ans = await ask(` Confirm? ${C.dim}[y/N]${C.reset}: `);
|
|
310
|
+
return ans.toLowerCase() === 'y';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function launchClaude(prompt, label) {
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log(` ${C.green}${C.bold}Launching Claude Code${label ? ` — ${label}` : ''}${C.reset}`);
|
|
316
|
+
console.log(` ${C.dim}Prompt: "${prompt.split('\n')[0]}${prompt.includes('\n') ? '...' : ''}"${C.reset}`);
|
|
317
|
+
console.log(`\n${'─'.repeat(50)}\n`);
|
|
318
|
+
|
|
319
|
+
// Pipe prompt via stdin so Claude runs interactively (tool calls visible)
|
|
320
|
+
const bin = process.platform === 'win32' ? 'claude.cmd' : 'claude';
|
|
321
|
+
const child = spawn(bin, ['--dangerously-skip-permissions'], {
|
|
322
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
323
|
+
shell: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
child.stdin.write(prompt + '\n');
|
|
327
|
+
child.stdin.end();
|
|
328
|
+
|
|
329
|
+
child.on('exit', code => {
|
|
330
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
331
|
+
if (code !== 0) {
|
|
332
|
+
console.log(`${C.red}Claude exited with code ${code}${C.reset}`);
|
|
333
|
+
process.exit(code);
|
|
334
|
+
} else {
|
|
335
|
+
console.log(`${C.green}Claude finished successfully.${C.reset}`);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function launchReal(tasks) {
|
|
341
|
+
const selected = await pickTasks(tasks);
|
|
342
|
+
const confirmed = await confirmLaunch(selected.length);
|
|
343
|
+
if (!confirmed) {
|
|
344
|
+
console.log(`\n${C.dim}Aborted.${C.reset}`);
|
|
345
|
+
exit(0);
|
|
346
|
+
}
|
|
347
|
+
launchClaude(buildPrompt(selected), 'autonomous mode');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── schedule ────────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
async function scheduleRun(tasks) {
|
|
353
|
+
const selected = await pickTasks(tasks);
|
|
354
|
+
const input = await ask(` Enter time ${C.dim}(HH:MM, 24h)${C.reset}: `);
|
|
355
|
+
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(input)) {
|
|
356
|
+
console.log(`${C.red}Invalid format. Use HH:MM (e.g. 22:30)${C.reset}`);
|
|
357
|
+
exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const [hh, mm] = input.split(':').map(Number);
|
|
361
|
+
const now = new Date();
|
|
362
|
+
const target = new Date();
|
|
363
|
+
target.setHours(hh, mm, 0, 0);
|
|
364
|
+
if (target <= now) target.setDate(target.getDate() + 1);
|
|
365
|
+
|
|
366
|
+
const waitSecs = Math.round((target - now) / 1000);
|
|
367
|
+
console.log(`\n ${C.cyan}Scheduled for ${input}${C.reset} — ${fmtDuration(waitSecs)} from now`);
|
|
368
|
+
console.log(` ${C.dim}Press Ctrl+C to cancel${C.reset}\n`);
|
|
369
|
+
|
|
370
|
+
await countdown(waitSecs, `Launching at ${input}`);
|
|
371
|
+
|
|
372
|
+
const confirmed = await confirmLaunch(selected.length);
|
|
373
|
+
if (!confirmed) {
|
|
374
|
+
console.log(`\n${C.dim}Aborted.${C.reset}`);
|
|
375
|
+
exit(0);
|
|
376
|
+
}
|
|
377
|
+
launchClaude(buildPrompt(selected), 'autonomous mode');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─── main ────────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
async function main() {
|
|
383
|
+
const args = process.argv.slice(2);
|
|
384
|
+
|
|
385
|
+
// Guard: warn if not in a project directory
|
|
386
|
+
const looksLikeProject = fs.existsSync('.git') || fs.existsSync('package.json') || fs.existsSync('.claude');
|
|
387
|
+
if (!looksLikeProject) {
|
|
388
|
+
console.log('');
|
|
389
|
+
console.log(`${C.yellow}⚠ No project detected in ${process.cwd()}${C.reset}`);
|
|
390
|
+
console.log(`${C.dim}claskit should be run from your project root, not ~/ or /${C.reset}`);
|
|
391
|
+
const ans = await ask(` Continue anyway? ${C.dim}[y/N]${C.reset}: `);
|
|
392
|
+
if (ans.toLowerCase() !== 'y') { exit(0); }
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (args.includes('--init')) {
|
|
397
|
+
runInit();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Onboarding — interactive setup if not initialized
|
|
402
|
+
if (!fs.existsSync(TODO_DIR)) {
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(`${C.yellow}claskit not set up in this project.${C.reset}`);
|
|
405
|
+
const doInit = await ask(` Initialize now? ${C.dim}[Y/n]${C.reset}: `);
|
|
406
|
+
if (doInit.toLowerCase() === 'n') {
|
|
407
|
+
console.log(`\n${C.dim}Run: claskit --init${C.reset}`);
|
|
408
|
+
exit(0);
|
|
409
|
+
}
|
|
410
|
+
runInit();
|
|
411
|
+
console.log('');
|
|
412
|
+
const doTest = await ask(` Add sample tasks to try claskit? ${C.dim}[Y/n]${C.reset}: `);
|
|
413
|
+
if (doTest.toLowerCase() !== 'n') {
|
|
414
|
+
const f1 = path.join(TODO_DIR, 'claskit-test-task-1.md');
|
|
415
|
+
const f2 = path.join(TODO_DIR, 'claskit-test-task-2.md');
|
|
416
|
+
fs.writeFileSync(f1, TEST_TASK_1);
|
|
417
|
+
fs.writeFileSync(f2, TEST_TASK_2);
|
|
418
|
+
console.log(` ${C.green}✓${C.reset} Created 2 sample tasks in ${TODO_DIR}/`);
|
|
419
|
+
}
|
|
420
|
+
console.log('');
|
|
421
|
+
console.log(`${'─'.repeat(42)}`);
|
|
422
|
+
console.log(` ${C.bold}Ready.${C.reset} Run: ${C.cyan}claskit --now${C.reset}`);
|
|
423
|
+
console.log(`${'─'.repeat(42)}`);
|
|
424
|
+
exit(0);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Scan tasks
|
|
428
|
+
const tasks = fs.readdirSync(TODO_DIR)
|
|
429
|
+
.filter(f => f.endsWith('.md'))
|
|
430
|
+
.sort()
|
|
431
|
+
.map(f => path.join(TODO_DIR, f));
|
|
432
|
+
|
|
433
|
+
const taskCount = tasks.length;
|
|
434
|
+
|
|
435
|
+
// Header
|
|
436
|
+
console.log('');
|
|
437
|
+
console.log(C.cyan + box(
|
|
438
|
+
`${name} v${version}`,
|
|
439
|
+
`Project: ${PROJECT_NAME}`,
|
|
440
|
+
`Tasks: ${taskCount} found`
|
|
441
|
+
) + C.reset);
|
|
442
|
+
console.log('');
|
|
443
|
+
|
|
444
|
+
if (args.includes('--test')) {
|
|
445
|
+
const f1 = path.join(TODO_DIR, 'claskit-test-task-1.md');
|
|
446
|
+
const f2 = path.join(TODO_DIR, 'claskit-test-task-2.md');
|
|
447
|
+
fs.writeFileSync(f1, TEST_TASK_1);
|
|
448
|
+
fs.writeFileSync(f2, TEST_TASK_2);
|
|
449
|
+
console.log(`${C.green}Test tasks created:${C.reset}`);
|
|
450
|
+
console.log(` ${C.dim}${f1}${C.reset}`);
|
|
451
|
+
console.log(` ${C.dim}${f2}${C.reset}`);
|
|
452
|
+
console.log('');
|
|
453
|
+
console.log(`${C.bold}Now run:${C.reset} ${C.cyan}claskit --now${C.reset} (or ${C.cyan}pnpm claskit --now${C.reset})`);
|
|
454
|
+
console.log(`${C.dim}Select both test tasks → confirm → watch Claude create the output files.${C.reset}`);
|
|
455
|
+
console.log('');
|
|
456
|
+
console.log(`${C.dim}When done, run: claskit --clean-test${C.reset}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (args.includes('--clean-test')) {
|
|
461
|
+
const toDelete = [
|
|
462
|
+
path.join(TODO_DIR, 'claskit-test-task-1.md'),
|
|
463
|
+
path.join(TODO_DIR, 'claskit-test-task-2.md'),
|
|
464
|
+
path.join(DONE_DIR, 'claskit-test-task-1.md'),
|
|
465
|
+
path.join(DONE_DIR, 'claskit-test-task-2.md'),
|
|
466
|
+
'.claude/tasks/claskit-test-1.txt',
|
|
467
|
+
'.claude/tasks/claskit-test-2.txt',
|
|
468
|
+
];
|
|
469
|
+
let removed = 0;
|
|
470
|
+
toDelete.forEach(f => {
|
|
471
|
+
if (fs.existsSync(f)) { fs.unlinkSync(f); removed++; }
|
|
472
|
+
});
|
|
473
|
+
console.log(`${C.green}Cleaned up ${removed} test file(s).${C.reset}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (taskCount === 0) {
|
|
478
|
+
console.log(`${C.yellow}No tasks found in ${TODO_DIR}${C.reset}`);
|
|
479
|
+
exit(0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// List tasks
|
|
483
|
+
console.log(`${C.bold}Queued tasks:${C.reset}`);
|
|
484
|
+
tasks.forEach((f, i) => {
|
|
485
|
+
const title = taskTitle(f);
|
|
486
|
+
console.log(` ${C.dim}${i + 1}.${C.reset} ${title}`);
|
|
487
|
+
console.log(` ${C.dim}${path.basename(f)}${C.reset}`);
|
|
488
|
+
});
|
|
489
|
+
console.log('');
|
|
490
|
+
|
|
491
|
+
if (args.includes('--now')) {
|
|
492
|
+
await launchReal(tasks);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Interactive menu
|
|
497
|
+
console.log(`${C.bold}When to run?${C.reset}`);
|
|
498
|
+
const choice = await menu(['Now', 'Schedule (HH:MM)', 'Exit']);
|
|
499
|
+
|
|
500
|
+
switch (choice) {
|
|
501
|
+
case '1':
|
|
502
|
+
await launchReal(tasks);
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case '2':
|
|
506
|
+
await scheduleRun(tasks);
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
case '3':
|
|
510
|
+
default:
|
|
511
|
+
console.log(`\n${C.dim}Bye.${C.reset}`);
|
|
512
|
+
exit(0);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (require.main === module) {
|
|
517
|
+
main().catch(err => {
|
|
518
|
+
console.error(`${C.red}${err.message}${C.reset}`);
|
|
519
|
+
exit(1);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = { pad, box, fmtDuration, taskTitle, buildPrompt };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claskit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Autonomous Claude Code task runner for spec-driven development",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "phucbm",
|
|
7
|
+
"url": "https://github.com/phucbm"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/phucbm/claskit#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/phucbm/claskit.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/phucbm/claskit/issues"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"claskit": "bin/claskit.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./bin/claskit.js",
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test test/*.test.js"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"ai",
|
|
37
|
+
"task-runner",
|
|
38
|
+
"automation",
|
|
39
|
+
"cli",
|
|
40
|
+
"spec-driven",
|
|
41
|
+
"autonomous"
|
|
42
|
+
]
|
|
43
|
+
}
|