@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,105 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { MODE_IDS, type ModeId, type ModeTemplate, type ModeSignal } from './types.js';
4
+
5
+ export class ModeLoader {
6
+ private builtInDir: string;
7
+ private userDir: string;
8
+ private cache: Map<ModeId, ModeTemplate> = new Map();
9
+
10
+ constructor(builtInDir: string, userDir: string) {
11
+ this.builtInDir = builtInDir;
12
+ this.userDir = userDir;
13
+ }
14
+
15
+ async loadAll(): Promise<Map<ModeId, ModeTemplate>> {
16
+ this.cache.clear();
17
+
18
+ // Load built-in modes first
19
+ await this.loadFromDir(this.builtInDir);
20
+
21
+ // User modes override built-ins
22
+ await this.loadFromDir(this.userDir);
23
+
24
+ return this.cache;
25
+ }
26
+
27
+ get(id: ModeId): ModeTemplate | undefined {
28
+ return this.cache.get(id);
29
+ }
30
+
31
+ getAll(): Map<ModeId, ModeTemplate> {
32
+ return this.cache;
33
+ }
34
+
35
+ async reload(): Promise<Map<ModeId, ModeTemplate>> {
36
+ return this.loadAll();
37
+ }
38
+
39
+ private async loadFromDir(dir: string): Promise<void> {
40
+ let entries: string[];
41
+ try {
42
+ entries = await fs.readdir(dir);
43
+ } catch {
44
+ return;
45
+ }
46
+
47
+ for (const entry of entries) {
48
+ if (!entry.endsWith('.md')) continue;
49
+ const id = path.basename(entry, '.md');
50
+ if (!MODE_IDS.includes(id as ModeId)) continue;
51
+
52
+ try {
53
+ const content = await fs.readFile(path.join(dir, entry), 'utf-8');
54
+ const template = this.parseMode(id as ModeId, content);
55
+ if (template) {
56
+ this.cache.set(template.id, template);
57
+ }
58
+ } catch {
59
+ // Skip invalid files
60
+ }
61
+ }
62
+ }
63
+
64
+ private parseMode(id: ModeId, content: string): ModeTemplate | null {
65
+ // Parse HTML comment metadata: <!-- mode: operator\n name: ...\n signals: run:0.8, execute:0.8 -->
66
+ const metaMatch = content.match(/^<!--\s*([\s\S]*?)-->\s*\n?/);
67
+ if (!metaMatch) return null;
68
+
69
+ let name: string = id;
70
+ let description = '';
71
+ const signals: ModeSignal[] = [];
72
+ const metaLines = metaMatch[1].split('\n');
73
+
74
+ for (const line of metaLines) {
75
+ const kv = line.match(/^\s*(\w+):\s*(.+)$/);
76
+ if (!kv) continue;
77
+ const [, key, value] = kv;
78
+
79
+ switch (key) {
80
+ case 'name':
81
+ name = value.trim();
82
+ break;
83
+ case 'description':
84
+ description = value.trim();
85
+ break;
86
+ case 'signals':
87
+ for (const pair of value.split(',')) {
88
+ const parts = pair.trim().split(':');
89
+ if (parts.length === 2) {
90
+ signals.push({
91
+ phrase: parts[0].trim().toLowerCase(),
92
+ weight: parseFloat(parts[1].trim()) || 0.5,
93
+ });
94
+ }
95
+ }
96
+ break;
97
+ }
98
+ }
99
+
100
+ // Everything after the comment block is prompt content
101
+ const promptContent = content.slice(metaMatch[0].length).trim();
102
+
103
+ return { id, name, description, promptContent, signals };
104
+ }
105
+ }
@@ -0,0 +1,278 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import {
3
+ getSoulPath,
4
+ getAgentsPath,
5
+ getIdentityPath,
6
+ getUserPath,
7
+ } from '@auxiora/core';
8
+ import type { AgentIdentity } from '@auxiora/config';
9
+ import type { ModeLoader } from './mode-loader.js';
10
+ import type { SessionModeState, UserPreferences, ModeId } from './types.js';
11
+ import type { SecurityContext } from '../security-floor.js';
12
+ import type { SecurityFloor } from '../security-floor.js';
13
+ import type { EscalationState } from '../escalation.js';
14
+
15
+ export class PromptAssembler {
16
+ private agent: AgentIdentity;
17
+ private modeLoader: ModeLoader;
18
+ private personalityAdapter?: { getPromptModifier(): Promise<string | null> };
19
+ private basePrompt: string = '';
20
+
21
+ constructor(
22
+ agent: AgentIdentity,
23
+ modeLoader: ModeLoader,
24
+ personalityAdapter?: { getPromptModifier(): Promise<string | null> },
25
+ ) {
26
+ this.agent = agent;
27
+ this.modeLoader = modeLoader;
28
+ this.personalityAdapter = personalityAdapter;
29
+ }
30
+
31
+ async buildBase(): Promise<string> {
32
+ const parts: string[] = [];
33
+
34
+ // 1. Identity preamble
35
+ parts.push(this.buildIdentityPreamble(this.agent));
36
+
37
+ // 2. Personality adaptations from living memory
38
+ if (this.personalityAdapter) {
39
+ const modifier = await this.personalityAdapter.getPromptModifier();
40
+ if (modifier) {
41
+ parts.push(modifier);
42
+ }
43
+ }
44
+
45
+ // 3. SOUL.md
46
+ try {
47
+ const soul = await fs.readFile(getSoulPath(), 'utf-8');
48
+ parts.push(soul);
49
+ } catch {
50
+ // No SOUL.md
51
+ }
52
+
53
+ // Custom instructions from config
54
+ if (this.agent.customInstructions) {
55
+ parts.push(`## Custom Instructions\n${this.agent.customInstructions}`);
56
+ }
57
+
58
+ // 4. AGENTS.md
59
+ try {
60
+ const agents = await fs.readFile(getAgentsPath(), 'utf-8');
61
+ parts.push(agents);
62
+ } catch {
63
+ // No AGENTS.md
64
+ }
65
+
66
+ // 5. IDENTITY.md
67
+ try {
68
+ const identity = await fs.readFile(getIdentityPath(), 'utf-8');
69
+ parts.push(identity);
70
+ } catch {
71
+ // No IDENTITY.md
72
+ }
73
+
74
+ // 6. USER.md
75
+ try {
76
+ const user = await fs.readFile(getUserPath(), 'utf-8');
77
+ parts.push(`\n## About the User\n${user}`);
78
+ } catch {
79
+ // No USER.md
80
+ }
81
+
82
+ if (parts.length > 1) {
83
+ this.basePrompt = parts.join('\n\n---\n\n');
84
+ } else {
85
+ this.basePrompt = `You are ${this.agent.name}, a helpful AI assistant. Be concise, accurate, and friendly.`;
86
+ }
87
+
88
+ return this.basePrompt;
89
+ }
90
+
91
+ getBasePrompt(): string {
92
+ return this.basePrompt;
93
+ }
94
+
95
+ /**
96
+ * Build a prompt for a security context: base prompt + security floor section + memories.
97
+ * No mode instructions or user preferences are included.
98
+ */
99
+ enrichForSecurityContext(
100
+ securityContext: SecurityContext,
101
+ securityFloor: SecurityFloor,
102
+ memorySection: string | null,
103
+ ): string {
104
+ const parts: string[] = [this.basePrompt];
105
+
106
+ // Inject security floor section
107
+ const sfSection = securityFloor.getSecurityPromptSection(securityContext);
108
+ if (sfSection) {
109
+ parts.push(`\n\n${sfSection}`);
110
+ }
111
+
112
+ // Inject memories (security context still benefits from memory)
113
+ if (memorySection) {
114
+ parts.push(memorySection);
115
+ }
116
+
117
+ return parts.join('');
118
+ }
119
+
120
+ enrichForMessage(
121
+ modeState: SessionModeState | undefined,
122
+ memorySection: string | null,
123
+ preferences?: UserPreferences,
124
+ escalationState?: EscalationState,
125
+ _channelType?: string,
126
+ ): string {
127
+ // If escalation is active, dampen tone in the identity preamble
128
+ let prompt: string;
129
+ if (escalationState && escalationState.level !== 'normal') {
130
+ const dampened = this.dampenToneForEscalation(escalationState);
131
+ const modifiedAgent = { ...this.agent, tone: dampened };
132
+ prompt = this.buildIdentityPreamble(modifiedAgent);
133
+
134
+ // Re-add the rest of the base prompt after identity (if base has more than just identity)
135
+ const identityEnd = this.basePrompt.indexOf('\n\n---\n\n');
136
+ if (identityEnd !== -1) {
137
+ prompt += this.basePrompt.slice(identityEnd);
138
+ }
139
+ } else {
140
+ prompt = this.basePrompt;
141
+ }
142
+
143
+ const parts: string[] = [prompt];
144
+
145
+ // Inject active mode instructions
146
+ if (modeState && modeState.activeMode !== 'auto' && modeState.activeMode !== 'off') {
147
+ const mode = this.modeLoader.get(modeState.activeMode as ModeId);
148
+ if (mode) {
149
+ parts.push(`\n\n## Active Mode: ${mode.name}\n${mode.promptContent}`);
150
+ }
151
+ }
152
+
153
+ // Inject preference overrides
154
+ if (preferences) {
155
+ const rendered = this.renderPreferences(preferences);
156
+ if (rendered) {
157
+ parts.push(`\n\n## User Preferences\n${rendered}`);
158
+ }
159
+ }
160
+
161
+ // Inject memories
162
+ if (memorySection) {
163
+ parts.push(memorySection);
164
+ }
165
+
166
+ return parts.join('');
167
+ }
168
+
169
+ renderPreferences(prefs: UserPreferences): string {
170
+ const lines: string[] = [];
171
+
172
+ if (prefs.verbosity <= 0.2) {
173
+ lines.push('- Be extremely concise. Use bullet points and short sentences.');
174
+ } else if (prefs.verbosity >= 0.8) {
175
+ lines.push('- Be thorough and detailed. Explain reasoning and provide examples.');
176
+ }
177
+
178
+ if (prefs.formality <= 0.2) {
179
+ lines.push('- Use casual, conversational language.');
180
+ } else if (prefs.formality >= 0.8) {
181
+ lines.push('- Use formal, professional language.');
182
+ }
183
+
184
+ if (prefs.proactiveness >= 0.8) {
185
+ lines.push('- Proactively suggest next steps, improvements, and related topics.');
186
+ } else if (prefs.proactiveness <= 0.2) {
187
+ lines.push('- Only answer what is directly asked. Do not volunteer extra information.');
188
+ }
189
+
190
+ if (prefs.riskTolerance >= 0.8) {
191
+ lines.push('- Be bold in recommendations. Favor decisive action over excessive caution.');
192
+ } else if (prefs.riskTolerance <= 0.2) {
193
+ lines.push('- Be cautious. Highlight risks and caveats prominently.');
194
+ }
195
+
196
+ if (prefs.humor >= 0.8) {
197
+ lines.push('- Feel free to be witty and playful in responses.');
198
+ } else if (prefs.humor <= 0.2) {
199
+ lines.push('- Keep responses serious and professional. Avoid humor.');
200
+ }
201
+
202
+ if (prefs.feedbackStyle === 'sandwich') {
203
+ lines.push('- When giving feedback, use the sandwich method: positive → constructive → positive.');
204
+ } else if (prefs.feedbackStyle === 'gentle') {
205
+ lines.push('- Give feedback gently with empathy. Lead with understanding.');
206
+ }
207
+
208
+ if (prefs.expertiseAssumption === 'beginner') {
209
+ lines.push('- Explain concepts from first principles. Define technical terms.');
210
+ } else if (prefs.expertiseAssumption === 'expert') {
211
+ lines.push('- Assume deep technical knowledge. Skip basic explanations.');
212
+ }
213
+
214
+ return lines.join('\n');
215
+ }
216
+
217
+ private dampenToneForEscalation(state: EscalationState): AgentIdentity['tone'] {
218
+ const tone = { ...this.agent.tone };
219
+ switch (state.level) {
220
+ case 'caution':
221
+ return { ...tone, humor: tone.humor * 0.5 };
222
+ case 'serious':
223
+ return { ...tone, humor: 0, directness: Math.max(tone.directness, 0.6) };
224
+ case 'lockdown':
225
+ return { warmth: tone.warmth, humor: 0, directness: Math.max(tone.directness, 0.7), formality: Math.max(tone.formality, 0.5) };
226
+ default:
227
+ return tone;
228
+ }
229
+ }
230
+
231
+ private buildIdentityPreamble(agent: AgentIdentity): string {
232
+ const lines: string[] = ['# Agent Identity'];
233
+ lines.push(`You are ${agent.name} (${agent.pronouns}).`);
234
+
235
+ if (agent.vibe) {
236
+ lines.push(`Vibe: ${agent.vibe}`);
237
+ }
238
+
239
+ lines.push('');
240
+ lines.push('## Personality');
241
+ lines.push(
242
+ `Warmth: ${agent.tone.warmth}/1.0 | Directness: ${agent.tone.directness}/1.0 | Humor: ${agent.tone.humor}/1.0 | Formality: ${agent.tone.formality}/1.0`,
243
+ );
244
+ lines.push(`Error handling style: ${agent.errorStyle}`);
245
+
246
+ if (agent.expertise.length > 0) {
247
+ lines.push('');
248
+ lines.push('## Expertise');
249
+ for (const area of agent.expertise) {
250
+ lines.push(`- ${area}`);
251
+ }
252
+ }
253
+
254
+ const phrases = Object.entries(agent.catchphrases).filter(([, v]) => v);
255
+ if (phrases.length > 0) {
256
+ lines.push('');
257
+ lines.push('## Catchphrases');
258
+ for (const [key, value] of phrases) {
259
+ lines.push(`- ${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`);
260
+ }
261
+ }
262
+
263
+ const hasJokeBoundaries = agent.boundaries.neverJokeAbout.length > 0;
264
+ const hasAdviseBoundaries = agent.boundaries.neverAdviseOn.length > 0;
265
+ if (hasJokeBoundaries || hasAdviseBoundaries) {
266
+ lines.push('');
267
+ lines.push('## Boundaries');
268
+ if (hasJokeBoundaries) {
269
+ lines.push(`Never joke about: ${agent.boundaries.neverJokeAbout.join(', ')}`);
270
+ }
271
+ if (hasAdviseBoundaries) {
272
+ lines.push(`Never advise on: ${agent.boundaries.neverAdviseOn.join(', ')}`);
273
+ }
274
+ }
275
+
276
+ return lines.join('\n');
277
+ }
278
+ }
@@ -0,0 +1,67 @@
1
+ import type { EscalationLevel } from '../escalation.js';
2
+
3
+ export const MODE_IDS = [
4
+ 'operator',
5
+ 'analyst',
6
+ 'advisor',
7
+ 'writer',
8
+ 'socratic',
9
+ 'legal',
10
+ 'roast',
11
+ 'companion',
12
+ ] as const;
13
+
14
+ export type ModeId = (typeof MODE_IDS)[number];
15
+
16
+ export interface ModeSignal {
17
+ phrase: string;
18
+ weight: number;
19
+ }
20
+
21
+ export interface ModeTemplate {
22
+ id: ModeId;
23
+ name: string;
24
+ description: string;
25
+ promptContent: string;
26
+ signals: ModeSignal[];
27
+ }
28
+
29
+ export interface ModeDetectionResult {
30
+ mode: ModeId;
31
+ confidence: number;
32
+ candidates: Array<{ mode: ModeId; score: number }>;
33
+ }
34
+
35
+ export interface UserPreferences {
36
+ verbosity: number; // 0-1: terse → verbose
37
+ formality: number; // 0-1: casual → formal
38
+ proactiveness: number; // 0-1: reactive → proactive
39
+ riskTolerance: number; // 0-1: cautious → bold
40
+ humor: number; // 0-1: serious → playful
41
+ feedbackStyle: 'direct' | 'sandwich' | 'gentle';
42
+ expertiseAssumption: 'beginner' | 'intermediate' | 'expert';
43
+ }
44
+
45
+ export const DEFAULT_PREFERENCES: UserPreferences = {
46
+ verbosity: 0.5,
47
+ formality: 0.5,
48
+ proactiveness: 0.5,
49
+ riskTolerance: 0.5,
50
+ humor: 0.3,
51
+ feedbackStyle: 'direct',
52
+ expertiseAssumption: 'intermediate',
53
+ };
54
+
55
+ export interface SessionModeState {
56
+ activeMode: ModeId | 'auto' | 'off';
57
+ autoDetected: boolean;
58
+ lastAutoMode?: ModeId;
59
+ lastSwitchAt?: number;
60
+ escalationLevel?: EscalationLevel;
61
+ suspendedMode?: ModeId | 'auto' | 'off';
62
+ }
63
+
64
+ export const DEFAULT_SESSION_MODE_STATE: SessionModeState = {
65
+ activeMode: 'auto',
66
+ autoDetected: false,
67
+ };
package/src/parser.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { SoulConfig } from './types.js';
2
+
3
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
4
+
5
+ /**
6
+ * Parse a simple YAML subset into a nested record.
7
+ * Supports: top-level keys, nested object keys (2-space indent),
8
+ * arrays at 2-space indent, sub-keys under nested objects (4-space indent),
9
+ * and arrays at 4-space indent.
10
+ */
11
+ function parseYaml(yaml: string): Record<string, unknown> {
12
+ const result: Record<string, unknown> = {};
13
+ const lines = yaml.split('\n');
14
+
15
+ // Track current hierarchy: topKey, nestedKey
16
+ let topKey = '';
17
+ let topObj: Record<string, unknown> | null = null;
18
+ let nestedKey = '';
19
+
20
+ for (const line of lines) {
21
+ if (line.trim() === '' || line.trim().startsWith('#')) continue;
22
+
23
+ // 4-space array item (under a nested object's sub-key)
24
+ const deep4ArrayMatch = line.match(/^ - (.+)$/);
25
+ if (deep4ArrayMatch && topObj && nestedKey) {
26
+ if (!Array.isArray(topObj[nestedKey])) {
27
+ topObj[nestedKey] = [];
28
+ }
29
+ (topObj[nestedKey] as unknown[]).push(parseYamlValue(deep4ArrayMatch[1]));
30
+ continue;
31
+ }
32
+
33
+ // 2-space key or array item
34
+ const indent2Match = line.match(/^ (\w+):\s*(.*)$/);
35
+ if (indent2Match && topKey) {
36
+ // This is a sub-key under the current top-level key
37
+ if (topObj === null) {
38
+ topObj = {};
39
+ result[topKey] = topObj;
40
+ }
41
+ const [, key, value] = indent2Match;
42
+ nestedKey = key;
43
+ if (value === '') {
44
+ // Sub-key with children (array or deeper object) — set undefined for now
45
+ topObj[key] = undefined;
46
+ } else {
47
+ topObj[key] = parseYamlValue(value);
48
+ }
49
+ continue;
50
+ }
51
+
52
+ const indent2ArrayMatch = line.match(/^ - (.+)$/);
53
+ if (indent2ArrayMatch && topKey) {
54
+ nestedKey = '';
55
+ if (!Array.isArray(result[topKey])) {
56
+ result[topKey] = [];
57
+ }
58
+ (result[topKey] as unknown[]).push(parseYamlValue(indent2ArrayMatch[1]));
59
+ continue;
60
+ }
61
+
62
+ // Top-level key
63
+ const topMatch = line.match(/^(\w+):\s*(.*)$/);
64
+ if (topMatch) {
65
+ const [, key, value] = topMatch;
66
+ topKey = key;
67
+ topObj = null;
68
+ nestedKey = '';
69
+ if (value === '') {
70
+ result[topKey] = undefined;
71
+ } else {
72
+ result[topKey] = parseYamlValue(value);
73
+ }
74
+ }
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ function parseYamlValue(value: string): unknown {
81
+ const trimmed = value.trim();
82
+ if (trimmed === 'true') return true;
83
+ if (trimmed === 'false') return false;
84
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
85
+ // Strip surrounding quotes
86
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
87
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
88
+ return trimmed.slice(1, -1);
89
+ }
90
+ return trimmed;
91
+ }
92
+
93
+ /**
94
+ * Parse a SOUL.md file content into a SoulConfig.
95
+ */
96
+ export function parseSoulMd(content: string): SoulConfig {
97
+ const match = content.match(FRONTMATTER_RE);
98
+ if (!match) {
99
+ throw new Error('Invalid SOUL.md: missing YAML frontmatter (expected --- delimiters)');
100
+ }
101
+
102
+ const [, yamlPart] = match;
103
+ const data = parseYaml(yamlPart);
104
+
105
+ const tone = (data.tone ?? {}) as Record<string, unknown>;
106
+ const boundaries = (data.boundaries ?? {}) as Record<string, unknown>;
107
+ const catchphrases = (data.catchphrases ?? {}) as Record<string, unknown>;
108
+
109
+ return {
110
+ name: String(data.name ?? 'Auxiora'),
111
+ pronouns: String(data.pronouns ?? 'they/them'),
112
+ tone: {
113
+ warmth: Number(tone.warmth ?? 0.6),
114
+ directness: Number(tone.directness ?? 0.5),
115
+ humor: Number(tone.humor ?? 0.3),
116
+ formality: Number(tone.formality ?? 0.5),
117
+ },
118
+ expertise: Array.isArray(data.expertise) ? data.expertise.map(String) : [],
119
+ errorStyle: String(data.errorStyle ?? 'professional'),
120
+ catchphrases: Object.fromEntries(
121
+ Object.entries(catchphrases).map(([k, v]) => [k, String(v)]),
122
+ ),
123
+ boundaries: {
124
+ neverJokeAbout: Array.isArray(boundaries.neverJokeAbout)
125
+ ? boundaries.neverJokeAbout.map(String)
126
+ : [],
127
+ neverAdviseOn: Array.isArray(boundaries.neverAdviseOn)
128
+ ? boundaries.neverAdviseOn.map(String)
129
+ : [],
130
+ },
131
+ };
132
+ }