@auxiora/compose 1.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.
- package/LICENSE +191 -0
- package/dist/composer.d.ts +13 -0
- package/dist/composer.d.ts.map +1 -0
- package/dist/composer.js +60 -0
- package/dist/composer.js.map +1 -0
- package/dist/grammar.d.ts +11 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +117 -0
- package/dist/grammar.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/language.d.ts +6 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +39 -0
- package/dist/language.js.map +1 -0
- package/dist/templates.d.ts +10 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +87 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/composer.ts +72 -0
- package/src/grammar.ts +139 -0
- package/src/index.ts +14 -0
- package/src/language.ts +47 -0
- package/src/templates.ts +97 -0
- package/src/types.ts +47 -0
- package/tests/composer.test.ts +89 -0
- package/tests/grammar.test.ts +49 -0
- package/tests/language.test.ts +38 -0
- package/tests/templates.test.ts +70 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;AAC7F,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzF,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auxiora/compose",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Context-aware writing assistant with tone adaptation, templates, and grammar checking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"nanoid": "^5.1.2",
|
|
16
|
+
"@auxiora/logger": "1.0.0",
|
|
17
|
+
"@auxiora/audit": "1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"clean": "rm -rf dist",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/composer.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Tone, Platform, ComposeRequest, ComposeResult, ComposeContext } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class ComposeEngine {
|
|
4
|
+
private defaultTone: Tone;
|
|
5
|
+
|
|
6
|
+
constructor(config?: { defaultTone?: Tone }) {
|
|
7
|
+
this.defaultTone = config?.defaultTone ?? 'professional';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
compose(request: ComposeRequest): ComposeResult {
|
|
11
|
+
const tone = request.context.tone ?? this.adaptToneForPlatform(request.context.platform);
|
|
12
|
+
let text = request.content ?? request.instruction ?? '';
|
|
13
|
+
|
|
14
|
+
text = this.enforceConstraints(text, request.context);
|
|
15
|
+
text = this.addSignOff(text, tone, request.context.platform);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
text,
|
|
19
|
+
tone,
|
|
20
|
+
platform: request.context.platform,
|
|
21
|
+
wordCount: this.countWords(text),
|
|
22
|
+
characterCount: text.length,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private adaptToneForPlatform(platform: Platform): Tone {
|
|
27
|
+
const map: Record<Platform, Tone> = {
|
|
28
|
+
email: 'formal',
|
|
29
|
+
slack: 'casual',
|
|
30
|
+
linkedin: 'professional',
|
|
31
|
+
twitter: 'brief',
|
|
32
|
+
reddit: 'casual',
|
|
33
|
+
generic: 'professional',
|
|
34
|
+
};
|
|
35
|
+
return map[platform];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private enforceConstraints(text: string, context: ComposeContext): string {
|
|
39
|
+
if (context.platform === 'twitter' && text.length > 280) {
|
|
40
|
+
text = text.slice(0, 277) + '...';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (context.maxLength && text.length > context.maxLength) {
|
|
44
|
+
text = text.slice(0, context.maxLength - 3) + '...';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private addSignOff(text: string, tone: Tone, platform: Platform): string {
|
|
51
|
+
if (platform !== 'email' && platform !== 'linkedin') {
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const signOffs: Record<Tone, string> = {
|
|
56
|
+
formal: '\n\nBest regards,',
|
|
57
|
+
professional: '\n\nBest,',
|
|
58
|
+
casual: '\n\nThanks!',
|
|
59
|
+
brief: '',
|
|
60
|
+
friendly: '\n\nCheers!',
|
|
61
|
+
assertive: '\n\nRegards,',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return text + signOffs[tone];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private countWords(text: string): number {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
if (trimmed.length === 0) return 0;
|
|
70
|
+
return trimmed.split(/\s+/).length;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/grammar.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { GrammarIssue } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class GrammarChecker {
|
|
4
|
+
check(text: string): GrammarIssue[] {
|
|
5
|
+
const issues: GrammarIssue[] = [
|
|
6
|
+
...this.checkDoubleSpaces(text),
|
|
7
|
+
...this.checkRepeatedWords(text),
|
|
8
|
+
...this.checkLongSentences(text),
|
|
9
|
+
...this.checkPassiveVoice(text),
|
|
10
|
+
...this.checkWeaselWords(text),
|
|
11
|
+
...this.checkMissingPeriod(text),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
issues.sort((a, b) => a.position.start - b.position.start);
|
|
15
|
+
return issues;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private checkDoubleSpaces(text: string): GrammarIssue[] {
|
|
19
|
+
const issues: GrammarIssue[] = [];
|
|
20
|
+
const pattern = / /g;
|
|
21
|
+
let match: RegExpExecArray | null;
|
|
22
|
+
|
|
23
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
24
|
+
issues.push({
|
|
25
|
+
type: 'style',
|
|
26
|
+
message: 'Double space detected',
|
|
27
|
+
position: { start: match.index, end: match.index + 2 },
|
|
28
|
+
suggestion: ' ',
|
|
29
|
+
severity: 'warning',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return issues;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private checkRepeatedWords(text: string): GrammarIssue[] {
|
|
37
|
+
const issues: GrammarIssue[] = [];
|
|
38
|
+
const pattern = /\b(\w+)\s+\1\b/gi;
|
|
39
|
+
let match: RegExpExecArray | null;
|
|
40
|
+
|
|
41
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
42
|
+
issues.push({
|
|
43
|
+
type: 'grammar',
|
|
44
|
+
message: `Repeated word: "${match[1]}"`,
|
|
45
|
+
position: { start: match.index, end: match.index + match[0].length },
|
|
46
|
+
suggestion: match[1],
|
|
47
|
+
severity: 'error',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private checkLongSentences(text: string): GrammarIssue[] {
|
|
55
|
+
const issues: GrammarIssue[] = [];
|
|
56
|
+
const sentences = text.split(/[.!?]+/);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
|
|
59
|
+
for (const sentence of sentences) {
|
|
60
|
+
const trimmed = sentence.trim();
|
|
61
|
+
const wordCount = trimmed.length > 0 ? trimmed.split(/\s+/).length : 0;
|
|
62
|
+
|
|
63
|
+
if (wordCount > 40) {
|
|
64
|
+
const start = text.indexOf(trimmed, offset);
|
|
65
|
+
issues.push({
|
|
66
|
+
type: 'style',
|
|
67
|
+
message: `Sentence is ${wordCount} words long (recommended: under 40)`,
|
|
68
|
+
position: { start, end: start + trimmed.length },
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
offset += sentence.length + 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return issues;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private checkPassiveVoice(text: string): GrammarIssue[] {
|
|
80
|
+
const issues: GrammarIssue[] = [];
|
|
81
|
+
const pattern = /\b(was|were|been|being)\s+\w+ed\b/gi;
|
|
82
|
+
let match: RegExpExecArray | null;
|
|
83
|
+
|
|
84
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
85
|
+
issues.push({
|
|
86
|
+
type: 'clarity',
|
|
87
|
+
message: 'Possible passive voice',
|
|
88
|
+
position: { start: match.index, end: match.index + match[0].length },
|
|
89
|
+
severity: 'info',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return issues;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private checkWeaselWords(text: string): GrammarIssue[] {
|
|
97
|
+
const issues: GrammarIssue[] = [];
|
|
98
|
+
const weaselWords = [
|
|
99
|
+
'very', 'really', 'quite', 'somewhat', 'fairly',
|
|
100
|
+
'rather', 'basically', 'essentially', 'generally',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const word of weaselWords) {
|
|
104
|
+
const pattern = new RegExp(`\\b${word}\\b`, 'gi');
|
|
105
|
+
let match: RegExpExecArray | null;
|
|
106
|
+
|
|
107
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
108
|
+
issues.push({
|
|
109
|
+
type: 'style',
|
|
110
|
+
message: `Weasel word: "${match[0]}"`,
|
|
111
|
+
position: { start: match.index, end: match.index + match[0].length },
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private checkMissingPeriod(text: string): GrammarIssue[] {
|
|
121
|
+
const trimmed = text.trim();
|
|
122
|
+
if (trimmed.length === 0) return [];
|
|
123
|
+
|
|
124
|
+
const lastChar = trimmed[trimmed.length - 1];
|
|
125
|
+
if (!['.', '!', '?', ':', ';'].includes(lastChar)) {
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
type: 'grammar',
|
|
129
|
+
message: 'Text does not end with punctuation',
|
|
130
|
+
position: { start: trimmed.length - 1, end: trimmed.length },
|
|
131
|
+
suggestion: trimmed + '.',
|
|
132
|
+
severity: 'warning',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ComposeEngine } from './composer.js';
|
|
2
|
+
export { TemplateEngine } from './templates.js';
|
|
3
|
+
export { GrammarChecker } from './grammar.js';
|
|
4
|
+
export { LanguageDetector } from './language.js';
|
|
5
|
+
export type {
|
|
6
|
+
Tone,
|
|
7
|
+
Platform,
|
|
8
|
+
ComposeRequest,
|
|
9
|
+
ComposeContext,
|
|
10
|
+
ComposeResult,
|
|
11
|
+
Template,
|
|
12
|
+
GrammarIssue,
|
|
13
|
+
LanguageResult,
|
|
14
|
+
} from './types.js';
|
package/src/language.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { LanguageResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
const LANGUAGE_INDICATORS: Record<string, string[]> = {
|
|
4
|
+
english: ['the', 'is', 'are', 'was', 'have', 'been', 'that', 'this', 'with', 'from'],
|
|
5
|
+
spanish: ['el', 'la', 'los', 'las', 'de', 'en', 'que', 'un', 'una', 'por', 'con', 'es'],
|
|
6
|
+
french: ['le', 'la', 'les', 'des', 'un', 'une', 'de', 'en', 'est', 'que', 'dans', 'avec'],
|
|
7
|
+
german: ['der', 'die', 'das', 'und', 'ist', 'ein', 'eine', 'mit', 'auf', 'für', 'von'],
|
|
8
|
+
portuguese: ['o', 'a', 'os', 'as', 'de', 'em', 'que', 'um', 'uma', 'com', 'por'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const RTL_LANGUAGES = new Set(['arabic', 'hebrew', 'persian', 'urdu']);
|
|
12
|
+
|
|
13
|
+
export class LanguageDetector {
|
|
14
|
+
detect(text: string): LanguageResult {
|
|
15
|
+
const words = text.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
16
|
+
if (words.length === 0) {
|
|
17
|
+
return { language: 'unknown', confidence: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let bestLanguage = 'unknown';
|
|
21
|
+
let bestCount = 0;
|
|
22
|
+
|
|
23
|
+
for (const [language, indicators] of Object.entries(LANGUAGE_INDICATORS)) {
|
|
24
|
+
let count = 0;
|
|
25
|
+
for (const word of words) {
|
|
26
|
+
if (indicators.includes(word)) {
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (count > bestCount) {
|
|
31
|
+
bestCount = count;
|
|
32
|
+
bestLanguage = language;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const confidence = bestCount / words.length;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
language: bestLanguage,
|
|
40
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isRTL(language: string): boolean {
|
|
45
|
+
return RTL_LANGUAGES.has(language.toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Template, Tone } from './types.js';
|
|
2
|
+
|
|
3
|
+
const BUILT_IN: readonly Omit<Template, 'id'>[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Meeting Follow-Up',
|
|
6
|
+
category: 'business',
|
|
7
|
+
body: 'Hi {{name}},\n\nThank you for taking the time to meet today. Here\'s a summary of what we discussed:\n\n{{summary}}\n\nNext steps:\n{{nextSteps}}\n\nPlease let me know if I missed anything.',
|
|
8
|
+
variables: ['name', 'summary', 'nextSteps'],
|
|
9
|
+
tone: 'formal' as Tone,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Introduction',
|
|
13
|
+
category: 'networking',
|
|
14
|
+
body: 'Hi {{name}},\n\nI\'m {{senderName}} from {{company}}. {{reason}}\n\nI\'d love to connect and discuss further.',
|
|
15
|
+
variables: ['name', 'senderName', 'company', 'reason'],
|
|
16
|
+
tone: 'professional' as Tone,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Thank You',
|
|
20
|
+
category: 'personal',
|
|
21
|
+
body: 'Hi {{name}},\n\nThank you so much for {{reason}}. I really appreciate it.\n\n{{additionalNote}}',
|
|
22
|
+
variables: ['name', 'reason', 'additionalNote'],
|
|
23
|
+
tone: 'friendly' as Tone,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Cold Outreach',
|
|
27
|
+
category: 'sales',
|
|
28
|
+
body: 'Hi {{name}},\n\n{{hook}}\n\n{{valueProposition}}\n\nWould you be open to a {{meetingLength}} minute chat this week?',
|
|
29
|
+
variables: ['name', 'hook', 'valueProposition', 'meetingLength'],
|
|
30
|
+
tone: 'professional' as Tone,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Status Update',
|
|
34
|
+
category: 'business',
|
|
35
|
+
body: '{{greeting}}\n\nHere\'s the status update for {{project}}:\n\n**Completed:**\n{{completed}}\n\n**In Progress:**\n{{inProgress}}\n\n**Blockers:**\n{{blockers}}',
|
|
36
|
+
variables: ['greeting', 'project', 'completed', 'inProgress', 'blockers'],
|
|
37
|
+
tone: 'professional' as Tone,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Apology',
|
|
41
|
+
category: 'personal',
|
|
42
|
+
body: 'Hi {{name}},\n\nI want to sincerely apologize for {{issue}}. {{explanation}}\n\nTo make this right, {{resolution}}.',
|
|
43
|
+
variables: ['name', 'issue', 'explanation', 'resolution'],
|
|
44
|
+
tone: 'formal' as Tone,
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export class TemplateEngine {
|
|
49
|
+
private templates: Map<string, Template> = new Map();
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
const ids = [
|
|
53
|
+
'meeting-follow-up',
|
|
54
|
+
'introduction',
|
|
55
|
+
'thank-you',
|
|
56
|
+
'cold-outreach',
|
|
57
|
+
'status-update',
|
|
58
|
+
'apology',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < BUILT_IN.length; i++) {
|
|
62
|
+
const builtin = BUILT_IN[i];
|
|
63
|
+
const template: Template = { id: ids[i], ...builtin };
|
|
64
|
+
this.templates.set(template.id, template);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
register(template: Template): void {
|
|
69
|
+
this.templates.set(template.id, template);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get(id: string): Template | undefined {
|
|
73
|
+
return this.templates.get(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
list(category?: string): Template[] {
|
|
77
|
+
const all = [...this.templates.values()];
|
|
78
|
+
if (category) {
|
|
79
|
+
return all.filter((t) => t.category === category);
|
|
80
|
+
}
|
|
81
|
+
return all;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render(templateId: string, variables: Record<string, string>): string {
|
|
85
|
+
const template = this.templates.get(templateId);
|
|
86
|
+
if (!template) {
|
|
87
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let result = template.body;
|
|
91
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
92
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type Tone = 'formal' | 'professional' | 'casual' | 'brief' | 'friendly' | 'assertive';
|
|
2
|
+
export type Platform = 'email' | 'slack' | 'linkedin' | 'twitter' | 'reddit' | 'generic';
|
|
3
|
+
|
|
4
|
+
export interface ComposeRequest {
|
|
5
|
+
content?: string;
|
|
6
|
+
context: ComposeContext;
|
|
7
|
+
instruction?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComposeContext {
|
|
11
|
+
platform: Platform;
|
|
12
|
+
audience?: string;
|
|
13
|
+
tone?: Tone;
|
|
14
|
+
replyTo?: string;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ComposeResult {
|
|
19
|
+
text: string;
|
|
20
|
+
tone: Tone;
|
|
21
|
+
platform: Platform;
|
|
22
|
+
wordCount: number;
|
|
23
|
+
characterCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Template {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
category: string;
|
|
30
|
+
body: string;
|
|
31
|
+
variables: string[];
|
|
32
|
+
tone: Tone;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GrammarIssue {
|
|
36
|
+
type: 'spelling' | 'grammar' | 'style' | 'clarity';
|
|
37
|
+
message: string;
|
|
38
|
+
position: { start: number; end: number };
|
|
39
|
+
suggestion?: string;
|
|
40
|
+
severity: 'error' | 'warning' | 'info';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LanguageResult {
|
|
44
|
+
language: string;
|
|
45
|
+
confidence: number;
|
|
46
|
+
script?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ComposeEngine } from '../src/composer.js';
|
|
3
|
+
|
|
4
|
+
describe('ComposeEngine', () => {
|
|
5
|
+
const engine = new ComposeEngine();
|
|
6
|
+
|
|
7
|
+
it('compose returns ComposeResult with text', () => {
|
|
8
|
+
const result = engine.compose({
|
|
9
|
+
content: 'Hello world',
|
|
10
|
+
context: { platform: 'slack' },
|
|
11
|
+
});
|
|
12
|
+
expect(result.text).toContain('Hello world');
|
|
13
|
+
expect(result.platform).toBe('slack');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('adaptToneForPlatform email returns formal', () => {
|
|
17
|
+
const result = engine.compose({
|
|
18
|
+
content: 'Test',
|
|
19
|
+
context: { platform: 'email' },
|
|
20
|
+
});
|
|
21
|
+
expect(result.tone).toBe('formal');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('adaptToneForPlatform slack returns casual', () => {
|
|
25
|
+
const result = engine.compose({
|
|
26
|
+
content: 'Test',
|
|
27
|
+
context: { platform: 'slack' },
|
|
28
|
+
});
|
|
29
|
+
expect(result.tone).toBe('casual');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('adaptToneForPlatform twitter returns brief', () => {
|
|
33
|
+
const result = engine.compose({
|
|
34
|
+
content: 'Test',
|
|
35
|
+
context: { platform: 'twitter' },
|
|
36
|
+
});
|
|
37
|
+
expect(result.tone).toBe('brief');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('twitter enforced to 280 chars', () => {
|
|
41
|
+
const longText = 'A'.repeat(300);
|
|
42
|
+
const result = engine.compose({
|
|
43
|
+
content: longText,
|
|
44
|
+
context: { platform: 'twitter' },
|
|
45
|
+
});
|
|
46
|
+
expect(result.text.length).toBeLessThanOrEqual(280);
|
|
47
|
+
expect(result.text.endsWith('...')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('sign-off added for email', () => {
|
|
51
|
+
const result = engine.compose({
|
|
52
|
+
content: 'Hello there',
|
|
53
|
+
context: { platform: 'email' },
|
|
54
|
+
});
|
|
55
|
+
expect(result.text).toContain('Best regards,');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('no sign-off for twitter', () => {
|
|
59
|
+
const result = engine.compose({
|
|
60
|
+
content: 'Hello there',
|
|
61
|
+
context: { platform: 'twitter' },
|
|
62
|
+
});
|
|
63
|
+
expect(result.text).toBe('Hello there');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('wordCount is accurate', () => {
|
|
67
|
+
const result = engine.compose({
|
|
68
|
+
content: 'one two three four five',
|
|
69
|
+
context: { platform: 'slack' },
|
|
70
|
+
});
|
|
71
|
+
expect(result.wordCount).toBe(5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('characterCount is accurate', () => {
|
|
75
|
+
const result = engine.compose({
|
|
76
|
+
content: 'hello',
|
|
77
|
+
context: { platform: 'slack' },
|
|
78
|
+
});
|
|
79
|
+
expect(result.characterCount).toBe(5);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('explicit tone overrides platform default', () => {
|
|
83
|
+
const result = engine.compose({
|
|
84
|
+
content: 'Test',
|
|
85
|
+
context: { platform: 'email', tone: 'casual' },
|
|
86
|
+
});
|
|
87
|
+
expect(result.tone).toBe('casual');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { GrammarChecker } from '../src/grammar.js';
|
|
3
|
+
|
|
4
|
+
describe('GrammarChecker', () => {
|
|
5
|
+
const checker = new GrammarChecker();
|
|
6
|
+
|
|
7
|
+
it('detects double spaces', () => {
|
|
8
|
+
const issues = checker.check('Hello world.');
|
|
9
|
+
expect(issues.some((i) => i.message === 'Double space detected')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('detects repeated words', () => {
|
|
13
|
+
const issues = checker.check('The the cat sat.');
|
|
14
|
+
expect(issues.some((i) => i.message.includes('Repeated word'))).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('detects long sentences (>40 words)', () => {
|
|
18
|
+
const words = Array.from({ length: 45 }, (_, i) => `word${i}`).join(' ') + '.';
|
|
19
|
+
const issues = checker.check(words);
|
|
20
|
+
expect(issues.some((i) => i.message.includes('words long'))).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('detects passive voice pattern', () => {
|
|
24
|
+
const issues = checker.check('The report was completed yesterday.');
|
|
25
|
+
expect(issues.some((i) => i.message === 'Possible passive voice')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('detects weasel words', () => {
|
|
29
|
+
const issues = checker.check('This is very important.');
|
|
30
|
+
expect(issues.some((i) => i.message.includes('Weasel word'))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects missing period', () => {
|
|
34
|
+
const issues = checker.check('Hello world');
|
|
35
|
+
expect(issues.some((i) => i.message === 'Text does not end with punctuation')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns empty for clean text', () => {
|
|
39
|
+
const issues = checker.check('The cat sat on the mat.');
|
|
40
|
+
expect(issues.length).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('issues sorted by position', () => {
|
|
44
|
+
const issues = checker.check('The the cat is very nice');
|
|
45
|
+
for (let i = 1; i < issues.length; i++) {
|
|
46
|
+
expect(issues[i].position.start).toBeGreaterThanOrEqual(issues[i - 1].position.start);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LanguageDetector } from '../src/language.js';
|
|
3
|
+
|
|
4
|
+
describe('LanguageDetector', () => {
|
|
5
|
+
const detector = new LanguageDetector();
|
|
6
|
+
|
|
7
|
+
it('detect English text', () => {
|
|
8
|
+
const result = detector.detect('The cat is sitting on the mat with this from that');
|
|
9
|
+
expect(result.language).toBe('english');
|
|
10
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('detect Spanish text', () => {
|
|
14
|
+
const result = detector.detect('El gato es un animal que los tiene en las casas por la noche');
|
|
15
|
+
expect(result.language).toBe('spanish');
|
|
16
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('detect French text', () => {
|
|
20
|
+
const result = detector.detect('Le chat est un animal que les gens ont dans une maison avec des amis');
|
|
21
|
+
expect(result.language).toBe('french');
|
|
22
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('detect German text', () => {
|
|
26
|
+
const result = detector.detect('Der Hund ist ein Tier und die Katze ist mit das Haus');
|
|
27
|
+
expect(result.language).toBe('german');
|
|
28
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('isRTL true for arabic', () => {
|
|
32
|
+
expect(detector.isRTL('arabic')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('isRTL false for english', () => {
|
|
36
|
+
expect(detector.isRTL('english')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|