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.
Files changed (81) hide show
  1. package/dist/bin/clawsouls.js +12 -2
  2. package/dist/commands/__tests__/init.test.js +0 -1
  3. package/dist/commands/checkpoint.js +0 -1
  4. package/dist/commands/diff.js +0 -1
  5. package/dist/commands/doctor.js +0 -1
  6. package/dist/commands/export.js +0 -1
  7. package/dist/commands/import.d.ts +9 -0
  8. package/dist/commands/import.js +218 -0
  9. package/dist/commands/info.js +0 -1
  10. package/dist/commands/init.js +0 -1
  11. package/dist/commands/install.js +24 -1
  12. package/dist/commands/list.js +0 -1
  13. package/dist/commands/login.js +0 -1
  14. package/dist/commands/migrate.js +0 -1
  15. package/dist/commands/platform.js +0 -1
  16. package/dist/commands/publish.js +0 -1
  17. package/dist/commands/restore.js +0 -1
  18. package/dist/commands/scan.js +0 -1
  19. package/dist/commands/search.js +0 -1
  20. package/dist/commands/soulscan.js +0 -1
  21. package/dist/commands/swarm.js +0 -1
  22. package/dist/commands/sync.js +0 -1
  23. package/dist/commands/test.js +0 -1
  24. package/dist/commands/update.js +0 -1
  25. package/dist/commands/use.js +0 -1
  26. package/dist/commands/validate.js +0 -1
  27. package/dist/commands/version.js +0 -1
  28. package/dist/import/classifier.d.ts +15 -0
  29. package/dist/import/classifier.js +156 -0
  30. package/dist/import/converter.d.ts +24 -0
  31. package/dist/import/converter.js +84 -0
  32. package/dist/import/parser.d.ts +20 -0
  33. package/dist/import/parser.js +79 -0
  34. package/dist/import/soul-json-generator.d.ts +28 -0
  35. package/dist/import/soul-json-generator.js +133 -0
  36. package/dist/lib/__tests__/scanner.test.js +0 -1
  37. package/dist/lib/age-crypto.js +0 -1
  38. package/dist/lib/llm-merge.js +0 -1
  39. package/dist/lib/rules-loader.js +0 -1
  40. package/dist/lib/scanner.js +0 -1
  41. package/dist/lib/semantic-analyzer.js +0 -1
  42. package/dist/storage/local.js +0 -1
  43. package/dist/storage/manager.js +0 -1
  44. package/dist/utils/config.js +0 -1
  45. package/dist/utils/platform.js +0 -1
  46. package/package.json +1 -1
  47. package/dist/bin/clawsouls.js.map +0 -1
  48. package/dist/commands/__tests__/init.test.js.map +0 -1
  49. package/dist/commands/checkpoint.js.map +0 -1
  50. package/dist/commands/diff.js.map +0 -1
  51. package/dist/commands/doctor.js.map +0 -1
  52. package/dist/commands/export.js.map +0 -1
  53. package/dist/commands/info.js.map +0 -1
  54. package/dist/commands/init.js.map +0 -1
  55. package/dist/commands/install.js.map +0 -1
  56. package/dist/commands/list.js.map +0 -1
  57. package/dist/commands/login.js.map +0 -1
  58. package/dist/commands/migrate.js.map +0 -1
  59. package/dist/commands/platform.js.map +0 -1
  60. package/dist/commands/publish.js.map +0 -1
  61. package/dist/commands/restore.js.map +0 -1
  62. package/dist/commands/scan.js.map +0 -1
  63. package/dist/commands/search.js.map +0 -1
  64. package/dist/commands/soulscan.js.map +0 -1
  65. package/dist/commands/swarm.js.map +0 -1
  66. package/dist/commands/sync.js.map +0 -1
  67. package/dist/commands/test.js.map +0 -1
  68. package/dist/commands/update.js.map +0 -1
  69. package/dist/commands/use.js.map +0 -1
  70. package/dist/commands/validate.js.map +0 -1
  71. package/dist/commands/version.js.map +0 -1
  72. package/dist/lib/__tests__/scanner.test.js.map +0 -1
  73. package/dist/lib/age-crypto.js.map +0 -1
  74. package/dist/lib/llm-merge.js.map +0 -1
  75. package/dist/lib/rules-loader.js.map +0 -1
  76. package/dist/lib/scanner.js.map +0 -1
  77. package/dist/lib/semantic-analyzer.js.map +0 -1
  78. package/dist/storage/local.js.map +0 -1
  79. package/dist/storage/manager.js.map +0 -1
  80. package/dist/utils/config.js.map +0 -1
  81. package/dist/utils/platform.js.map +0 -1
@@ -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
@@ -58,4 +58,3 @@ describe('clawsouls init --spec', () => {
58
58
  expect(console.error).toHaveBeenCalledWith(expect.stringContaining('already exists'));
59
59
  });
60
60
  });
61
- //# sourceMappingURL=init.test.js.map
@@ -433,4 +433,3 @@ export async function checkpointScanCommand(dir, opts) {
433
433
  }
434
434
  console.log('');
435
435
  }
436
- //# sourceMappingURL=checkpoint.js.map
@@ -62,4 +62,3 @@ export async function diffCommand(soul, v1, v2) {
62
62
  chalk.green(`+${totalAdditions} additions`) + ', ' +
63
63
  chalk.red(`-${totalDeletions} deletions`));
