@unitsvc/cc-helper 1.0.6
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/LICENSE +661 -0
- package/README.md +126 -0
- package/bin/cc-helper.js +331 -0
- package/index.js +9 -0
- package/install.js +253 -0
- package/package.json +38 -0
- package/uninstall.js +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# cc-helper
|
|
2
|
+
|
|
3
|
+
> Enable/disable the `/loop` feature in Claude Code CLI
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Claude Code v2.1.71+
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @anthropic-ai/claude-code@v2.1.71
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Using npx (no install)
|
|
17
|
+
npx @unitsvc/cc-helper enable
|
|
18
|
+
|
|
19
|
+
# Or install globally
|
|
20
|
+
npm install -g @unitsvc/cc-helper
|
|
21
|
+
cc-helper enable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Command
|
|
27
|
+
|
|
28
|
+
| Command | Description |
|
|
29
|
+
| --------------------- | ----------------------- |
|
|
30
|
+
| `cc-helper enable` | Enable `/loop` feature |
|
|
31
|
+
| `cc-helper disable` | Restore original |
|
|
32
|
+
| `cc-helper status` | Check current status |
|
|
33
|
+
| `cc-helper uninstall` | Uninstall (clean cache) |
|
|
34
|
+
|
|
35
|
+
## Sponsors
|
|
36
|
+
|
|
37
|
+
🚀 **GLM Coding Plan**
|
|
38
|
+
|
|
39
|
+
👉 [Enjoy full support for Claude Code, Cline, and 20+ top coding tools — starting at just $10/month. Subscribe now and grab the limited-time deal!](https://z.ai/subscribe?ic=1YVKN4IRCQ)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## What is `/loop`?
|
|
44
|
+
|
|
45
|
+
`/loop` is a built-in command in Claude Code CLI that lets you schedule recurring prompts. It's useful for:
|
|
46
|
+
|
|
47
|
+
- Polling deployments or builds
|
|
48
|
+
- Babysitting PRs
|
|
49
|
+
- Setting reminders
|
|
50
|
+
- Running workflows on an interval
|
|
51
|
+
|
|
52
|
+
### Usage Syntax
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/loop [interval] <prompt>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Examples:**
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
/loop 5m check if the deployment finished
|
|
62
|
+
/loop 30m /review-pr 1234
|
|
63
|
+
/loop remind me to push the release at 3pm
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Interval Formats
|
|
67
|
+
|
|
68
|
+
| Form | Example | Parsed Interval |
|
|
69
|
+
| ----------------------- | --------------------------- | ---------------------------- |
|
|
70
|
+
| Leading token | `/loop 30m check` | every 30 minutes |
|
|
71
|
+
| Trailing `every` clause | `/loop check every 2 hours` | every 2 hours |
|
|
72
|
+
| No interval | `/loop check` | defaults to every 10 minutes |
|
|
73
|
+
|
|
74
|
+
Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days)
|
|
75
|
+
|
|
76
|
+
### Key Features
|
|
77
|
+
|
|
78
|
+
- **Session-scoped**: Tasks live in the current Claude Code session and are gone when you exit
|
|
79
|
+
- **Auto-expiry**: Recurring tasks expire after 3 days
|
|
80
|
+
- **Jitter protection**: Adds small offsets to prevent API thundering herd
|
|
81
|
+
- **Low priority**: Scheduled prompts fire between your turns, not while Claude is busy
|
|
82
|
+
|
|
83
|
+
### Managing Tasks
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
what scheduled tasks do I have? # List all tasks
|
|
87
|
+
cancel the deploy check job # Cancel by description or ID
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Features
|
|
91
|
+
|
|
92
|
+
- Enable `/loop` with one command
|
|
93
|
+
- Easy restore functionality
|
|
94
|
+
- Automatic backup
|
|
95
|
+
|
|
96
|
+
### Examples
|
|
97
|
+
|
|
98
|
+
After enabling, use the `/loop` command in Claude Code:
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
102
|
+
Example of executing a loop command:
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
## Platforms
|
|
107
|
+
|
|
108
|
+
- macOS (amd64, arm64)
|
|
109
|
+
- Linux (amd64, arm64)
|
|
110
|
+
- Windows (amd64)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
AGPL-3.0 - see [LICENSE](./LICENSE)
|
|
115
|
+
|
|
116
|
+
## Security
|
|
117
|
+
|
|
118
|
+
### Reporting Vulnerabilities
|
|
119
|
+
|
|
120
|
+
If you discover a security vulnerability in cc-helper, please report it responsibly:
|
|
121
|
+
|
|
122
|
+
1. **Do not** open a public issue
|
|
123
|
+
2. Send an email to the maintainer with details
|
|
124
|
+
3. Allow reasonable time for the issue to be addressed before public disclosure
|
|
125
|
+
|
|
126
|
+
We take security seriously and will respond to reports as quickly as possible.
|
package/bin/cc-helper.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawn, execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const GITHUB_OWNER = 'next-bin';
|
|
10
|
+
const GITHUB_REPO = 'cc-helper';
|
|
11
|
+
const CACHE_DIR = path.join(os.homedir(), '.cache', 'cc-helper');
|
|
12
|
+
const VERSION_FILE = path.join(CACHE_DIR, 'version.txt');
|
|
13
|
+
|
|
14
|
+
// Handle uninstall command directly
|
|
15
|
+
function handleUninstall() {
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(CACHE_DIR)) {
|
|
18
|
+
console.log('Removing cached binaries...');
|
|
19
|
+
fs.rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
20
|
+
console.log('Cleanup complete!');
|
|
21
|
+
} else {
|
|
22
|
+
console.log('No cached binaries found.');
|
|
23
|
+
}
|
|
24
|
+
console.log('\nTo complete uninstallation, run:');
|
|
25
|
+
console.log(' npm uninstall -g @unitsvc/cc-helper');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Error during uninstall:', err.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read version from package.json
|
|
34
|
+
function getPackageVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
38
|
+
return pkg.version;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new Error('Cannot read package version: ' + err.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getPlatform() {
|
|
45
|
+
const platform = os.platform();
|
|
46
|
+
const arch = os.arch();
|
|
47
|
+
|
|
48
|
+
const platformMap = {
|
|
49
|
+
'darwin': 'darwin',
|
|
50
|
+
'linux': 'linux',
|
|
51
|
+
'win32': 'windows'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const archMap = {
|
|
55
|
+
'x64': 'amd64',
|
|
56
|
+
'arm64': 'arm64'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mappedPlatform = platformMap[platform];
|
|
60
|
+
const mappedArch = archMap[arch];
|
|
61
|
+
|
|
62
|
+
if (!mappedPlatform || !mappedArch) {
|
|
63
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Windows arm64 not supported by GoReleaser
|
|
67
|
+
if (platform === 'win32' && arch === 'arm64') {
|
|
68
|
+
throw new Error('Windows arm64 is not supported. Please use Windows amd64 or macOS/Linux arm64.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { platform: mappedPlatform, arch: mappedArch };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getArchiveName(version, platform, arch) {
|
|
75
|
+
// GitHub release uses version without 'v' prefix in filename
|
|
76
|
+
const ver = version.replace(/^v/, '');
|
|
77
|
+
if (platform === 'windows') {
|
|
78
|
+
return `cc-helper_${ver}_${platform}_${arch}.zip`;
|
|
79
|
+
}
|
|
80
|
+
return `cc-helper_${ver}_${platform}_${arch}.tar.gz`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getBinaryName(platform) {
|
|
84
|
+
return platform === 'windows' ? 'cc-helper.exe' : 'cc-helper';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getBinaryPath() {
|
|
88
|
+
const { platform } = getPlatform();
|
|
89
|
+
const binaryName = getBinaryName(platform);
|
|
90
|
+
return path.join(CACHE_DIR, binaryName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function downloadFile(url, dest) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const file = fs.createWriteStream(dest);
|
|
96
|
+
|
|
97
|
+
const requestUrl = url.startsWith('https://') ? url : 'https://github.com' + url;
|
|
98
|
+
|
|
99
|
+
const request = https.get(requestUrl, { headers: { 'User-Agent': 'cc-helper-npm-installer' } }, (response) => {
|
|
100
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
101
|
+
file.close();
|
|
102
|
+
if (fs.existsSync(dest)) {
|
|
103
|
+
fs.unlinkSync(dest);
|
|
104
|
+
}
|
|
105
|
+
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (response.statusCode !== 200) {
|
|
110
|
+
file.close();
|
|
111
|
+
if (fs.existsSync(dest)) {
|
|
112
|
+
fs.unlinkSync(dest);
|
|
113
|
+
}
|
|
114
|
+
reject(new Error(`Download failed with status ${response.statusCode}: ${url}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
response.pipe(file);
|
|
119
|
+
|
|
120
|
+
file.on('finish', () => {
|
|
121
|
+
file.close();
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
request.on('error', (err) => {
|
|
127
|
+
if (fs.existsSync(dest)) {
|
|
128
|
+
fs.unlinkSync(dest);
|
|
129
|
+
}
|
|
130
|
+
reject(err);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
file.on('error', (err) => {
|
|
134
|
+
if (fs.existsSync(dest)) {
|
|
135
|
+
fs.unlinkSync(dest);
|
|
136
|
+
}
|
|
137
|
+
reject(err);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractTarGz(tarGzPath, destDir) {
|
|
143
|
+
try {
|
|
144
|
+
// Use system tar command (available on macOS and Linux)
|
|
145
|
+
execSync(`tar -xzf "${tarGzPath}" -C "${destDir}"`, { stdio: 'pipe' });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
throw new Error(`Failed to extract tar.gz archive: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractZip(zipPath, destDir) {
|
|
152
|
+
try {
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
// Use PowerShell on Windows (built-in, no external dependencies)
|
|
155
|
+
// -NoProfile: Skip loading user profile scripts (faster, more reliable)
|
|
156
|
+
// -ExecutionPolicy Bypass: Allow script execution without policy restrictions
|
|
157
|
+
// -Command: Execute inline command
|
|
158
|
+
const psCommand = `
|
|
159
|
+
try {
|
|
160
|
+
Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force -ErrorAction Stop
|
|
161
|
+
exit 0
|
|
162
|
+
} catch {
|
|
163
|
+
exit 1
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psCommand}"`, { stdio: 'pipe' });
|
|
167
|
+
} else {
|
|
168
|
+
// Unix/Linux/macOS - use unzip
|
|
169
|
+
// Check if unzip is available first
|
|
170
|
+
try {
|
|
171
|
+
execSync('which unzip', { stdio: 'pipe' });
|
|
172
|
+
} catch {
|
|
173
|
+
throw new Error(
|
|
174
|
+
'unzip command not found. Please install unzip:\n' +
|
|
175
|
+
' Ubuntu/Debian: sudo apt-get install unzip\n' +
|
|
176
|
+
' macOS: brew install unzip\n' +
|
|
177
|
+
' CentOS/RHEL: sudo yum install unzip'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err.message.includes('unzip command not found')) {
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Failed to extract zip archive: ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function extractArchive(archivePath, destDir, platform) {
|
|
191
|
+
if (platform === 'windows') {
|
|
192
|
+
extractZip(archivePath, destDir);
|
|
193
|
+
} else {
|
|
194
|
+
extractTarGz(archivePath, destDir);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findBinary(dir, platform) {
|
|
199
|
+
const binaryName = getBinaryName(platform);
|
|
200
|
+
const files = fs.readdirSync(dir);
|
|
201
|
+
|
|
202
|
+
// First check root
|
|
203
|
+
const rootBinary = path.join(dir, binaryName);
|
|
204
|
+
if (fs.existsSync(rootBinary)) {
|
|
205
|
+
return rootBinary;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check subdirectories
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const fullPath = path.join(dir, file);
|
|
211
|
+
const stat = fs.statSync(fullPath);
|
|
212
|
+
if (stat.isDirectory()) {
|
|
213
|
+
const binaryInSubdir = path.join(fullPath, binaryName);
|
|
214
|
+
if (fs.existsSync(binaryInSubdir)) {
|
|
215
|
+
return binaryInSubdir;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new Error(`Binary ${binaryName} not found in archive`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function ensureBinary() {
|
|
224
|
+
const binaryPath = getBinaryPath();
|
|
225
|
+
const packageVersion = getPackageVersion();
|
|
226
|
+
|
|
227
|
+
// Check if binary exists and matches package version
|
|
228
|
+
if (fs.existsSync(binaryPath) && fs.existsSync(VERSION_FILE)) {
|
|
229
|
+
const currentVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim();
|
|
230
|
+
if (currentVersion === packageVersion) {
|
|
231
|
+
return binaryPath;
|
|
232
|
+
}
|
|
233
|
+
console.log(`Updating cc-helper from ${currentVersion} to ${packageVersion}...`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Create cache directory
|
|
237
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
238
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Download archive with fixed version from package.json
|
|
242
|
+
const { platform, arch } = getPlatform();
|
|
243
|
+
const archiveName = getArchiveName(packageVersion, platform, arch);
|
|
244
|
+
const tagVersion = packageVersion.startsWith('v') ? packageVersion : `v${packageVersion}`;
|
|
245
|
+
const downloadUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/${tagVersion}/${archiveName}`;
|
|
246
|
+
const archivePath = path.join(CACHE_DIR, archiveName);
|
|
247
|
+
|
|
248
|
+
console.log(`Downloading cc-helper ${packageVersion} for ${platform}-${arch}...`);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await downloadFile(downloadUrl, archivePath);
|
|
252
|
+
|
|
253
|
+
// Extract archive
|
|
254
|
+
console.log('Extracting archive...');
|
|
255
|
+
const tempDir = path.join(CACHE_DIR, 'temp_extract_' + Date.now());
|
|
256
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
257
|
+
|
|
258
|
+
await extractArchive(archivePath, tempDir, platform);
|
|
259
|
+
|
|
260
|
+
// Find and move binary
|
|
261
|
+
const extractedBinaryPath = findBinary(tempDir, platform);
|
|
262
|
+
|
|
263
|
+
// Remove old binary if exists
|
|
264
|
+
if (fs.existsSync(binaryPath)) {
|
|
265
|
+
fs.unlinkSync(binaryPath);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Move binary to cache dir
|
|
269
|
+
fs.copyFileSync(extractedBinaryPath, binaryPath);
|
|
270
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
271
|
+
|
|
272
|
+
// Clean up
|
|
273
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
274
|
+
fs.unlinkSync(archivePath);
|
|
275
|
+
|
|
276
|
+
// Save version
|
|
277
|
+
fs.writeFileSync(VERSION_FILE, packageVersion);
|
|
278
|
+
|
|
279
|
+
console.log('Download complete!');
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// Clean up on failure
|
|
282
|
+
if (fs.existsSync(archivePath)) {
|
|
283
|
+
fs.unlinkSync(archivePath);
|
|
284
|
+
}
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return binaryPath;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
// Handle uninstall command
|
|
293
|
+
const args = process.argv.slice(2);
|
|
294
|
+
if (args.length > 0 && args[0] === 'uninstall') {
|
|
295
|
+
handleUninstall();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const binaryPath = await ensureBinary();
|
|
301
|
+
|
|
302
|
+
// Run the binary with all arguments
|
|
303
|
+
// Windows requires shell: true for .exe files
|
|
304
|
+
const isWindows = process.platform === 'win32';
|
|
305
|
+
const child = spawn(binaryPath, args, {
|
|
306
|
+
stdio: 'inherit',
|
|
307
|
+
detached: false,
|
|
308
|
+
shell: isWindows
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
child.on('close', (code) => {
|
|
312
|
+
process.exit(code);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
child.on('error', (err) => {
|
|
316
|
+
console.error('Failed to start cc-helper:', err.message);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error('Error:', err.message);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Export for use as module
|
|
326
|
+
module.exports = { main, ensureBinary, getBinaryPath, getPlatform, getPackageVersion };
|
|
327
|
+
|
|
328
|
+
// Run if called directly
|
|
329
|
+
if (require.main === module) {
|
|
330
|
+
main();
|
|
331
|
+
}
|