clamper-ai 1.1.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/PRD.md +416 -0
- package/README.md +156 -0
- package/bin/clamper.mjs +66 -0
- package/dashboard/app.js +287 -0
- package/dashboard/index.html +137 -0
- package/dashboard/style.css +121 -0
- package/package.json +24 -0
- package/skills/clawkit-sync/SKILL.md +160 -0
- package/skills/code-mentor/SKILL.md +85 -0
- package/skills/csv-pipeline/SKILL.md +82 -0
- package/skills/developer/SKILL.md +57 -0
- package/skills/image/SKILL.md +71 -0
- package/skills/json/SKILL.md +59 -0
- package/skills/productivity/SKILL.md +68 -0
- package/skills/quick-reminders/SKILL.md +70 -0
- package/skills/svg-draw/SKILL.md +75 -0
- package/skills/weather/SKILL.md +72 -0
- package/skills/workspace-manager/SKILL.md +69 -0
- package/src/cron.mjs +30 -0
- package/src/dashboard.mjs +69 -0
- package/src/doctor.mjs +69 -0
- package/src/init.mjs +145 -0
- package/src/log-activity.mjs +26 -0
- package/src/scaffold.mjs +57 -0
- package/src/skills.mjs +52 -0
- package/src/status.mjs +43 -0
- package/src/sync.mjs +585 -0
- package/src/update.mjs +100 -0
- package/src/upgrade.mjs +104 -0
- package/src/utils.mjs +67 -0
- package/src/validate.mjs +66 -0
- package/templates/AGENTS.md +77 -0
- package/templates/HEARTBEAT.md +12 -0
- package/templates/IDENTITY.md +7 -0
- package/templates/LEARNINGS.md +14 -0
- package/templates/MEMORY.md +14 -0
- package/templates/PROTOCOL_COST_EFFICIENCY.md +30 -0
- package/templates/SKILLS-INDEX.md +21 -0
- package/templates/SOUL.md +22 -0
- package/templates/TOOLS.md +14 -0
- package/templates/USER.md +5 -0
- package/templates/memory/knowledge/about-me.md +13 -0
- package/templates/scripts/nightly-consolidation.sh +54 -0
- package/templates/tasks/QUEUE.md +10 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: svg-draw
|
|
3
|
+
description: Create SVG images and convert them to PNG without external graphics libraries.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SVG Draw
|
|
7
|
+
|
|
8
|
+
Generate vector graphics using pure SVG code. No external libraries required.
|
|
9
|
+
|
|
10
|
+
## Creating SVGs
|
|
11
|
+
|
|
12
|
+
### Basic Structure
|
|
13
|
+
```xml
|
|
14
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
|
|
15
|
+
<rect width="400" height="400" fill="#1a1a2e"/> <!-- background -->
|
|
16
|
+
<!-- your content here -->
|
|
17
|
+
</svg>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Always include:** `xmlns`, `viewBox`, background `<rect>`, `width`/`height`.
|
|
21
|
+
|
|
22
|
+
### Common Elements
|
|
23
|
+
```xml
|
|
24
|
+
<!-- Shapes -->
|
|
25
|
+
<rect x="10" y="10" width="80" height="60" rx="10" fill="#3498db"/>
|
|
26
|
+
<circle cx="200" cy="200" r="50" fill="#e74c3c"/>
|
|
27
|
+
<ellipse cx="200" cy="200" rx="80" ry="50" fill="#2ecc71"/>
|
|
28
|
+
<polygon points="200,50 350,350 50,350" fill="#f39c12"/>
|
|
29
|
+
|
|
30
|
+
<!-- Paths (for complex shapes) -->
|
|
31
|
+
<path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="#333" fill="none" stroke-width="2"/>
|
|
32
|
+
|
|
33
|
+
<!-- Text -->
|
|
34
|
+
<text x="200" y="200" text-anchor="middle" font-family="Arial" font-size="24" fill="white">Hello</text>
|
|
35
|
+
|
|
36
|
+
<!-- Groups with transforms -->
|
|
37
|
+
<g transform="translate(100,100) rotate(45)">
|
|
38
|
+
<rect width="50" height="50" fill="blue"/>
|
|
39
|
+
</g>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Gradients & Effects
|
|
43
|
+
```xml
|
|
44
|
+
<defs>
|
|
45
|
+
<linearGradient id="sky" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
46
|
+
<stop offset="0%" stop-color="#87CEEB"/>
|
|
47
|
+
<stop offset="100%" stop-color="#E0F7FA"/>
|
|
48
|
+
</linearGradient>
|
|
49
|
+
<filter id="shadow">
|
|
50
|
+
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.3"/>
|
|
51
|
+
</filter>
|
|
52
|
+
</defs>
|
|
53
|
+
<rect width="400" height="400" fill="url(#sky)"/>
|
|
54
|
+
<circle cx="200" cy="200" r="50" fill="gold" filter="url(#shadow)"/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Converting to PNG
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Using rsvg-convert (install: apt install librsvg2-bin)
|
|
61
|
+
rsvg-convert -w 800 -h 800 input.svg -o output.png
|
|
62
|
+
|
|
63
|
+
# Using ImageMagick
|
|
64
|
+
convert -density 150 input.svg output.png
|
|
65
|
+
|
|
66
|
+
# Using Inkscape CLI
|
|
67
|
+
inkscape input.svg --export-type=png --export-filename=output.png --export-width=800
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Tips
|
|
71
|
+
- Use `viewBox` for responsive scaling — set it once, resize via width/height
|
|
72
|
+
- Layer elements back to front (SVG paints in document order)
|
|
73
|
+
- Use `opacity` for transparency, `stroke-dasharray` for dashed lines
|
|
74
|
+
- Emoji work in `<text>` elements on most renderers
|
|
75
|
+
- Test complex SVGs in a browser before converting
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: weather
|
|
3
|
+
description: Get current weather and forecasts using wttr.in (no API key required).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Weather
|
|
7
|
+
|
|
8
|
+
Get weather information for any location using the free wttr.in service.
|
|
9
|
+
|
|
10
|
+
## Quick Commands
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Current weather for a city
|
|
14
|
+
curl -s "wttr.in/Toronto?format=3"
|
|
15
|
+
# Output: Toronto: ⛅️ +5°C
|
|
16
|
+
|
|
17
|
+
# Detailed forecast (3 days)
|
|
18
|
+
curl -s "wttr.in/Toronto"
|
|
19
|
+
|
|
20
|
+
# One-line format with custom fields
|
|
21
|
+
curl -s "wttr.in/Toronto?format=%l:+%c+%t+%h+%w"
|
|
22
|
+
# Output: Toronto: ⛅️ +5°C 65% →15km/h
|
|
23
|
+
|
|
24
|
+
# JSON format for parsing
|
|
25
|
+
curl -s "wttr.in/Toronto?format=j1"
|
|
26
|
+
|
|
27
|
+
# Specific day (0=today, 1=tomorrow, 2=day after)
|
|
28
|
+
curl -s "wttr.in/Toronto?1"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Format Codes
|
|
32
|
+
|
|
33
|
+
| Code | Meaning |
|
|
34
|
+
|------|---------|
|
|
35
|
+
| `%c` | Weather condition icon |
|
|
36
|
+
| `%C` | Weather condition text |
|
|
37
|
+
| `%t` | Temperature |
|
|
38
|
+
| `%f` | Feels like |
|
|
39
|
+
| `%h` | Humidity |
|
|
40
|
+
| `%w` | Wind |
|
|
41
|
+
| `%p` | Precipitation (mm) |
|
|
42
|
+
| `%P` | Pressure |
|
|
43
|
+
| `%D` | Dawn |
|
|
44
|
+
| `%S` | Sunrise |
|
|
45
|
+
| `%s` | Sunset |
|
|
46
|
+
| `%l` | Location |
|
|
47
|
+
|
|
48
|
+
## Usage Patterns
|
|
49
|
+
|
|
50
|
+
**Morning briefing:** `curl -s "wttr.in/{city}?format=%c+%t+feels+like+%f,+%C"`
|
|
51
|
+
|
|
52
|
+
**Should I bring an umbrella?** Check precipitation:
|
|
53
|
+
```bash
|
|
54
|
+
curl -s "wttr.in/{city}?format=j1" | python3 -c "
|
|
55
|
+
import json,sys; d=json.load(sys.stdin)
|
|
56
|
+
rain=float(d['weather'][0]['hourly'][4].get('precipMM','0'))
|
|
57
|
+
print('☔ Yes, rain expected' if rain>0.5 else '☀️ No rain expected')
|
|
58
|
+
"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Multi-city comparison:**
|
|
62
|
+
```bash
|
|
63
|
+
for city in Toronto NYC London; do
|
|
64
|
+
echo "$(curl -s "wttr.in/$city?format=3")"
|
|
65
|
+
done
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
- No API key needed
|
|
70
|
+
- Rate limited — don't hit it more than a few times per minute
|
|
71
|
+
- Supports airports (ICAO codes), landmarks, IP-based geolocation
|
|
72
|
+
- Add `?lang=fr` for other languages
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workspace-manager
|
|
3
|
+
description: Workspace setup and organization assistant.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workspace Manager
|
|
7
|
+
|
|
8
|
+
Help users create and maintain well-organized workspaces.
|
|
9
|
+
|
|
10
|
+
## Workspace Structure Templates
|
|
11
|
+
|
|
12
|
+
### Engineering
|
|
13
|
+
```
|
|
14
|
+
infrastructure/ # Cloud & infra docs
|
|
15
|
+
devops/ # CI/CD, pipelines
|
|
16
|
+
architecture/ # ADRs, system designs
|
|
17
|
+
security/ # Audits, compliance
|
|
18
|
+
team/ # Processes, hiring
|
|
19
|
+
daily-notes/ # Daily logs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Business
|
|
23
|
+
```
|
|
24
|
+
clients/ # Per-client folders
|
|
25
|
+
projects/ # Active projects
|
|
26
|
+
meetings/ # Notes & agendas
|
|
27
|
+
strategy/ # Plans, OKRs
|
|
28
|
+
templates/ # Reusable docs
|
|
29
|
+
archive/ # Completed work
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Personal Knowledge
|
|
33
|
+
```
|
|
34
|
+
areas/ # Life areas (health, finance, career)
|
|
35
|
+
projects/ # Active projects
|
|
36
|
+
resources/ # Reference material
|
|
37
|
+
archive/ # Done projects
|
|
38
|
+
inbox/ # Unsorted notes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## File Naming Conventions
|
|
42
|
+
- Dates: `YYYY-MM-DD` prefix for time-sensitive files
|
|
43
|
+
- Projects: `project-name-description.md`
|
|
44
|
+
- No spaces: use hyphens (`my-document.md`)
|
|
45
|
+
- Lowercase everything
|
|
46
|
+
|
|
47
|
+
## Organization Principles
|
|
48
|
+
1. **Flat over deep** — Max 3 levels of nesting
|
|
49
|
+
2. **Action-oriented** — Name folders by what you DO, not what things ARE
|
|
50
|
+
3. **Archive aggressively** — Done projects → archive/
|
|
51
|
+
4. **One inbox** — Single capture point, sort regularly
|
|
52
|
+
5. **README in every folder** — What's this folder for? What goes here?
|
|
53
|
+
|
|
54
|
+
## Maintenance Tasks
|
|
55
|
+
- Weekly: Process inbox, archive completed projects
|
|
56
|
+
- Monthly: Review folder structure, clean unused files
|
|
57
|
+
- Quarterly: Major reorganization if needed
|
|
58
|
+
|
|
59
|
+
## Quick Commands
|
|
60
|
+
```bash
|
|
61
|
+
# Find large files
|
|
62
|
+
find . -type f -size +10M | head -20
|
|
63
|
+
|
|
64
|
+
# Find recently modified
|
|
65
|
+
find . -type f -mtime -7 -name "*.md" | head -20
|
|
66
|
+
|
|
67
|
+
# Count files per directory
|
|
68
|
+
find . -maxdepth 2 -type f | cut -d/ -f2 | sort | uniq -c | sort -rn
|
|
69
|
+
```
|
package/src/cron.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { log, cmdExists } from './utils.mjs';
|
|
3
|
+
|
|
4
|
+
export function registerCron(workspace) {
|
|
5
|
+
if (process.platform === 'win32') {
|
|
6
|
+
log.warn('Cron not available on Windows — set up nightly consolidation manually');
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const scriptPath = `${workspace}/scripts/nightly-consolidation.sh`;
|
|
11
|
+
const cronLine = `0 2 * * * CLAMPER_WORKSPACE="${workspace}" bash "${scriptPath}" >> "${workspace}/scripts/consolidation.log" 2>&1`;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
let existing = '';
|
|
15
|
+
try { existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { /* empty crontab */ }
|
|
16
|
+
|
|
17
|
+
if (existing.includes('nightly-consolidation.sh')) {
|
|
18
|
+
log.info('Nightly consolidation cron already registered');
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const updated = existing.trimEnd() + '\n' + cronLine + '\n';
|
|
23
|
+
execSync(`echo "${updated.replace(/"/g, '\\"')}" | crontab -`, { encoding: 'utf8' });
|
|
24
|
+
log.ok('Registered nightly consolidation cron (2 AM daily)');
|
|
25
|
+
return true;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
log.warn('Could not register cron job — set up nightly consolidation manually');
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, extname } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { c, log } from './utils.mjs';
|
|
7
|
+
|
|
8
|
+
const MIME = {
|
|
9
|
+
'.html': 'text/html',
|
|
10
|
+
'.css': 'text/css',
|
|
11
|
+
'.js': 'application/javascript',
|
|
12
|
+
'.json': 'application/json',
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function runDashboard(flags) {
|
|
18
|
+
const port = parseInt(flags.port) || 8080;
|
|
19
|
+
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
|
20
|
+
const dashDir = join(thisDir, '..', 'dashboard');
|
|
21
|
+
|
|
22
|
+
if (!existsSync(join(dashDir, 'index.html'))) {
|
|
23
|
+
log.err('Dashboard files not found. Reinstall Clamper.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const server = createServer((req, res) => {
|
|
28
|
+
let filePath = req.url === '/' ? '/index.html' : req.url;
|
|
29
|
+
filePath = filePath.split('?')[0]; // strip query
|
|
30
|
+
const full = join(dashDir, filePath);
|
|
31
|
+
|
|
32
|
+
if (!existsSync(full)) {
|
|
33
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
34
|
+
res.end('Not found');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ext = extname(full);
|
|
39
|
+
const mime = MIME[ext] || 'application/octet-stream';
|
|
40
|
+
try {
|
|
41
|
+
const data = readFileSync(full);
|
|
42
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
43
|
+
res.end(data);
|
|
44
|
+
} catch {
|
|
45
|
+
res.writeHead(500);
|
|
46
|
+
res.end('Error');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
server.listen(port, '0.0.0.0', () => {
|
|
51
|
+
const url = `http://localhost:${port}`;
|
|
52
|
+
console.log(`\n ${c.green}⚡${c.reset} Dashboard running at ${c.cyan}${url}${c.reset}\n`);
|
|
53
|
+
console.log(` ${c.dim}Press Ctrl+C to stop${c.reset}\n`);
|
|
54
|
+
|
|
55
|
+
// Open browser (non-blocking — execSync blocks the event loop!)
|
|
56
|
+
try {
|
|
57
|
+
const { exec: execNB } = require !== undefined ? require('child_process') : {};
|
|
58
|
+
const platform = process.platform;
|
|
59
|
+
const cmd = platform === 'darwin' ? `open ${url}` : platform === 'win32' ? `start ${url}` : `xdg-open ${url} 2>/dev/null || true`;
|
|
60
|
+
import('child_process').then(cp => cp.exec(cmd));
|
|
61
|
+
} catch {}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
process.on('SIGINT', () => {
|
|
65
|
+
console.log(`\n ${c.dim}Shutting down...${c.reset}`);
|
|
66
|
+
server.close();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
}
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, accessSync, constants } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { c, log, detectWorkspace, cmdExists } from './utils.mjs';
|
|
4
|
+
|
|
5
|
+
export function runDoctor() {
|
|
6
|
+
const checks = [];
|
|
7
|
+
const pass = (name) => checks.push({ name, ok: true });
|
|
8
|
+
const fail = (name, hint) => checks.push({ name, ok: false, hint });
|
|
9
|
+
|
|
10
|
+
// 1. OpenClaw installed
|
|
11
|
+
cmdExists('openclaw') ? pass('OpenClaw CLI installed') : fail('OpenClaw CLI installed', 'Install: npm i -g openclaw');
|
|
12
|
+
|
|
13
|
+
// 2. Workspace
|
|
14
|
+
const workspace = detectWorkspace();
|
|
15
|
+
if (workspace && existsSync(workspace)) {
|
|
16
|
+
pass('Workspace directory exists');
|
|
17
|
+
} else {
|
|
18
|
+
fail('Workspace directory exists', 'Set CLAMPER_WORKSPACE env var');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (workspace) {
|
|
22
|
+
// 3. AGENTS.md
|
|
23
|
+
existsSync(join(workspace, 'AGENTS.md')) ? pass('AGENTS.md present') : fail('AGENTS.md present', 'Run: clamper init');
|
|
24
|
+
|
|
25
|
+
// 4. Config
|
|
26
|
+
const cfgPath = join(workspace, 'skills', '.clamper', 'config.json');
|
|
27
|
+
if (existsSync(cfgPath)) {
|
|
28
|
+
try { JSON.parse(readFileSync(cfgPath, 'utf8')); pass('Config valid'); }
|
|
29
|
+
catch { fail('Config valid', 'config.json is corrupt — re-run clamper init'); }
|
|
30
|
+
} else {
|
|
31
|
+
fail('Config valid', 'Run: clamper init');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 5. Skills
|
|
35
|
+
const sd = join(workspace, 'skills');
|
|
36
|
+
if (existsSync(sd)) {
|
|
37
|
+
const cnt = readdirSync(sd, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('.')).length;
|
|
38
|
+
cnt > 0 ? pass(`Skills installed (${cnt})`) : fail('Skills installed', 'Run: clamper init');
|
|
39
|
+
} else {
|
|
40
|
+
fail('Skills installed', 'Run: clamper init');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 6. Consolidation script
|
|
44
|
+
const cs = join(workspace, 'scripts/nightly-consolidation.sh');
|
|
45
|
+
if (existsSync(cs)) {
|
|
46
|
+
try { accessSync(cs, constants.X_OK); pass('Consolidation script executable'); }
|
|
47
|
+
catch { fail('Consolidation script executable', `Run: chmod +x ${cs}`); }
|
|
48
|
+
} else {
|
|
49
|
+
fail('Consolidation script present', 'Run: clamper init');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 7. Memory dirs
|
|
53
|
+
existsSync(join(workspace, 'memory/daily')) && existsSync(join(workspace, 'memory/knowledge'))
|
|
54
|
+
? pass('Memory directories') : fail('Memory directories', 'Run: clamper init');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 5. API key
|
|
58
|
+
process.env.CLAMPER_API_KEY ? pass('API key set') : fail('API key set', 'Set CLAMPER_API_KEY env var');
|
|
59
|
+
|
|
60
|
+
console.log(`\n${c.bold}${c.cyan}🩺 Clamper Doctor${c.reset}\n`);
|
|
61
|
+
for (const ch of checks) {
|
|
62
|
+
const icon = ch.ok ? `${c.green}✔${c.reset}` : `${c.red}✖${c.reset}`;
|
|
63
|
+
console.log(` ${icon} ${ch.name}`);
|
|
64
|
+
if (!ch.ok && ch.hint) console.log(` ${c.dim}→ ${ch.hint}${c.reset}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const passed = checks.filter(c => c.ok).length;
|
|
68
|
+
console.log(`\n ${passed}/${checks.length} checks passed\n`);
|
|
69
|
+
}
|
package/src/init.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { c, log, detectWorkspace, detectTimezone, today } from './utils.mjs';
|
|
5
|
+
import { readApiKey, parseTier, validateKey } from './validate.mjs';
|
|
6
|
+
import { scaffoldWorkspace, backupWorkspace } from './scaffold.mjs';
|
|
7
|
+
import { installSkills } from './skills.mjs';
|
|
8
|
+
|
|
9
|
+
function ask(rl, question, defaultVal) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
rl.question(`${c.cyan}?${c.reset} ${question} ${c.dim}(${defaultVal})${c.reset} `, (answer) => {
|
|
12
|
+
resolve(answer.trim() || defaultVal);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runInit(flags) {
|
|
18
|
+
const yes = flags.yes || flags.y;
|
|
19
|
+
const force = flags.force;
|
|
20
|
+
|
|
21
|
+
console.log(`\n${c.bold}${c.magenta}🧰 Clamper Init${c.reset}\n`);
|
|
22
|
+
|
|
23
|
+
// Step 1: Detect workspace
|
|
24
|
+
log.step(1, 'Detecting workspace...');
|
|
25
|
+
let workspace = detectWorkspace();
|
|
26
|
+
|
|
27
|
+
const rl = !yes ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
28
|
+
|
|
29
|
+
if (!workspace && !yes) {
|
|
30
|
+
workspace = await ask(rl, 'Workspace path:', join(process.env.HOME || '~', '.openclaw', 'workspace'));
|
|
31
|
+
}
|
|
32
|
+
if (!workspace) {
|
|
33
|
+
workspace = join(process.env.HOME || '/tmp', '.openclaw', 'workspace');
|
|
34
|
+
}
|
|
35
|
+
mkdirSync(workspace, { recursive: true });
|
|
36
|
+
log.ok(`Workspace: ${workspace}`);
|
|
37
|
+
|
|
38
|
+
// Step 2: Check existing
|
|
39
|
+
log.step(2, 'Checking existing files...');
|
|
40
|
+
if (existsSync(join(workspace, 'AGENTS.md'))) {
|
|
41
|
+
if (!yes && !force) {
|
|
42
|
+
const overwrite = await ask(rl, 'Existing workspace detected. Backup & overwrite? (y/n):', 'y');
|
|
43
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
44
|
+
log.info('Aborted.');
|
|
45
|
+
rl?.close();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const bk = backupWorkspace(workspace);
|
|
50
|
+
log.ok(`Backed up to ${bk}`);
|
|
51
|
+
} else {
|
|
52
|
+
log.ok('Clean workspace');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 3: Read API key
|
|
56
|
+
log.step(3, 'Reading API key...');
|
|
57
|
+
let apiKey = readApiKey();
|
|
58
|
+
if (!apiKey) {
|
|
59
|
+
console.log(`\n ${c.yellow}An API key is required to use Clamper.${c.reset}`);
|
|
60
|
+
console.log(` ${c.dim}Get your free key at: ${c.cyan}https://clamper.tech/get-started${c.reset}\n`);
|
|
61
|
+
if (!yes) {
|
|
62
|
+
apiKey = await ask(rl, 'Paste your Clamper API key:', '');
|
|
63
|
+
}
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
console.log(`\n ${c.yellow}No API key provided. Continuing with limited free tier.${c.reset}`);
|
|
66
|
+
console.log(` ${c.dim}Run ${c.cyan}clamper upgrade --key YOUR_KEY${c.reset}${c.dim} later to activate.${c.reset}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const tier = parseTier(apiKey);
|
|
70
|
+
if (apiKey) {
|
|
71
|
+
log.ok(`API key found (tier: ${tier})`);
|
|
72
|
+
} else {
|
|
73
|
+
log.warn('No API key — limited free tier');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 4: Validate
|
|
77
|
+
log.step(4, 'Validating...');
|
|
78
|
+
const validation = await validateKey(apiKey);
|
|
79
|
+
if (!validation.valid && !validation.offline) {
|
|
80
|
+
log.err(`Validation failed: ${validation.error}`);
|
|
81
|
+
log.info('Continuing with free tier...');
|
|
82
|
+
} else if (validation.offline) {
|
|
83
|
+
log.warn('Offline mode — continuing with key prefix tier');
|
|
84
|
+
} else {
|
|
85
|
+
log.ok('API key validated');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const effectiveTier = validation.tier || tier;
|
|
89
|
+
|
|
90
|
+
// Step 5: Prompts
|
|
91
|
+
log.step(5, 'Configuration...');
|
|
92
|
+
const agentName = yes ? 'Atlas' : await ask(rl, 'What should your agent be called?', 'Atlas');
|
|
93
|
+
const userName = yes ? 'Human' : await ask(rl, "What's your name?", 'Human');
|
|
94
|
+
const timezone = yes ? detectTimezone() : await ask(rl, 'Your timezone?', detectTimezone());
|
|
95
|
+
|
|
96
|
+
rl?.close();
|
|
97
|
+
|
|
98
|
+
const vars = { agentName, userName, timezone, tier: effectiveTier, date: today() };
|
|
99
|
+
|
|
100
|
+
// Step 6: Scaffold
|
|
101
|
+
log.step(6, 'Scaffolding workspace...');
|
|
102
|
+
scaffoldWorkspace(workspace, vars);
|
|
103
|
+
log.ok('Template files written');
|
|
104
|
+
|
|
105
|
+
// Step 7: Skills
|
|
106
|
+
log.step(7, 'Installing skills...');
|
|
107
|
+
const skillCount = installSkills(workspace, effectiveTier);
|
|
108
|
+
|
|
109
|
+
// Step 8: Config
|
|
110
|
+
log.step(8, 'Writing config...');
|
|
111
|
+
const configDir = join(workspace, 'skills', '.clamper');
|
|
112
|
+
mkdirSync(configDir, { recursive: true });
|
|
113
|
+
const config = {
|
|
114
|
+
version: '1.0.0',
|
|
115
|
+
tier: effectiveTier,
|
|
116
|
+
apiKey: apiKey || '',
|
|
117
|
+
installedAt: new Date().toISOString(),
|
|
118
|
+
agentName,
|
|
119
|
+
userName,
|
|
120
|
+
timezone,
|
|
121
|
+
features: validation.features || {},
|
|
122
|
+
};
|
|
123
|
+
writeFileSync(join(configDir, 'config.json'), JSON.stringify(config, null, 2), 'utf8');
|
|
124
|
+
log.ok('Config saved');
|
|
125
|
+
|
|
126
|
+
// Step 9: Success
|
|
127
|
+
console.log(`
|
|
128
|
+
${c.green}╔══════════════════════════════════════════════╗
|
|
129
|
+
║ 🧰 Clamper initialized successfully! ║
|
|
130
|
+
╠══════════════════════════════════════════════╣
|
|
131
|
+
║ ║
|
|
132
|
+
║ Agent: ${(agentName + ' ').slice(0, 36)}║
|
|
133
|
+
║ Tier: ${(effectiveTier + ' ').slice(0, 37)}║
|
|
134
|
+
║ Skills: ${(skillCount + ' installed ').slice(0, 35)}║
|
|
135
|
+
║ Memory: 3-layer system active ║
|
|
136
|
+
║ ║
|
|
137
|
+
║ Next steps: ║
|
|
138
|
+
║ 1. openclaw gateway restart ║
|
|
139
|
+
║ 2. Start chatting — your agent is ready! ║
|
|
140
|
+
║ ║
|
|
141
|
+
║ Run clamper status to check health ║
|
|
142
|
+
║ Run clamper upgrade to go Pro ║
|
|
143
|
+
╚══════════════════════════════════════════════╝${c.reset}
|
|
144
|
+
`);
|
|
145
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { detectWorkspace } from './utils.mjs';
|
|
4
|
+
|
|
5
|
+
export function logActivity(message) {
|
|
6
|
+
const workspace = detectWorkspace();
|
|
7
|
+
if (!workspace) return;
|
|
8
|
+
|
|
9
|
+
const logPath = join(workspace, 'logs', 'activity.jsonl');
|
|
10
|
+
const logDir = dirname(logPath);
|
|
11
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const entry = JSON.stringify({ ts: new Date().toISOString(), msg: message }) + '\n';
|
|
14
|
+
appendFileSync(logPath, entry);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// CLI usage: clamper log "Did something"
|
|
18
|
+
export function runLog(flags) {
|
|
19
|
+
const msg = flags._ || process.argv.slice(3).join(' ');
|
|
20
|
+
if (!msg) {
|
|
21
|
+
console.log('Usage: clamper log "Your activity message"');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
logActivity(msg);
|
|
25
|
+
console.log(` ✔ Logged: ${msg}`);
|
|
26
|
+
}
|
package/src/scaffold.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, chmodSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PKG_ROOT = join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
export function replaceVars(text, vars) {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/\{\{AGENT_NAME\}\}/g, vars.agentName || 'Atlas')
|
|
11
|
+
.replace(/\{\{USER_NAME\}\}/g, vars.userName || 'Human')
|
|
12
|
+
.replace(/\{\{TIMEZONE\}\}/g, vars.timezone || 'UTC')
|
|
13
|
+
.replace(/\{\{TIER\}\}/g, vars.tier || 'free')
|
|
14
|
+
.replace(/\{\{DATE\}\}/g, vars.date || new Date().toISOString().slice(0, 10));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TEMPLATE_FILES = [
|
|
18
|
+
'AGENTS.md', 'SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md',
|
|
19
|
+
'MEMORY.md', 'LEARNINGS.md', 'PROTOCOL_COST_EFFICIENCY.md', 'SKILLS-INDEX.md',
|
|
20
|
+
'memory/knowledge/about-me.md', 'tasks/QUEUE.md', 'scripts/nightly-consolidation.sh',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function scaffoldWorkspace(workspace, vars) {
|
|
24
|
+
for (const d of ['memory/daily', 'memory/knowledge', 'memory/para', 'tasks', 'scripts', 'skills']) {
|
|
25
|
+
mkdirSync(join(workspace, d), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const f of TEMPLATE_FILES) {
|
|
29
|
+
const src = join(PKG_ROOT, 'templates', f);
|
|
30
|
+
const dest = join(workspace, f);
|
|
31
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
32
|
+
const content = readFileSync(src, 'utf8');
|
|
33
|
+
writeFileSync(dest, replaceVars(content, vars), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Make consolidation script executable
|
|
37
|
+
const scriptPath = join(workspace, 'scripts/nightly-consolidation.sh');
|
|
38
|
+
if (existsSync(scriptPath)) {
|
|
39
|
+
chmodSync(scriptPath, 0o755);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function backupWorkspace(workspace) {
|
|
44
|
+
const backupDir = join(workspace, '.clamper-backup');
|
|
45
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
46
|
+
const dest = join(backupDir, ts);
|
|
47
|
+
mkdirSync(dest, { recursive: true });
|
|
48
|
+
for (const f of TEMPLATE_FILES) {
|
|
49
|
+
const src = join(workspace, f);
|
|
50
|
+
if (existsSync(src)) {
|
|
51
|
+
const d = join(dest, f);
|
|
52
|
+
mkdirSync(dirname(d), { recursive: true });
|
|
53
|
+
cpSync(src, d);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return dest;
|
|
57
|
+
}
|
package/src/skills.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { log, cmdExists } from './utils.mjs';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PKG_ROOT = join(__dirname, '..');
|
|
9
|
+
const BUNDLED_DIR = join(PKG_ROOT, 'skills');
|
|
10
|
+
|
|
11
|
+
const PRO_SKILLS = [
|
|
12
|
+
'reminder', 'data-analyst', 'automation-workflows', 'sales',
|
|
13
|
+
'security-monitor', 'nano-pdf', 'pdf-to-structured',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function installSkills(workspace, tier) {
|
|
17
|
+
const skillsDir = join(workspace, 'skills');
|
|
18
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Install bundled free skills
|
|
21
|
+
let count = 0;
|
|
22
|
+
const entries = readdirSync(BUNDLED_DIR, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isDirectory()) continue;
|
|
25
|
+
const srcSkill = join(BUNDLED_DIR, entry.name, 'SKILL.md');
|
|
26
|
+
if (!existsSync(srcSkill)) continue;
|
|
27
|
+
const destDir = join(skillsDir, entry.name);
|
|
28
|
+
mkdirSync(destDir, { recursive: true });
|
|
29
|
+
const content = readFileSync(srcSkill, 'utf8');
|
|
30
|
+
writeFileSync(join(destDir, 'SKILL.md'), content, 'utf8');
|
|
31
|
+
count++;
|
|
32
|
+
}
|
|
33
|
+
log.ok(`Installed ${count} free-tier skills`);
|
|
34
|
+
|
|
35
|
+
// Pro skills via clawhub
|
|
36
|
+
if (tier === 'pro') {
|
|
37
|
+
if (cmdExists('clawhub')) {
|
|
38
|
+
for (const slug of PRO_SKILLS) {
|
|
39
|
+
try {
|
|
40
|
+
execSync(`clawhub install ${slug} --force`, { cwd: workspace, stdio: 'ignore', timeout: 30000 });
|
|
41
|
+
log.ok(`Installed pro skill: ${slug}`);
|
|
42
|
+
} catch {
|
|
43
|
+
log.warn(`Could not install pro skill: ${slug}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
log.warn('clawhub CLI not found — skipping pro skill installation. Run: npm i -g clawhub');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return count;
|
|
52
|
+
}
|