@holdyourvoice/hyv 2.9.27 → 3.0.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 (44) hide show
  1. package/LICENSE +17 -22
  2. package/Readme.md +247 -0
  3. package/dist/ai-editor.js +9 -0
  4. package/dist/ai-editor.test.js +61 -0
  5. package/dist/cli.js +106 -0
  6. package/dist/cli.test.js +119 -0
  7. package/dist/contracts.js +1 -0
  8. package/dist/pipeline.js +64 -0
  9. package/dist/pipeline.test.js +36 -0
  10. package/dist/release-audit.test.js +29 -0
  11. package/dist/text.js +42 -0
  12. package/dist/text.test.js +16 -0
  13. package/dist/voice-dna.js +77 -0
  14. package/dist/voice-dna.test.js +32 -0
  15. package/package.json +14 -74
  16. package/CHANGELOG.md +0 -235
  17. package/README.md +0 -218
  18. package/agents/AGENTS.md +0 -80
  19. package/agents/README.md +0 -20
  20. package/agents/chatgpt.md +0 -45
  21. package/agents/claude-code.md +0 -50
  22. package/agents/codex.md +0 -30
  23. package/agents/cursor.md +0 -35
  24. package/agents/generic.md +0 -47
  25. package/agents/windsurf.md +0 -19
  26. package/assets/FREE-PAID.md +0 -46
  27. package/assets/README.md +0 -20
  28. package/assets/ai-eliminator-rules.md +0 -140
  29. package/assets/chatgpt-instructions.txt +0 -8
  30. package/assets/detection-rules.json +0 -18
  31. package/assets/economic-drift-voice.md +0 -42
  32. package/assets/voice-dna-template.md +0 -88
  33. package/assets/voice-profile-schema.json +0 -28
  34. package/dist/index.js +0 -20909
  35. package/scripts/README.md +0 -21
  36. package/scripts/check-no-duplicates.js +0 -32
  37. package/scripts/install.ps1 +0 -101
  38. package/scripts/install.sh +0 -155
  39. package/scripts/postinstall-lib.js +0 -796
  40. package/scripts/postinstall.js +0 -35
  41. package/skills/README.md +0 -20
  42. package/skills/ai-writing-eliminator/SKILL.md +0 -63
  43. package/skills/hold-your-voice/SKILL.md +0 -174
  44. package/skills/voice-matcher/SKILL.md +0 -57
