@auxiora/personality 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.
Files changed (149) hide show
  1. package/LICENSE +191 -0
  2. package/dist/__tests__/builder.test.d.ts +2 -0
  3. package/dist/__tests__/builder.test.d.ts.map +1 -0
  4. package/dist/__tests__/builder.test.js +67 -0
  5. package/dist/__tests__/builder.test.js.map +1 -0
  6. package/dist/__tests__/conversation-builder.test.d.ts +2 -0
  7. package/dist/__tests__/conversation-builder.test.d.ts.map +1 -0
  8. package/dist/__tests__/conversation-builder.test.js +324 -0
  9. package/dist/__tests__/conversation-builder.test.js.map +1 -0
  10. package/dist/__tests__/escalation.test.d.ts +2 -0
  11. package/dist/__tests__/escalation.test.d.ts.map +1 -0
  12. package/dist/__tests__/escalation.test.js +143 -0
  13. package/dist/__tests__/escalation.test.js.map +1 -0
  14. package/dist/__tests__/manager.test.d.ts +2 -0
  15. package/dist/__tests__/manager.test.d.ts.map +1 -0
  16. package/dist/__tests__/manager.test.js +96 -0
  17. package/dist/__tests__/manager.test.js.map +1 -0
  18. package/dist/__tests__/parser.test.d.ts +2 -0
  19. package/dist/__tests__/parser.test.d.ts.map +1 -0
  20. package/dist/__tests__/parser.test.js +89 -0
  21. package/dist/__tests__/parser.test.js.map +1 -0
  22. package/dist/__tests__/security-floor.test.d.ts +2 -0
  23. package/dist/__tests__/security-floor.test.d.ts.map +1 -0
  24. package/dist/__tests__/security-floor.test.js +183 -0
  25. package/dist/__tests__/security-floor.test.js.map +1 -0
  26. package/dist/builder.d.ts +6 -0
  27. package/dist/builder.d.ts.map +1 -0
  28. package/dist/builder.js +65 -0
  29. package/dist/builder.js.map +1 -0
  30. package/dist/conversation-builder.d.ts +30 -0
  31. package/dist/conversation-builder.d.ts.map +1 -0
  32. package/dist/conversation-builder.js +232 -0
  33. package/dist/conversation-builder.js.map +1 -0
  34. package/dist/escalation.d.ts +35 -0
  35. package/dist/escalation.d.ts.map +1 -0
  36. package/dist/escalation.js +134 -0
  37. package/dist/escalation.js.map +1 -0
  38. package/dist/index.d.ts +21 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +20 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/manager.d.ts +28 -0
  43. package/dist/manager.d.ts.map +1 -0
  44. package/dist/manager.js +114 -0
  45. package/dist/manager.js.map +1 -0
  46. package/dist/marketplace/__tests__/scanner.test.d.ts +2 -0
  47. package/dist/marketplace/__tests__/scanner.test.d.ts.map +1 -0
  48. package/dist/marketplace/__tests__/scanner.test.js +134 -0
  49. package/dist/marketplace/__tests__/scanner.test.js.map +1 -0
  50. package/dist/marketplace/__tests__/schema.test.d.ts +2 -0
  51. package/dist/marketplace/__tests__/schema.test.d.ts.map +1 -0
  52. package/dist/marketplace/__tests__/schema.test.js +243 -0
  53. package/dist/marketplace/__tests__/schema.test.js.map +1 -0
  54. package/dist/marketplace/scanner.d.ts +19 -0
  55. package/dist/marketplace/scanner.d.ts.map +1 -0
  56. package/dist/marketplace/scanner.js +62 -0
  57. package/dist/marketplace/scanner.js.map +1 -0
  58. package/dist/marketplace/schema.d.ts +150 -0
  59. package/dist/marketplace/schema.d.ts.map +1 -0
  60. package/dist/marketplace/schema.js +122 -0
  61. package/dist/marketplace/schema.js.map +1 -0
  62. package/dist/modes/__tests__/mode-detector.test.d.ts +2 -0
  63. package/dist/modes/__tests__/mode-detector.test.d.ts.map +1 -0
  64. package/dist/modes/__tests__/mode-detector.test.js +130 -0
  65. package/dist/modes/__tests__/mode-detector.test.js.map +1 -0
  66. package/dist/modes/__tests__/mode-loader.test.d.ts +2 -0
  67. package/dist/modes/__tests__/mode-loader.test.d.ts.map +1 -0
  68. package/dist/modes/__tests__/mode-loader.test.js +91 -0
  69. package/dist/modes/__tests__/mode-loader.test.js.map +1 -0
  70. package/dist/modes/__tests__/prompt-assembler.test.d.ts +2 -0
  71. package/dist/modes/__tests__/prompt-assembler.test.d.ts.map +1 -0
  72. package/dist/modes/__tests__/prompt-assembler.test.js +241 -0
  73. package/dist/modes/__tests__/prompt-assembler.test.js.map +1 -0
  74. package/dist/modes/mode-detector.d.ts +10 -0
  75. package/dist/modes/mode-detector.d.ts.map +1 -0
  76. package/dist/modes/mode-detector.js +70 -0
  77. package/dist/modes/mode-detector.js.map +1 -0
  78. package/dist/modes/mode-loader.d.ts +14 -0
  79. package/dist/modes/mode-loader.d.ts.map +1 -0
  80. package/dist/modes/mode-loader.js +94 -0
  81. package/dist/modes/mode-loader.js.map +1 -0
  82. package/dist/modes/prompt-assembler.d.ts +27 -0
  83. package/dist/modes/prompt-assembler.d.ts.map +1 -0
  84. package/dist/modes/prompt-assembler.js +224 -0
  85. package/dist/modes/prompt-assembler.js.map +1 -0
  86. package/dist/modes/types.d.ts +42 -0
  87. package/dist/modes/types.d.ts.map +1 -0
  88. package/dist/modes/types.js +24 -0
  89. package/dist/modes/types.js.map +1 -0
  90. package/dist/parser.d.ts +6 -0
  91. package/dist/parser.d.ts.map +1 -0
  92. package/dist/parser.js +122 -0
  93. package/dist/parser.js.map +1 -0
  94. package/dist/security-floor.d.ts +31 -0
  95. package/dist/security-floor.d.ts.map +1 -0
  96. package/dist/security-floor.js +113 -0
  97. package/dist/security-floor.js.map +1 -0
  98. package/dist/types.d.ts +26 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +2 -0
  101. package/dist/types.js.map +1 -0
  102. package/dist/voice-profiles.d.ts +23 -0
  103. package/dist/voice-profiles.d.ts.map +1 -0
  104. package/dist/voice-profiles.js +72 -0
  105. package/dist/voice-profiles.js.map +1 -0
  106. package/modes/advisor.md +24 -0
  107. package/modes/analyst.md +25 -0
  108. package/modes/companion.md +24 -0
  109. package/modes/legal.md +1188 -0
  110. package/modes/operator.md +24 -0
  111. package/modes/roast.md +24 -0
  112. package/modes/socratic.md +24 -0
  113. package/modes/writer.md +23 -0
  114. package/package.json +27 -0
  115. package/src/__tests__/builder.test.ts +78 -0
  116. package/src/__tests__/conversation-builder.test.ts +386 -0
  117. package/src/__tests__/escalation.test.ts +172 -0
  118. package/src/__tests__/manager.test.ts +141 -0
  119. package/src/__tests__/parser.test.ts +101 -0
  120. package/src/__tests__/security-floor.test.ts +212 -0
  121. package/src/builder.ts +75 -0
  122. package/src/conversation-builder.ts +279 -0
  123. package/src/escalation.ts +162 -0
  124. package/src/index.ts +55 -0
  125. package/src/manager.ts +119 -0
  126. package/src/marketplace/__tests__/scanner.test.ts +159 -0
  127. package/src/marketplace/__tests__/schema.test.ts +269 -0
  128. package/src/marketplace/scanner.ts +85 -0
  129. package/src/marketplace/schema.ts +141 -0
  130. package/src/modes/__tests__/mode-detector.test.ts +149 -0
  131. package/src/modes/__tests__/mode-loader.test.ts +143 -0
  132. package/src/modes/__tests__/prompt-assembler.test.ts +291 -0
  133. package/src/modes/mode-detector.ts +84 -0
  134. package/src/modes/mode-loader.ts +105 -0
  135. package/src/modes/prompt-assembler.ts +278 -0
  136. package/src/modes/types.ts +67 -0
  137. package/src/parser.ts +132 -0
  138. package/src/security-floor.ts +147 -0
  139. package/src/types.ts +27 -0
  140. package/src/voice-profiles.ts +88 -0
  141. package/templates/chill.md +30 -0
  142. package/templates/creative.md +29 -0
  143. package/templates/friendly.md +28 -0
  144. package/templates/mentor.md +31 -0
  145. package/templates/minimal.md +24 -0
  146. package/templates/professional.md +28 -0
  147. package/templates/technical.md +30 -0
  148. package/tsconfig.json +12 -0
  149. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,279 @@
