@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/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
+ ![/loop command hint](./docs/images/loop-1.png)
101
+
102
+ Example of executing a loop command:
103
+
104
+ ![/loop execution example](./docs/images/loop-2.png)
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.
@@ -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
+ }
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('./bin/cc-helper.js');
4
+
5
+ // Run the main function
6
+ main().catch((err) => {
7
+ console.error('Error:', err.message);
8
+ process.exit(1);
9
+ });