@@ -0,0 +1,64 @@
1
+ import { analyzeAiEditor } from './ai-editor.js';
2
+ import { analyzeVoiceDna } from './voice-dna.js';
3
+ import { words } from './text.js';
4
+ export function analyze(text, profile) {
5
+ const voiceDna = analyzeVoiceDna(text, profile);
6
+ const aiEditor = analyzeAiEditor(text);
7
+ return { version: '2', voiceDna, aiEditor, passed: voiceDna.passed && aiEditor.passed };
8
+ }
9
+ function formatFindings(findings) {
10
+ return findings.map((finding) => `- Sentence ${finding.sentence} [${finding.engine}/${finding.id}]: ${finding.reason} Repair: ${finding.suggestion}`);
11
+ }
12
+ export function rewritePrompt(draft, profile) {
13
+ const result = analyze(draft, profile);
14
+ const allFindings = [...result.voiceDna.findings, ...result.aiEditor.findings];
15
+ const redFindings = allFindings.filter((finding) => finding.severity === 'red');
16
+ const yellowFindings = allFindings.filter((finding) => finding.severity === 'yellow');
17
+ const metrics = profile.metrics;
18
+ return [
19
+ '# Tier 0 — non-negotiable preservation',
20
+ 'Preserve facts, names, numbers, claims, and every unflagged sentence exactly. Do not add claims, examples, sections, hooks, or CTAs.',
21
+ '',
22
+ '# Tier 1 — release blockers',
23
+ `VoiceDNA: ${result.voiceDna.score}/100 (${result.voiceDna.passed ? 'pass' : 'fail'}).`,
24
+ `AI Editor: ${result.aiEditor.score}/100 (${result.aiEditor.passed ? 'pass' : 'fail'}).`,
25
+ ...profile.avoid.map((phrase) => `- Never use: ${phrase}`),
26
+ ...(redFindings.length ? formatFindings(redFindings) : ['- None.']),
27
+ '',
28
+ '# Tier 2 — VoiceDNA fidelity',
29
+ `- Sentence length: ${metrics.sentenceLength}; sentence variation: ${metrics.sentenceVariation}; sentence structure: ${metrics.sentenceStructure.join(', ') || 'none recorded'}; rhythm: ${metrics.rhythm}.`,
30
+ `- Paragraph length: ${metrics.paragraphLength}; lexical density: ${metrics.lexicalDensity}; point of view: ${metrics.pointOfView}; punctuation: ${Object.entries(metrics.punctuation).map(([mark, count]) => `${mark} ${count}`).join(', ')}; case style: ${metrics.caseStyle}; question rate: ${metrics.questionRate}.`,
31
+ `- Openings: ${metrics.openingMoves.join(', ') || 'none recorded'}.`,
32
+ `- Vocabulary: ${metrics.vocabulary.join(', ') || 'none recorded'}.`,
33
+ `- Transitions: ${metrics.transitions.join(', ') || 'none recorded'}.`,
34
+ '',
35
+ '# Tier 3 — AI Editor improvements',
36
+ ...(yellowFindings.length ? formatFindings(yellowFindings) : ['- None.']),
37
+ '',
38
+ '# Tier 4 — output contract',
39
+ 'Return only replacement sentences keyed by sentence number. Do not rewrite clean sentences. The candidate will be checked again by both engines.',
40
+ '',
41
+ '# Draft',
42
+ draft,
43
+ ].join('\n');
44
+ }
45
+ function preservationScore(original, candidate) {
46
+ const baseline = new Set(words(original.toLowerCase()).filter((word) => word.length > 4));
47
+ const rewritten = new Set(words(candidate.toLowerCase()));
48
+ return baseline.size ? Math.round([...baseline].filter((word) => rewritten.has(word)).length / baseline.size * 100) : 100;
49
+ }
50
+ export function verify(original, candidate, profile) {
51
+ const baseline = analyze(original, profile);
52
+ const checked = analyze(candidate, profile);
53
+ const known = new Set([...baseline.voiceDna.findings, ...baseline.aiEditor.findings].map((finding) => `${finding.engine}:${finding.id}:${finding.sentence}`));
54
+ const regressions = [...checked.voiceDna.findings, ...checked.aiEditor.findings].filter((finding) => !known.has(`${finding.engine}:${finding.id}:${finding.sentence}`));
55
+ const preservation = preservationScore(original, candidate);
56
+ return {
57
+ version: '2',
58
+ original: baseline,
59
+ candidate: checked,
60
+ preservationScore: preservation,
61
+ regressions,
62
+ passed: checked.passed && !regressions.some((finding) => finding.severity === 'red') && preservation >= 70,
63
+ };
64
+ }
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { analyze, rewritePrompt, verify } from './pipeline.js';
4
+ import { buildProfile } from './voice-dna.js';
5
+ const profile = buildProfile([
6
+ 'I ship clear ideas. The details stay concrete. I explain the mechanism without fuss.',
7
+ 'I write short sentences. Then I explain the mechanism. My work stays plain and specific.',
8
+ ], ['leverage']);
9
+ test('keeps the two engine scores independent', () => {
10
+ const result = analyze('Firstly, we leverage a holistic strategy.', profile);
11
+ assert.equal(result.aiEditor.passed, false);
12
+ assert.equal(typeof result.voiceDna.score, 'number');
13
+ });
14
+ test('builds all thirteen VoiceDNA measurements', () => {
15
+ assert.deepEqual(Object.keys(profile.metrics), ['sentenceLength', 'sentenceVariation', 'sentenceStructure', 'rhythm', 'paragraphLength', 'openingMoves', 'vocabulary', 'lexicalDensity', 'pointOfView', 'punctuation', 'caseStyle', 'questionRate', 'transitions']);
16
+ });
17
+ test('orders rewrite instructions by importance tier', () => {
18
+ const prompt = rewritePrompt('Firstly, we leverage a holistic strategy.', profile);
19
+ assert.ok(prompt.indexOf('# Tier 0') < prompt.indexOf('# Tier 1'));
20
+ assert.ok(prompt.indexOf('# Tier 1') < prompt.indexOf('# Tier 2'));
21
+ assert.ok(prompt.indexOf('# Tier 2') < prompt.indexOf('# Tier 3'));
22
+ assert.ok(prompt.indexOf('# Tier 3') < prompt.indexOf('# Tier 4'));
23
+ });
24
+ test('post gate reports a new AI regression', () => {
25
+ const result = verify('I ship clear ideas.', 'I ship clear ideas — a game-changer.', profile);
26
+ assert.equal(result.passed, false);
27
+ assert.ok(result.regressions.length > 0);
28
+ assert.equal(result.original.aiEditor.passed, true);
29
+ assert.equal(result.candidate.aiEditor.passed, false);
30
+ });
31
+ test('puts all thirteen VoiceDNA elements in the rewrite brief', () => {
32
+ const prompt = rewritePrompt('I ship clear ideas.', profile);
33
+ for (const element of ['Sentence length', 'sentence variation', 'sentence structure', 'rhythm', 'Paragraph length', 'lexical density', 'point of view', 'punctuation', 'case style', 'question rate', 'Openings', 'Vocabulary', 'Transitions']) {
34
+ assert.match(prompt, new RegExp(element));
35
+ }
36
+ });
@@ -0,0 +1,29 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+ import test from 'node:test';
7
+ const audit = new URL('../scripts/release-audit.mjs', import.meta.url).pathname;
8
+ function fixture(files) {
9
+ const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-audit-'));
10
+ execFileSync('git', ['init', '--quiet'], { cwd: directory });
11
+ for (const [file, text] of Object.entries(files)) {
12
+ const path = join(directory, file);
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ writeFileSync(path, text);
15
+ }
16
+ execFileSync('git', ['add', 'README.md'], { cwd: directory });
17
+ return directory;
18
+ }
19
+ test('rejects unquoted credentials in an untracked source file', () => {
20
+ const directory = fixture({ 'README.md': '# public', 'src/unsafe.ts': ['const ', 'API_KEY', '=topsecret;'].join('') });
21
+ try {
22
+ const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
23
+ assert.notEqual(result.status, 0);
24
+ assert.match(result.stderr, /credential marker: src\/unsafe\.ts/);
25
+ }
26
+ finally {
27
+ rmSync(directory, { recursive: true, force: true });
28
+ }
29
+ });
package/dist/text.js ADDED
@@ -0,0 +1,42 @@
1
+ export const words = (text) => text.match(/\p{L}[\p{L}\p{M}'’-]*/gu) ?? [];
2
+ export const mean = (values) => values.length ? values.reduce((total, value) => total + value, 0) / values.length : 0;
3
+ export const deviation = (values, average = mean(values)) => values.length ? Math.sqrt(mean(values.map((value) => (value - average) ** 2))) : 0;
4
+ export function sentences(text) {
5
+ const output = [];
6
+ const abbreviations = new Set(['dr', 'mr', 'mrs', 'ms', 'prof', 'sr', 'jr', 'vs', 'etc', 'fig', 'no', 'inc', 'ltd', 'co', 'jan', 'feb', 'mar', 'apr', 'jun', 'jul', 'aug', 'sep', 'sept', 'oct', 'nov', 'dec']);
7
+ let start = 0;
8
+ const add = (end) => {
9
+ const raw = text.slice(start, end);
10
+ const value = raw.trim();
11
+ if (!words(value).length)
12
+ return;
13
+ const offset = start + raw.indexOf(value);
14
+ output.push({ index: output.length + 1, start: offset, end: offset + value.length, text: value });
15
+ };
16
+ for (let index = 0; index < text.length; index += 1) {
17
+ const character = text[index];
18
+ if (character === '\n') {
19
+ add(index);
20
+ start = index + 1;
21
+ continue;
22
+ }
23
+ if (!'.!?'.includes(character))
24
+ continue;
25
+ if (character === '.') {
26
+ const previous = text[index - 1] ?? '';
27
+ const next = text[index + 1] ?? '';
28
+ const previousWord = text.slice(start, index).match(/(\p{L}+)$/u)?.[1]?.toLowerCase();
29
+ if ((/\d/.test(previous) && /\d/.test(next)) || /\p{L}/u.test(next) || (previousWord && abbreviations.has(previousWord)))
30
+ continue;
31
+ }
32
+ let end = index + 1;
33
+ while (end < text.length && '.!?'.includes(text[end]))
34
+ end += 1;
35
+ add(end);
36
+ start = end;
37
+ index = end - 1;
38
+ }
39
+ add(text.length);
40
+ return output;
41
+ }
42
+ export const paragraphs = (text) => text.split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean);
@@ -0,0 +1,16 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { sentences, words } from './text.js';
4
+ test('keeps decimals and common abbreviations inside a sentence', () => {
5
+ assert.deepEqual(sentences('Dr. Shah raised $3.5m. It funded the work.').map((sentence) => sentence.text), ['Dr. Shah raised $3.5m.', 'It funded the work.']);
6
+ });
7
+ test('records exact sentence offsets across newlines', () => {
8
+ const text = 'First line\nSecond line.';
9
+ assert.deepEqual(sentences(text), [
10
+ { index: 1, start: 0, end: 10, text: 'First line' },
11
+ { index: 2, start: 11, end: 23, text: 'Second line.' },
12
+ ]);
13
+ });
14
+ test('counts Unicode writing as words', () => {
15
+ assert.deepEqual(words('café नमस्ते दुनिया'), ['café', 'नमस्ते', 'दुनिया']);
16
+ });
@@ -0,0 +1,77 @@
1
+ import { deviation, mean, paragraphs, sentences, words } from './text.js';
2
+ const STOP_WORDS = new Set(['the', 'and', 'that', 'with', 'this', 'from', 'your', 'have', 'were', 'they', 'will', 'into', 'about', 'what', 'when', 'where']);
3
+ const TRANSITIONS = ['but', 'because', 'instead', 'then', 'still', 'so', 'yet', 'therefore'];
4
+ function top(items, limit) {
5
+ const counts = new Map();
6
+ for (const item of items.filter(Boolean))
7
+ counts.set(item, (counts.get(item) ?? 0) + 1);
8
+ return [...counts].sort((left, right) => right[1] - left[1]).slice(0, limit).map(([item]) => item);
9
+ }
10
+ function profileMetrics(text) {
11
+ const draftSentences = sentences(text);
12
+ const draftWords = words(text);
13
+ const lengths = draftSentences.map((sentence) => words(sentence.text).length);
14
+ const starters = draftSentences.map((sentence) => words(sentence.text)[0]?.toLowerCase() ?? '');
15
+ const structures = draftSentences.map((sentence) => words(sentence.text).slice(0, 3).map((word) => word.toLowerCase()).join(' '));
16
+ const lowerWords = draftWords.map((word) => word.toLowerCase());
17
+ const nonStop = lowerWords.filter((word) => word.length > 3 && !STOP_WORDS.has(word));
18
+ const first = lowerWords.filter((word) => ['i', 'we', 'my', 'our', 'us'].includes(word)).length;
19
+ const second = lowerWords.filter((word) => ['you', 'your', 'yours'].includes(word)).length;
20
+ const third = lowerWords.filter((word) => ['he', 'she', 'they', 'their', 'them'].includes(word)).length;
21
+ const pov = first > second && first > third ? 'first_person' : second > first && second > third ? 'second_person' : third > first && third > second ? 'third_person' : 'mixed';
22
+ const paragraphSizes = paragraphs(text).map((paragraph) => sentences(paragraph).length);
23
+ const punctuation = Object.fromEntries(['!', '?', ';', ':', '—'].map((mark) => [mark, (text.match(new RegExp(mark.replace(/[?*+^$.[\]\\(){}|-]/g, '\\$&'), 'g')) ?? []).length]));
24
+ const sentenceLength = mean(lengths);
25
+ const sentenceVariation = deviation(lengths, sentenceLength);
26
+ const rhythm = lengths.length > 1 ? mean(lengths.slice(1).map((length, index) => Math.abs(length - lengths[index]))) : 0;
27
+ const uppercaseStarts = draftSentences.filter((sentence) => /^\p{Lu}/u.test(sentence.text)).length;
28
+ const caseStyle = uppercaseStarts / Math.max(1, draftSentences.length) > 0.8 ? 'standard' : uppercaseStarts === 0 ? 'lowercase' : 'mixed';
29
+ return {
30
+ sentenceLength: Number(sentenceLength.toFixed(2)),
31
+ sentenceVariation: Number(sentenceVariation.toFixed(2)),
32
+ sentenceStructure: top(structures, 8),
33
+ rhythm: Number(rhythm.toFixed(2)),
34
+ paragraphLength: Number(mean(paragraphSizes).toFixed(2)),
35
+ openingMoves: top(starters, 8),
36
+ vocabulary: top(nonStop, 20),
37
+ lexicalDensity: Number((nonStop.length / Math.max(1, lowerWords.length)).toFixed(3)),
38
+ pointOfView: pov,
39
+ punctuation,
40
+ caseStyle,
41
+ questionRate: Number((draftSentences.filter((sentence) => sentence.text.endsWith('?')).length / Math.max(1, draftSentences.length)).toFixed(3)),
42
+ transitions: top(lowerWords.filter((word) => TRANSITIONS.includes(word)), 8),
43
+ };
44
+ }
45
+ export function buildProfile(samples, avoid = []) {
46
+ if (samples.length < 2)
47
+ throw new Error('Provide at least two local writing samples.');
48
+ if (samples.some((sample) => !words(sample).length))
49
+ throw new Error('Every local writing sample must contain writing.');
50
+ return { version: '2', sampleCount: samples.length, metrics: profileMetrics(samples.join('\n\n')), avoid };
51
+ }
52
+ function finding(id, severity, sentence, excerpt, reason, suggestion) {
53
+ return { engine: 'voice_dna', id, severity, sentence, excerpt, reason, suggestion };
54
+ }
55
+ export function analyzeVoiceDna(text, profile) {
56
+ const metrics = profileMetrics(text);
57
+ const findings = [];
58
+ const tolerance = Math.max(8, profile.metrics.sentenceVariation * 2.2);
59
+ for (const sentence of sentences(text)) {
60
+ const count = words(sentence.text).length;
61
+ if (Math.abs(count - profile.metrics.sentenceLength) > tolerance)
62
+ findings.push(finding('dna.sentence-length', 'yellow', sentence.index, sentence.text, `Sentence length (${count}) is outside the profile band around ${profile.metrics.sentenceLength}.`, 'Restore the writer’s usual sentence length where it improves clarity.'));
63
+ for (const banned of profile.avoid)
64
+ if (sentence.text.toLowerCase().includes(banned.toLowerCase()))
65
+ findings.push(finding('dna.avoid-list', 'red', sentence.index, sentence.text, `Uses profile avoid-list phrase: ${banned}.`, 'Replace it with the writer’s natural language.'));
66
+ }
67
+ if (Math.abs(metrics.questionRate - profile.metrics.questionRate) > 0.25)
68
+ findings.push(finding('dna.question-rate', 'yellow', 1, text.slice(0, 160), 'Question frequency differs materially from the profile.', 'Match the writer’s usual use of questions.'));
69
+ if (metrics.caseStyle !== profile.metrics.caseStyle)
70
+ findings.push(finding('dna.case-style', 'yellow', 1, text.slice(0, 160), `Draft uses ${metrics.caseStyle} casing; profile uses ${profile.metrics.caseStyle}.`, 'Use the profile’s normal casing.'));
71
+ if (metrics.pointOfView !== profile.metrics.pointOfView && profile.metrics.pointOfView !== 'mixed')
72
+ findings.push(finding('dna.point-of-view', 'yellow', 1, text.slice(0, 160), `Draft point of view is ${metrics.pointOfView}; profile is ${profile.metrics.pointOfView}.`, 'Restore the writer’s normal narrative distance.'));
73
+ const red = findings.filter((item) => item.severity === 'red').length;
74
+ const yellow = findings.length - red;
75
+ const score = Math.max(0, 100 - red * 25 - yellow * 7);
76
+ return { engine: 'voice_dna', version: '2', score, passed: red === 0, findings };
77
+ }
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { analyzeVoiceDna, buildProfile } from './voice-dna.js';
4
+ test('requires two local samples before creating a profile', () => {
5
+ assert.throws(() => buildProfile(['One sample is not enough.']), /at least two local writing samples/);
6
+ });
7
+ test('requires every local sample to contain writing', () => {
8
+ assert.throws(() => buildProfile(['', ' ']), /must contain writing/);
9
+ });
10
+ test('reports local avoid-list matches as red sentence findings', () => {
11
+ const profile = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.'], ['unlock']);
12
+ const report = analyzeVoiceDna('i unlock the answer. i name the work.', profile);
13
+ assert.deepEqual(report.findings.filter((finding) => finding.id === 'dna.avoid-list').map((finding) => [finding.severity, finding.sentence]), [['red', 1]]);
14
+ assert.equal(report.passed, false);
15
+ });
16
+ test('keeps a case-style mismatch as a yellow review cue', () => {
17
+ const profile = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.']);
18
+ const report = analyzeVoiceDna('This sentence starts in standard case.', profile);
19
+ assert.ok(report.findings.some((finding) => finding.id === 'dna.case-style' && finding.severity === 'yellow'));
20
+ assert.equal(report.passed, true);
21
+ });
22
+ test('keeps the documented VoiceDNA review checks as yellow findings', () => {
23
+ const profile = buildProfile([
24
+ 'i write short sentences. i ask no questions. i name the mechanism.',
25
+ 'i keep the language plain. i explain the work. i use first person.',
26
+ ]);
27
+ const report = analyzeVoiceDna('You explain a much longer sentence with several extra words so the reader can follow the full mechanism in detail. Do you agree?', profile);
28
+ for (const id of ['dna.sentence-length', 'dna.question-rate', 'dna.point-of-view']) {
29
+ assert.ok(report.findings.some((finding) => finding.id === id && finding.severity === 'yellow'), id);
30
+ }
31
+ assert.equal(report.passed, true);
32
+ });
package/package.json CHANGED
@@ -1,77 +1,17 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "2.9.27",
4
- "description": "Free local AI writing scan for cursor & claude. MCP server, 220+ pattern detection, voice profiles. npx @holdyourvoice/hyv welcome",
5
- "main": "dist/index.js",
6
- "bin": {
7
- "hyv": "dist/index.js",
8
- "hyvoice": "dist/index.js"
9
- },
10
- "scripts": {
11
- "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=cjs --external:canvas --banner:js='#!/usr/bin/env node'",
12
- "build:debug": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=cjs --sourcemap --banner:js='#!/usr/bin/env node'",
13
- "dev": "npm run build && node dist/index.js",
14
- "validate:publish": "npm run build && node scripts/validate-publish.js",
15
- "prepublishOnly": "npm run validate:publish",
16
- "postinstall": "node scripts/postinstall.js",
17
- "prepare": "npm run build",
18
- "test": "npm run build && vitest run",
19
- "test:smoke": "npm run build && bash scripts/smoke-test.sh",
20
- "test:watch": "vitest",
21
- "release:patch": "npm version patch && npm publish --access public",
22
- "release:minor": "npm version minor && npm publish --access public",
23
- "release:major": "npm version major && npm publish --access public"
24
- },
25
- "keywords": [
26
- "voice",
27
- "writing",
28
- "ai",
29
- "cli",
30
- "brand-voice",
31
- "content-gate",
32
- "hyv",
33
- "cursor",
34
- "claude",
35
- "mcp",
36
- "ai-agent",
37
- "ai-writing",
38
- "voice-profile"
39
- ],
40
- "author": "Hold Your Voice",
41
- "license": "UNLICENSED",
42
- "private": false,
43
- "dependencies": {
44
- "canvas": "^3.2.3",
45
- "chalk": "^4.1.2",
46
- "commander": "^12.1.0",
47
- "glob": "^13.0.6",
48
- "open": "^8.4.2"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^20.19.42",
52
- "esbuild": "^0.20.0",
53
- "typescript": "^5.3.3",
54
- "vitest": "^2.0.0"
55
- },
56
- "engines": {
57
- "node": ">=18"
58
- },
59
- "files": [
60
- "dist/",
61
- "scripts/postinstall.js",
62
- "scripts/postinstall-lib.js",
63
- "scripts/install.sh",
64
- "scripts/install.ps1",
65
- "scripts/check-no-duplicates.js",
66
- "assets/",
67
- "skills/",
68
- "agents/",
69
- "README.md",
70
- "CHANGELOG.md",
71
- "LICENSE"
72
- ],
73
- "repository": {
74
- "type": "git",
75
- "url": "git+https://github.com/shashank-sn/hold-your-voice-app.git"
76
- }
3
+ "version": "3.0.0",
4
+ "description": "A local-first dual-engine writing gate that protects voice and catches generic AI patterns.",
5
+ "type": "module",
6
+ "bin": { "hyv": "dist/cli.js" },
7
+ "files": ["dist", "Readme.md", "LICENSE"],
8
+ "scripts": { "build": "tsc -p tsconfig.json", "test": "npm run build && node --test dist/**/*.test.js", "check:release": "node scripts/release-audit.mjs", "prepack": "npm run check:release && npm test" },
9
+ "engines": { "node": ">=20" },
10
+ "license": "MIT",
11
+ "keywords": ["ai-writing", "cli", "editing", "voice", "writing"],
12
+ "repository": { "type": "git", "url": "git+https://github.com/shashank-sn/holdyourvoice.git" },
13
+ "bugs": { "url": "https://github.com/shashank-sn/holdyourvoice/issues" },
14
+ "homepage": "https://github.com/shashank-sn/holdyourvoice#readme",
15
+ "publishConfig": { "access": "public" },
16
+ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.7.0" }
77
17
  }