1
+ import type { SoulConfig, ToneSettings } from './types.js';
2
+ import { buildSoulMd } from './builder.js';
3
+ import { scanAllStringFields } from './marketplace/scanner.js';
4
+
5
+ export interface ConversationQuestion {
6
+ id: string;
7
+ text: string;
8
+ hint?: string;
9
+ }
10
+
11
+ export interface ConversationStep {
12
+ question: ConversationQuestion;
13
+ warning?: string;
14
+ done: false;
15
+ }
16
+
17
+ export interface ConversationComplete {
18
+ config: SoulConfig;
19
+ soulMd: string;
20
+ warnings?: string[];
21
+ done: true;
22
+ }
23
+
24
+ export type ConversationResult = ConversationStep | ConversationComplete;
25
+
26
+ interface PartialConfig {
27
+ name?: string;
28
+ pronouns?: string;
29
+ errorStyle?: string;
30
+ humor?: number;
31
+ communicationStyle?: string;
32
+ expertise?: string[];
33
+ boundaries?: { neverJokeAbout: string[]; neverAdviseOn: string[] };
34
+ catchphrases?: Record<string, string>;
35
+ }
36
+
37
+ const NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9 _-]{0,63}$/;
38
+
39
+ const VALID_ERROR_STYLES = [
40
+ 'professional',
41
+ 'apologetic',
42
+ 'matter_of_fact',
43
+ 'self_deprecating',
44
+ 'gentle',
45
+ 'detailed',
46
+ 'encouraging',
47
+ 'terse',
48
+ 'educational',
49
+ ];
50
+
51
+ const QUESTIONS: ConversationQuestion[] = [
52
+ {
53
+ id: 'name',
54
+ text: 'What should I call myself? Pick a name for your AI assistant.',
55
+ hint: 'e.g. Nova, Atlas, Jasper, or any name you like',
56
+ },
57
+ {
58
+ id: 'error_style',
59
+ text: 'When I make a mistake or hit an error, how should I communicate it?',
60
+ hint: 'Options: professional, apologetic, matter_of_fact, self_deprecating, gentle, detailed, encouraging, terse, educational',
61
+ },
62
+ {
63
+ id: 'humor',
64
+ text: 'How much humor should I use? (0 = serious, 10 = maximum fun)',
65
+ hint: 'Enter a number from 0 to 10',
66
+ },
67
+ {
68
+ id: 'advice_boundaries',
69
+ text: 'Are there topics I should never give advice on? (comma-separated, or "none")',
70
+ hint: 'e.g. legal, medical, financial',
71
+ },
72
+ {
73
+ id: 'joke_boundaries',
74
+ text: 'Are there topics I should never joke about? (comma-separated, or "none")',
75
+ hint: 'e.g. health, politics, religion',
76
+ },
77
+ {
78
+ id: 'expertise',
79
+ text: 'What are my areas of expertise? (comma-separated, or "general")',
80
+ hint: 'e.g. TypeScript, DevOps, Python, Data Science',
81
+ },
82
+ {
83
+ id: 'catchphrases',
84
+ text: 'Any catchphrases you want me to use? Format: greeting=Hello there!, farewell=See ya! (or "none")',
85
+ hint: 'key=value pairs separated by commas',
86
+ },
87
+ {
88
+ id: 'communication_style',
89
+ text: 'Describe your preferred communication style in a few words.',
90
+ hint: 'e.g. warm and casual, formal and precise, brief and direct',
91
+ },
92
+ ];
93
+
94
+ /** Check tone values for unusual combinations. */
95
+ function checkToneCoherence(tone: ToneSettings): string[] {
96
+ const warnings: string[] = [];
97
+ if (tone.humor > 0.8 && tone.formality > 0.8) {
98
+ warnings.push('High humor + high formality is unusual. The result may feel inconsistent.');
99
+ }
100
+ if (tone.warmth < 0.2 && tone.humor > 0.6) {
101
+ warnings.push('Low warmth + high humor can come across as mean-spirited.');
102
+ }
103
+ if (tone.directness > 0.9 && tone.warmth > 0.9) {
104
+ warnings.push('Very direct + very warm can feel contradictory.');
105
+ }
106
+ return warnings;
107
+ }
108
+
109
+ export class SoulConversationBuilder {
110
+ private currentStep = 0;
111
+ private partial: PartialConfig = {};
112
+ private lastWarning?: string;
113
+
114
+ startConversation(): ConversationResult {
115
+ this.currentStep = 0;
116
+ this.partial = {};
117
+ this.lastWarning = undefined;
118
+ return {
119
+ question: QUESTIONS[0],
120
+ done: false,
121
+ };
122
+ }
123
+
124
+ processAnswer(answer: string): ConversationResult {
125
+ const questionId = QUESTIONS[this.currentStep].id;
126
+ this.lastWarning = undefined;
127
+ this.applyAnswer(questionId, answer.trim());
128
+ this.currentStep++;
129
+
130
+ if (this.currentStep >= QUESTIONS.length) {
131
+ const config = this.buildConfig();
132
+ const warnings = checkToneCoherence(config.tone);
133
+ return {
134
+ config,
135
+ soulMd: buildSoulMd(config),
136
+ warnings: warnings.length > 0 ? warnings : undefined,
137
+ done: true,
138
+ };
139
+ }
140
+
141
+ const step: ConversationStep = {
142
+ question: QUESTIONS[this.currentStep],
143
+ done: false,
144
+ };
145
+ if (this.lastWarning) {
146
+ step.warning = this.lastWarning;
147
+ }
148
+ return step;
149
+ }
150
+
151
+ getProgress(): number {
152
+ return Math.round((this.currentStep / QUESTIONS.length) * 100);
153
+ }
154
+
155
+ private applyAnswer(questionId: string, answer: string): void {
156
+ switch (questionId) {
157
+ case 'name':
158
+ if (answer && NAME_REGEX.test(answer)) {
159
+ this.partial.name = answer;
160
+ } else if (answer) {
161
+ this.partial.name = 'Auxiora';
162
+ this.lastWarning = `Name "${answer}" contains invalid characters. Using default "Auxiora".`;
163
+ } else {
164
+ this.partial.name = 'Auxiora';
165
+ }
166
+ break;
167
+
168
+ case 'error_style': {
169
+ const normalized = answer.toLowerCase().replace(/[\s-]/g, '_');
170
+ this.partial.errorStyle = VALID_ERROR_STYLES.includes(normalized) ? normalized : 'professional';
171
+ break;
172
+ }
173
+
174
+ case 'humor': {
175
+ const parsed = parseInt(answer, 10);
176
+ this.partial.humor = Number.isNaN(parsed) ? 0.3 : Math.max(0, Math.min(10, parsed)) / 10;
177
+ break;
178
+ }
179
+
180
+ case 'advice_boundaries': {
181
+ if (answer.toLowerCase() === 'none' || !answer) {
182
+ this.partial.boundaries = { ...this.partial.boundaries ?? { neverJokeAbout: [], neverAdviseOn: [] }, neverAdviseOn: [] };
183
+ } else {
184
+ const items = answer.split(',').map(s => s.trim()).filter(Boolean);
185
+ this.partial.boundaries = { ...this.partial.boundaries ?? { neverJokeAbout: [], neverAdviseOn: [] }, neverAdviseOn: items };
186
+ }
187
+ break;
188
+ }
189
+
190
+ case 'joke_boundaries': {
191
+ if (answer.toLowerCase() === 'none' || !answer) {
192
+ this.partial.boundaries = { ...this.partial.boundaries ?? { neverJokeAbout: [], neverAdviseOn: [] }, neverJokeAbout: [] };
193
+ } else {
194
+ const items = answer.split(',').map(s => s.trim()).filter(Boolean);
195
+ this.partial.boundaries = { ...this.partial.boundaries ?? { neverJokeAbout: [], neverAdviseOn: [] }, neverJokeAbout: items };
196
+ }
197
+ break;
198
+ }
199
+
200
+ case 'expertise': {
201
+ if (answer.toLowerCase() === 'general' || !answer) {
202
+ this.partial.expertise = [];
203
+ } else {
204
+ this.partial.expertise = answer.split(',').map(s => s.trim()).filter(Boolean);
205
+ }
206
+ break;
207
+ }
208
+
209
+ case 'catchphrases': {
210
+ if (answer.toLowerCase() === 'none' || !answer) {
211
+ this.partial.catchphrases = {};
212
+ } else {
213
+ const phrases: Record<string, string> = {};
214
+ const pairs = answer.split(',');
215
+ for (const pair of pairs) {
216
+ const eqIndex = pair.indexOf('=');
217
+ if (eqIndex > 0) {
218
+ const key = pair.slice(0, eqIndex).trim();
219
+ const value = pair.slice(eqIndex + 1).trim();
220
+ if (key && value) {
221
+ phrases[key] = value;
222
+ }
223
+ }
224
+ }
225
+
226
+ // Scan catchphrases for injection patterns
227
+ const scanResult = scanAllStringFields(phrases);
228
+ if (!scanResult.clean) {
229
+ this.partial.catchphrases = {};
230
+ this.lastWarning = 'Catchphrases contain disallowed patterns and were rejected.';
231
+ } else {
232
+ this.partial.catchphrases = phrases;
233
+ }
234
+ }
235
+ break;
236
+ }
237
+
238
+ case 'communication_style':
239
+ this.partial.communicationStyle = answer || 'balanced';
240
+ break;
241
+ }
242
+ }
243
+
244
+ private buildConfig(): SoulConfig {
245
+ const style = this.partial.communicationStyle?.toLowerCase() ?? 'balanced';
246
+ const tone = this.inferTone(style);
247
+
248
+ return {
249
+ name: this.partial.name ?? 'Auxiora',
250
+ pronouns: this.partial.pronouns ?? 'they/them',
251
+ tone: {
252
+ warmth: tone.warmth,
253
+ directness: tone.directness,
254
+ humor: this.partial.humor ?? 0.3,
255
+ formality: tone.formality,
256
+ },
257
+ expertise: this.partial.expertise ?? [],
258
+ errorStyle: this.partial.errorStyle ?? 'professional',
259
+ catchphrases: this.partial.catchphrases ?? {},
260
+ boundaries: this.partial.boundaries ?? { neverJokeAbout: [], neverAdviseOn: [] },
261
+ };
262
+ }
263
+
264
+ private inferTone(style: string): { warmth: number; directness: number; formality: number } {
265
+ if (/warm|friendly|casual/.test(style)) {
266
+ return { warmth: 0.8, directness: 0.5, formality: 0.2 };
267
+ }
268
+ if (/formal|precise|professional/.test(style)) {
269
+ return { warmth: 0.4, directness: 0.7, formality: 0.8 };
270
+ }
271
+ if (/brief|direct|concise/.test(style)) {
272
+ return { warmth: 0.4, directness: 0.9, formality: 0.5 };
273
+ }
274
+ if (/playful|fun/.test(style)) {
275
+ return { warmth: 0.9, directness: 0.4, formality: 0.1 };
276
+ }
277
+ return { warmth: 0.6, directness: 0.6, formality: 0.5 };
278
+ }
279
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Escalation state machine for response severity management.
3
+ * Tracks escalation level across a session and dampens personality tone accordingly.
4
+ */
5
+
6
+ import type { ToneSettings } from './types.js';
7
+ import { SecurityFloor } from './security-floor.js';
8
+
9
+ export const ESCALATION_LEVELS = ['normal', 'caution', 'serious', 'lockdown'] as const;
10
+ export type EscalationLevel = (typeof ESCALATION_LEVELS)[number];
11
+
12
+ export const SEVERITY_LEVELS = ['low', 'medium', 'high', 'critical'] as const;
13
+ export type SeverityLevel = (typeof SEVERITY_LEVELS)[number];
14
+
15
+ export const RESPONSE_CATEGORIES = [
16
+ 'uncertainty',
17
+ 'access_failure',
18
+ 'policy_block',
19
+ 'destructive_confirmation',
20
+ 'security_incident',
21
+ 'rate_limit',
22
+ 'provider_unavailable',
23
+ 'partial_success',
24
+ ] as const;
25
+ export type ResponseCategory = (typeof RESPONSE_CATEGORIES)[number];
26
+
27
+ export interface EscalationTableEntry {
28
+ severity: SeverityLevel;
29
+ securityFloorRequired: boolean;
30
+ canonicalPhrase: string;
31
+ }
32
+
33
+ export const ESCALATION_TABLE: Record<ResponseCategory, EscalationTableEntry> = {
34
+ uncertainty: {
35
+ severity: 'low',
36
+ securityFloorRequired: false,
37
+ canonicalPhrase: "I don't have enough information to answer that confidently.",
38
+ },
39
+ access_failure: {
40
+ severity: 'medium',
41
+ securityFloorRequired: false,
42
+ canonicalPhrase: "I can't reach that resource right now.",
43
+ },
44
+ policy_block: {
45
+ severity: 'medium',
46
+ securityFloorRequired: true,
47
+ canonicalPhrase: 'That action is restricted by your configured policies.',
48
+ },
49
+ destructive_confirmation: {
50
+ severity: 'high',
51
+ securityFloorRequired: true,
52
+ canonicalPhrase: "This will [impact]. This cannot be undone. Type '[verb]' to confirm.",
53
+ },
54
+ security_incident: {
55
+ severity: 'critical',
56
+ securityFloorRequired: true,
57
+ canonicalPhrase: "I've detected something unusual. Here's what I see.",
58
+ },
59
+ rate_limit: {
60
+ severity: 'low',
61
+ securityFloorRequired: false,
62
+ canonicalPhrase: "I've hit a rate limit. I'll retry in [N] seconds.",
63
+ },
64
+ provider_unavailable: {
65
+ severity: 'medium',
66
+ securityFloorRequired: false,
67
+ canonicalPhrase: "My AI provider isn't responding. Trying fallback.",
68
+ },
69
+ partial_success: {
70
+ severity: 'low',
71
+ securityFloorRequired: false,
72
+ canonicalPhrase: "I completed part of that. Here's what worked and what didn't.",
73
+ },
74
+ };
75
+
76
+ export interface EscalationState {
77
+ level: EscalationLevel;
78
+ lastEvent?: ResponseCategory;
79
+ enteredAt?: number;
80
+ }
81
+
82
+ const SEVERITY_TO_LEVEL: Record<SeverityLevel, EscalationLevel> = {
83
+ low: 'caution',
84
+ medium: 'serious',
85
+ high: 'lockdown',
86
+ critical: 'lockdown',
87
+ };
88
+
89
+ const LEVEL_ORDER: Record<EscalationLevel, number> = {
90
+ normal: 0,
91
+ caution: 1,
92
+ serious: 2,
93
+ lockdown: 3,
94
+ };
95
+
96
+ export class EscalationStateMachine {
97
+ private state: EscalationState = { level: 'normal' };
98
+ private securityFloor = new SecurityFloor();
99
+
100
+ /** Process a response category event and transition escalation state. */
101
+ processEvent(event: ResponseCategory): EscalationState {
102
+ const entry = ESCALATION_TABLE[event];
103
+ const targetLevel = SEVERITY_TO_LEVEL[entry.severity];
104
+
105
+ // Only escalate upward, never downward from an event
106
+ if (LEVEL_ORDER[targetLevel] > LEVEL_ORDER[this.state.level]) {
107
+ this.state = {
108
+ level: targetLevel,
109
+ lastEvent: event,
110
+ enteredAt: Date.now(),
111
+ };
112
+ } else {
113
+ this.state = { ...this.state, lastEvent: event };
114
+ }
115
+
116
+ return { ...this.state };
117
+ }
118
+
119
+ /** Resolve toward normal. Transitions one step down. */
120
+ resolve(): EscalationState {
121
+ switch (this.state.level) {
122
+ case 'lockdown':
123
+ this.state = { level: 'normal', enteredAt: Date.now() };
124
+ break;
125
+ case 'serious':
126
+ this.state = { level: 'normal', enteredAt: Date.now() };
127
+ break;
128
+ case 'caution':
129
+ this.state = { level: 'normal', enteredAt: Date.now() };
130
+ break;
131
+ case 'normal':
132
+ break;
133
+ }
134
+ return { ...this.state };
135
+ }
136
+
137
+ /** Get current escalation state. */
138
+ getState(): EscalationState {
139
+ return { ...this.state };
140
+ }
141
+
142
+ /** Dampen tone values based on current escalation level. */
143
+ dampenTone(tone: ToneSettings): ToneSettings {
144
+ switch (this.state.level) {
145
+ case 'normal':
146
+ return { ...tone };
147
+ case 'caution':
148
+ return {
149
+ ...tone,
150
+ humor: tone.humor * 0.5,
151
+ };
152
+ case 'serious':
153
+ return {
154
+ ...tone,
155
+ humor: 0,
156
+ directness: Math.max(tone.directness, 0.6),
157
+ };
158
+ case 'lockdown':
159
+ return this.securityFloor.applyFloor(tone);
160
+ }
161
+ }
162
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ export type { PersonalityTemplate, SoulConfig, ToneSettings } from './types.js';
2
+ export { parseSoulMd } from './parser.js';
3
+ export { buildSoulMd } from './builder.js';
4
+ export { PersonalityManager } from './manager.js';
5
+ export { SoulConversationBuilder } from './conversation-builder.js';
6
+ export type { ConversationQuestion, ConversationStep, ConversationComplete, ConversationResult } from './conversation-builder.js';
7
+ // Modes system
8
+ export { ModeLoader } from './modes/mode-loader.js';
9
+ export { ModeDetector } from './modes/mode-detector.js';
10
+ export { PromptAssembler } from './modes/prompt-assembler.js';
11
+ export type {
12
+ ModeId,
13
+ ModeTemplate,
14
+ ModeSignal,
15
+ ModeDetectionResult,
16
+ UserPreferences,
17
+ SessionModeState,
18
+ } from './modes/types.js';
19
+ export { MODE_IDS, DEFAULT_PREFERENCES, DEFAULT_SESSION_MODE_STATE } from './modes/types.js';
20
+ // [P15] Voice profiles
21
+ export {
22
+ getVoiceProfile,
23
+ listVoiceProfiles,
24
+ DEFAULT_VOICE_PROFILE,
25
+ type VoiceProfile,
26
+ } from './voice-profiles.js';
27
+ // Security floor
28
+ export { SecurityFloor, SECURITY_TOOL_PATTERNS, SECURITY_MESSAGE_PATTERNS } from './security-floor.js';
29
+ export type { SecurityContext, SecurityFloorRule, SecurityDetectionInput } from './security-floor.js';
30
+ // Escalation
31
+ export {
32
+ EscalationStateMachine,
33
+ ESCALATION_LEVELS,
34
+ SEVERITY_LEVELS,
35
+ RESPONSE_CATEGORIES,
36
+ ESCALATION_TABLE,
37
+ } from './escalation.js';
38
+ export type {
39
+ EscalationLevel,
40
+ SeverityLevel,
41
+ ResponseCategory,
42
+ EscalationState,
43
+ EscalationTableEntry,
44
+ } from './escalation.js';
45
+ // Marketplace scanner
46
+ export { scanString, scanAllStringFields, BLOCKED_PATTERNS } from './marketplace/scanner.js';
47
+ export type { ScanViolation, ScanResult } from './marketplace/scanner.js';
48
+ // Marketplace schema
49
+ export {
50
+ validatePersonalityConfig,
51
+ PersonalityConfigSchema,
52
+ FORBIDDEN_FIELD_NAMES,
53
+ FORBIDDEN_FIELD_PATTERNS,
54
+ } from './marketplace/schema.js';
55
+ export type { ValidationResult } from './marketplace/schema.js';
package/src/manager.ts ADDED
@@ -0,0 +1,119 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import type { PersonalityTemplate, SoulConfig } from './types.js';
4
+ import { parseSoulMd } from './parser.js';
5
+ import { buildSoulMd } from './builder.js';
6
+
7
+ export class PersonalityManager {
8
+ private templatesDir: string;
9
+ private workspaceDir: string;
10
+
11
+ constructor(templatesDir: string, workspaceDir: string) {
12
+ this.templatesDir = templatesDir;
13
+ this.workspaceDir = workspaceDir;
14
+ }
15
+
16
+ /**
17
+ * List all available personality templates from the templates directory.
18
+ */
19
+ async listTemplates(): Promise<PersonalityTemplate[]> {
20
+ let entries: string[];
21
+ try {
22
+ entries = await fs.readdir(this.templatesDir);
23
+ } catch {
24
+ return [];
25
+ }
26
+
27
+ const templates: PersonalityTemplate[] = [];
28
+ for (const entry of entries) {
29
+ if (!entry.endsWith('.md')) continue;
30
+ const template = await this.loadTemplateFile(entry);
31
+ if (template) templates.push(template);
32
+ }
33
+ return templates;
34
+ }
35
+
36
+ /**
37
+ * Get a specific personality template by ID.
38
+ */
39
+ async getTemplate(id: string): Promise<PersonalityTemplate | undefined> {
40
+ const filename = `${id}.md`;
41
+ return this.loadTemplateFile(filename);
42
+ }
43
+
44
+ /**
45
+ * Apply a template by copying its content to the workspace SOUL.md.
46
+ */
47
+ async applyTemplate(id: string): Promise<void> {
48
+ const template = await this.getTemplate(id);
49
+ if (!template) {
50
+ throw new Error(`Template not found: ${id}`);
51
+ }
52
+ // Inject template ID into frontmatter so we can identify it later
53
+ let content = template.soulContent;
54
+ content = content.replace(/^---\n/, `---\ntemplate: ${id}\n`);
55
+ const soulPath = path.join(this.workspaceDir, 'SOUL.md');
56
+ await fs.mkdir(this.workspaceDir, { recursive: true });
57
+ await fs.writeFile(soulPath, content, 'utf-8');
58
+ }
59
+
60
+ /**
61
+ * Read and parse the current SOUL.md from the workspace.
62
+ */
63
+ async getCurrentPersonality(): Promise<SoulConfig | undefined> {
64
+ const soulPath = path.join(this.workspaceDir, 'SOUL.md');
65
+ try {
66
+ const content = await fs.readFile(soulPath, 'utf-8');
67
+ return parseSoulMd(content);
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Build a custom SOUL.md from a SoulConfig and write it to the workspace.
75
+ */
76
+ async buildCustom(config: SoulConfig, bodyMarkdown?: string): Promise<string> {
77
+ const content = buildSoulMd(config, bodyMarkdown);
78
+ const soulPath = path.join(this.workspaceDir, 'SOUL.md');
79
+ await fs.mkdir(this.workspaceDir, { recursive: true });
80
+ await fs.writeFile(soulPath, content, 'utf-8');
81
+ return content;
82
+ }
83
+
84
+ private async loadTemplateFile(filename: string): Promise<PersonalityTemplate | undefined> {
85
+ const filePath = path.join(this.templatesDir, filename);
86
+ let content: string;
87
+ try {
88
+ content = await fs.readFile(filePath, 'utf-8');
89
+ } catch {
90
+ return undefined;
91
+ }
92
+
93
+ const id = path.basename(filename, '.md');
94
+
95
+ // Extract template metadata from a comment block at the top before frontmatter
96
+ // Format: <!-- name: ...\n description: ...\n preview: ... -->
97
+ const metaMatch = content.match(/^<!--\s*([\s\S]*?)-->\s*\n/);
98
+ let name = id;
99
+ let description = '';
100
+ let preview = '';
101
+
102
+ if (metaMatch) {
103
+ const metaLines = metaMatch[1].split('\n');
104
+ for (const line of metaLines) {
105
+ const kv = line.match(/^\s*(\w+):\s*(.+)$/);
106
+ if (kv) {
107
+ const [, key, value] = kv;
108
+ if (key === 'name') name = value.trim();
109
+ else if (key === 'description') description = value.trim();
110
+ else if (key === 'preview') preview = value.trim();
111
+ }
112
+ }
113
+ // Remove the comment block from the soul content
114
+ content = content.slice(metaMatch[0].length);
115
+ }
116
+
117
+ return { id, name, description, preview, soulContent: content };
118
+ }
119
+ }