twinclaw 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 (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. package/src/types/websocket.ts +122 -0
@@ -0,0 +1,186 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ const TWINBOT_HOME = '.twinbot';
5
+ const LEGACY_TWINCLAW_HOME = '.twinclaw';
6
+ export function getProfileName() {
7
+ const profile = process.env.TWINBOT_PROFILE || process.env.TWINCLAW_PROFILE;
8
+ if (profile && profile.trim().length > 0) {
9
+ return profile.trim();
10
+ }
11
+ return 'default';
12
+ }
13
+ export function getWorkspaceDir() {
14
+ const profile = getProfileName();
15
+ const home = os.homedir();
16
+ if (profile === 'default') {
17
+ return path.join(home, TWINBOT_HOME, 'workspace');
18
+ }
19
+ const sanitizedProfile = profile.replace(/[^a-zA-Z0-9_-]/g, '_');
20
+ return path.join(home, TWINBOT_HOME, `workspace-${sanitizedProfile}`);
21
+ }
22
+ export function getWorkspaceSubdir(subdir) {
23
+ return path.join(getWorkspaceDir(), subdir);
24
+ }
25
+ export function getConfigPath() {
26
+ return path.join(getWorkspaceDir(), 'twinbot.json');
27
+ }
28
+ export function getDatabasePath() {
29
+ return path.join(getWorkspaceDir(), 'memory', 'twinbot.db');
30
+ }
31
+ export function getIdentityDir() {
32
+ return path.join(getWorkspaceDir(), 'identity');
33
+ }
34
+ export function getSecretsVaultPath() {
35
+ return path.join(getWorkspaceDir(), 'secrets.sqlite');
36
+ }
37
+ export function getTranscriptsDir() {
38
+ return path.join(getWorkspaceDir(), 'transcripts');
39
+ }
40
+ export function ensureWorkspaceDir() {
41
+ const workspaceDir = getWorkspaceDir();
42
+ if (!fs.existsSync(workspaceDir)) {
43
+ fs.mkdirSync(workspaceDir, { recursive: true });
44
+ }
45
+ }
46
+ export function ensureWorkspaceSubdirs() {
47
+ ensureWorkspaceDir();
48
+ const subdirs = ['memory', 'identity', 'transcripts'];
49
+ for (const subdir of subdirs) {
50
+ const fullPath = getWorkspaceSubdir(subdir);
51
+ if (!fs.existsSync(fullPath)) {
52
+ fs.mkdirSync(fullPath, { recursive: true });
53
+ }
54
+ }
55
+ }
56
+ export function getLegacyConfigPath() {
57
+ // 1. Check for twinbot.json in the new workspace first (partial migration state)
58
+ const newWorkspaceLegacyFile = path.join(getWorkspaceDir(), 'twinbot.json');
59
+ if (fs.existsSync(newWorkspaceLegacyFile)) {
60
+ return newWorkspaceLegacyFile;
61
+ }
62
+ // 2. Then check the old .twinbot home (flat structure)
63
+ const oldTwinBotFile = path.join(os.homedir(), TWINBOT_HOME, 'twinbot.json');
64
+ if (fs.existsSync(oldTwinBotFile)) {
65
+ return oldTwinBotFile;
66
+ }
67
+ // 3. Finally, check the legacy .twinclaw home
68
+ const legacyTwinClawFile = path.join(os.homedir(), LEGACY_TWINCLAW_HOME, 'twinclaw.json');
69
+ return legacyTwinClawFile;
70
+ }
71
+ export function hasLegacyConfig() {
72
+ const legacyPath = getLegacyConfigPath();
73
+ const workspacePath = getConfigPath();
74
+ return fs.existsSync(legacyPath) && !fs.existsSync(workspacePath);
75
+ }
76
+ export function migrateLegacyConfig() {
77
+ const legacyPath = getLegacyConfigPath();
78
+ const workspacePath = getConfigPath();
79
+ if (!fs.existsSync(legacyPath)) {
80
+ return { migrated: false, sourcePath: null, targetPath: null };
81
+ }
82
+ if (fs.existsSync(workspacePath)) {
83
+ return { migrated: false, sourcePath: legacyPath, targetPath: workspacePath };
84
+ }
85
+ const workspaceDir = getWorkspaceDir();
86
+ if (!fs.existsSync(workspaceDir)) {
87
+ fs.mkdirSync(workspaceDir, { recursive: true });
88
+ }
89
+ try {
90
+ const content = fs.readFileSync(legacyPath, 'utf-8');
91
+ fs.writeFileSync(workspacePath, content, { encoding: 'utf-8', mode: 0o600 });
92
+ const legacyDir = path.dirname(legacyPath);
93
+ const legacyFiles = ['identity', 'memory', 'transcripts', 'secrets.sqlite', 'twinbot.db', 'twinclaw.db'];
94
+ for (const file of legacyFiles) {
95
+ const legacyFile = path.join(legacyDir, file);
96
+ if (fs.existsSync(legacyFile)) {
97
+ let targetFileName = file;
98
+ if (file === 'twinclaw.db')
99
+ targetFileName = 'twinbot.db';
100
+ const targetFile = path.join(workspaceDir, targetFileName);
101
+ const targetFileDir = path.dirname(targetFile);
102
+ if (!fs.existsSync(targetFileDir)) {
103
+ fs.mkdirSync(targetFileDir, { recursive: true });
104
+ }
105
+ if (fs.statSync(legacyFile).isDirectory()) {
106
+ fs.cpSync(legacyFile, targetFile, { recursive: true });
107
+ }
108
+ else {
109
+ fs.copyFileSync(legacyFile, targetFile);
110
+ }
111
+ }
112
+ }
113
+ return { migrated: true, sourcePath: legacyPath, targetPath: workspacePath };
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ return { migrated: false, sourcePath: legacyPath, targetPath: workspacePath, error: message };
118
+ }
119
+ }
120
+ export const WORKSPACE_GITIGNORE = `# TwinBot Workspace .gitignore
121
+ # Auto-generated by TwinBot workspace initialization
122
+
123
+ # Sensitive databases
124
+ *.sqlite
125
+ *.sqlite-journal
126
+ *.sqlite-wal
127
+ *.sqlite-shm
128
+
129
+ # Secrets vault (never commit)
130
+ secrets.sqlite
131
+
132
+ # Memory databases
133
+ memory/*.db
134
+ memory/*.db-journal
135
+
136
+ # Session transcripts (may contain sensitive data)
137
+ transcripts/*.md
138
+
139
+ # Runtime state files
140
+ runtime-state.json
141
+ *.pid
142
+
143
+ # Logs
144
+ logs/
145
+ *.log
146
+
147
+ # Temporary files
148
+ *.tmp
149
+ *.temp
150
+ .cache/
151
+
152
+ # OS files
153
+ .DS_Store
154
+ Thumbs.db
155
+ `;
156
+ export function initializeWorkspaceGitignore() {
157
+ const workspaceDir = getWorkspaceDir();
158
+ const gitignorePath = path.join(workspaceDir, '.gitignore');
159
+ if (fs.existsSync(gitignorePath)) {
160
+ return false;
161
+ }
162
+ try {
163
+ fs.writeFileSync(gitignorePath, WORKSPACE_GITIGNORE, { encoding: 'utf-8' });
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ export function initializeWorkspace() {
171
+ ensureWorkspaceSubdirs();
172
+ initializeWorkspaceGitignore();
173
+ }
174
+ export function getWorkspaceSummary() {
175
+ const workspaceDir = getWorkspaceDir();
176
+ return {
177
+ workspaceDir,
178
+ configPath: getConfigPath(),
179
+ databasePath: getDatabasePath(),
180
+ identityDir: getIdentityDir(),
181
+ secretsVaultPath: getSecretsVaultPath(),
182
+ transcriptsDir: getTranscriptsDir(),
183
+ profileName: getProfileName(),
184
+ exists: fs.existsSync(workspaceDir),
185
+ };
186
+ }
@@ -0,0 +1,77 @@
1
+ import WAWebJS from 'whatsapp-web.js';
2
+ import qrcode from 'qrcode-terminal';
3
+ const { Client, LocalAuth } = WAWebJS;
4
+ function printUsage() {
5
+ console.log(`Channel commands:
6
+ channels login whatsapp Start interactive login for WhatsApp (QR Code)`);
7
+ }
8
+ async function runWhatsappLogin() {
9
+ console.log('[TwinBot Channels] Starting WhatsApp login sequence...');
10
+ console.log('Initializing secure browser environment...');
11
+ const client = new Client({
12
+ authStrategy: new LocalAuth({ dataPath: './memory/whatsapp_auth' }),
13
+ puppeteer: {
14
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
15
+ },
16
+ });
17
+ client.on('qr', (qr) => {
18
+ console.log('\n[TwinBot Channels] ══════════════════════════════════════════════════');
19
+ console.log('[TwinBot Channels] ACTION REQUIRED: SCAN TO LINK WHATSAPP');
20
+ console.log('[TwinBot Channels] 1. Open WhatsApp on your primary phone');
21
+ console.log('[TwinBot Channels] 2. Tap Menu (⋮) or Settings -> Linked Devices');
22
+ console.log('[TwinBot Channels] 3. Tap "Link a Device" and scan the code below:');
23
+ qrcode.generate(qr, { small: true });
24
+ console.log('[TwinBot Channels] ══════════════════════════════════════════════════\n');
25
+ console.log('Awaiting scan...');
26
+ });
27
+ client.on('ready', () => {
28
+ console.log('\n[TwinBot Channels] ✓ Login Successful!');
29
+ console.log('[TwinBot Channels] WhatsApp session is now explicitly linked.');
30
+ console.log('[TwinBot Channels] You may now close this setup screen.');
31
+ console.log('\nNext Steps:');
32
+ console.log('1. Run `node src/index.ts doctor` to verify channel readiness.');
33
+ console.log('2. Ensure your number is authorized per DM Pairing Policy.');
34
+ // Clean exit
35
+ void client.destroy().then(() => {
36
+ process.exit(0);
37
+ });
38
+ });
39
+ client.on('authenticated', () => {
40
+ console.log('[TwinBot Channels] Authentication state captured. Finalizing session mapping...');
41
+ });
42
+ client.on('auth_failure', (msg) => {
43
+ console.error(`\n[TwinBot Channels] ✗ Authentication Failed: ${msg}`);
44
+ console.error('[TwinBot Channels] Remediation: Try deleting the ./memory/whatsapp_auth directory and run `channels login whatsapp` again.');
45
+ process.exitCode = 1;
46
+ void client.destroy().then(() => {
47
+ process.exit(1);
48
+ });
49
+ });
50
+ try {
51
+ await client.initialize();
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ console.error(`\n[TwinBot Channels] ✗ Client initialization failed: ${message}`);
56
+ process.exitCode = 1;
57
+ }
58
+ }
59
+ /**
60
+ * Handle \`channels\` CLI commands.
61
+ */
62
+ export async function handleChannelsCli(argv) {
63
+ const topCommand = argv[0];
64
+ if (topCommand !== 'channels') {
65
+ return false;
66
+ }
67
+ const subcommand = argv[1];
68
+ const target = argv[2];
69
+ if (subcommand === 'login' && target === 'whatsapp') {
70
+ // Fire and forget because runWhatsappLogin manages process.exit internally
71
+ void runWhatsappLogin();
72
+ return true;
73
+ }
74
+ printUsage();
75
+ process.exitCode = 1;
76
+ return true;
77
+ }
@@ -0,0 +1,119 @@
1
+ import { runDoctorChecks, formatDoctorReport } from './doctor.js';
2
+ // ── Help text ────────────────────────────────────────────────────────────────
3
+ const HELP_TEXT = `
4
+ Usage: twinbot [command] [options]
5
+
6
+ Commands:
7
+ start Start the TwinBot gateway runtime
8
+ doctor Run diagnostics and validate prerequisites
9
+ onboard Run the interactive onboarding wizard (twinbot.json)
10
+ install One-command Windows bootstrap + optional onboarding handoff
11
+ setup Run the guided configuration wizard
12
+ pairing Manage DM pairing approvals (list/approve)
13
+ secret <subcommand> Manage secrets in the secure vault
14
+ channels <subcmd> Manage messaging channels (e.g. login)
15
+ gateway <subcmd> Manage the TwinBot background service daemon
16
+ logs [--follow] Stream daily structured memory logs from the daemon
17
+ --onboard Run the interactive AI persona-building session
18
+
19
+ Options:
20
+ --help, -h Show this help message
21
+ --json Output in machine-readable JSON format (doctor only)
22
+
23
+ Examples:
24
+ twinbot start
25
+ twinbot doctor
26
+ twinbot doctor --json
27
+ twinbot onboard
28
+ twinbot setup
29
+ twinbot pairing list telegram
30
+ twinbot pairing approve telegram ABCD1234
31
+ twinbot secret list
32
+ twinbot secret set API_SECRET mysecret
33
+ twinbot secret rotate API_SECRET newsecret
34
+ twinbot secret revoke API_SECRET
35
+ twinbot secret doctor
36
+ twinbot gateway install
37
+ twinbot gateway status
38
+ twinbot gateway status --deep
39
+ twinbot gateway restart
40
+ twinbot gateway tailscale
41
+ twinbot logs --follow
42
+ `.trim();
43
+ // ── Command handlers ─────────────────────────────────────────────────────────
44
+ /**
45
+ * Handle the `doctor` command.
46
+ * Runs all diagnostic checks and emits a report.
47
+ * Returns `true` when the command was recognized and handled.
48
+ */
49
+ export function handleDoctorCli(argv) {
50
+ if (argv[0] !== 'doctor')
51
+ return false;
52
+ const asJson = argv.includes('--json');
53
+ try {
54
+ const report = runDoctorChecks();
55
+ const output = formatDoctorReport(report, asJson);
56
+ console.log(output);
57
+ if (report.status === 'critical') {
58
+ process.exitCode = 2;
59
+ }
60
+ else if (report.status === 'degraded') {
61
+ process.exitCode = 1;
62
+ }
63
+ else {
64
+ process.exitCode = 0;
65
+ }
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ console.error(`[TwinBot] Doctor check failed: ${message}`);
70
+ process.exitCode = 1;
71
+ }
72
+ return true;
73
+ }
74
+ /**
75
+ * Handle `--help` or `-h` flags.
76
+
77
+ * Returns `true` when the flag was found.
78
+ */
79
+ export function handleHelpCli(argv) {
80
+ if (!argv.includes('--help') && !argv.includes('-h'))
81
+ return false;
82
+ console.log(HELP_TEXT);
83
+ process.exitCode = 0;
84
+ return true;
85
+ }
86
+ /**
87
+ * Guard against unknown or mistyped top-level commands.
88
+ * Known commands are allowed to fall through to their own handlers.
89
+ * Returns `true` and sets a non-zero exit code when an unknown command is detected.
90
+ */
91
+ export function handleUnknownCommand(argv) {
92
+ if (argv.length === 0)
93
+ return false;
94
+ const command = argv[0];
95
+ // Flags and known commands pass through
96
+ const KNOWN_COMMANDS = new Set([
97
+ 'pairing',
98
+ 'secret',
99
+ 'doctor',
100
+ 'start',
101
+ 'onboard',
102
+ 'install',
103
+ 'setup',
104
+ 'channels',
105
+ 'gateway',
106
+ 'logs',
107
+ '--onboard',
108
+ '--help',
109
+ '-h',
110
+ '--json',
111
+ ]);
112
+ if (KNOWN_COMMANDS.has(command) || command.startsWith('--')) {
113
+ return false;
114
+ }
115
+ console.error(`[TwinBot] Unknown command: '${command}'`);
116
+ console.error(`Run 'twinbot --help' to see available commands.`);
117
+ process.exitCode = 1;
118
+ return true;
119
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getIdentityDir } from '../config/workspace.js';
4
+ export async function assembleContext(additionalRuntimeContext = '') {
5
+ let soul = '';
6
+ let identity = '';
7
+ let user = '';
8
+ const identityDir = getIdentityDir();
9
+ const readOptionalFile = async (fileName) => {
10
+ try {
11
+ const filePath = path.join(identityDir, fileName);
12
+ return await fs.readFile(filePath, 'utf-8');
13
+ }
14
+ catch {
15
+ return '';
16
+ }
17
+ };
18
+ soul = await readOptionalFile('soul.md');
19
+ identity = await readOptionalFile('identity.md');
20
+ user = await readOptionalFile('user.md');
21
+ const compiled = `
22
+ You are TwinBot. Follow your core directives exactly.
23
+
24
+ ${soul ? `### CORE SOUL & DIRECTIVES\n${soul}` : ''}
25
+
26
+ ${identity ? `### IDENTITY & PERSONA\n${identity}` : ''}
27
+
28
+ ${user ? `### USER PREFERENCES\n${user}` : ''}
29
+
30
+ ${additionalRuntimeContext ? `### ADDITIONAL CONTEXT (RAG MEMORY)\n${additionalRuntimeContext}` : ''}
31
+ `.trim();
32
+ return compiled;
33
+ }