package/CHANGELOG.md DELETED
@@ -1,235 +0,0 @@
1
- # Changelog — @holdyourvoice/hyv
2
-
3
- All notable CLI changes. Also mirrored to [holdyourvoice.com/changelog](https://holdyourvoice.com/changelog) for user-facing releases.
4
-
5
- ## [2.9.27] — 2026-07-15
6
-
7
- ### Fixed
8
- - **MCP scan hang** — `hyv_scan`, `hyv_fix`, `hyv_check`, `hyv_score`, `hyv_diff`, and `hyv_validate` no longer block on account profile sync; they use local `~/.hyv` cache immediately (matching `hyv scan` CLI) and hydrate profiles in the background
9
-
10
- ## [2.9.26] — 2026-07-09
11
-
12
- ### Improved
13
- - **Tier 1 structural detection** — cross-sentence binary reframes (`this is not X. it is Y.`), sermon anaphora, and founder-carousel metaphors now sort first in scan output and rewrite prompts
14
- - **Rewrite prompts** split tier 1 structural hits vs other patterns so the model fixes architecture before vocabulary slop
15
- - **18 new structural cousin rules** synced in local scan engine (`scan.ts`, `tier-one.ts`)
16
-
17
- ## [2.9.25] — 2026-07-04
18
-
19
- ### Fixed
20
- - **MCP stdio pollution** — `hyv mcp` no longer prints setup banners to stdout when spawned by Cursor/Claude/ChatGPT (fixes "Unexpected token … is not valid JSON" toast spam on app open)
21
- - Auto-detects piped stdout (`!isTTY`) or explicit `--stdio` flag; MCP configs now use `mcp --stdio`
22
- - Interactive terminal runs still show setup on stderr before the JSON-RPC server starts
23
-
24
- ## [2.9.15] — 2026-06-18
25
-
26
- ### Added
27
- - `mcp-profile-hydrate` — syncs voice profile into MCP agent configs after setup
28
- - `mcp-integrate` — writes MCP server entries for cursor, claude, codex, windsurf, and more
29
- - Postinstall detects installed agents and suggests `hyv mcp --setup`
30
-
31
- ### Fixed
32
- - `hyv mcp --test` validates 13 setup checks including ChatGPT actions
33
- - Agent docs updated for one-command MCP setup across all supported editors
34
-
35
- ## [2.9.11] — 2026-06-12
36
-
37
- ### Fixed
38
- - Data safety: preserve profiles with voice anchors, learned patterns, or dashboard-edited markdown
39
- - Welcome sync accepts `force: true` to intentionally overwrite preserved server profiles
40
- - `hyv sync` keeps newer local profiles (uses `updated_at` metadata); `--force` overwrites
41
- - JSON profile cache (`.hyv/cache/profiles/*.json`) backs up before overwrite; skips stale server snapshots
42
- - `~/.hyv/auth.json` backed up to `.hyv.bak` before each write
43
- - Web login: fixed email `body` ReferenceError in strict mode; Google sign-in warms `/ready` and retries on 503
44
- - Legacy `dashboard.html` email login fixed (`user` response, OTP `code` field, cold-start retries)
45
- - Static copy/sitemap: `holdyourvoice.com/dashboard` → `/app`
46
-
47
- ## [2.9.10] — 2026-06-12
48
-
49
- ### Fixed
50
- - **Data safety** — welcome profile sync no longer overwrites enriched dashboard profiles (keywords, signature, rules, flashcard onboarding)
51
- - Local profile cache backs up to `.hyv.bak` before overwrite; refuses empty content replacing real data
52
- - `hyv sync` skips empty server profile payloads instead of wiping local cache
53
-
54
- ## [2.9.9] — 2026-06-12
55
-
56
- ### Fixed
57
- - Welcome step 4 failure copy — says `hyv welcome` retry, not `hyv sync` (sync requires paid plan)
58
- - CLI plan/init/export/api messages point to `/app` and `/app/billing`, not `/dashboard`
59
- - Rewrite prompts no longer silently pick a cached profile when none is passed (generic rules as intended)
60
- - Web auth forgot-password and session check retry on cold-start 503s
61
- - Marketing email links point to `/app` not `/dashboard`
62
-
63
- ## [2.9.7] — 2026-06-12
64
-
65
- ### Fixed
66
- - `hyv sync` — profiles with spaces or missing slugs (e.g. "Say About Us") no longer crash with "Invalid profile name"
67
- - Welcome step 4 — clearer sync failure messages (HTTP status / server error)
68
-
69
- ## [2.9.6] — 2026-06-12
70
-
71
- ### Fixed
72
- - Welcome profile sync — server accepts `content` markdown from step 4 (upsert, no duplicate-profile 403)
73
- - Dashboard handoff — opens `/app/billing` with session cookie; falls back to billing URL if handoff fails
74
- - Edge worker handles `/cli/auth/web-handoff` (no container lag)
75
-
76
- ### Changed
77
- - `hyv open dashboard` and paid-feature copy point to `/app` not `/dashboard`
78
-
79
- ## [2.9.5] — 2026-06-12
80
-
81
- ### Fixed
82
- - Post-signup browser opens `/app/billing` (live dashboard) instead of `/dashboard` (404)
83
- - Worker redirects `/dashboard` → `/app` for old links
84
-
85
- ## [2.9.4] — 2026-06-12
86
-
87
- ### Fixed
88
- - Welcome step 4 — one sign-in only; opens dashboard billing tab (no second Google login on marketing site)
89
- - `hyv plan --upgrade` opens authenticated dashboard billing instead of public pricing page
90
-
91
- ### Changed
92
- - Step 4 copy tightened; spinners while account + profile sync run
93
-
94
- ## [2.9.3] — 2026-06-12
95
-
96
- ### Fixed
97
- - Browser signup OAuth — clearer error when Google denies; server fix for `redirect_uri_mismatch` (requires API deploy)
98
-
99
- ## [2.9.2] — 2026-06-12
100
-
101
- ### Changed
102
- - Welcome step 4 copy — warmer signup pitch, highlights $1 first month and what paid unlocks
103
-
104
- ## [2.9.1] — 2026-06-12
105
-
106
- ### Added
107
- - Welcome step 3 is skippable (Enter or `s`)
108
- - Multiline draft paste for testing (no more single-line shell chaos)
109
- - Document extraction for samples and scans: `.md`, `.txt`, `.html`, `.docx`, `.pdf`, `.skill`
110
- - Recursive folder scan; single files (including `.skill` archives) work in step 2
111
- - Minimal terminal spinners while reading, saving, and scanning
112
-
113
- ### Fixed
114
- - Step 3 crash when scanning pasted text (`Cannot read properties of undefined (reading 'match')`) — profile now loads correctly
115
- - `ENOTDIR` when pointing at a file instead of a folder
116
-
117
- ## [2.9.0] — 2026-06-12
118
-
119
- ### Changed
120
- - `hyv welcome` — profile-first flow: name → samples (paste/folder/link/chat) → test draft → signup
121
- - Keeps tagline; drops install/init/mcp step list from welcome
122
- - MCP `hyv_welcome` supports `step`, `mode=extract_prompt`, and `profile` for agent-led onboarding
123
- - Postinstall points to `hyv welcome` instead of `hyv init`
124
-
125
- ## [2.8.10] — 2026-06-12
126
-
127
- ### Fixed
128
- - `hyv init` browser login opens Google OAuth again (`assertSafeOAuthUrl` allows `accounts.google.com`)
129
-
130
- ## [2.8.9] — 2026-06-12
131
-
132
- ### Improved
133
- - `hyv fix --in-place` matches batch/watch safeguards: interactive `[y/N]` or `--yes` + `.bak` backup
134
- - `hyv watch --command fix` ignores self-triggered saves after writing fixes
135
- - `hyv doctor` checks auth.json permissions, stale `hyv.md` vs `hyv.mdc`, and MCP stdio health
136
- - Shared `resolveCliEntry()` for MCP setup and stdio probes
137
- - Auth refresh + destructive-write unit tests
138
-
139
- ### Changed
140
- - `dist/` built at publish/prepare — no longer committed to git
141
-
142
- ## [2.8.8] — 2026-06-12
143
-
144
- ### Security
145
- - OAuth browser flow verifies server `state` on callback (CSRF protection)
146
- - Automatic token refresh via `getValidToken()` before API calls
147
- - `HYV_API_URL` host allowlist; `assertSafeOpenUrl` for checkout/OAuth redirects
148
- - Profile name path traversal blocked; MCP file reads symlink-safe with `isError` responses
149
- - Sensitive `~/.hyv` files and dirs use `0o600`/`0o700` permissions
150
-
151
- ### Fixed
152
- - `hyv doctor --fix-agents` uses correct postinstall-lib path
153
- - `hyv import` registered; `--fail-on-hit` exits code 2 (scan + batch)
154
- - Queue sync sends `original_text` / `accepted_text` for learning reinforce
155
- - Postinstall: Claude Desktop MCP merge with backup, Cursor `alwaysApply` rule, absolute MCP command path
156
-
157
- ### Improved
158
- - `hyv mcp --test` spawns stdio subprocess and validates JSON-RPC `tools/list`
159
- - `hyv batch --fix --in-place` and `hyv watch --command fix` require `--yes` (`.bak` backups)
160
- - Stale duplicate agent/skill markdown removed from `assets/` (canonical copies in `agents/` + `skills/`)
161
-
162
- ## [2.8.6] — 2026-06-12
163
-
164
- ### Changed
165
- - `hyv welcome` — short onboarding: 4 steps, one-line demo, subscribe prompt (full lists moved to `hyv free`)
166
- - Shorter postinstall message before auto-welcome
167
-
168
- ## [2.8.5] — 2026-06-12
169
-
170
- ### Changed
171
- - `hyv welcome` slimmed down (superseded by 2.8.6 — use `npm i -g @holdyourvoice/hyv@2.8.6`)
172
-
173
- ## [2.8.4] — 2026-06-12
174
-
175
- ### Fixed
176
- - Postinstall only auto-runs welcome when npm shows script output; otherwise first bare `hyv` shows full onboarding (fixes silent global installs)
177
-
178
- ## [2.8.3] — 2026-06-12
179
-
180
- ### Fixed
181
- - `npm i -g` runs `hyv welcome` onboarding automatically on first install (no y/N prompt)
182
- - First bare `hyv` shows full onboarding when npm hid postinstall output
183
- - Upgraded `glob` to v13 — removes deprecated glob@11 warning during install
184
-
185
- ## [2.8.2] — 2026-06-12
186
-
187
- ### Added
188
- - Phase 5 marketing: `hyv content` (blog outlines, CI snippets, share templates)
189
- - `hyv plan --free` — canonical free vs paid matrix in terminal
190
- - `cli/assets/FREE-PAID.md` — shipped free/paid reference
191
- - Scan funnel hints + `scan_complete` / `init_success` telemetry events
192
- - Landing + npm README: npx-first install, free CLI section, SEO keywords
193
-
194
- ### Improved
195
- - Postinstall, welcome, plan, and init flows promote npx + free local tier
196
- - npm package description optimized for cursor/claude/MCP discovery
197
-
198
- ## [2.8.1] — 2026-06-12
199
-
200
- ### Fixed
201
- - `postinstall-lib.js` now included in published tarball — `npm install` no longer fails on postinstall
202
-
203
- ## [2.8.0] — 2026-06-12
204
-
205
- ### Added
206
- - Full Phase 1–4 product plan: free local engine, hybrid analysis, MCP `hyv_analyze` / `hyv_clean`, learning loop UX, packaging hardening
207
- - `npm run test:smoke` — 28+ CLI regression checks
208
- - `npm run validate:publish` — prepublish tarball verification
209
- - Golden prompt tests, postinstall idempotency tests, edge-case suite
210
-
211
- ### Fixed
212
- - `hyv check ""` no longer reports false clean — rejects empty input
213
- - Bundled CLI resolves correct package + rules version from `dist/`
214
- - Smoke tests avoid SIGPIPE false failures when piping CLI output
215
-
216
- ## [2.7.1] — 2026-06-12
217
-
218
- ### Added
219
- - `hyv upgrade` — check and install latest global CLI
220
- - `hyv mcp --setup` and `hyv mcp --test` — agent setup + health check
221
- - MCP tools: `hyv_analyze` (hybrid server+local), `hyv_clean` (scan→fix→validate pipeline)
222
- - `hyv reinforce --last` — learn from last fix/rewrite session
223
- - Free-first local engine: scan, fix, check, score, diff work offline without subscription
224
- - `hyv welcome` / `hyv free` — onboarding with live demo
225
- - Profile-aware local pipeline: never-list, learned patterns, anchors, cadence
226
- - Opt-in telemetry: `HYV_TELEMETRY=1`
227
-
228
- ### Improved
229
- - Postinstall upgrades agent instructions via `~/.hyv/agents-version.json`
230
- - `hyv status` shows engine/rules version, drift, evolution summary
231
- - `hyv doctor` reports full engine label + `--fix-agents`
232
- - Published package excludes Python dev scripts and `src/`
233
-
234
- ### Fixed
235
- - Bundled CLI reads correct `package.json` version from `dist/`