64
64
  }
65
- //# sourceMappingURL=diff.js.map
@@ -127,4 +127,3 @@ export async function doctorCommand() {
127
127
  }
128
128
  console.log();
129
129
  }
130
- //# sourceMappingURL=doctor.js.map
@@ -157,4 +157,3 @@ export async function exportCommand(format, options) {
157
157
  console.log(chalk.dim(' 2. Claude Code picks it up automatically'));
158
158
  }
159
159
  }
160
- //# sourceMappingURL=export.js.map
@@ -0,0 +1,9 @@
1
+ export interface ImportOptions {
2
+ input?: string;
3
+ output?: string;
4
+ dryRun?: boolean;
5
+ smart?: boolean;
6
+ name?: string;
7
+ force?: boolean;
8
+ }
9
+ export declare function importCommand(format: string, options: ImportOptions): Promise<void>;
@@ -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
+ }
@@ -66,4 +66,3 @@ export async function infoCommand(nameArg) {
66
66
  console.log(`\n ${chalk.cyan(soulUrl)}`);
67
67
  console.log(chalk.dim(`\n Install: ${chalk.cyan(`clawsouls install ${nameArg}`)}\n`));
68
68
  }
69
- //# sourceMappingURL=info.js.map
@@ -250,4 +250,3 @@ export async function initCommand(name, specVersion, environment) {
250
250
  console.log(chalk.cyan(` clawsouls validate .`));
251
251
  console.log(chalk.cyan(` clawsouls publish .`));
252
252
  }
253
- //# sourceMappingURL=init.js.map
@@ -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
@@ -18,4 +18,3 @@ export async function listCommand() {
18
18
  }
19
19
  console.log();
20
20
  }
21
- //# sourceMappingURL=list.js.map
@@ -78,4 +78,3 @@ export async function whoamiCommand() {
78
78
  process.exit(1);
79
79
  }
80
80
  }
81
- //# sourceMappingURL=login.js.map
@@ -71,4 +71,3 @@ export async function migrateCommand(opts) {
71
71
  }
72
72
  console.log();
73
73
  }
74
- //# sourceMappingURL=migrate.js.map
@@ -34,4 +34,3 @@ export async function platformCommand() {
34
34
  console.log(` ${getKnownPlatformNames().join(', ')}, or any custom path`);
35
35
  console.log();
36
36
  }
37
- //# sourceMappingURL=platform.js.map
@@ -167,4 +167,3 @@ export async function publishCommand(dir, options) {
167
167
  process.exit(1);
168
168
  }
169
169
  }
170
- //# sourceMappingURL=publish.js.map
@@ -20,4 +20,3 @@ export async function restoreCommand() {
20
20
  process.exit(1);
21
21
  }
22
22
  }
23
- //# sourceMappingURL=restore.js.map
@@ -216,4 +216,3 @@ function printSemanticFindings(findings, model, durationMs) {
216
216
  }
217
217
  console.log(chalk.gray(`\n Model: ${model} | ${durationMs}ms\n`));
218
218
  }
219
- //# sourceMappingURL=scan.js.map
@@ -49,4 +49,3 @@ export async function searchCommand(query) {
49
49
  }
50
50
  console.log(chalk.dim(`\n Install with: ${chalk.cyan('clawsouls install owner/name')}\n`));
51
51
  }
52
- //# sourceMappingURL=search.js.map
@@ -508,4 +508,3 @@ function printScanResults(checks) {
508
508
  }
509
509
  console.log('');
510
510
  }
511
- //# sourceMappingURL=soulscan.js.map
@@ -598,4 +598,3 @@ function splitSections(content) {
598
598
  sections.push(current.trim());
599
599
  return sections;
600
600
  }
601
- //# sourceMappingURL=swarm.js.map
@@ -320,4 +320,3 @@ export async function syncImportKeyCommand(key) {
320
320
  console.log(chalk.red('Invalid key. Could not derive public key.'));
321
321
  }
322
322
  }
323
- //# sourceMappingURL=sync.js.map
@@ -327,4 +327,3 @@ export async function testCommand(opts) {
327
327
  if (!allPassed)
328
328
  process.exit(1);
329
329
  }
330
- //# sourceMappingURL=test.js.map
@@ -70,4 +70,3 @@ export async function updateCommand() {
70
70
  console.log(chalk.green('\n All souls are up to date.\n'));
71
71
  }
72
72
  }
73
- //# sourceMappingURL=update.js.map
@@ -95,4 +95,3 @@ export async function useCommand(name, opts) {
95
95
  process.exit(1);
96
96
  }
97
97
  }
98
- //# sourceMappingURL=use.js.map
@@ -284,4 +284,3 @@ function printResults(results) {
284
284
  console.log(chalk.green.bold('✓ Soul is valid and ready to publish!') + chalk.dim(` (${passed} checks passed)`));
285
285
  }
286
286
  }
287
- //# sourceMappingURL=validate.js.map
@@ -42,4 +42,3 @@ export async function versionBumpCommand(type, options) {
42
42
  }
43
43
  console.log(`✅ Bumped version: ${currentVersion} → ${newVersion}`);
44
44
  }
45
- //# sourceMappingURL=version.js.map
@@ -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 {};