@holdyourvoice/hyv 2.9.28 → 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 -241
  17. package/README.md +0 -218
  18. package/agents/AGENTS.md +0 -82
  19. package/agents/README.md +0 -20
  20. package/agents/chatgpt.md +0 -47
  21. package/agents/claude-code.md +0 -45
  22. package/agents/codex.md +0 -31
  23. package/agents/cursor.md +0 -36
  24. package/agents/generic.md +0 -49
  25. package/agents/windsurf.md +0 -20
  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 -20911
  35. package/scripts/README.md +0 -23
  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.28",
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,241 +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.28] — 2026-07-31
6
-
7
- ### Fixed
8
- - **Scoped MCP activation** — installed rules, export prompts, and MCP metadata now use HYV automatically for publishable copy or an explicit voice request, while routine chat and engineering work bypass it
9
- - **Agent refresh** — `hyv doctor --fix-agents` refreshes the new scoped instructions through the package version marker
10
-
11
- ## [2.9.27] — 2026-07-15
12
-
13
- ### Fixed
14
- - **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
15
-
16
- ## [2.9.26] — 2026-07-09
17
-
18
- ### Improved
19
- - **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
20
- - **Rewrite prompts** split tier 1 structural hits vs other patterns so the model fixes architecture before vocabulary slop
21
- - **18 new structural cousin rules** synced in local scan engine (`scan.ts`, `tier-one.ts`)
22
-
23
- ## [2.9.25] — 2026-07-04
24
-
25
- ### Fixed
26
- - **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)
27
- - Auto-detects piped stdout (`!isTTY`) or explicit `--stdio` flag; MCP configs now use `mcp --stdio`
28
- - Interactive terminal runs still show setup on stderr before the JSON-RPC server starts
29
-
30
- ## [2.9.15] — 2026-06-18
31
-
32
- ### Added
33
- - `mcp-profile-hydrate` — syncs voice profile into MCP agent configs after setup
34
- - `mcp-integrate` — writes MCP server entries for cursor, claude, codex, windsurf, and more
35
- - Postinstall detects installed agents and suggests `hyv mcp --setup`
36
-
37
- ### Fixed
38
- - `hyv mcp --test` validates 13 setup checks including ChatGPT actions
39
- - Agent docs updated for one-command MCP setup across all supported editors
40
-
41
- ## [2.9.11] — 2026-06-12
42
-
43
- ### Fixed
44
- - Data safety: preserve profiles with voice anchors, learned patterns, or dashboard-edited markdown
45
- - Welcome sync accepts `force: true` to intentionally overwrite preserved server profiles
46
- - `hyv sync` keeps newer local profiles (uses `updated_at` metadata); `--force` overwrites
47
- - JSON profile cache (`.hyv/cache/profiles/*.json`) backs up before overwrite; skips stale server snapshots
48
- - `~/.hyv/auth.json` backed up to `.hyv.bak` before each write
49
- - Web login: fixed email `body` ReferenceError in strict mode; Google sign-in warms `/ready` and retries on 503
50
- - Legacy `dashboard.html` email login fixed (`user` response, OTP `code` field, cold-start retries)
51
- - Static copy/sitemap: `holdyourvoice.com/dashboard` → `/app`
52
-
53
- ## [2.9.10] — 2026-06-12
54
-
55
- ### Fixed
56
- - **Data safety** — welcome profile sync no longer overwrites enriched dashboard profiles (keywords, signature, rules, flashcard onboarding)
57
- - Local profile cache backs up to `.hyv.bak` before overwrite; refuses empty content replacing real data
58
- - `hyv sync` skips empty server profile payloads instead of wiping local cache
59
-
60
- ## [2.9.9] — 2026-06-12
61
-
62
- ### Fixed
63
- - Welcome step 4 failure copy — says `hyv welcome` retry, not `hyv sync` (sync requires paid plan)
64
- - CLI plan/init/export/api messages point to `/app` and `/app/billing`, not `/dashboard`
65
- - Rewrite prompts no longer silently pick a cached profile when none is passed (generic rules as intended)
66
- - Web auth forgot-password and session check retry on cold-start 503s
67
- - Marketing email links point to `/app` not `/dashboard`
68
-
69
- ## [2.9.7] — 2026-06-12
70
-
71
- ### Fixed
72
- - `hyv sync` — profiles with spaces or missing slugs (e.g. "Say About Us") no longer crash with "Invalid profile name"
73
- - Welcome step 4 — clearer sync failure messages (HTTP status / server error)
74
-
75
- ## [2.9.6] — 2026-06-12
76
-
77
- ### Fixed
78
- - Welcome profile sync — server accepts `content` markdown from step 4 (upsert, no duplicate-profile 403)
79
- - Dashboard handoff — opens `/app/billing` with session cookie; falls back to billing URL if handoff fails
80
- - Edge worker handles `/cli/auth/web-handoff` (no container lag)
81
-
82
- ### Changed
83
- - `hyv open dashboard` and paid-feature copy point to `/app` not `/dashboard`
84
-
85
- ## [2.9.5] — 2026-06-12
86
-
87
- ### Fixed
88
- - Post-signup browser opens `/app/billing` (live dashboard) instead of `/dashboard` (404)
89
- - Worker redirects `/dashboard` → `/app` for old links
90
-
91
- ## [2.9.4] — 2026-06-12
92
-
93
- ### Fixed
94
- - Welcome step 4 — one sign-in only; opens dashboard billing tab (no second Google login on marketing site)
95
- - `hyv plan --upgrade` opens authenticated dashboard billing instead of public pricing page
96
-
97
- ### Changed
98
- - Step 4 copy tightened; spinners while account + profile sync run
99
-
100
- ## [2.9.3] — 2026-06-12
101
-
102
- ### Fixed
103
- - Browser signup OAuth — clearer error when Google denies; server fix for `redirect_uri_mismatch` (requires API deploy)
104
-
105
- ## [2.9.2] — 2026-06-12
106
-
107
- ### Changed
108
- - Welcome step 4 copy — warmer signup pitch, highlights $1 first month and what paid unlocks
109
-
110
- ## [2.9.1] — 2026-06-12
111
-
112
- ### Added
113
- - Welcome step 3 is skippable (Enter or `s`)
114
- - Multiline draft paste for testing (no more single-line shell chaos)
115
- - Document extraction for samples and scans: `.md`, `.txt`, `.html`, `.docx`, `.pdf`, `.skill`
116
- - Recursive folder scan; single files (including `.skill` archives) work in step 2
117
- - Minimal terminal spinners while reading, saving, and scanning
118
-
119
- ### Fixed
120
- - Step 3 crash when scanning pasted text (`Cannot read properties of undefined (reading 'match')`) — profile now loads correctly
121
- - `ENOTDIR` when pointing at a file instead of a folder
122
-
123
- ## [2.9.0] — 2026-06-12
124
-
125
- ### Changed
126
- - `hyv welcome` — profile-first flow: name → samples (paste/folder/link/chat) → test draft → signup
127
- - Keeps tagline; drops install/init/mcp step list from welcome
128
- - MCP `hyv_welcome` supports `step`, `mode=extract_prompt`, and `profile` for agent-led onboarding
129
- - Postinstall points to `hyv welcome` instead of `hyv init`
130
-
131
- ## [2.8.10] — 2026-06-12
132
-
133
- ### Fixed
134
- - `hyv init` browser login opens Google OAuth again (`assertSafeOAuthUrl` allows `accounts.google.com`)
135
-
136
- ## [2.8.9] — 2026-06-12
137
-
138
- ### Improved
139
- - `hyv fix --in-place` matches batch/watch safeguards: interactive `[y/N]` or `--yes` + `.bak` backup
140
- - `hyv watch --command fix` ignores self-triggered saves after writing fixes
141
- - `hyv doctor` checks auth.json permissions, stale `hyv.md` vs `hyv.mdc`, and MCP stdio health
142
- - Shared `resolveCliEntry()` for MCP setup and stdio probes
143
- - Auth refresh + destructive-write unit tests
144
-
145
- ### Changed
146
- - `dist/` built at publish/prepare — no longer committed to git
147
-
148
- ## [2.8.8] — 2026-06-12
149
-
150
- ### Security
151
- - OAuth browser flow verifies server `state` on callback (CSRF protection)
152
- - Automatic token refresh via `getValidToken()` before API calls
153
- - `HYV_API_URL` host allowlist; `assertSafeOpenUrl` for checkout/OAuth redirects
154
- - Profile name path traversal blocked; MCP file reads symlink-safe with `isError` responses
155
- - Sensitive `~/.hyv` files and dirs use `0o600`/`0o700` permissions
156
-
157
- ### Fixed
158
- - `hyv doctor --fix-agents` uses correct postinstall-lib path
159
- - `hyv import` registered; `--fail-on-hit` exits code 2 (scan + batch)
160
- - Queue sync sends `original_text` / `accepted_text` for learning reinforce
161
- - Postinstall: Claude Desktop MCP merge with backup, Cursor `alwaysApply` rule, absolute MCP command path
162
-
163
- ### Improved
164
- - `hyv mcp --test` spawns stdio subprocess and validates JSON-RPC `tools/list`
165
- - `hyv batch --fix --in-place` and `hyv watch --command fix` require `--yes` (`.bak` backups)
166
- - Stale duplicate agent/skill markdown removed from `assets/` (canonical copies in `agents/` + `skills/`)
167
-
168
- ## [2.8.6] — 2026-06-12
169
-
170
- ### Changed
171
- - `hyv welcome` — short onboarding: 4 steps, one-line demo, subscribe prompt (full lists moved to `hyv free`)
172
- - Shorter postinstall message before auto-welcome
173
-
174
- ## [2.8.5] — 2026-06-12
175
-
176
- ### Changed
177
- - `hyv welcome` slimmed down (superseded by 2.8.6 — use `npm i -g @holdyourvoice/hyv@2.8.6`)
178
-
179
- ## [2.8.4] — 2026-06-12
180
-
181
- ### Fixed
182
- - Postinstall only auto-runs welcome when npm shows script output; otherwise first bare `hyv` shows full onboarding (fixes silent global installs)
183
-
184
- ## [2.8.3] — 2026-06-12
185
-
186
- ### Fixed
187
- - `npm i -g` runs `hyv welcome` onboarding automatically on first install (no y/N prompt)
188
- - First bare `hyv` shows full onboarding when npm hid postinstall output
189
- - Upgraded `glob` to v13 — removes deprecated glob@11 warning during install
190
-
191
- ## [2.8.2] — 2026-06-12
192
-
193
- ### Added
194
- - Phase 5 marketing: `hyv content` (blog outlines, CI snippets, share templates)
195
- - `hyv plan --free` — canonical free vs paid matrix in terminal
196
- - `cli/assets/FREE-PAID.md` — shipped free/paid reference
197
- - Scan funnel hints + `scan_complete` / `init_success` telemetry events
198
- - Landing + npm README: npx-first install, free CLI section, SEO keywords
199
-
200
- ### Improved
201
- - Postinstall, welcome, plan, and init flows promote npx + free local tier
202
- - npm package description optimized for cursor/claude/MCP discovery
203
-
204
- ## [2.8.1] — 2026-06-12
205
-
206
- ### Fixed
207
- - `postinstall-lib.js` now included in published tarball — `npm install` no longer fails on postinstall
208
-
209
- ## [2.8.0] — 2026-06-12
210
-
211
- ### Added
212
- - Full Phase 1–4 product plan: free local engine, hybrid analysis, MCP `hyv_analyze` / `hyv_clean`, learning loop UX, packaging hardening
213
- - `npm run test:smoke` — 28+ CLI regression checks
214
- - `npm run validate:publish` — prepublish tarball verification
215
- - Golden prompt tests, postinstall idempotency tests, edge-case suite
216
-
217
- ### Fixed
218
- - `hyv check ""` no longer reports false clean — rejects empty input
219
- - Bundled CLI resolves correct package + rules version from `dist/`
220
- - Smoke tests avoid SIGPIPE false failures when piping CLI output
221
-
222
- ## [2.7.1] — 2026-06-12
223
-
224
- ### Added
225
- - `hyv upgrade` — check and install latest global CLI
226
- - `hyv mcp --setup` and `hyv mcp --test` — agent setup + health check
227
- - MCP tools: `hyv_analyze` (hybrid server+local), `hyv_clean` (scan→fix→validate pipeline)
228
- - `hyv reinforce --last` — learn from last fix/rewrite session
229
- - Free-first local engine: scan, fix, check, score, diff work offline without subscription
230
- - `hyv welcome` / `hyv free` — onboarding with live demo
231
- - Profile-aware local pipeline: never-list, learned patterns, anchors, cadence
232
- - Opt-in telemetry: `HYV_TELEMETRY=1`
233
-
234
- ### Improved
235
- - Postinstall upgrades agent instructions via `~/.hyv/agents-version.json`
236
- - `hyv status` shows engine/rules version, drift, evolution summary
237
- - `hyv doctor` reports full engine label + `--fix-agents`
238
- - Published package excludes Python dev scripts and `src/`
239
-
240
- ### Fixed
241
- - Bundled CLI reads correct `package.json` version from `dist/`