@prave/cli 0.2.1 → 0.3.3
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/dist/commands/conflicts.js +50 -0
- package/dist/commands/deploy.js +141 -0
- package/dist/commands/find.js +82 -0
- package/dist/commands/import.js +22 -1
- package/dist/commands/install.js +33 -1
- package/dist/commands/list.js +108 -11
- package/dist/commands/optimize.js +60 -0
- package/dist/commands/overview.js +68 -0
- package/dist/commands/settings.js +177 -0
- package/dist/commands/whatdoes.js +78 -0
- package/dist/index.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { log } from '../utils/logger.js';
|
|
5
|
+
function describe(c) {
|
|
6
|
+
const a = c.skill_a_name ?? c.skill_a_id;
|
|
7
|
+
const b = c.skill_b_name ?? c.skill_b_id;
|
|
8
|
+
switch (c.conflict_type) {
|
|
9
|
+
case 'trigger_overlap':
|
|
10
|
+
return `Both trigger on ${chalk.bold(a)} ↔ ${chalk.bold(b)}${c.conflict_detail ? chalk.dim(' — ' + c.conflict_detail) : ''}`;
|
|
11
|
+
case 'auto_invocation_collision':
|
|
12
|
+
return `Both auto-invoke ${chalk.bold(a)} ↔ ${chalk.bold(b)}${c.conflict_detail ? chalk.dim(' — ' + c.conflict_detail) : ''}`;
|
|
13
|
+
case 'dependency_conflict':
|
|
14
|
+
return `Requires ${chalk.bold(a)} but missing — ${chalk.bold(b)}${c.conflict_detail ? chalk.dim(' — ' + c.conflict_detail) : ''}`;
|
|
15
|
+
default:
|
|
16
|
+
return `${a} ↔ ${b}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function conflictsCommand(opts = {}) {
|
|
20
|
+
const spinner = ora('Detecting conflicts…').start();
|
|
21
|
+
try {
|
|
22
|
+
const { data: conflicts } = await api.get('/api/v1/intelligence/conflicts?refresh=true', true);
|
|
23
|
+
spinner.stop();
|
|
24
|
+
if (conflicts.length === 0) {
|
|
25
|
+
// Pull skill count for the empty state.
|
|
26
|
+
let total = 0;
|
|
27
|
+
try {
|
|
28
|
+
const { data: skills } = await api.get('/api/v1/intelligence/skills', true);
|
|
29
|
+
total = skills.length;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* best-effort */
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.green(`✓ No conflicts detected in your ${total} skills.`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const c of conflicts) {
|
|
38
|
+
console.log(`${chalk.yellow('⚠️ ')} ${describe(c)}`);
|
|
39
|
+
}
|
|
40
|
+
if (opts.fix) {
|
|
41
|
+
console.log();
|
|
42
|
+
log.dim('Interactive fix coming soon — for now run `prave whatdoes <skill>` and adjust frontmatter manually.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
spinner.stop();
|
|
47
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { AGENT_REGISTRY } from '@prave/shared';
|
|
7
|
+
import { api, ApiError } from '../lib/api.js';
|
|
8
|
+
import { CONFIG } from '../lib/config.js';
|
|
9
|
+
import { log } from '../utils/logger.js';
|
|
10
|
+
function detectOsKey(detected) {
|
|
11
|
+
if (detected === 'windows')
|
|
12
|
+
return 'windows';
|
|
13
|
+
// mac + linux both use POSIX paths under "mac" in the registry.
|
|
14
|
+
return 'mac';
|
|
15
|
+
}
|
|
16
|
+
function expandHome(p, os) {
|
|
17
|
+
if (os === 'windows')
|
|
18
|
+
return p;
|
|
19
|
+
if (p.startsWith('~')) {
|
|
20
|
+
return join(homedir(), p.slice(1).replace(/^[\\/]/, ''));
|
|
21
|
+
}
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
async function readLocalSkill(slug) {
|
|
25
|
+
const path = join(CONFIG.skillsDir, slug, 'SKILL.md');
|
|
26
|
+
try {
|
|
27
|
+
return await readFile(path, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function fetchRemoteSkill(slug) {
|
|
34
|
+
const { data } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, true);
|
|
35
|
+
if (!data.content) {
|
|
36
|
+
throw new ApiError(`Remote skill "${slug}" has no content.`, 404);
|
|
37
|
+
}
|
|
38
|
+
return data.content;
|
|
39
|
+
}
|
|
40
|
+
function describeContentForCursor(content, slug) {
|
|
41
|
+
// Has frontmatter? best-effort rename triggers: → globs:
|
|
42
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
43
|
+
if (fmMatch && fmMatch[1] !== undefined) {
|
|
44
|
+
const body = content.slice(fmMatch[0].length);
|
|
45
|
+
const fmInner = fmMatch[1].replace(/(^|\n)triggers:/g, '$1globs:');
|
|
46
|
+
return `---\n${fmInner}\n---\n${body}`;
|
|
47
|
+
}
|
|
48
|
+
// No frontmatter — synthesize
|
|
49
|
+
const trimmed = content.trim().replace(/\s+/g, ' ').slice(0, 200);
|
|
50
|
+
return `---\nname: ${slug}\ndescription: ${trimmed}\n---\n${content}`;
|
|
51
|
+
}
|
|
52
|
+
function buildDestPath(agent, basePath, os, slug) {
|
|
53
|
+
const expanded = expandHome(basePath, os);
|
|
54
|
+
if (agent === 'cursor') {
|
|
55
|
+
// .cursor/rules/<slug>.mdc — the basePath already points at .cursor/rules
|
|
56
|
+
return {
|
|
57
|
+
dir: expanded,
|
|
58
|
+
file: join(expanded, `${slug}.mdc`),
|
|
59
|
+
converted: true,
|
|
60
|
+
display: `${basePath.replace(/[\\/]+$/, '')}/${slug}.mdc`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
dir: join(expanded, slug),
|
|
65
|
+
file: join(expanded, slug, 'SKILL.md'),
|
|
66
|
+
converted: false,
|
|
67
|
+
display: `${basePath.replace(/[\\/]+$/, '')}/${slug}/SKILL.md`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function deployCommand(skillName, opts = {}) {
|
|
71
|
+
const start = Date.now();
|
|
72
|
+
let settings;
|
|
73
|
+
try {
|
|
74
|
+
const { data } = await api.get('/api/v1/settings/agents', true);
|
|
75
|
+
settings = data;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const targetFilter = opts.agent && opts.agent.toLowerCase() !== 'all'
|
|
83
|
+
? opts.agent.toLowerCase()
|
|
84
|
+
: null;
|
|
85
|
+
const targets = settings.enabled_agents.filter((a) => targetFilter === null || a === targetFilter);
|
|
86
|
+
if (targets.length === 0) {
|
|
87
|
+
log.warn('No matching agents enabled. Run `prave settings` to configure.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Load source content (local first, fall back to remote).
|
|
91
|
+
let source = await readLocalSkill(skillName);
|
|
92
|
+
if (!source) {
|
|
93
|
+
try {
|
|
94
|
+
source = await fetchRemoteSkill(skillName);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const os = detectOsKey(settings.detected_os);
|
|
103
|
+
console.log(`Deploying "${chalk.bold(skillName)}" to ${targets.length} agent${targets.length === 1 ? '' : 's'}${opts.dryRun ? chalk.dim(' (dry-run)') : ''}…`);
|
|
104
|
+
const spinner = ora('Writing files…').start();
|
|
105
|
+
let okCount = 0;
|
|
106
|
+
for (const agent of targets) {
|
|
107
|
+
const meta = AGENT_REGISTRY[agent];
|
|
108
|
+
const paths = settings.skill_paths[agent] ?? meta.defaultPath;
|
|
109
|
+
const basePath = os === 'windows' ? paths.windows : paths.mac;
|
|
110
|
+
const dest = buildDestPath(agent, basePath, os, skillName);
|
|
111
|
+
const content = dest.converted
|
|
112
|
+
? describeContentForCursor(source, skillName)
|
|
113
|
+
: source;
|
|
114
|
+
spinner.text = `→ ${meta.label}`;
|
|
115
|
+
if (opts.dryRun) {
|
|
116
|
+
okCount += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await mkdir(dest.dir, { recursive: true });
|
|
121
|
+
await writeFile(dest.file, content, 'utf8');
|
|
122
|
+
okCount += 1;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
spinner.warn(`Failed: ${meta.label} — ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
spinner.stop();
|
|
129
|
+
for (const agent of targets) {
|
|
130
|
+
const meta = AGENT_REGISTRY[agent];
|
|
131
|
+
const paths = settings.skill_paths[agent] ?? meta.defaultPath;
|
|
132
|
+
const basePath = os === 'windows' ? paths.windows : paths.mac;
|
|
133
|
+
const dest = buildDestPath(agent, basePath, os, skillName);
|
|
134
|
+
const tag = dest.converted ? chalk.dim(' (converted)') : '';
|
|
135
|
+
console.log(`${chalk.green('✓')} ${meta.label.padEnd(14)} → ${dest.display}${tag}`);
|
|
136
|
+
}
|
|
137
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
138
|
+
console.log(opts.dryRun
|
|
139
|
+
? chalk.dim(`Dry-run complete in ${elapsed}s — ${okCount} agents would receive the skill.`)
|
|
140
|
+
: chalk.dim(`Deployed in ${elapsed}s`));
|
|
141
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { tokenTier } from '@prave/shared';
|
|
5
|
+
import { api, ApiError } from '../lib/api.js';
|
|
6
|
+
import { log } from '../utils/logger.js';
|
|
7
|
+
const TIER_EMOJI = {
|
|
8
|
+
lean: '🟢',
|
|
9
|
+
medium: '🟡',
|
|
10
|
+
heavy: '🔴',
|
|
11
|
+
};
|
|
12
|
+
function formatTokens(n) {
|
|
13
|
+
if (n < 1000)
|
|
14
|
+
return `~${n}`;
|
|
15
|
+
return `~${(n / 1000).toFixed(1)}k`;
|
|
16
|
+
}
|
|
17
|
+
function renderResult(r, idx) {
|
|
18
|
+
const badge = r.source === 'local'
|
|
19
|
+
? chalk.cyan('[local]')
|
|
20
|
+
: chalk.magenta('[marketplace]');
|
|
21
|
+
const tier = TIER_EMOJI[tokenTier(r.estimated_tokens)];
|
|
22
|
+
const slash = r.slash_command ? chalk.dim(` ${r.slash_command}`) : '';
|
|
23
|
+
const installs = r.source === 'marketplace' && typeof r.install_count === 'number'
|
|
24
|
+
? chalk.dim(` · ${r.install_count} installs`)
|
|
25
|
+
: '';
|
|
26
|
+
console.log(`${chalk.bold(`${idx + 1}.`)} ${badge} ${chalk.bold(r.name)}${slash} ${tier} ${chalk.dim(formatTokens(r.estimated_tokens))}${installs}`);
|
|
27
|
+
if (r.description)
|
|
28
|
+
log.dim(` ${r.description}`);
|
|
29
|
+
}
|
|
30
|
+
export async function findCommand(query, opts = {}) {
|
|
31
|
+
const scope = opts.local
|
|
32
|
+
? 'local'
|
|
33
|
+
: opts.marketplace
|
|
34
|
+
? 'marketplace'
|
|
35
|
+
: 'both';
|
|
36
|
+
const spinner = ora(opts.smart ? 'Smart-searching…' : 'Searching…').start();
|
|
37
|
+
try {
|
|
38
|
+
let results;
|
|
39
|
+
if (opts.smart) {
|
|
40
|
+
const { data } = await api.post('/api/v1/intelligence/llm-search', { query }, true);
|
|
41
|
+
spinner.stop();
|
|
42
|
+
console.log(chalk.dim(`Task: ${data.task}`));
|
|
43
|
+
if (data.suggested_triggers.length > 0) {
|
|
44
|
+
log.dim(`Suggested triggers: ${data.suggested_triggers.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
results = data.results;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const { data } = await api.post('/api/v1/intelligence/search', { query, scope }, true);
|
|
51
|
+
spinner.stop();
|
|
52
|
+
results = data;
|
|
53
|
+
}
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
log.dim('No matches.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const top = results.slice(0, 5);
|
|
59
|
+
top.forEach((r, i) => renderResult(r, i));
|
|
60
|
+
if (opts.local)
|
|
61
|
+
return; // skip prompt when local-only
|
|
62
|
+
const installable = top.find((r) => r.source === 'marketplace' && !r.is_installed);
|
|
63
|
+
if (!installable)
|
|
64
|
+
return;
|
|
65
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
66
|
+
try {
|
|
67
|
+
const answer = (await rl.question(`\nInstall ${chalk.bold(installable.slug)}? [y/N] `)).trim().toLowerCase();
|
|
68
|
+
if (answer === 'y' || answer === 'yes') {
|
|
69
|
+
const { installCommand } = await import('./install.js');
|
|
70
|
+
await installCommand(installable.slug, {});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
rl.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
spinner.stop();
|
|
79
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/commands/import.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { api } from '../lib/api.js';
|
|
6
6
|
import { CONFIG } from '../lib/config.js';
|
|
7
|
+
import { loadCredentials } from '../lib/credentials.js';
|
|
7
8
|
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
9
10
|
/**
|
|
@@ -40,7 +41,19 @@ export async function importCommand(opts) {
|
|
|
40
41
|
console.log(` ${chalk.cyan('•')} ${s.slug} ${chalk.dim(`(${s.sizeBytes}B)`)}`);
|
|
41
42
|
}
|
|
42
43
|
if (!opts.upload) {
|
|
43
|
-
|
|
44
|
+
const creds = await loadCredentials();
|
|
45
|
+
if (creds && skills.length > 0) {
|
|
46
|
+
const analyzeSpinner = ora(`Analyzing ${skills.length} skills…`).start();
|
|
47
|
+
for (const s of skills) {
|
|
48
|
+
await api
|
|
49
|
+
.post('/api/v1/intelligence/analyze', { content: s.content, file_path: s.path, agent_type: 'claude' }, true)
|
|
50
|
+
.catch(() => { });
|
|
51
|
+
}
|
|
52
|
+
analyzeSpinner.succeed(`✓ Imported ${skills.length} skills. Run \`prave overview\` to see your full analysis.`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
log.dim('\nRe-run with --upload to push these to your Prave account.');
|
|
56
|
+
}
|
|
44
57
|
return;
|
|
45
58
|
}
|
|
46
59
|
// Plan gate: clamp the queue to the caller's import / private quota so
|
|
@@ -105,4 +118,12 @@ export async function importCommand(opts) {
|
|
|
105
118
|
uploadSpinner.succeed(tail);
|
|
106
119
|
if (gated > 0)
|
|
107
120
|
log.dim(formatUpgradeHint('explorer'));
|
|
121
|
+
// Always analyze after upload — best effort, never blocks the user.
|
|
122
|
+
const analyzeSpinner = ora(`Analyzing ${queue.length} skills…`).start();
|
|
123
|
+
for (const s of queue) {
|
|
124
|
+
await api
|
|
125
|
+
.post('/api/v1/intelligence/analyze', { content: s.content, file_path: s.path, agent_type: 'claude' }, true)
|
|
126
|
+
.catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
analyzeSpinner.succeed(`✓ Imported ${queue.length} skills. Run \`prave overview\` to see your full analysis.`);
|
|
108
129
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import ora from 'ora';
|
|
5
6
|
import { api, ApiError } from '../lib/api.js';
|
|
@@ -18,6 +19,7 @@ import { log } from '../utils/logger.js';
|
|
|
18
19
|
*/
|
|
19
20
|
export async function installCommand(slug, opts = {}) {
|
|
20
21
|
const spinner = ora(`Resolving ${slug}…`).start();
|
|
22
|
+
let installedSlugs = [];
|
|
21
23
|
try {
|
|
22
24
|
const slugs = opts.noDeps ? [slug] : await resolveOrder(slug);
|
|
23
25
|
spinner.text = `Installing ${slugs.length} skill${slugs.length === 1 ? '' : 's'}…`;
|
|
@@ -26,6 +28,7 @@ export async function installCommand(slug, opts = {}) {
|
|
|
26
28
|
spinner.text = `↓ ${s}`;
|
|
27
29
|
await pullOne(s, { hasSession: Boolean(session), force: Boolean(opts.force) });
|
|
28
30
|
}
|
|
31
|
+
installedSlugs = slugs;
|
|
29
32
|
spinner.succeed(`Installed ${slugs.length} skill${slugs.length === 1 ? '' : 's'} → ${CONFIG.skillsDir}`);
|
|
30
33
|
if (slugs.length > 1) {
|
|
31
34
|
log.dim(` chain: ${slugs.join(' → ')}`);
|
|
@@ -34,6 +37,35 @@ export async function installCommand(slug, opts = {}) {
|
|
|
34
37
|
catch (err) {
|
|
35
38
|
spinner.fail(formatError(err));
|
|
36
39
|
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const session = await loadCredentials();
|
|
43
|
+
if (!session)
|
|
44
|
+
return;
|
|
45
|
+
// Best-effort analyze pass on the freshly written files.
|
|
46
|
+
for (const s of installedSlugs) {
|
|
47
|
+
const path = join(CONFIG.skillsDir, s, 'SKILL.md');
|
|
48
|
+
try {
|
|
49
|
+
const content = await readFile(path, 'utf8');
|
|
50
|
+
await api
|
|
51
|
+
.post('/api/v1/intelligence/analyze', { content, file_path: path, agent_type: 'claude' }, true)
|
|
52
|
+
.catch(() => { });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* file unreadable — skip */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Offer to deploy to all configured agents.
|
|
59
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
60
|
+
try {
|
|
61
|
+
const ans = (await rl.question('\nDeploy to all configured agents? [y/N] ')).trim().toLowerCase();
|
|
62
|
+
if (ans === 'y' || ans === 'yes') {
|
|
63
|
+
const { deployCommand } = await import('./deploy.js');
|
|
64
|
+
await deployCommand(slug, {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
rl.close();
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
71
|
async function pullOne(slug, ctx) {
|
package/dist/commands/list.js
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import { readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import { tokenTier } from '@prave/shared';
|
|
4
5
|
import { api } from '../lib/api.js';
|
|
5
6
|
import { CONFIG } from '../lib/config.js';
|
|
6
7
|
import { log } from '../utils/logger.js';
|
|
7
|
-
|
|
8
|
+
const TIER_EMOJI = {
|
|
9
|
+
lean: '🟢',
|
|
10
|
+
medium: '🟡',
|
|
11
|
+
heavy: '🔴',
|
|
12
|
+
};
|
|
13
|
+
const AGENT_ICON = {
|
|
14
|
+
claude: '🤖',
|
|
15
|
+
codex: '🧪',
|
|
16
|
+
cursor: '🟪',
|
|
17
|
+
gemini: '✦',
|
|
18
|
+
cline: '🛠',
|
|
19
|
+
amp: '⚡',
|
|
20
|
+
};
|
|
21
|
+
function formatTokens(n) {
|
|
22
|
+
if (n < 1000)
|
|
23
|
+
return `~${n}`;
|
|
24
|
+
return `~${(n / 1000).toFixed(1)}k`;
|
|
25
|
+
}
|
|
26
|
+
async function readLocalSlugs() {
|
|
27
|
+
try {
|
|
28
|
+
const entries = await readdir(CONFIG.skillsDir);
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const name of entries) {
|
|
31
|
+
const dir = join(CONFIG.skillsDir, name);
|
|
32
|
+
if ((await stat(dir).catch(() => null))?.isDirectory())
|
|
33
|
+
out.push(name);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function listCommand(opts = {}) {
|
|
8
42
|
if (opts.remote) {
|
|
9
43
|
const { data: skills } = await api.get('/api/v1/skills?limit=50', true);
|
|
10
44
|
if (skills.length === 0) {
|
|
@@ -16,19 +50,82 @@ export async function listCommand(opts) {
|
|
|
16
50
|
}
|
|
17
51
|
return;
|
|
18
52
|
}
|
|
53
|
+
const enriched = opts.verbose || opts.conflicts || opts.heavy;
|
|
54
|
+
const localSlugs = await readLocalSlugs();
|
|
55
|
+
if (!enriched) {
|
|
56
|
+
if (localSlugs.length === 0) {
|
|
57
|
+
log.warn(`No skills directory at ${CONFIG.skillsDir}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const name of localSlugs) {
|
|
61
|
+
console.log(` ${chalk.cyan('•')} ${name}`);
|
|
62
|
+
}
|
|
63
|
+
log.dim(`\n${localSlugs.length} local skill${localSlugs.length === 1 ? '' : 's'} in ${CONFIG.skillsDir}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Enriched path — pull intelligence and merge by slug (best-effort).
|
|
67
|
+
let metadata = [];
|
|
19
68
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
69
|
+
const { data } = await api.get('/api/v1/intelligence/skills', true);
|
|
70
|
+
metadata = data;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
log.warn(`Could not fetch intelligence — ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
const bySlug = new Map();
|
|
76
|
+
for (const m of metadata) {
|
|
77
|
+
const slug = m.name ?? deriveSlug(m.file_path);
|
|
78
|
+
if (slug && !bySlug.has(slug))
|
|
79
|
+
bySlug.set(slug, m);
|
|
80
|
+
}
|
|
81
|
+
let conflictSlugs = null;
|
|
82
|
+
if (opts.conflicts) {
|
|
83
|
+
try {
|
|
84
|
+
const { data: cs } = await api.get('/api/v1/intelligence/conflicts', true);
|
|
85
|
+
conflictSlugs = new Set();
|
|
86
|
+
const idToSlug = new Map();
|
|
87
|
+
for (const [slug, m] of bySlug)
|
|
88
|
+
idToSlug.set(m.id, slug);
|
|
89
|
+
for (const c of cs) {
|
|
90
|
+
const a = idToSlug.get(c.skill_a_id) ?? c.skill_a_name;
|
|
91
|
+
const b = idToSlug.get(c.skill_b_id) ?? c.skill_b_name;
|
|
92
|
+
if (a)
|
|
93
|
+
conflictSlugs.add(a);
|
|
94
|
+
if (b)
|
|
95
|
+
conflictSlugs.add(b);
|
|
27
96
|
}
|
|
28
97
|
}
|
|
29
|
-
|
|
98
|
+
catch (err) {
|
|
99
|
+
log.warn(`Could not fetch conflicts — ${err.message}`);
|
|
100
|
+
}
|
|
30
101
|
}
|
|
31
|
-
|
|
32
|
-
|
|
102
|
+
const merged = localSlugs.map((slug) => ({ slug, meta: bySlug.get(slug) ?? null }));
|
|
103
|
+
let rows = merged;
|
|
104
|
+
if (opts.heavy) {
|
|
105
|
+
rows = rows.filter((r) => (r.meta?.estimated_tokens ?? 0) > 5000);
|
|
106
|
+
}
|
|
107
|
+
if (opts.conflicts && conflictSlugs) {
|
|
108
|
+
rows = rows.filter((r) => conflictSlugs.has(r.slug));
|
|
109
|
+
}
|
|
110
|
+
if (rows.length === 0) {
|
|
111
|
+
log.dim('No matching skills.');
|
|
112
|
+
return;
|
|
33
113
|
}
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
if (!r.meta) {
|
|
116
|
+
console.log(` ${chalk.cyan('•')} ${r.slug} ${chalk.dim('— no metadata yet')}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const slash = r.meta.slash_command ? chalk.dim(` ${r.meta.slash_command}`) : '';
|
|
120
|
+
const tier = TIER_EMOJI[tokenTier(r.meta.estimated_tokens)];
|
|
121
|
+
const agentIcon = AGENT_ICON[r.meta.agent_type] ?? '•';
|
|
122
|
+
console.log(` ${chalk.cyan('•')} ${chalk.bold(r.slug)}${slash} ${chalk.dim(formatTokens(r.meta.estimated_tokens))} ${tier} [${agentIcon}]`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function deriveSlug(filePath) {
|
|
126
|
+
const parts = filePath.split(/[\\/]/).filter(Boolean);
|
|
127
|
+
// .../<slug>/SKILL.md
|
|
128
|
+
if (parts.length >= 2)
|
|
129
|
+
return parts[parts.length - 2] ?? null;
|
|
130
|
+
return null;
|
|
34
131
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { log } from '../utils/logger.js';
|
|
5
|
+
function formatTokens(n) {
|
|
6
|
+
if (n < 1000)
|
|
7
|
+
return `~${n}`;
|
|
8
|
+
return `~${(n / 1000).toFixed(1)}k`;
|
|
9
|
+
}
|
|
10
|
+
function nameOf(s) {
|
|
11
|
+
return s.name ?? s.file_path;
|
|
12
|
+
}
|
|
13
|
+
export async function optimizeCommand(opts = {}) {
|
|
14
|
+
const spinner = ora('Analyzing your skill set…').start();
|
|
15
|
+
try {
|
|
16
|
+
const { data } = await api.get('/api/v1/intelligence/optimize', true);
|
|
17
|
+
spinner.stop();
|
|
18
|
+
console.log(chalk.bold('Underused (last 30 days)'));
|
|
19
|
+
if (data.underused.length === 0) {
|
|
20
|
+
log.dim(' (none)');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
for (const s of data.underused) {
|
|
24
|
+
console.log(` ${chalk.cyan('•')} ${nameOf(s)} ${chalk.dim(formatTokens(s.estimated_tokens))}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(chalk.bold('Merge candidates'));
|
|
29
|
+
if (data.merge_candidates.length === 0) {
|
|
30
|
+
log.dim(' (none)');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const m of data.merge_candidates) {
|
|
34
|
+
console.log(` ${chalk.cyan('•')} ${nameOf(m.a)} ${chalk.dim('+')} ${nameOf(m.b)}`);
|
|
35
|
+
log.dim(` ${m.reason}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.bold('Heavy skills (>5k tokens)'));
|
|
40
|
+
if (data.heavy.length === 0) {
|
|
41
|
+
log.dim(' (none)');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (const s of data.heavy) {
|
|
45
|
+
console.log(` ${chalk.cyan('•')} ${nameOf(s)} ${chalk.dim(formatTokens(s.estimated_tokens))}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.bold(`Total estimated savings: ${formatTokens(data.total_savings_estimate_tokens)} tokens / session`));
|
|
50
|
+
if (opts.apply) {
|
|
51
|
+
console.log();
|
|
52
|
+
log.dim('Auto-apply not yet available — review the suggestions and adjust manually.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
spinner.stop();
|
|
57
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { log } from '../utils/logger.js';
|
|
5
|
+
function formatThousands(n) {
|
|
6
|
+
if (n < 1000)
|
|
7
|
+
return `~${n}`;
|
|
8
|
+
return `~${Math.round(n / 1000)}k`;
|
|
9
|
+
}
|
|
10
|
+
function pad(s, width) {
|
|
11
|
+
if (s.length >= width)
|
|
12
|
+
return s;
|
|
13
|
+
return s + ' '.repeat(width - s.length);
|
|
14
|
+
}
|
|
15
|
+
export async function overviewCommand(opts = {}) {
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
try {
|
|
18
|
+
const { data } = await api.get('/api/v1/intelligence/overview', true);
|
|
19
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
process.stdout.write(JSON.stringify({ error: err instanceof Error ? err.message : 'unknown' }) + '\n');
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const spinner = ora('Fetching skill overview…').start();
|
|
28
|
+
try {
|
|
29
|
+
const { data: overview } = await api.get('/api/v1/intelligence/overview', true);
|
|
30
|
+
spinner.stop();
|
|
31
|
+
const inner = 38;
|
|
32
|
+
const top = '╔' + '═'.repeat(inner) + '╗';
|
|
33
|
+
const sep = '╠' + '═'.repeat(inner) + '╣';
|
|
34
|
+
const bottom = '╚' + '═'.repeat(inner) + '╝';
|
|
35
|
+
const row = (label, value, tail = '') => {
|
|
36
|
+
const body = ` ${pad(label, 18)}${pad(value, 14)}${tail}`;
|
|
37
|
+
return '║' + pad(body, inner) + '║';
|
|
38
|
+
};
|
|
39
|
+
const title = '║' + pad(' Prave Skill Overview', inner) + '║';
|
|
40
|
+
console.log(top);
|
|
41
|
+
console.log(title);
|
|
42
|
+
console.log(sep);
|
|
43
|
+
console.log(row('Total Skills:', String(overview.total_skills)));
|
|
44
|
+
console.log(row('Auto-trigger:', String(overview.auto_trigger_count)));
|
|
45
|
+
console.log(row('User-invocable:', String(overview.user_invocable_count)));
|
|
46
|
+
console.log(row('Est. total tokens:', formatThousands(overview.total_estimated_tokens)));
|
|
47
|
+
console.log(row('Conflicts found:', String(overview.conflict_count), overview.conflict_count > 0 ? chalk.yellow(' ⚠️') : ''));
|
|
48
|
+
console.log(bottom);
|
|
49
|
+
console.log();
|
|
50
|
+
log.dim('Run "prave conflicts" to see details.');
|
|
51
|
+
log.dim('Run "prave optimize" for recommendations.');
|
|
52
|
+
if (opts.agent) {
|
|
53
|
+
const target = opts.agent.toLowerCase();
|
|
54
|
+
const { data: skills } = await api.get('/api/v1/intelligence/skills', true);
|
|
55
|
+
const filtered = skills.filter((s) => s.agent_type === target);
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(chalk.bold(`Skills for agent "${target}": ${filtered.length}`));
|
|
58
|
+
for (const s of filtered) {
|
|
59
|
+
console.log(` ${chalk.cyan('•')} ${s.name ?? '(unnamed)'} ${chalk.dim(s.file_path)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
spinner.stop();
|
|
65
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { AGENT_REGISTRY, AGENT_TYPES } from '@prave/shared';
|
|
5
|
+
import { api, ApiError } from '../lib/api.js';
|
|
6
|
+
import { CONFIG } from '../lib/config.js';
|
|
7
|
+
import { log } from '../utils/logger.js';
|
|
8
|
+
function detectOs() {
|
|
9
|
+
if (process.platform === 'darwin')
|
|
10
|
+
return 'mac';
|
|
11
|
+
if (process.platform === 'win32')
|
|
12
|
+
return 'windows';
|
|
13
|
+
return 'linux';
|
|
14
|
+
}
|
|
15
|
+
async function loadLocalConfig() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = await readFile(CONFIG.configPath, 'utf8');
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function saveLocalConfig(settings) {
|
|
25
|
+
await mkdir(CONFIG.praveDir, { recursive: true });
|
|
26
|
+
const existing = await loadLocalConfig();
|
|
27
|
+
const next = { ...existing, agentSettings: settings };
|
|
28
|
+
await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
|
|
29
|
+
}
|
|
30
|
+
async function fetchSettings() {
|
|
31
|
+
const { data } = await api.get('/api/v1/settings/agents', true);
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
async function patchSettings(patch) {
|
|
35
|
+
const { data } = await api.put('/api/v1/settings/agents', patch, true);
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
function isAgentType(s) {
|
|
39
|
+
return AGENT_TYPES.includes(s);
|
|
40
|
+
}
|
|
41
|
+
async function configureAgents(rl, current) {
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(chalk.bold('Available agents:'));
|
|
44
|
+
for (const id of AGENT_TYPES) {
|
|
45
|
+
const meta = AGENT_REGISTRY[id];
|
|
46
|
+
const enabled = current.enabled_agents.includes(id) ? chalk.green('✓') : ' ';
|
|
47
|
+
console.log(` ${enabled} ${chalk.cyan(meta.id.padEnd(8))} ${meta.label} ${chalk.dim('— ' + meta.description)}`);
|
|
48
|
+
}
|
|
49
|
+
const answer = (await rl.question(`\nEnter comma-separated agents to enable (default: ${current.enabled_agents.join(',') || 'claude'}): `)).trim();
|
|
50
|
+
let enabled = current.enabled_agents;
|
|
51
|
+
if (answer) {
|
|
52
|
+
const tokens = answer
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((t) => t.trim().toLowerCase())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
const invalid = tokens.filter((t) => !isAgentType(t));
|
|
57
|
+
if (invalid.length > 0) {
|
|
58
|
+
log.warn(`Unknown agents skipped: ${invalid.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
enabled = tokens.filter(isAgentType);
|
|
61
|
+
if (enabled.length === 0) {
|
|
62
|
+
log.warn('No valid agents selected — keeping previous selection.');
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const updated = await patchSettings({ enabled_agents: enabled });
|
|
67
|
+
await saveLocalConfig(updated);
|
|
68
|
+
log.success(`Enabled agents: ${updated.enabled_agents.join(', ')}`);
|
|
69
|
+
return updated;
|
|
70
|
+
}
|
|
71
|
+
async function configurePaths(rl, current) {
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(chalk.bold('Skill paths (press enter to keep current):'));
|
|
74
|
+
const skill_paths = {};
|
|
75
|
+
for (const id of current.enabled_agents) {
|
|
76
|
+
const meta = AGENT_REGISTRY[id];
|
|
77
|
+
const existing = current.skill_paths[id] ?? meta.defaultPath;
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(chalk.cyan(meta.label) + chalk.dim(` (${id})`));
|
|
80
|
+
const macAns = (await rl.question(` mac [${existing.mac}]: `)).trim();
|
|
81
|
+
const winAns = (await rl.question(` windows [${existing.windows}]: `)).trim();
|
|
82
|
+
const next = {};
|
|
83
|
+
if (macAns)
|
|
84
|
+
next.mac = macAns;
|
|
85
|
+
if (winAns)
|
|
86
|
+
next.windows = winAns;
|
|
87
|
+
if (next.mac || next.windows) {
|
|
88
|
+
skill_paths[id] = next;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!skill_paths || Object.keys(skill_paths).length === 0) {
|
|
92
|
+
log.dim('No path changes.');
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
const updated = await patchSettings({ skill_paths });
|
|
96
|
+
await saveLocalConfig(updated);
|
|
97
|
+
log.success('Skill paths saved.');
|
|
98
|
+
return updated;
|
|
99
|
+
}
|
|
100
|
+
async function configureOs(rl, current) {
|
|
101
|
+
console.log();
|
|
102
|
+
const detected = detectOs();
|
|
103
|
+
console.log(`Detected OS: ${chalk.cyan(detected)}`);
|
|
104
|
+
console.log(`Current setting: ${chalk.cyan(current.detected_os ?? '(unset)')}`);
|
|
105
|
+
const ans = (await rl.question('Override OS [mac/windows/linux] (enter to keep): ')).trim().toLowerCase();
|
|
106
|
+
let target = detected;
|
|
107
|
+
if (ans === '') {
|
|
108
|
+
if (current.detected_os)
|
|
109
|
+
return current;
|
|
110
|
+
target = detected;
|
|
111
|
+
}
|
|
112
|
+
else if (ans === 'mac' || ans === 'windows' || ans === 'linux') {
|
|
113
|
+
target = ans;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
log.warn('Invalid OS — keeping current.');
|
|
117
|
+
return current;
|
|
118
|
+
}
|
|
119
|
+
const updated = await patchSettings({ detected_os: target });
|
|
120
|
+
await saveLocalConfig(updated);
|
|
121
|
+
log.success(`OS set to ${updated.detected_os}`);
|
|
122
|
+
return updated;
|
|
123
|
+
}
|
|
124
|
+
async function showAccount() {
|
|
125
|
+
console.log();
|
|
126
|
+
log.dim('Account info:');
|
|
127
|
+
log.dim(` API: ${CONFIG.apiUrl}`);
|
|
128
|
+
log.dim(` Credentials: ${CONFIG.credentialsPath}`);
|
|
129
|
+
log.dim(` Local config: ${CONFIG.configPath}`);
|
|
130
|
+
log.dim('Run `prave whoami` for full identity, or `prave logout` to sign out.');
|
|
131
|
+
}
|
|
132
|
+
export async function settingsCommand() {
|
|
133
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
134
|
+
try {
|
|
135
|
+
let current;
|
|
136
|
+
try {
|
|
137
|
+
current = await fetchSettings();
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
while (true) {
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(chalk.bold('Prave Settings'));
|
|
147
|
+
console.log(` ${chalk.cyan('1)')} Agent Configuration`);
|
|
148
|
+
console.log(` ${chalk.cyan('2)')} Skill Paths`);
|
|
149
|
+
console.log(` ${chalk.cyan('3)')} OS Settings`);
|
|
150
|
+
console.log(` ${chalk.cyan('4)')} Account`);
|
|
151
|
+
console.log(` ${chalk.cyan('5)')} Exit`);
|
|
152
|
+
const ans = (await rl.question('\nChoose [1-5]: ')).trim();
|
|
153
|
+
try {
|
|
154
|
+
if (ans === '1')
|
|
155
|
+
current = await configureAgents(rl, current);
|
|
156
|
+
else if (ans === '2')
|
|
157
|
+
current = await configurePaths(rl, current);
|
|
158
|
+
else if (ans === '3')
|
|
159
|
+
current = await configureOs(rl, current);
|
|
160
|
+
else if (ans === '4')
|
|
161
|
+
await showAccount();
|
|
162
|
+
else if (ans === '5' || ans === '' || ans.toLowerCase() === 'exit') {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
log.warn('Unknown choice.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
rl.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { tokenTier } from '@prave/shared';
|
|
4
|
+
import { api, ApiError } from '../lib/api.js';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
const TIER_BADGE = {
|
|
7
|
+
lean: chalk.green('🟢 Lean'),
|
|
8
|
+
medium: chalk.yellow('🟡 Medium'),
|
|
9
|
+
heavy: chalk.red('🔴 Heavy'),
|
|
10
|
+
};
|
|
11
|
+
const RULE = '─────────────────────────────────────────';
|
|
12
|
+
function formatTokens(n) {
|
|
13
|
+
if (n < 1000)
|
|
14
|
+
return `~${n}`;
|
|
15
|
+
return `~${(n / 1000).toFixed(1)}k`;
|
|
16
|
+
}
|
|
17
|
+
export async function whatdoesCommand(skillName) {
|
|
18
|
+
const spinner = ora(`Looking up ${skillName}…`).start();
|
|
19
|
+
try {
|
|
20
|
+
const { data } = await api.get(`/api/v1/intelligence/whatdoes/${encodeURIComponent(skillName)}`, true);
|
|
21
|
+
spinner.stop();
|
|
22
|
+
if (!data.found) {
|
|
23
|
+
log.warn('Skill not found.');
|
|
24
|
+
if (data.similar && data.similar.length > 0) {
|
|
25
|
+
log.dim('Did you mean:');
|
|
26
|
+
for (const s of data.similar) {
|
|
27
|
+
console.log(` ${chalk.cyan('•')} ${s.slug} ${chalk.dim(`— ${s.name}`)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (data.source === 'marketplace' && data.marketplace) {
|
|
34
|
+
const m = data.marketplace;
|
|
35
|
+
console.log(chalk.dim(RULE));
|
|
36
|
+
console.log(`📋 ${chalk.bold(m.name)} ${chalk.dim('— ' + (m.description ?? ''))}`);
|
|
37
|
+
console.log(chalk.dim(RULE));
|
|
38
|
+
console.log(`⭐ Rating: ${m.rating ?? 'n/a'}`);
|
|
39
|
+
console.log(`📦 Installs: ${m.install_count}`);
|
|
40
|
+
console.log(`🔖 Slug: ${m.slug}`);
|
|
41
|
+
console.log(chalk.dim(RULE));
|
|
42
|
+
log.dim(`Run \`prave install ${m.slug}\` to install`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const meta = data.metadata;
|
|
46
|
+
if (!meta) {
|
|
47
|
+
log.warn('No metadata available for this skill.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const triggers = meta.triggers.length > 0
|
|
51
|
+
? meta.triggers.map((t) => `"${t}"`).join(', ')
|
|
52
|
+
: 'none';
|
|
53
|
+
const tier = tokenTier(meta.estimated_tokens);
|
|
54
|
+
const userInvocable = meta.user_invocable
|
|
55
|
+
? `Yes ${meta.slash_command ? chalk.dim(`(${meta.slash_command})`) : ''}`
|
|
56
|
+
: 'No';
|
|
57
|
+
const requires = meta.requires.length > 0 ? meta.requires.join(', ') : 'none';
|
|
58
|
+
const conflicts = data.conflicts.length > 0
|
|
59
|
+
? data.conflicts
|
|
60
|
+
.map((c) => c.conflict_detail ?? c.conflict_type)
|
|
61
|
+
.join('; ')
|
|
62
|
+
: 'None detected';
|
|
63
|
+
console.log(chalk.dim(RULE));
|
|
64
|
+
console.log(`📋 ${chalk.bold(meta.name ?? skillName)} ${chalk.dim('— ' + (meta.description ?? ''))}`);
|
|
65
|
+
console.log(chalk.dim(RULE));
|
|
66
|
+
console.log(`🔥 Auto-triggers: ${triggers}`);
|
|
67
|
+
console.log(`👤 User-invocable: ${userInvocable}`);
|
|
68
|
+
console.log(`⚡ Est. tokens: ${formatTokens(meta.estimated_tokens)} ${TIER_BADGE[tier]}`);
|
|
69
|
+
console.log(`🔗 Requires: ${requires}`);
|
|
70
|
+
console.log(`${data.conflicts.length > 0 ? chalk.yellow('⚠️ ') : '⚠️ '}Conflicts: ${conflicts}`);
|
|
71
|
+
console.log(chalk.dim(RULE));
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
spinner.stop();
|
|
75
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,16 +3,23 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
+
import { conflictsCommand } from './commands/conflicts.js';
|
|
7
|
+
import { deployCommand } from './commands/deploy.js';
|
|
6
8
|
import { diffCommand } from './commands/diff.js';
|
|
7
9
|
import { exportCommand } from './commands/export.js';
|
|
10
|
+
import { findCommand } from './commands/find.js';
|
|
8
11
|
import { importCommand } from './commands/import.js';
|
|
9
12
|
import { installCommand } from './commands/install.js';
|
|
10
13
|
import { listCommand } from './commands/list.js';
|
|
11
14
|
import { loginCommand } from './commands/login.js';
|
|
12
15
|
import { logoutCommand } from './commands/logout.js';
|
|
16
|
+
import { optimizeCommand } from './commands/optimize.js';
|
|
17
|
+
import { overviewCommand } from './commands/overview.js';
|
|
13
18
|
import { searchCommand } from './commands/search.js';
|
|
19
|
+
import { settingsCommand } from './commands/settings.js';
|
|
14
20
|
import { syncCommand } from './commands/sync.js';
|
|
15
21
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
22
|
+
import { whatdoesCommand } from './commands/whatdoes.js';
|
|
16
23
|
import { whoamiCommand } from './commands/whoami.js';
|
|
17
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
25
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -47,6 +54,9 @@ program
|
|
|
47
54
|
.command('list')
|
|
48
55
|
.description('List installed Skills (default) or remote ones')
|
|
49
56
|
.option('--remote', 'list Skills from your Prave account')
|
|
57
|
+
.option('--verbose', 'show enriched intelligence (tokens, tier, agents)')
|
|
58
|
+
.option('--conflicts', 'only show skills involved in conflicts')
|
|
59
|
+
.option('--heavy', 'only show skills with >5k estimated tokens')
|
|
50
60
|
.action(listCommand);
|
|
51
61
|
program.command('search <query>').description('Search public Skills').action(searchCommand);
|
|
52
62
|
program
|
|
@@ -58,6 +68,43 @@ program
|
|
|
58
68
|
.command('diff <slug>')
|
|
59
69
|
.description('Show local vs registry diff for an installed Skill')
|
|
60
70
|
.action(diffCommand);
|
|
71
|
+
program
|
|
72
|
+
.command('settings')
|
|
73
|
+
.description('Configure agent paths + enabled agents')
|
|
74
|
+
.action(settingsCommand);
|
|
75
|
+
program
|
|
76
|
+
.command('whatdoes <skillname>')
|
|
77
|
+
.description("Inspect a skill's triggers, tokens, conflicts")
|
|
78
|
+
.action(whatdoesCommand);
|
|
79
|
+
program
|
|
80
|
+
.command('overview')
|
|
81
|
+
.description('Summary of your skill set, conflicts, and token cost')
|
|
82
|
+
.option('--json', 'emit raw JSON for scripting')
|
|
83
|
+
.option('--agent <name>', 'filter the breakdown by agent')
|
|
84
|
+
.action(overviewCommand);
|
|
85
|
+
program
|
|
86
|
+
.command('conflicts')
|
|
87
|
+
.description('Detect overlap, collisions, and missing dependencies')
|
|
88
|
+
.option('--fix', 'placeholder for interactive fix flow')
|
|
89
|
+
.action(conflictsCommand);
|
|
90
|
+
program
|
|
91
|
+
.command('optimize')
|
|
92
|
+
.description('Recommendations: underused, mergeable, and heavy skills')
|
|
93
|
+
.option('--apply', 'placeholder for auto-apply')
|
|
94
|
+
.action(optimizeCommand);
|
|
95
|
+
program
|
|
96
|
+
.command('find <query>')
|
|
97
|
+
.description('Smart skill search across local and marketplace')
|
|
98
|
+
.option('--local', 'only search local skills')
|
|
99
|
+
.option('--marketplace', 'only search the marketplace')
|
|
100
|
+
.option('--smart', 'use the LLM-assisted search endpoint')
|
|
101
|
+
.action(findCommand);
|
|
102
|
+
program
|
|
103
|
+
.command('deploy <skillname>')
|
|
104
|
+
.description('Deploy a skill to one or all configured agents')
|
|
105
|
+
.option('--agent <agent>', 'target a single agent (default: all)')
|
|
106
|
+
.option('--dry-run', 'log destinations but write no files')
|
|
107
|
+
.action(deployCommand);
|
|
61
108
|
program.parseAsync().catch((err) => {
|
|
62
109
|
console.error(err.message);
|
|
63
110
|
process.exit(1);
|