clawsouls 0.13.6 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/clawsouls.js +12 -2
- package/dist/commands/__tests__/init.test.js +0 -1
- package/dist/commands/checkpoint.js +0 -1
- package/dist/commands/diff.js +0 -1
- package/dist/commands/doctor.js +0 -1
- package/dist/commands/export.js +0 -1
- package/dist/commands/import.d.ts +9 -0
- package/dist/commands/import.js +218 -0
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +24 -1
- package/dist/commands/list.js +0 -1
- package/dist/commands/login.js +0 -1
- package/dist/commands/migrate.js +0 -1
- package/dist/commands/platform.js +0 -1
- package/dist/commands/publish.js +0 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/scan.js +0 -1
- package/dist/commands/search.js +0 -1
- package/dist/commands/soulscan.js +0 -1
- package/dist/commands/swarm.js +0 -1
- package/dist/commands/sync.js +0 -1
- package/dist/commands/test.js +0 -1
- package/dist/commands/update.js +0 -1
- package/dist/commands/use.js +0 -1
- package/dist/commands/validate.js +0 -1
- package/dist/commands/version.js +0 -1
- package/dist/import/classifier.d.ts +15 -0
- package/dist/import/classifier.js +156 -0
- package/dist/import/converter.d.ts +24 -0
- package/dist/import/converter.js +84 -0
- package/dist/import/parser.d.ts +20 -0
- package/dist/import/parser.js +79 -0
- package/dist/import/soul-json-generator.d.ts +28 -0
- package/dist/import/soul-json-generator.js +133 -0
- package/dist/lib/__tests__/scanner.test.js +0 -1
- package/dist/lib/age-crypto.js +0 -1
- package/dist/lib/llm-merge.js +0 -1
- package/dist/lib/rules-loader.js +0 -1
- package/dist/lib/scanner.js +0 -1
- package/dist/lib/semantic-analyzer.js +0 -1
- package/dist/storage/local.js +0 -1
- package/dist/storage/manager.js +0 -1
- package/dist/utils/config.js +0 -1
- package/dist/utils/platform.js +0 -1
- package/package.json +1 -1
- package/dist/bin/clawsouls.js.map +0 -1
- package/dist/commands/__tests__/init.test.js.map +0 -1
- package/dist/commands/checkpoint.js.map +0 -1
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/info.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/migrate.js.map +0 -1
- package/dist/commands/platform.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/restore.js.map +0 -1
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/soulscan.js.map +0 -1
- package/dist/commands/swarm.js.map +0 -1
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/use.js.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/version.js.map +0 -1
- package/dist/lib/__tests__/scanner.test.js.map +0 -1
- package/dist/lib/age-crypto.js.map +0 -1
- package/dist/lib/llm-merge.js.map +0 -1
- package/dist/lib/rules-loader.js.map +0 -1
- package/dist/lib/scanner.js.map +0 -1
- package/dist/lib/semantic-analyzer.js.map +0 -1
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/manager.js.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/platform.js.map +0 -1
package/dist/bin/clawsouls.js
CHANGED
|
@@ -14,6 +14,7 @@ import { soulscanCommand } from '../commands/soulscan.js';
|
|
|
14
14
|
import { scanCommand as localScanCommand } from '../commands/scan.js';
|
|
15
15
|
import { platformCommand } from '../commands/platform.js';
|
|
16
16
|
import { exportCommand } from '../commands/export.js';
|
|
17
|
+
import { importCommand } from '../commands/import.js';
|
|
17
18
|
import { syncInitCommand, syncPushCommand, syncPullCommand, syncStatusCommand, syncExportKeyCommand, syncImportKeyCommand } from '../commands/sync.js';
|
|
18
19
|
import { versionBumpCommand } from '../commands/version.js';
|
|
19
20
|
import { diffCommand } from '../commands/diff.js';
|
|
@@ -44,7 +45,7 @@ program
|
|
|
44
45
|
.command('install <name>')
|
|
45
46
|
.description('Install a soul from the registry')
|
|
46
47
|
.option('-f, --force', 'Overwrite if already installed')
|
|
47
|
-
.option('--use <platform>', 'Auto-activate after install (claude-code, cursor, windsurf, openclaw)')
|
|
48
|
+
.option('--use <platform>', 'Auto-activate after install (claude-code, cursor, windsurf, openclaw, hermes)')
|
|
48
49
|
.action(installCommand);
|
|
49
50
|
program
|
|
50
51
|
.command('use <name>')
|
|
@@ -119,6 +120,16 @@ program
|
|
|
119
120
|
.option('-o, --output <path>', 'Output file path')
|
|
120
121
|
.option('-d, --dir <path>', 'Source soul directory (default: active workspace)')
|
|
121
122
|
.action((format, opts) => exportCommand(format, opts));
|
|
123
|
+
program
|
|
124
|
+
.command('import <format>')
|
|
125
|
+
.description('Import persona from other formats (claude-md, system-prompt, cursorrules)')
|
|
126
|
+
.option('-i, --input <path>', 'Source file path (default: auto-detect)')
|
|
127
|
+
.option('-o, --output <path>', 'Output directory (default: ./)')
|
|
128
|
+
.option('--dry-run', 'Preview classification without writing files')
|
|
129
|
+
.option('--smart', 'Use LLM-assisted classification (requires API key)')
|
|
130
|
+
.option('--name <name>', 'Soul name for soul.json')
|
|
131
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
132
|
+
.action((format, opts) => importCommand(format, opts));
|
|
122
133
|
// ─── Sync Commands ───────────────────────────────────────
|
|
123
134
|
const sync = program
|
|
124
135
|
.command('sync')
|
|
@@ -387,4 +398,3 @@ swKeys.command('rotate')
|
|
|
387
398
|
catch { /* ignore — don't block CLI on network issues */ }
|
|
388
399
|
})();
|
|
389
400
|
program.parse();
|
|
390
|
-
//# sourceMappingURL=clawsouls.js.map
|
package/dist/commands/diff.js
CHANGED
package/dist/commands/doctor.js
CHANGED
package/dist/commands/export.js
CHANGED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, statSync } from 'fs';
|
|
3
|
+
import { join, resolve, basename } from 'path';
|
|
4
|
+
import { parseMarkdown } from '../import/parser.js';
|
|
5
|
+
import { classifySections } from '../import/classifier.js';
|
|
6
|
+
import { convertSections, estimateSize } from '../import/converter.js';
|
|
7
|
+
import { generateSoulJson } from '../import/soul-json-generator.js';
|
|
8
|
+
const SUPPORTED_FORMATS = ['claude-md', 'system-prompt', 'cursorrules', 'windsurfrules'];
|
|
9
|
+
/**
|
|
10
|
+
* Auto-detect input file based on format.
|
|
11
|
+
*/
|
|
12
|
+
function autoDetectInput(format, cwd) {
|
|
13
|
+
switch (format) {
|
|
14
|
+
case 'claude-md':
|
|
15
|
+
for (const candidate of ['CLAUDE.md', 'claude.md']) {
|
|
16
|
+
const p = join(cwd, candidate);
|
|
17
|
+
if (existsSync(p))
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
case 'cursorrules':
|
|
22
|
+
for (const candidate of ['.cursorrules', '.cursor/rules/persona.md', '.cursor/rules/main.md']) {
|
|
23
|
+
const p = join(cwd, candidate);
|
|
24
|
+
if (existsSync(p))
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
case 'windsurfrules':
|
|
29
|
+
for (const candidate of ['.windsurfrules']) {
|
|
30
|
+
const p = join(cwd, candidate);
|
|
31
|
+
if (existsSync(p))
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
case 'system-prompt':
|
|
36
|
+
for (const candidate of ['system-prompt.txt', 'system_prompt.txt', 'prompt.txt']) {
|
|
37
|
+
const p = join(cwd, candidate);
|
|
38
|
+
if (existsSync(p))
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
default:
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* For system-prompt format: wrap entire content in a single SOUL.md section.
|
|
48
|
+
*/
|
|
49
|
+
function importAsSystemPrompt(content) {
|
|
50
|
+
const sections = parseMarkdown(content);
|
|
51
|
+
if (sections.length === 0) {
|
|
52
|
+
return [{
|
|
53
|
+
heading: '',
|
|
54
|
+
content,
|
|
55
|
+
level: 0,
|
|
56
|
+
lineStart: 0,
|
|
57
|
+
target: 'SOUL.md',
|
|
58
|
+
confidence: 80,
|
|
59
|
+
reason: 'system-prompt: entire content',
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
// Classify normally but bump all to SOUL.md
|
|
63
|
+
return classifySections(sections).map(s => ({
|
|
64
|
+
...s,
|
|
65
|
+
target: 'SOUL.md',
|
|
66
|
+
confidence: 70,
|
|
67
|
+
reason: 'system-prompt format: routed to SOUL.md',
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
function formatBytes(n) {
|
|
71
|
+
if (n < 1024)
|
|
72
|
+
return `${n} B`;
|
|
73
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
74
|
+
}
|
|
75
|
+
function confidenceColor(n) {
|
|
76
|
+
if (n >= 80)
|
|
77
|
+
return chalk.green(`${n}%`);
|
|
78
|
+
if (n >= 60)
|
|
79
|
+
return chalk.yellow(`${n}%`);
|
|
80
|
+
return chalk.red(`${n}%`) + chalk.dim(' ⚠ low confidence');
|
|
81
|
+
}
|
|
82
|
+
function targetColor(target) {
|
|
83
|
+
switch (target) {
|
|
84
|
+
case 'SOUL.md': return chalk.magenta(target);
|
|
85
|
+
case 'IDENTITY.md': return chalk.cyan(target);
|
|
86
|
+
case 'AGENTS.md': return chalk.blue(target);
|
|
87
|
+
case 'STYLE.md': return chalk.yellow(target);
|
|
88
|
+
case 'HEARTBEAT.md': return chalk.green(target);
|
|
89
|
+
case 'RULES.md': return chalk.red(target);
|
|
90
|
+
case 'TOOLS.md': return chalk.gray(target);
|
|
91
|
+
case 'SKIP': return chalk.dim('SKIP');
|
|
92
|
+
default: return target;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function importCommand(format, options) {
|
|
96
|
+
if (!SUPPORTED_FORMATS.includes(format)) {
|
|
97
|
+
console.error(chalk.red(`Unknown format: "${format}"`));
|
|
98
|
+
console.error(`Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const cwd = process.cwd();
|
|
102
|
+
const outputDir = resolve(options.output || cwd);
|
|
103
|
+
// Resolve input file
|
|
104
|
+
let inputPath;
|
|
105
|
+
if (options.input) {
|
|
106
|
+
inputPath = resolve(options.input);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const detected = autoDetectInput(format, cwd);
|
|
110
|
+
if (!detected) {
|
|
111
|
+
console.error(chalk.red(`Could not auto-detect input file for format "${format}"`));
|
|
112
|
+
console.error(`Use --input <path> to specify the file.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
inputPath = detected;
|
|
116
|
+
}
|
|
117
|
+
if (!existsSync(inputPath)) {
|
|
118
|
+
console.error(chalk.red(`File not found: ${inputPath}`));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const content = readFileSync(inputPath, 'utf-8');
|
|
122
|
+
const fileSize = statSync(inputPath).size;
|
|
123
|
+
// Parse
|
|
124
|
+
const rawSections = parseMarkdown(content);
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(chalk.bold(`Analyzing: ${chalk.cyan(basename(inputPath))} `) +
|
|
127
|
+
chalk.dim(`(${formatBytes(fileSize)}, ${rawSections.length} section${rawSections.length !== 1 ? 's' : ''})`));
|
|
128
|
+
console.log('');
|
|
129
|
+
// Classify
|
|
130
|
+
let classified = format === 'system-prompt'
|
|
131
|
+
? importAsSystemPrompt(content)
|
|
132
|
+
: classifySections(rawSections);
|
|
133
|
+
if (options.smart) {
|
|
134
|
+
console.log(chalk.dim(' --smart: LLM-assisted classification not yet implemented, using rule-based.'));
|
|
135
|
+
}
|
|
136
|
+
// Group for summary
|
|
137
|
+
const grouped = new Map();
|
|
138
|
+
for (const s of classified) {
|
|
139
|
+
const arr = grouped.get(s.target) || [];
|
|
140
|
+
arr.push(s);
|
|
141
|
+
grouped.set(s.target, arr);
|
|
142
|
+
}
|
|
143
|
+
// Print classification table
|
|
144
|
+
console.log(chalk.bold('Classification Results:'));
|
|
145
|
+
for (const s of classified) {
|
|
146
|
+
const heading = s.heading
|
|
147
|
+
? s.heading.slice(0, 45).padEnd(46)
|
|
148
|
+
: chalk.dim('(no heading)'.padEnd(46));
|
|
149
|
+
const arrow = chalk.dim('→');
|
|
150
|
+
const target = targetColor(s.target).padEnd(20);
|
|
151
|
+
const conf = confidenceColor(s.confidence);
|
|
152
|
+
console.log(` ${heading} ${arrow} ${target} (confidence: ${conf})`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(chalk.bold('Files to create:'));
|
|
156
|
+
const allTargets = ['SOUL.md', 'IDENTITY.md', 'AGENTS.md', 'STYLE.md', 'HEARTBEAT.md', 'RULES.md', 'TOOLS.md'];
|
|
157
|
+
let hasLowConfidence = false;
|
|
158
|
+
for (const target of allTargets) {
|
|
159
|
+
const sections = grouped.get(target);
|
|
160
|
+
if (!sections || sections.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
const sizeStr = formatBytes(estimateSize(sections));
|
|
163
|
+
const count = sections.length;
|
|
164
|
+
const isExtras = ['RULES.md', 'TOOLS.md'].includes(target);
|
|
165
|
+
const label = isExtras ? `extras/${target}` : target;
|
|
166
|
+
const lowConf = sections.some(s => s.confidence < 60);
|
|
167
|
+
if (lowConf)
|
|
168
|
+
hasLowConfidence = true;
|
|
169
|
+
console.log(` ${targetColor(target).padEnd(22)} ${String(count).padStart(2)} section${count !== 1 ? 's' : ' '} (~${sizeStr})`);
|
|
170
|
+
}
|
|
171
|
+
console.log(` ${'soul.json'.padEnd(22)} auto-generated`);
|
|
172
|
+
const skipCount = (grouped.get('SKIP') || []).length;
|
|
173
|
+
const lowConfCount = classified.filter(s => s.confidence < 60 && s.target !== 'SKIP').length;
|
|
174
|
+
console.log('');
|
|
175
|
+
if (skipCount > 0) {
|
|
176
|
+
console.log(chalk.dim(`Skipped: ${skipCount} section${skipCount !== 1 ? 's' : ''} (memory/config, not persona)`));
|
|
177
|
+
}
|
|
178
|
+
if (lowConfCount > 0) {
|
|
179
|
+
console.log(chalk.yellow(`Low confidence: ${lowConfCount} section${lowConfCount !== 1 ? 's' : ''} — use --smart for LLM-assisted classification`));
|
|
180
|
+
}
|
|
181
|
+
if (options.dryRun) {
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.dim('Dry run — no files written. Remove --dry-run to create files.'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
// Write files
|
|
188
|
+
const result = convertSections(classified, {
|
|
189
|
+
outputDir,
|
|
190
|
+
force: options.force,
|
|
191
|
+
dryRun: false,
|
|
192
|
+
});
|
|
193
|
+
// Write soul.json
|
|
194
|
+
const soulJson = generateSoulJson(classified, { name: options.name });
|
|
195
|
+
const soulJsonPath = join(outputDir, 'soul.json');
|
|
196
|
+
if (!existsSync(soulJsonPath) || options.force) {
|
|
197
|
+
writeFileSync(soulJsonPath, JSON.stringify(soulJson, null, 2) + '\n', 'utf-8');
|
|
198
|
+
result.written.push(soulJsonPath);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
result.existing.push(soulJsonPath);
|
|
202
|
+
}
|
|
203
|
+
// Summary
|
|
204
|
+
console.log(chalk.bold('Summary:'));
|
|
205
|
+
for (const f of result.written) {
|
|
206
|
+
console.log(` ${chalk.green('✓')} ${f}`);
|
|
207
|
+
}
|
|
208
|
+
if (result.existing.length > 0) {
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(chalk.yellow(` Skipped (already exist — use --force to overwrite):`));
|
|
211
|
+
for (const f of result.existing) {
|
|
212
|
+
console.log(chalk.dim(` ${f}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(chalk.green(`Done! Soul Spec files written to: ${chalk.bold(outputDir)}`));
|
|
217
|
+
console.log(chalk.dim(' Run `clawsouls validate` to check the generated package.'));
|
|
218
|
+
}
|
package/dist/commands/info.js
CHANGED
package/dist/commands/init.js
CHANGED
package/dist/commands/install.js
CHANGED
|
@@ -73,6 +73,29 @@ export function applyToPlatform(platform, soulDir, manifest) {
|
|
|
73
73
|
writeFileSync(target, combined, 'utf-8');
|
|
74
74
|
return target;
|
|
75
75
|
}
|
|
76
|
+
case 'hermes': {
|
|
77
|
+
// Hermes Agent: SOUL.md → ~/.hermes/SOUL.md (identity slot #1)
|
|
78
|
+
// IDENTITY.md + STYLE.md merged into SOUL.md
|
|
79
|
+
// AGENTS.md → CWD/AGENTS.md (Hermes discovers it automatically)
|
|
80
|
+
const hermesHome = process.env.HERMES_HOME || join(process.env.HOME || '~', '.hermes');
|
|
81
|
+
mkdirSync(hermesHome, { recursive: true });
|
|
82
|
+
// Build combined SOUL.md (soul + identity + style)
|
|
83
|
+
const hermesSoulSections = [];
|
|
84
|
+
if (soulText)
|
|
85
|
+
hermesSoulSections.push(soulText);
|
|
86
|
+
if (identityText)
|
|
87
|
+
hermesSoulSections.push(`\n---\n\n${identityText}`);
|
|
88
|
+
if (styleText)
|
|
89
|
+
hermesSoulSections.push(`\n---\n\n${styleText}`);
|
|
90
|
+
const hermesSoulPath = join(hermesHome, 'SOUL.md');
|
|
91
|
+
writeFileSync(hermesSoulPath, hermesSoulSections.join('\n'), 'utf-8');
|
|
92
|
+
// AGENTS.md → CWD (Hermes auto-discovers)
|
|
93
|
+
if (agentsText) {
|
|
94
|
+
const agentsPath = join(cwd, 'AGENTS.md');
|
|
95
|
+
writeFileSync(agentsPath, agentsText, 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
return hermesSoulPath;
|
|
98
|
+
}
|
|
76
99
|
case 'openclaw': {
|
|
77
100
|
// Delegate to existing useCommand for OpenClaw workspace
|
|
78
101
|
return ''; // Signal to use existing flow
|
|
@@ -142,6 +165,7 @@ export async function installCommand(nameWithVersion, options) {
|
|
|
142
165
|
'claude-code': 'Claude Code',
|
|
143
166
|
'cursor': 'Cursor',
|
|
144
167
|
'windsurf': 'Windsurf',
|
|
168
|
+
'hermes': 'Hermes Agent',
|
|
145
169
|
};
|
|
146
170
|
const label = platformLabels[options.use] || options.use;
|
|
147
171
|
spinner.succeed(`Installed ${chalk.green(displayName)} v${ver} → ${chalk.cyan(label)}\n` +
|
|
@@ -160,4 +184,3 @@ export async function installCommand(nameWithVersion, options) {
|
|
|
160
184
|
process.exit(1);
|
|
161
185
|
}
|
|
162
186
|
}
|
|
163
|
-
//# sourceMappingURL=install.js.map
|
package/dist/commands/list.js
CHANGED
package/dist/commands/login.js
CHANGED
package/dist/commands/migrate.js
CHANGED
package/dist/commands/publish.js
CHANGED
package/dist/commands/restore.js
CHANGED
package/dist/commands/scan.js
CHANGED
package/dist/commands/search.js
CHANGED
package/dist/commands/swarm.js
CHANGED
package/dist/commands/sync.js
CHANGED
package/dist/commands/test.js
CHANGED
package/dist/commands/update.js
CHANGED
package/dist/commands/use.js
CHANGED
package/dist/commands/version.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-based classifier for Soul Spec section routing.
|
|
3
|
+
* Classifies parsed markdown sections into target Soul Spec files.
|
|
4
|
+
*/
|
|
5
|
+
import { Section } from './parser.js';
|
|
6
|
+
export type SoulTarget = 'SOUL.md' | 'IDENTITY.md' | 'AGENTS.md' | 'STYLE.md' | 'HEARTBEAT.md' | 'RULES.md' | 'TOOLS.md' | 'SKIP';
|
|
7
|
+
export interface ClassifiedSection extends Section {
|
|
8
|
+
target: SoulTarget;
|
|
9
|
+
confidence: number;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Classify all sections and return enriched array.
|
|
14
|
+
*/
|
|
15
|
+
export declare function classifySections(sections: Section[]): ClassifiedSection[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-based classifier for Soul Spec section routing.
|
|
3
|
+
* Classifies parsed markdown sections into target Soul Spec files.
|
|
4
|
+
*/
|
|
5
|
+
import { headingText } from './parser.js';
|
|
6
|
+
const CLASSIFICATION_RULES = [
|
|
7
|
+
{
|
|
8
|
+
target: 'SOUL.md',
|
|
9
|
+
keywords: [
|
|
10
|
+
'personality', 'character', 'traits', 'values', 'principles',
|
|
11
|
+
'who you are', 'core beliefs', 'philosophy', 'soul', 'persona',
|
|
12
|
+
'성격', '원칙', '가치관', '철학', '핵심',
|
|
13
|
+
],
|
|
14
|
+
weight: 1.0,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
target: 'IDENTITY.md',
|
|
18
|
+
keywords: [
|
|
19
|
+
'name', 'identity', 'background', 'role', 'creature', 'species',
|
|
20
|
+
'who am i', 'i am', 'about me',
|
|
21
|
+
'이름', '정체성', '역할', '배경', '자기소개',
|
|
22
|
+
],
|
|
23
|
+
weight: 1.0,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
target: 'AGENTS.md',
|
|
27
|
+
keywords: [
|
|
28
|
+
'workflow', 'work style', 'safety', 'agents', 'protocol', 'procedure',
|
|
29
|
+
'rules', 'guidelines', 'constraints', 'boundaries', 'limits', 'behavior',
|
|
30
|
+
'process', 'steps', 'checklist', 'bootstrap', 'session start',
|
|
31
|
+
'워크플로우', '작업', '안전', '규칙', '절차', '가이드', '제약',
|
|
32
|
+
],
|
|
33
|
+
weight: 1.0,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
target: 'STYLE.md',
|
|
37
|
+
keywords: [
|
|
38
|
+
'style', 'tone', 'voice', 'communication', 'language', 'writing',
|
|
39
|
+
'speak', 'response format', 'format', 'output', 'formatting',
|
|
40
|
+
'말투', '스타일', '어조', '언어', '커뮤니케이션', '표현',
|
|
41
|
+
],
|
|
42
|
+
weight: 1.0,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
target: 'HEARTBEAT.md',
|
|
46
|
+
keywords: [
|
|
47
|
+
'heartbeat', 'periodic', 'cron', 'autonomous', 'check-in', 'schedule',
|
|
48
|
+
'interval', 'background', 'monitoring', 'health check', 'self-check',
|
|
49
|
+
'자율', '점검', '주기', '헬스체크', '배경', '크론',
|
|
50
|
+
],
|
|
51
|
+
weight: 1.2, // slightly higher — heartbeat is very distinct
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
target: 'RULES.md',
|
|
55
|
+
keywords: [
|
|
56
|
+
'do not', "don't", 'never', 'always', 'must', 'forbidden', 'prohibited',
|
|
57
|
+
'restriction', 'constraint', 'boundary',
|
|
58
|
+
'금지', '절대', '반드시', '제한',
|
|
59
|
+
],
|
|
60
|
+
weight: 0.9,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
target: 'TOOLS.md',
|
|
64
|
+
keywords: [
|
|
65
|
+
'tools', 'permissions', 'allowed', 'access', 'capabilities', 'mcp',
|
|
66
|
+
'function', 'api', 'integration',
|
|
67
|
+
'도구', '권한', '허용', '접근', '기능',
|
|
68
|
+
],
|
|
69
|
+
weight: 0.9,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Keywords that signal a section should be SKIPPED (not persona content).
|
|
74
|
+
* These are meta/config sections, not character/persona.
|
|
75
|
+
*/
|
|
76
|
+
const SKIP_KEYWORDS = [
|
|
77
|
+
'memory', 'context window', 'session', 'bootstrap', 'token',
|
|
78
|
+
'oh-my-claudecode', 'omc', 'mcp tools', 'hooks', 'state files',
|
|
79
|
+
'notepad', 'compaction', 'worktree',
|
|
80
|
+
'메모리', '컨텍스트', '세션', '부트스트랩',
|
|
81
|
+
];
|
|
82
|
+
/**
|
|
83
|
+
* Check if a section should be skipped based on heading or content.
|
|
84
|
+
*/
|
|
85
|
+
function shouldSkip(heading, content) {
|
|
86
|
+
const lower = (heading + ' ' + content.slice(0, 300)).toLowerCase();
|
|
87
|
+
for (const kw of SKIP_KEYWORDS) {
|
|
88
|
+
if (lower.includes(kw.toLowerCase())) {
|
|
89
|
+
return { skip: true, confidence: 85 };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { skip: false, confidence: 0 };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Score a section against a rule based on keyword matches in heading + content preview.
|
|
96
|
+
*/
|
|
97
|
+
function scoreSection(heading, content, rule) {
|
|
98
|
+
const text = (heading + ' ' + content.slice(0, 500)).toLowerCase();
|
|
99
|
+
let matches = 0;
|
|
100
|
+
let totalWeight = 0;
|
|
101
|
+
for (const kw of rule.keywords) {
|
|
102
|
+
if (text.includes(kw.toLowerCase())) {
|
|
103
|
+
matches++;
|
|
104
|
+
totalWeight += kw.split(' ').length > 1 ? 2 : 1; // multi-word keywords score higher
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (matches === 0)
|
|
108
|
+
return 0;
|
|
109
|
+
// Normalize: max score is 100
|
|
110
|
+
const rawScore = (totalWeight / rule.keywords.length) * 100 * rule.weight;
|
|
111
|
+
return Math.min(rawScore, 100);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Classify a single section.
|
|
115
|
+
*/
|
|
116
|
+
function classifySection(section) {
|
|
117
|
+
const htxt = headingText(section).toLowerCase();
|
|
118
|
+
const content = section.content;
|
|
119
|
+
// Skip check first
|
|
120
|
+
const skipResult = shouldSkip(htxt, content);
|
|
121
|
+
if (skipResult.skip) {
|
|
122
|
+
return { target: 'SKIP', confidence: skipResult.confidence, reason: 'memory/config content' };
|
|
123
|
+
}
|
|
124
|
+
// Score against all rules
|
|
125
|
+
const scores = [];
|
|
126
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
127
|
+
const score = scoreSection(htxt, content, rule);
|
|
128
|
+
if (score > 0) {
|
|
129
|
+
scores.push({ target: rule.target, score });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (scores.length === 0) {
|
|
133
|
+
// No match — fallback to SOUL.md with low confidence
|
|
134
|
+
return { target: 'SOUL.md', confidence: 30, reason: 'no keyword match — defaulting to SOUL.md' };
|
|
135
|
+
}
|
|
136
|
+
// Pick highest scoring
|
|
137
|
+
scores.sort((a, b) => b.score - a.score);
|
|
138
|
+
const best = scores[0];
|
|
139
|
+
const confidence = Math.round(Math.min(best.score + 20, 100)); // add base boost
|
|
140
|
+
const matchedRule = CLASSIFICATION_RULES.find(r => r.target === best.target);
|
|
141
|
+
const matchedKws = matchedRule.keywords.filter(kw => (htxt + ' ' + content.slice(0, 500).toLowerCase()).includes(kw.toLowerCase())).slice(0, 3);
|
|
142
|
+
return {
|
|
143
|
+
target: best.target,
|
|
144
|
+
confidence,
|
|
145
|
+
reason: `keyword match: ${matchedKws.join(', ')}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Classify all sections and return enriched array.
|
|
150
|
+
*/
|
|
151
|
+
export function classifySections(sections) {
|
|
152
|
+
return sections.map(section => {
|
|
153
|
+
const { target, confidence, reason } = classifySection(section);
|
|
154
|
+
return { ...section, target, confidence, reason };
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converter: takes classified sections and writes Soul Spec files.
|
|
3
|
+
*/
|
|
4
|
+
import { ClassifiedSection } from './classifier.js';
|
|
5
|
+
interface ConvertOptions {
|
|
6
|
+
outputDir: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface ConvertResult {
|
|
11
|
+
written: string[];
|
|
12
|
+
skipped: string[];
|
|
13
|
+
existing: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Write Soul Spec files from classified sections.
|
|
17
|
+
* Returns a summary of what was written/skipped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function convertSections(sections: ClassifiedSection[], options: ConvertOptions): ConvertResult;
|
|
20
|
+
/**
|
|
21
|
+
* Estimate byte size of content for a group of sections.
|
|
22
|
+
*/
|
|
23
|
+
export declare function estimateSize(sections: ClassifiedSection[]): number;
|
|
24
|
+
export {};
|