siduri 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 (131) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +84 -0
  3. package/dist/builtin-manifests.d.ts +2 -0
  4. package/dist/builtin-manifests.js +524 -0
  5. package/dist/clean-machine-e2e.test.d.ts +1 -0
  6. package/dist/clean-machine-e2e.test.js +160 -0
  7. package/dist/clean-machine-smoke.test.d.ts +1 -0
  8. package/dist/clean-machine-smoke.test.js +165 -0
  9. package/dist/colors.d.ts +8 -0
  10. package/dist/colors.js +11 -0
  11. package/dist/configurators/behavior.d.ts +2 -0
  12. package/dist/configurators/behavior.js +77 -0
  13. package/dist/configurators/body.d.ts +2 -0
  14. package/dist/configurators/body.js +107 -0
  15. package/dist/configurators/brain.d.ts +6 -0
  16. package/dist/configurators/brain.js +80 -0
  17. package/dist/configurators/ear.d.ts +2 -0
  18. package/dist/configurators/ear.js +43 -0
  19. package/dist/configurators/hands.d.ts +2 -0
  20. package/dist/configurators/hands.js +43 -0
  21. package/dist/configurators/index.d.ts +24 -0
  22. package/dist/configurators/index.js +78 -0
  23. package/dist/configurators/knowledge.d.ts +6 -0
  24. package/dist/configurators/knowledge.js +146 -0
  25. package/dist/configurators/memory.d.ts +2 -0
  26. package/dist/configurators/memory.js +26 -0
  27. package/dist/configurators/mouth.d.ts +2 -0
  28. package/dist/configurators/mouth.js +35 -0
  29. package/dist/configurators/observation.d.ts +2 -0
  30. package/dist/configurators/observation.js +38 -0
  31. package/dist/configurators/types.d.ts +12 -0
  32. package/dist/configurators/types.js +2 -0
  33. package/dist/configurators/vision.d.ts +2 -0
  34. package/dist/configurators/vision.js +47 -0
  35. package/dist/configurators/voice.d.ts +2 -0
  36. package/dist/configurators/voice.js +373 -0
  37. package/dist/configurators.test.d.ts +1 -0
  38. package/dist/configurators.test.js +464 -0
  39. package/dist/db.d.ts +13 -0
  40. package/dist/db.js +15 -0
  41. package/dist/discovery.d.ts +13 -0
  42. package/dist/discovery.js +85 -0
  43. package/dist/discovery.test.d.ts +1 -0
  44. package/dist/discovery.test.js +69 -0
  45. package/dist/doctor-db.test.d.ts +1 -0
  46. package/dist/doctor-db.test.js +52 -0
  47. package/dist/doctor.d.ts +19 -0
  48. package/dist/doctor.js +266 -0
  49. package/dist/generator.d.ts +23 -0
  50. package/dist/generator.js +1094 -0
  51. package/dist/generator.test.d.ts +1 -0
  52. package/dist/generator.test.js +252 -0
  53. package/dist/index.d.ts +18 -0
  54. package/dist/index.js +607 -0
  55. package/dist/manifest.d.ts +33 -0
  56. package/dist/manifest.js +40 -0
  57. package/dist/project-dir.test.d.ts +1 -0
  58. package/dist/project-dir.test.js +37 -0
  59. package/dist/providers/knowledge-hub.d.ts +44 -0
  60. package/dist/providers/knowledge-hub.js +162 -0
  61. package/dist/providers/openrouter.d.ts +32 -0
  62. package/dist/providers/openrouter.js +201 -0
  63. package/dist/release-check.d.ts +9 -0
  64. package/dist/release-check.js +128 -0
  65. package/dist/runtime-parity.test.d.ts +1 -0
  66. package/dist/runtime-parity.test.js +141 -0
  67. package/dist/schema-validator.d.ts +9 -0
  68. package/dist/schema-validator.js +107 -0
  69. package/dist/web-dist/404/index.html +1 -0
  70. package/dist/web-dist/404.html +1 -0
  71. package/dist/web-dist/__next.__PAGE__.txt +34 -0
  72. package/dist/web-dist/__next._full.txt +32 -0
  73. package/dist/web-dist/__next._tree.txt +4 -0
  74. package/dist/web-dist/_next/static/4aL_iP3FHsLSyHzCnRQYD/_buildManifest.js +1 -0
  75. package/dist/web-dist/_next/static/4aL_iP3FHsLSyHzCnRQYD/_ssgManifest.js +1 -0
  76. package/dist/web-dist/_next/static/chunks/136-1d10af93b770a1c8.js +52 -0
  77. package/dist/web-dist/_next/static/chunks/1629039a.f535b3ff45566416.js +1 -0
  78. package/dist/web-dist/_next/static/chunks/202.59b8eb8a8c45f711.js +1 -0
  79. package/dist/web-dist/_next/static/chunks/2be4ccb0.ab2975eb50b4e166.js +1 -0
  80. package/dist/web-dist/_next/static/chunks/411.d68b2e48424f1e47.js +1 -0
  81. package/dist/web-dist/_next/static/chunks/426-9f7f6e7fffd4c3c3.js +1 -0
  82. package/dist/web-dist/_next/static/chunks/563b947f-8e2871a2ab7cd91e.js +1 -0
  83. package/dist/web-dist/_next/static/chunks/92e71660.11f182d50bce2be0.js +342 -0
  84. package/dist/web-dist/_next/static/chunks/app/_global-error/page-bce6d30fb2b1a3b0.js +1 -0
  85. package/dist/web-dist/_next/static/chunks/app/_not-found/page-74658f4988eabcc7.js +1 -0
  86. package/dist/web-dist/_next/static/chunks/app/chat/page-ab56048b707a3ddd.js +34 -0
  87. package/dist/web-dist/_next/static/chunks/app/layout-14f1a3f31e58be17.js +1 -0
  88. package/dist/web-dist/_next/static/chunks/app/operator/page-6b2d0ba884183ad7.js +1 -0
  89. package/dist/web-dist/_next/static/chunks/app/page-fc4bf56cf33ae27b.js +1 -0
  90. package/dist/web-dist/_next/static/chunks/framework-7afb7035d9872ea0.js +1 -0
  91. package/dist/web-dist/_next/static/chunks/main-8553b996986f1210.js +5 -0
  92. package/dist/web-dist/_next/static/chunks/main-app-9e6a144626e5a5b9.js +1 -0
  93. package/dist/web-dist/_next/static/chunks/next/dist/client/components/builtin/app-error-bce6d30fb2b1a3b0.js +1 -0
  94. package/dist/web-dist/_next/static/chunks/next/dist/client/components/builtin/forbidden-bce6d30fb2b1a3b0.js +1 -0
  95. package/dist/web-dist/_next/static/chunks/next/dist/client/components/builtin/global-error-68ea4ac05dcaf701.js +1 -0
  96. package/dist/web-dist/_next/static/chunks/next/dist/client/components/builtin/not-found-bce6d30fb2b1a3b0.js +1 -0
  97. package/dist/web-dist/_next/static/chunks/next/dist/client/components/builtin/unauthorized-bce6d30fb2b1a3b0.js +1 -0
  98. package/dist/web-dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  99. package/dist/web-dist/_next/static/chunks/webpack-9073f434bc17ce16.js +1 -0
  100. package/dist/web-dist/_next/static/css/ceeb8e5596664b11.css +1 -0
  101. package/dist/web-dist/_not-found/__next._full.txt +17 -0
  102. package/dist/web-dist/_not-found/__next._not-found.__PAGE__.txt +23 -0
  103. package/dist/web-dist/_not-found/__next._tree.txt +3 -0
  104. package/dist/web-dist/_not-found/index.html +1 -0
  105. package/dist/web-dist/_not-found/index.txt +17 -0
  106. package/dist/web-dist/apple-icon.png +0 -0
  107. package/dist/web-dist/apple-touch-icon.png +0 -0
  108. package/dist/web-dist/chat/__next._full.txt +18 -0
  109. package/dist/web-dist/chat/__next._tree.txt +3 -0
  110. package/dist/web-dist/chat/__next.chat.__PAGE__.txt +24 -0
  111. package/dist/web-dist/chat/index.html +1 -0
  112. package/dist/web-dist/chat/index.txt +18 -0
  113. package/dist/web-dist/favicon.ico +0 -0
  114. package/dist/web-dist/favicon.svg +4 -0
  115. package/dist/web-dist/icon.svg +4 -0
  116. package/dist/web-dist/index.html +1 -0
  117. package/dist/web-dist/index.txt +32 -0
  118. package/dist/web-dist/live2d/live2dcubismcore.min.js +9 -0
  119. package/dist/web-dist/logo-circle.png +0 -0
  120. package/dist/web-dist/logo-circle.svg +4 -0
  121. package/dist/web-dist/logo-white.svg +3 -0
  122. package/dist/web-dist/logo.png +0 -0
  123. package/dist/web-dist/logo.svg +3 -0
  124. package/dist/web-dist/operator/__next._full.txt +18 -0
  125. package/dist/web-dist/operator/__next._tree.txt +3 -0
  126. package/dist/web-dist/operator/__next.operator.__PAGE__.txt +24 -0
  127. package/dist/web-dist/operator/index.html +1 -0
  128. package/dist/web-dist/operator/index.txt +18 -0
  129. package/dist/web-template.d.ts +2 -0
  130. package/dist/web-template.js +589 -0
  131. package/package.json +44 -0
@@ -0,0 +1,1094 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateInstanceFiles = generateInstanceFiles;
4
+ const web_template_1 = require("./web-template");
5
+ function getDefaultConfigForManifest(manifest) {
6
+ const schema = manifest.configSchema || {};
7
+ const props = schema.properties || {};
8
+ const config = {};
9
+ for (const [key, val] of Object.entries(props)) {
10
+ if (val.default !== undefined) {
11
+ config[key] = val.default;
12
+ }
13
+ else if (val.enum && val.enum.length > 0) {
14
+ config[key] = val.enum[0];
15
+ }
16
+ else if (val.type === 'string') {
17
+ config[key] = '';
18
+ }
19
+ else if (val.type === 'number') {
20
+ config[key] = 0;
21
+ }
22
+ else if (val.type === 'boolean') {
23
+ config[key] = false;
24
+ }
25
+ else if (val.type === 'array') {
26
+ config[key] = [];
27
+ }
28
+ else if (val.type === 'object') {
29
+ config[key] = {};
30
+ }
31
+ }
32
+ // Provide sensible defaults for known common keys if missing
33
+ if (manifest.organType === 'brain') {
34
+ config.provider = config.provider || 'openrouter';
35
+ config.model = config.model || 'anthropic/claude-3.5-sonnet';
36
+ config.apiKeyEnv = 'OPENROUTER_API_KEY';
37
+ }
38
+ else if (manifest.organType === 'memory') {
39
+ config.provider = config.provider || 'sqlite';
40
+ }
41
+ else if (manifest.organType === 'voice') {
42
+ config.provider = config.provider || 'voicevox';
43
+ config.speakerId = config.speakerId || 1;
44
+ config.baseUrl = config.baseUrl || 'http://localhost:50021';
45
+ }
46
+ else if (manifest.organType === 'body') {
47
+ config.provider = config.provider || 'live2d';
48
+ config.initialExpression = config.initialExpression || 'neutral';
49
+ config.modelPath = config.modelPath || './assets/body/default/model.model3.json';
50
+ config.modelUrl = config.modelUrl || '/assets/body/default/model.model3.json';
51
+ }
52
+ else if (manifest.organType === 'hands') {
53
+ config.defaultTimeoutMs = config.defaultTimeoutMs || 10000;
54
+ config.providers = config.providers || [];
55
+ }
56
+ else if (manifest.organType === 'knowledge') {
57
+ config.provider = config.provider || 'unified';
58
+ config.lifeDatabase = config.lifeDatabase ?? true;
59
+ config.dbPath = config.dbPath || 'siduri.sqlite';
60
+ }
61
+ else if (manifest.organType === 'behavior') {
62
+ config.provider = config.provider || 'active_self';
63
+ }
64
+ else if (manifest.organType === 'vision') {
65
+ config.provider = config.provider || 'openrouter';
66
+ config.model = config.model || 'gpt-4-vision';
67
+ }
68
+ else if (manifest.organType === 'mouth') {
69
+ config.defaultMedium = 'web';
70
+ config.maxTextLength = config.maxTextLength || 8000;
71
+ }
72
+ return config;
73
+ }
74
+ function generateInstanceFiles(options) {
75
+ const instanceName = options.name || 'my-siduri';
76
+ const instanceId = options.id || 'default';
77
+ const coreVersion = options.coreVersion || '^1.0.0';
78
+ const cliVersion = options.cliVersion || '^2.1.0';
79
+ const canonicalOrder = ['brain', 'archive', 'knowledge', 'behavior', 'voice', 'body', 'mouth', 'hands', 'vision', 'ear', 'observation'];
80
+ const manifests = [...options.selectedManifests].sort((a, b) => {
81
+ const idxA = canonicalOrder.indexOf(a.organType);
82
+ const idxB = canonicalOrder.indexOf(b.organType);
83
+ return (idxA === -1 ? 99 : idxA) - (idxB === -1 ? 99 : idxB);
84
+ });
85
+ const hasArchive = manifests.some((m) => m.organType === 'archive');
86
+ const hasVoice = manifests.some((m) => m.organType === 'voice');
87
+ const hasBody = manifests.some((m) => m.organType === 'body');
88
+ const hasMouth = manifests.some((m) => m.organType === 'mouth');
89
+ const hasVision = manifests.some((m) => m.organType === 'vision');
90
+ const voiceConfig = options.organConfigs?.voice || options.organConfigs?.['@sidurijs/voice'];
91
+ const isVoicevox = hasVoice && (!voiceConfig || voiceConfig.provider === 'voicevox');
92
+ // 1. package.json
93
+ const dependencies = {};
94
+ if (options.localPath) {
95
+ const repoPath = options.localPath.replace(/\\/g, '/');
96
+ dependencies['@sidurijs/core'] = `file:${repoPath}/packages/core`;
97
+ for (const m of manifests) {
98
+ const organRel = ['@sidurijs/archive', '@sidurijs/knowledge', '@sidurijs/self'].includes(m.name)
99
+ ? `packages/${m.name.slice('@sidurijs/'.length)}`
100
+ : `packages/organs/${m.name.slice('@sidurijs/'.length)}`;
101
+ dependencies[m.name] = `file:${repoPath}/${organRel}`;
102
+ }
103
+ }
104
+ else {
105
+ dependencies['@sidurijs/core'] = coreVersion;
106
+ for (const m of manifests) {
107
+ dependencies[m.name] = `^${m.version || '1.0.0'}`;
108
+ }
109
+ }
110
+ const scripts = {
111
+ start: 'node src/index.js',
112
+ dev: 'node --watch src/index.js',
113
+ reset: 'siduri reset',
114
+ doctor: 'siduri doctor',
115
+ db: 'siduri db',
116
+ };
117
+ const devDependencies = {
118
+ '@vxnus/siduri': cliVersion,
119
+ };
120
+ const packageJsonObj = {
121
+ name: instanceName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'my-siduri',
122
+ private: true,
123
+ type: 'module',
124
+ engines: {
125
+ node: '>=22.16.0',
126
+ },
127
+ scripts,
128
+ dependencies,
129
+ devDependencies,
130
+ };
131
+ const packageJson = JSON.stringify(packageJsonObj, null, 2) + '\n';
132
+ // 3. siduri.config.json
133
+ const organsConfig = {};
134
+ for (const m of manifests) {
135
+ const customConfig = options.organConfigs?.[m.configKey] || options.organConfigs?.[m.organType];
136
+ organsConfig[m.configKey] = customConfig || getDefaultConfigForManifest(m);
137
+ }
138
+ const configObj = {
139
+ $schema: './siduri.schema.json',
140
+ id: instanceId,
141
+ name: instanceName,
142
+ organs: organsConfig,
143
+ };
144
+ const siduriConfigJson = JSON.stringify(configObj, null, 2) + '\n';
145
+ // 4. siduri.schema.json
146
+ const organPropertiesSchema = {};
147
+ for (const m of manifests) {
148
+ organPropertiesSchema[m.configKey] = m.configSchema || { type: 'object' };
149
+ }
150
+ const schemaObj = {
151
+ $schema: 'http://json-schema.org/draft-07/schema#',
152
+ title: `Siduri Configuration Schema (${instanceName})`,
153
+ type: 'object',
154
+ required: ['id', 'name', 'organs'],
155
+ additionalProperties: false,
156
+ properties: {
157
+ $schema: { type: 'string' },
158
+ id: { type: 'string', description: 'Unique companion isolation ID' },
159
+ name: { type: 'string', description: 'Display name of the companion' },
160
+ organs: {
161
+ type: 'object',
162
+ additionalProperties: false,
163
+ properties: organPropertiesSchema,
164
+ },
165
+ },
166
+ };
167
+ const siduriSchemaJson = JSON.stringify(schemaObj, null, 2) + '\n';
168
+ // 5. .env.example
169
+ const envLines = [];
170
+ for (const m of manifests) {
171
+ if (m.environment && m.environment.length > 0) {
172
+ envLines.push(`# ${m.displayName || m.name}`);
173
+ for (const envVar of m.environment) {
174
+ if (envVar.description) {
175
+ envLines.push(`# ${envVar.description}${envVar.required ? ' (required)' : ' (optional)'}`);
176
+ }
177
+ const defaultVal = envVar.default || '';
178
+ envLines.push(`${envVar.name}=${defaultVal}`);
179
+ }
180
+ envLines.push('');
181
+ }
182
+ }
183
+ const envExample = envLines.length > 0 ? envLines.join('\n') : '# No external environment variables required\n';
184
+ // 6. src/index.js
185
+ const importLines = [
186
+ `import { createServer } from 'node:http';`,
187
+ `import { readFile, stat, readdir } from 'node:fs/promises';`,
188
+ `import path from 'node:path';`,
189
+ `import { fileURLToPath } from 'node:url';`,
190
+ `import { SiduriRuntime, dispatchCompanionChat, validateCompanionConfig } from '@sidurijs/core';`,
191
+ ];
192
+ for (const m of manifests) {
193
+ if (m.name === '@sidurijs/self') {
194
+ importLines.push(`import { ActiveSelfCompiler, SqliteSelfRepository, SelfPackageParser, scanDirective, compilePersonaDocument } from '@sidurijs/self';`);
195
+ }
196
+ else {
197
+ importLines.push(`import { ${m.factory} } from '${m.name}';`);
198
+ }
199
+ }
200
+ const instantiationLines = [];
201
+ const organMapEntries = [];
202
+ for (const m of manifests) {
203
+ if (m.name === '@sidurijs/self') {
204
+ instantiationLines.push(`const self = new SqliteSelfRepository({ dbPath: path.resolve(rootDir, 'siduri.sqlite') });`);
205
+ instantiationLines.push(`const behavior = new ActiveSelfCompiler(config.organs.behavior);`);
206
+ // NOTE: .self file is NOT auto-installed on startup.
207
+ // It is detected by GET /teach/detected-self and surfaced in the chat UI
208
+ // for user review and approval via the inline persona proposal card.
209
+ organMapEntries.push(` behavior,`);
210
+ organMapEntries.push(` self,`);
211
+ }
212
+ else if (m.name === '@sidurijs/archive') {
213
+ instantiationLines.push(`const archive = new ${m.factory}({ ...config.organs.${m.configKey}, dbPath: path.resolve(rootDir, config.organs.${m.configKey}?.dbPath || 'siduri.sqlite') });`);
214
+ organMapEntries.push(` ${m.configKey},`);
215
+ }
216
+ else if (m.name === '@sidurijs/knowledge') {
217
+ instantiationLines.push(`const knowledge = new ${m.factory}({ ...config.organs.${m.configKey}, dbPath: path.resolve(rootDir, config.organs.${m.configKey}?.dbPath || 'siduri.sqlite') });`);
218
+ organMapEntries.push(` ${m.configKey},`);
219
+ }
220
+ else if (m.name === '@sidurijs/observation') {
221
+ const visionArg = hasVision ? 'vision' : '{ analyze: async () => JSON.stringify({ readings: [] }) }';
222
+ instantiationLines.push(`const observation = new ${m.factory}(${visionArg});`);
223
+ organMapEntries.push(` ${m.configKey},`);
224
+ }
225
+ else {
226
+ const varName = m.configKey;
227
+ instantiationLines.push(`const ${varName} = new ${m.factory}(config.organs.${m.configKey});`);
228
+ organMapEntries.push(` ${varName},`);
229
+ }
230
+ }
231
+ if (hasMouth && hasVoice) {
232
+ instantiationLines.push(`if (typeof mouth?.setVoiceOrgan === 'function') mouth.setVoiceOrgan(voice);`);
233
+ }
234
+ const selectedDisplayNames = manifests.map((m) => m.displayName.split(' ')[0] || m.organType).join(', ');
235
+ const srcIndexJs = [
236
+ ...importLines,
237
+ '',
238
+ `const __filename = fileURLToPath(import.meta.url);`,
239
+ `const __dirname = path.dirname(__filename);`,
240
+ `const rootDir = path.resolve(__dirname, '..');`,
241
+ '',
242
+ `// Load local .env into process.env, giving project-level .env precedence over ambient shell variables`,
243
+ `try {`,
244
+ ` const envPath = path.join(rootDir, '.env');`,
245
+ ` const envContent = await readFile(envPath, 'utf8');`,
246
+ ` for (const line of envContent.split('\\n')) {`,
247
+ ` const trimmed = line.trim();`,
248
+ ` if (!trimmed || trimmed.startsWith('#')) continue;`,
249
+ ` const eqIdx = trimmed.indexOf('=');`,
250
+ ` if (eqIdx !== -1) {`,
251
+ ` const key = trimmed.slice(0, eqIdx).trim();`,
252
+ ` let val = trimmed.slice(eqIdx + 1).trim();`,
253
+ ` if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {`,
254
+ ` val = val.slice(1, -1);`,
255
+ ` }`,
256
+ ` if (val) {`,
257
+ ` process.env[key] = val;`,
258
+ ` }`,
259
+ ` }`,
260
+ ` }`,
261
+ `} catch (err) {`,
262
+ ` if (err?.code !== 'ENOENT') {`,
263
+ ` console.error('Failed to load .env file:', err.message);`,
264
+ ` }`,
265
+ `}`,
266
+ '',
267
+ `const config = JSON.parse(`,
268
+ ` await readFile(path.join(rootDir, 'siduri.config.json'), 'utf8')`,
269
+ `);`,
270
+ `const schemaPath = path.join(rootDir, 'siduri.schema.json');`,
271
+ `try {`,
272
+ ` const schema = JSON.parse(await readFile(schemaPath, 'utf8'));`,
273
+ ` validateCompanionConfig(config, schema);`,
274
+ `} catch (err) {`,
275
+ ` if (err?.code !== 'ENOENT') {`,
276
+ ` console.error('Config validation failed:', err.message);`,
277
+ ` throw err;`,
278
+ ` }`,
279
+ `}`,
280
+ '',
281
+ ...instantiationLines,
282
+ '',
283
+ `const runtime = new SiduriRuntime(config.id, config, {`,
284
+ ...organMapEntries,
285
+ `});`,
286
+ '',
287
+ `await runtime.initialize();`,
288
+ '',
289
+ `const audioCache = new Map();`,
290
+ '',
291
+ `const server = createServer(async (req, res) => {`,
292
+ ` const parsedUrl = new URL(req.url, \`http://\${req.headers.host || 'localhost'}\`);`,
293
+ ` const pathname = parsedUrl.pathname;`,
294
+ '',
295
+ ` // API: Status & Health Probes (minimal safe operational state, no secrets/credentials/config.organs)`,
296
+ ` if ((pathname === '/health' || pathname === '/api/status') && req.method === 'GET') {`,
297
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
298
+ ` res.end(JSON.stringify({`,
299
+ ` status: 'ok',`,
300
+ ` uptime: process.uptime(),`,
301
+ ` }));`,
302
+ ` return;`,
303
+ ` }`,
304
+ '',
305
+ ` // API: Ready probe`,
306
+ ` if (pathname === '/ready' && req.method === 'GET') {`,
307
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
308
+ ` res.end(JSON.stringify({ status: 'ready', companionId: config.id }));`,
309
+ ` return;`,
310
+ ` }`,
311
+ '',
312
+ ` // API: Version info`,
313
+ ` if (pathname === '/version' && req.method === 'GET') {`,
314
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
315
+ ` res.end(JSON.stringify({ version: '2.0.3', name: config.name, id: config.id }));`,
316
+ ` return;`,
317
+ ` }`,
318
+ '',
319
+ ` // API: Identity info`,
320
+ ` if ((pathname === '/me' || pathname === '/teach/identity') && req.method === 'GET') {`,
321
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
322
+ ` let identity = null;`,
323
+ ` try {`,
324
+ ` identity = typeof self?.getIdentity === 'function' ? await self.getIdentity(config.id) : null;`,
325
+ ` } catch {}`,
326
+ ` res.end(JSON.stringify({`,
327
+ ` id: config.id,`,
328
+ ` companionId: config.id,`,
329
+ ` name: identity?.name || undefined,`,
330
+ ` archetype: identity?.archetype || identity?.role,`,
331
+ ` origin: identity?.origin,`,
332
+ ` ethos: identity?.ethos,`,
333
+ ` version: identity?.version || '1.0.0',`,
334
+ ` organs: Object.keys(config.organs || {}),`,
335
+ ` }));`,
336
+ ` return;`,
337
+ ` }`,
338
+ '',
339
+ ` // API: Teach Mode detected-self`,
340
+ ` if (pathname === '/teach/detected-self' && req.method === 'GET') {`,
341
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
342
+ ` if (typeof SelfPackageParser === 'undefined') {`,
343
+ ` res.end(JSON.stringify({ detected: false }));`,
344
+ ` return;`,
345
+ ` }`,
346
+ ` try {`,
347
+ ` const companionId = parsedUrl.searchParams.get('companionId') || config.id || 'default';`,
348
+ ` const explicitPath = parsedUrl.searchParams.get('path');`,
349
+ ` const envPath = process.env.SIDURI_SELF_PATH || process.env.SELF_PATH;`,
350
+ ` const configPath = config.organs?.behavior?.selfPath;`,
351
+ ` const candidates = [];`,
352
+ ` if (explicitPath) candidates.push(path.resolve(rootDir, explicitPath));`,
353
+ ` if (envPath) candidates.push(path.resolve(rootDir, envPath));`,
354
+ ` if (configPath) candidates.push(path.resolve(rootDir, configPath));`,
355
+ ` candidates.push(path.resolve(rootDir, 'assets', 'self', companionId + '.self'));`,
356
+ ` candidates.push(path.resolve(rootDir, 'assets', 'self', 'default.self'));`,
357
+ ` candidates.push(path.resolve(rootDir, companionId + '.self'));`,
358
+ '',
359
+ ` const searchDirs = [`,
360
+ ` path.resolve(rootDir, 'assets', 'self'),`,
361
+ ` path.resolve(rootDir, 'assets'),`,
362
+ ` path.resolve(rootDir),`,
363
+ ` ];`,
364
+ '',
365
+ ` let matchedFilePath = null;`,
366
+ ` for (const candidate of candidates) {`,
367
+ ` try {`,
368
+ ` const s = await stat(candidate);`,
369
+ ` if (s.isFile()) {`,
370
+ ` matchedFilePath = candidate;`,
371
+ ` break;`,
372
+ ` }`,
373
+ ` } catch {}`,
374
+ ` }`,
375
+ '',
376
+ ` if (!matchedFilePath) {`,
377
+ ` for (const dir of searchDirs) {`,
378
+ ` try {`,
379
+ ` const entries = await readdir(dir);`,
380
+ ` const selfFiles = entries.filter((f) => f.endsWith('.self'));`,
381
+ ` if (selfFiles.length > 0) {`,
382
+ ` const companionSelf = selfFiles.find((f) => f === companionId + '.self');`,
383
+ ` matchedFilePath = path.join(dir, companionSelf || selfFiles[0]);`,
384
+ ` break;`,
385
+ ` }`,
386
+ ` } catch {}`,
387
+ ` }`,
388
+ ` }`,
389
+ '',
390
+ ` if (!matchedFilePath) {`,
391
+ ` res.end(JSON.stringify({ detected: false }));`,
392
+ ` return;`,
393
+ ` }`,
394
+ '',
395
+ ` const content = await readFile(matchedFilePath, 'utf8');`,
396
+ ` const parsed = typeof compilePersonaDocument === 'function'`,
397
+ ` ? await compilePersonaDocument(content, { brain: runtime?.brain, companionId })`,
398
+ ` : SelfPackageParser.parse(content);`,
399
+ ` let alreadyInstalled = false;`,
400
+ ` if (typeof self?.getIdentity === 'function') {`,
401
+ ` try {`,
402
+ ` const existingIdentity = await self.getIdentity(companionId);`,
403
+ ` const existingDirectives = await self.getActiveDirectives(companionId);`,
404
+ ` if (`,
405
+ ` existingIdentity &&`,
406
+ ` parsed.manifest?.identity?.name &&`,
407
+ ` existingIdentity.name.toLowerCase() === parsed.manifest.identity.name.toLowerCase() &&`,
408
+ ` existingDirectives.length > 0`,
409
+ ` ) {`,
410
+ ` alreadyInstalled = true;`,
411
+ ` }`,
412
+ ` } catch {}`,
413
+ ` }`,
414
+ '',
415
+ ` res.end(JSON.stringify({`,
416
+ ` detected: true,`,
417
+ ` filename: path.basename(matchedFilePath),`,
418
+ ` path: path.relative(rootDir, matchedFilePath),`,
419
+ ` content,`,
420
+ ` parsed,`,
421
+ ` alreadyInstalled,`,
422
+ ` }));`,
423
+ ` } catch (err) {`,
424
+ ` res.end(JSON.stringify({ detected: false, error: err.message }));`,
425
+ ` }`,
426
+ ` return;`,
427
+ ` }`,
428
+ '',
429
+ ` // API: Teach Mode upload-self`,
430
+ ` if (pathname === '/teach/upload-self' && req.method === 'POST') {`,
431
+ ` let body = '';`,
432
+ ` req.on('data', (chunk) => { body += chunk; });`,
433
+ ` req.on('end', async () => {`,
434
+ ` if (typeof compilePersonaDocument === 'undefined' && typeof SelfPackageParser === 'undefined') {`,
435
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
436
+ ` res.end(JSON.stringify({ error: 'Self organ is not configured' }));`,
437
+ ` return;`,
438
+ ` }`,
439
+ ` try {`,
440
+ ` const { content, companionId } = JSON.parse(body || '{}');`,
441
+ ` if (!content) {`,
442
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
443
+ ` res.end(JSON.stringify({ error: 'Missing content' }));`,
444
+ ` return;`,
445
+ ` }`,
446
+ ` const targetId = companionId || config.id || 'default';`,
447
+ ` const parsed = typeof compilePersonaDocument === 'function'`,
448
+ ` ? await compilePersonaDocument(content, { brain: runtime?.brain, companionId: targetId })`,
449
+ ` : SelfPackageParser.parse(content);`,
450
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
451
+ ` res.end(JSON.stringify(parsed));`,
452
+ ` } catch (err) {`,
453
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
454
+ ` res.end(JSON.stringify({ error: err.message }));`,
455
+ ` }`,
456
+ ` });`,
457
+ ` return;`,
458
+ ` }`,
459
+ '',
460
+ ` // API: Teach Mode install-self`,
461
+ ` if (pathname === '/teach/install-self' && req.method === 'POST') {`,
462
+ ` let body = '';`,
463
+ ` req.on('data', (chunk) => { body += chunk; });`,
464
+ ` req.on('end', async () => {`,
465
+ ` if (typeof self === 'undefined' || typeof self?.setIdentity !== 'function') {`,
466
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
467
+ ` res.end(JSON.stringify({ error: 'Self organ is not configured' }));`,
468
+ ` return;`,
469
+ ` }`,
470
+ ` try {`,
471
+ ` const { companionId, manifest, approvedDirectiveIds } = JSON.parse(body || '{}');`,
472
+ ` const cid = companionId || config.id || 'default';`,
473
+ ` if (!manifest || !Array.isArray(approvedDirectiveIds)) {`,
474
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
475
+ ` res.end(JSON.stringify({ error: 'Missing required fields' }));`,
476
+ ` return;`,
477
+ ` }`,
478
+ ` if (!manifest.identity || !manifest.identity.name) {`,
479
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
480
+ ` res.end(JSON.stringify({ error: 'Invalid manifest: missing identity.name' }));`,
481
+ ` return;`,
482
+ ` }`,
483
+ ` const directivesToCommit = manifest.directives?.filter((d) => approvedDirectiveIds.includes(d.id)) || [];`,
484
+ ` for (const d of directivesToCommit) {`,
485
+ ` if (!d || typeof d.directive !== 'string') {`,
486
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
487
+ ` res.end(JSON.stringify({ error: 'Invalid directive entry: missing directive string' }));`,
488
+ ` return;`,
489
+ ` }`,
490
+ ` const scan = typeof scanDirective === 'function' ? scanDirective(d.directive) : { safe: true };`,
491
+ ` if (!scan.safe) {`,
492
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
493
+ ` res.end(JSON.stringify({ error: 'Safety check failed for directive: ' + scan.reason, directiveId: d.id, reason: scan.reason }));`,
494
+ ` return;`,
495
+ ` }`,
496
+ ` }`,
497
+ '',
498
+ ` await self.setIdentity({`,
499
+ ` companionId: cid,`,
500
+ ` name: manifest.identity.name,`,
501
+ ` archetype: manifest.identity.archetype,`,
502
+ ` origin: manifest.identity.origin,`,
503
+ ` ethos: manifest.identity.ethos,`,
504
+ ` version: manifest.version || '1.0.0',`,
505
+ ` updatedAt: new Date().toISOString(),`,
506
+ ` });`,
507
+ ` if (directivesToCommit.length > 0 && typeof self.commitDirectives === 'function') {`,
508
+ ` await self.commitDirectives(cid, directivesToCommit.map((d) => ({`,
509
+ ` id: d.id,`,
510
+ ` companionId: cid,`,
511
+ ` directive: d.directive,`,
512
+ ` category: d.category || 'behavioral',`,
513
+ ` status: 'active',`,
514
+ ` priority: d.priority || 50,`,
515
+ ` createdAt: new Date().toISOString(),`,
516
+ ` })));`,
517
+ ` }`,
518
+ '',
519
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
520
+ ` res.end(JSON.stringify({ success: true, companionId: cid, installedDirectives: directivesToCommit.length }));`,
521
+ ` } catch (err) {`,
522
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
523
+ ` res.end(JSON.stringify({ error: err.message }));`,
524
+ ` }`,
525
+ ` });`,
526
+ ` return;`,
527
+ ` }`,
528
+ '',
529
+ ` // API: Voice & Observation health probes`,
530
+ ` if (pathname === '/voice/health' && req.method === 'GET') {`,
531
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
532
+ ` res.end(JSON.stringify({ provider: config.organs?.voice?.provider || 'none', configured: Boolean(runtime.voice) }));`,
533
+ ` return;`,
534
+ ` }`,
535
+ '',
536
+ ` if (pathname === '/obs/health' && req.method === 'GET') {`,
537
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
538
+ ` res.end(JSON.stringify({ provider: config.organs?.vision?.provider || 'none', configured: Boolean(runtime.vision) }));`,
539
+ ` return;`,
540
+ ` }`,
541
+ '',
542
+ ` // API: Evidence & observations`,
543
+ ` if ((pathname === '/evidence' || pathname === '/observations') && req.method === 'GET') {`,
544
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
545
+ ` res.end(JSON.stringify({ items: [] }));`,
546
+ ` return;`,
547
+ ` }`,
548
+ '',
549
+ ` // API: Audio buffer retrieval for voice playback`,
550
+ ` if (pathname.startsWith('/api/audio/') && req.method === 'GET') {`,
551
+ ` const audioId = pathname.slice('/api/audio/'.length);`,
552
+ ` const buffer = audioCache.get(audioId);`,
553
+ ` if (buffer) {`,
554
+ ` res.writeHead(200, { 'Content-Type': 'audio/wav' });`,
555
+ ` res.end(Buffer.from(buffer));`,
556
+ ` return;`,
557
+ ` }`,
558
+ ` res.writeHead(404, { 'Content-Type': 'application/json' });`,
559
+ ` res.end(JSON.stringify({ error: 'Audio not found' }));`,
560
+ ` return;`,
561
+ ` }`,
562
+ '',
563
+ ` // API: Memory Claims & Items`,
564
+ ` if ((pathname === '/memory' || pathname === '/memory/claims' || pathname === '/api/memory/claims') && req.method === 'GET') {`,
565
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
566
+ ` try {`,
567
+ ` const claims = typeof memory?.getAllClaims === 'function' ? await memory.getAllClaims() : (typeof memory?.getClaims === 'function' ? await memory.getClaims() : []);`,
568
+ ` res.end(JSON.stringify({ claims, items: claims }));`,
569
+ ` } catch (e) {`,
570
+ ` res.end(JSON.stringify({ claims: [], items: [] }));`,
571
+ ` }`,
572
+ ` return;`,
573
+ ` }`,
574
+ '',
575
+ ` // API: Memory Proposals`,
576
+ ` if (pathname === '/memory/proposals' && req.method === 'GET') {`,
577
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
578
+ ` try {`,
579
+ ` const proposals = typeof memory?.getPendingClaims === 'function' ? await memory.getPendingClaims() : [];`,
580
+ ` res.end(JSON.stringify({ proposals }));`,
581
+ ` } catch (e) {`,
582
+ ` res.end(JSON.stringify({ proposals: [] }));`,
583
+ ` }`,
584
+ ` return;`,
585
+ ` }`,
586
+ '',
587
+ ` // API: Memory Directives`,
588
+ ` if ((pathname === '/memory/behavioral' || pathname === '/api/memory/directives') && req.method === 'GET') {`,
589
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
590
+ ` try {`,
591
+ ` const directives = typeof memory?.getDirectives === 'function' ? await memory.getDirectives() : [`,
592
+ ` { domain: 'personality', name: 'Active Self Tone', content: 'Warm, empathetic, and thoughtful conversational style.' },`,
593
+ ` { domain: 'cognition', name: 'Authoritative Memory', content: 'Ground responses in verified claims and personal history.' }`,
594
+ ` ];`,
595
+ ` res.end(JSON.stringify({ directives }));`,
596
+ ` } catch (e) {`,
597
+ ` res.end(JSON.stringify({ directives: [] }));`,
598
+ ` }`,
599
+ ` return;`,
600
+ ` }`,
601
+ '',
602
+ ` // API: Memory Proposal & Behavioral Approval / Rejection`,
603
+ ` if ((pathname === '/memory/proposals/approve' || pathname === '/memory/proposals/reject' || pathname === '/memory/behavioral/approve' || pathname === '/memory/behavioral/reject') && req.method === 'POST') {`,
604
+ ` let body = '';`,
605
+ ` req.on('data', (chunk) => { body += chunk; });`,
606
+ ` req.on('end', async () => {`,
607
+ ` try {`,
608
+ ` const payload = JSON.parse(body || '{}');`,
609
+ ` const claimId = payload.id || payload.claimId || payload.directiveId;`,
610
+ ` if (!claimId) {`,
611
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
612
+ ` res.end(JSON.stringify({ error: 'Missing required id' }));`,
613
+ ` return;`,
614
+ ` }`,
615
+ ` if (pathname.endsWith('approve')) {`,
616
+ ` let name = null;`,
617
+ ` if (pathname.includes('behavioral')) {`,
618
+ ` if (typeof runtime?.approveDirective === 'function') {`,
619
+ ` const bRes = await runtime.approveDirective(claimId, { companionId: config.id });`,
620
+ ` name = bRes?.name;`,
621
+ ` } else if (typeof self?.approveDirective === 'function') {`,
622
+ ` await self.approveDirective(claimId, config.id);`,
623
+ ` }`,
624
+ ` } else if (typeof runtime?.approveProposal === 'function') {`,
625
+ ` const pRes = await runtime.approveProposal(claimId, { companionId: config.id });`,
626
+ ` name = pRes?.name;`,
627
+ ` } else if (typeof memory?.approveClaim === 'function') {`,
628
+ ` await memory.approveClaim(claimId);`,
629
+ ` }`,
630
+ ` if (!name && typeof self?.getIdentity === 'function') {`,
631
+ ` try {`,
632
+ ` const ident = await self.getIdentity(config.id);`,
633
+ ` name = ident?.name;`,
634
+ ` } catch {}`,
635
+ ` }`,
636
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
637
+ ` res.end(JSON.stringify({ approved: true, id: claimId, status: 'approved', name: name || undefined }));`,
638
+ ` } else {`,
639
+ ` if (pathname.includes('behavioral')) {`,
640
+ ` if (typeof runtime?.rejectDirective === 'function') {`,
641
+ ` await runtime.rejectDirective(claimId, { companionId: config.id });`,
642
+ ` } else if (typeof self?.rejectDirective === 'function') {`,
643
+ ` await self.rejectDirective(claimId, config.id);`,
644
+ ` }`,
645
+ ` } else if (typeof memory?.rejectClaim === 'function') {`,
646
+ ` await memory.rejectClaim(claimId);`,
647
+ ` }`,
648
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
649
+ ` res.end(JSON.stringify({ rejected: true, id: claimId, status: 'rejected' }));`,
650
+ ` }`,
651
+ ` } catch (err) {`,
652
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
653
+ ` res.end(JSON.stringify({ error: err.message }));`,
654
+ ` }`,
655
+ ` });`,
656
+ ` return;`,
657
+ ` }`,
658
+ ``,
659
+ ` // API: System Logs`,
660
+ ` if ((pathname === '/system/logs' || pathname === '/api/system/logs') && req.method === 'GET') {`,
661
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
662
+ ` try {`,
663
+ ` const level = parsedUrl.searchParams.get('level') || undefined;`,
664
+ ` const subsystem = parsedUrl.searchParams.get('subsystem') || undefined;`,
665
+ ` const q = parsedUrl.searchParams.get('q') || undefined;`,
666
+ ` const limit = parseInt(parsedUrl.searchParams.get('limit') || '100', 10);`,
667
+ ` const offset = parseInt(parsedUrl.searchParams.get('offset') || '0', 10);`,
668
+ ` const logs = typeof runtime?.queryLogs === 'function'`,
669
+ ` ? runtime.queryLogs({ companionId: config.id, level, subsystem, q, limit, offset })`,
670
+ ` : (typeof self?.db?.queryLogs === 'function' ? self.db.queryLogs({ companionId: config.id, level, subsystem, q, limit, offset }) : []);`,
671
+ ` res.end(JSON.stringify({ logs }));`,
672
+ ` } catch (e) {`,
673
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
674
+ ` res.end(JSON.stringify({ error: e.message, logs: [] }));`,
675
+ ` }`,
676
+ ` return;`,
677
+ ` }`,
678
+ ``,
679
+ ` if ((pathname === '/system/logs/clear' || pathname === '/api/system/logs/clear' || (pathname === '/system/logs' && req.method === 'DELETE')) && (req.method === 'POST' || req.method === 'DELETE')) {`,
680
+ ` try {`,
681
+ ` if (typeof runtime?.clearLogs === 'function') {`,
682
+ ` runtime.clearLogs(config.id);`,
683
+ ` } else if (typeof self?.db?.clearLogs === 'function') {`,
684
+ ` self.db.clearLogs(config.id);`,
685
+ ` }`,
686
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
687
+ ` res.end(JSON.stringify({ cleared: true }));`,
688
+ ` } catch (e) {`,
689
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
690
+ ` res.end(JSON.stringify({ error: e.message }));`,
691
+ ` }`,
692
+ ` return;`,
693
+ ` }`,
694
+ '',
695
+ ` // API: Memory & State Reset (Return to blank slate)`,
696
+ ` if ((pathname === '/api/reset' || pathname === '/memory/reset' || pathname === '/dev/memory/reset') && req.method === 'POST') {`,
697
+ ` try {`,
698
+ ` if (typeof memory?.resetMemory === 'function') {`,
699
+ ` await memory.resetMemory(config.id);`,
700
+ ` }`,
701
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
702
+ ` res.end(JSON.stringify({ reset: true, companionId: config.id, status: 'blank_slate' }));`,
703
+ ` } catch (err) {`,
704
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
705
+ ` res.end(JSON.stringify({ error: err.message }));`,
706
+ ` }`,
707
+ ` return;`,
708
+ ` }`,
709
+ '',
710
+ ` // API: Evidence packs`,
711
+ ` if (pathname === '/evidence/packs' && req.method === 'GET') {`,
712
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
713
+ ` res.end(JSON.stringify({ packs: [] }));`,
714
+ ` return;`,
715
+ ` }`,
716
+ '',
717
+ ` // API: Chat interaction (routed through canonical dispatchCompanionChat from @sidurijs/core)`,
718
+ ` if ((pathname === '/chat' || pathname === '/api/chat') && req.method === 'POST') {`,
719
+ ` let body = '';`,
720
+ ` req.on('data', (chunk) => { body += chunk; });`,
721
+ ` req.on('end', async () => {`,
722
+ ` try {`,
723
+ ` const payload = JSON.parse(body || '{}');`,
724
+ ` const chatContext = {`,
725
+ ` companionId: config.id,`,
726
+ ` actor: {`,
727
+ ` actorId: 'owner-user',`,
728
+ ` sessionId: \`sess-\${config.id}\`,`,
729
+ ` authorizationRole: 'administrator',`,
730
+ ` capabilities: ['chat', 'memory:approve', 'action:execute'],`,
731
+ ` authenticated: true,`,
732
+ ` },`,
733
+ ` conversation: {`,
734
+ ` channel: 'direct',`,
735
+ ` correlationId: \`corr-\${Date.now()}\`,`,
736
+ ` },`,
737
+ ` source: 'local',`,
738
+ ` ...(payload.context || {}),`,
739
+ ` ...(payload.mode ? { mode: payload.mode } : {}),`,
740
+ ` };`,
741
+ ` const response = await dispatchCompanionChat(runtime, {`,
742
+ ` id: config.id,`,
743
+ ` companionId: config.id,`,
744
+ ` message: payload.message || payload.text || '',`,
745
+ ` context: chatContext,`,
746
+ ` history: Array.isArray(payload.history) ? payload.history : [],`,
747
+ ` subtitleLanguage: payload.subtitleLanguage || payload.subtitle_language,`,
748
+ ` });`,
749
+ '',
750
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
751
+ ` res.end(JSON.stringify(response));`,
752
+ ` } catch (err) {`,
753
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
754
+ ` res.end(JSON.stringify({ error: err.message }));`,
755
+ ` }`,
756
+ ` });`,
757
+ ` return;`,
758
+ ` }`,
759
+ '',
760
+ ` // API: Real-time SSE streaming (Mouth transport)`,
761
+ ` if ((pathname === '/chat/stream' || pathname === '/api/chat/stream') && req.method === 'POST') {`,
762
+ ` let body = '';`,
763
+ ` req.on('data', (chunk) => { body += chunk; });`,
764
+ ` req.on('end', async () => {`,
765
+ ` res.writeHead(200, {`,
766
+ ` 'Content-Type': 'text/event-stream',`,
767
+ ` 'Cache-Control': 'no-cache, no-transform',`,
768
+ ` 'Connection': 'keep-alive',`,
769
+ ` });`,
770
+ ` const abortController = new AbortController();`,
771
+ ` const onClose = () => {`,
772
+ ` if (!res.writableEnded) {`,
773
+ ` abortController.abort('client_disconnect');`,
774
+ ` if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {`,
775
+ ` runtime.mouth.interrupt('client_disconnect');`,
776
+ ` }`,
777
+ ` }`,
778
+ ` };`,
779
+ ` res.on('close', onClose);`,
780
+ '',
781
+ ` try {`,
782
+ ` const payload = JSON.parse(body || '{}');`,
783
+ ` const chatContext = {`,
784
+ ` companionId: config.id,`,
785
+ ` actor: {`,
786
+ ` actorId: 'owner-user',`,
787
+ ` sessionId: \`sess-\${config.id}\`,`,
788
+ ` authorizationRole: 'administrator',`,
789
+ ` capabilities: ['chat', 'memory:approve', 'action:execute'],`,
790
+ ` authenticated: true,`,
791
+ ` },`,
792
+ ` conversation: {`,
793
+ ` channel: 'direct',`,
794
+ ` correlationId: \`corr-\${Date.now()}\`,`,
795
+ ` },`,
796
+ ` source: 'local',`,
797
+ ` ...(payload.context || {}),`,
798
+ ` ...(payload.mode ? { mode: payload.mode } : {}),`,
799
+ ` };`,
800
+ ` const response = await dispatchCompanionChat(runtime, {`,
801
+ ` id: config.id,`,
802
+ ` companionId: config.id,`,
803
+ ` message: payload.message || payload.text || '',`,
804
+ ` context: chatContext,`,
805
+ ` history: Array.isArray(payload.history) ? payload.history : [],`,
806
+ ` medium: 'web',`,
807
+ ` signal: abortController.signal,`,
808
+ ` subtitleLanguage: payload.subtitleLanguage || payload.subtitle_language,`,
809
+ ` });`,
810
+ '',
811
+ ` res.write(\`event: staged\\ndata: \${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\\n\\n\`);`,
812
+ '',
813
+ ` const avatarEvent = response.metadata?.events?.find(`,
814
+ ` (e) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval?.toLowerCase() === 'approved' || !e.approval)`,
815
+ ` );`,
816
+ ` if (avatarEvent) {`,
817
+ ` res.write(\`event: avatar\\ndata: \${JSON.stringify(avatarEvent)}\\n\\n\`);`,
818
+ ` }`,
819
+ '',
820
+ ` const speechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';`,
821
+ ` const utterance = {`,
822
+ ` utteranceId: response.response_id || 'utt-stream',`,
823
+ ` companionId: config.id,`,
824
+ ` responseId: response.response_id,`,
825
+ ` correlationId: response.correlation_id,`,
826
+ ` text: speechText,`,
827
+ ` medium: 'web',`,
828
+ ` expression: avatarEvent?.expression,`,
829
+ ` action: avatarEvent?.action,`,
830
+ ` signal: abortController.signal,`,
831
+ ` };`,
832
+ '',
833
+ ` if (runtime.mouth && typeof runtime.mouth.stream === 'function') {`,
834
+ ` for await (const chunk of runtime.mouth.stream(utterance)) {`,
835
+ ` if (abortController.signal.aborted) {`,
836
+ ` res.write(\`event: chunk\\ndata: \${JSON.stringify({ ...chunk, interrupted: true })}\\n\\n\`);`,
837
+ ` break;`,
838
+ ` }`,
839
+ ` res.write(\`event: chunk\\ndata: \${JSON.stringify(chunk)}\\n\\n\`);`,
840
+ ` }`,
841
+ ` } else {`,
842
+ ` res.write(\`event: chunk\\ndata: \${JSON.stringify({ utteranceId: utterance.utteranceId, index: 1, deltaText: speechText, isComplete: true, medium: 'web' })}\\n\\n\`);`,
843
+ ` }`,
844
+ '',
845
+ ` res.write(\`event: done\\ndata: \${JSON.stringify(response)}\\n\\n\`);`,
846
+ ` res.end();`,
847
+ ` } catch (err) {`,
848
+ ` if (abortController.signal.aborted) {`,
849
+ ` res.write(\`event: interrupted\\ndata: \${JSON.stringify({ reason: abortController.signal.reason })}\\n\\n\`);`,
850
+ ` } else {`,
851
+ ` res.write(\`event: error\\ndata: \${JSON.stringify({ error: err.message })}\\n\\n\`);`,
852
+ ` }`,
853
+ ` res.end();`,
854
+ ` } finally {`,
855
+ ` res.removeListener('close', onClose);`,
856
+ ` }`,
857
+ ` });`,
858
+ ` return;`,
859
+ ` }`,
860
+ '',
861
+ ` // API: Barge-in interruption`,
862
+ ` if ((pathname === '/chat/interrupt' || pathname === '/mouth/interrupt' || pathname === '/api/chat/interrupt') && req.method === 'POST') {`,
863
+ ` let body = '';`,
864
+ ` req.on('data', (chunk) => { body += chunk; });`,
865
+ ` req.on('end', () => {`,
866
+ ` try {`,
867
+ ` const payload = JSON.parse(body || '{}');`,
868
+ ` const reason = payload.reason || 'user_barge_in';`,
869
+ ` if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {`,
870
+ ` runtime.mouth.interrupt(reason);`,
871
+ ` }`,
872
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
873
+ ` res.end(JSON.stringify({ success: true, interrupted: true, reason }));`,
874
+ ` } catch (e) {`,
875
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
876
+ ` res.end(JSON.stringify({ error: e.message }));`,
877
+ ` }`,
878
+ ` });`,
879
+ ` return;`,
880
+ ` }`,
881
+ '',
882
+ ` // API: Mouth channels & health`,
883
+ ` if ((pathname === '/mouth/health' || pathname === '/api/mouth/health') && req.method === 'GET') {`,
884
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
885
+ ` res.end(JSON.stringify({ provider: 'siduri-mouth', configured: Boolean(runtime.mouth) }));`,
886
+ ` return;`,
887
+ ` }`,
888
+ '',
889
+ ` if ((pathname === '/mouth/channels' || pathname === '/api/mouth/channels') && req.method === 'GET') {`,
890
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
891
+ ` const channels = typeof runtime.mouth?.getRegisteredChannels === 'function' ? runtime.mouth.getRegisteredChannels() : [];`,
892
+ ` res.end(JSON.stringify({ channels }));`,
893
+ ` return;`,
894
+ ` }`,
895
+ '',
896
+ ` // API: Model Catalog Discovery (Body & Voice)`,
897
+ ` if (pathname === '/api/models/body' && req.method === 'GET') {`,
898
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
899
+ ` try {`,
900
+ ` const bodyDir = path.join(rootDir, 'assets', 'body');`,
901
+ ` const entries = await readdir(bodyDir, { withFileTypes: true }).catch(() => []);`,
902
+ ` const models = entries.filter((e) => e.isDirectory()).map((e) => e.name);`,
903
+ ` res.end(JSON.stringify({ models: models.length ? models : ['default'] }));`,
904
+ ` } catch {`,
905
+ ` res.end(JSON.stringify({ models: ['default'] }));`,
906
+ ` }`,
907
+ ` return;`,
908
+ ` }`,
909
+ '',
910
+ ` if (pathname === '/api/models/voice' && req.method === 'GET') {`,
911
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
912
+ ` try {`,
913
+ ` const voiceDir = path.join(rootDir, 'assets', 'voice');`,
914
+ ` const entries = await readdir(voiceDir, { withFileTypes: true }).catch(() => []);`,
915
+ ` const models = entries.filter((e) => e.isDirectory()).map((e) => e.name);`,
916
+ ` res.end(JSON.stringify({ models: models.length ? models : ['default'] }));`,
917
+ ` } catch {`,
918
+ ` res.end(JSON.stringify({ models: ['default'] }));`,
919
+ ` }`,
920
+ ` return;`,
921
+ ` }`,
922
+ '',
923
+ ` // Static files & Next.js apps/web export routing with strict boundary verification`,
924
+ ` let decodedPathname = pathname;`,
925
+ ` try {`,
926
+ ` while (decodedPathname.includes('%')) {`,
927
+ ` const next = decodeURIComponent(decodedPathname);`,
928
+ ` if (next === decodedPathname) break;`,
929
+ ` decodedPathname = next;`,
930
+ ` }`,
931
+ ` } catch {`,
932
+ ` res.writeHead(400, { 'Content-Type': 'text/plain' });`,
933
+ ` res.end('Bad Request');`,
934
+ ` return;`,
935
+ ` }`,
936
+ ` const normalizedPath = path.normalize(decodedPathname).replace(/^[/\\\\]+/, '');`,
937
+ ` const canonicalPublicRoot = path.resolve(rootDir, 'public');`,
938
+ ` const canonicalAssetsRoot = path.resolve(rootDir, 'assets');`,
939
+ ` const candidatePairs = [`,
940
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath, 'index.html') },`,
941
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath + '.html') },`,
942
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath) },`,
943
+ ` ];`,
944
+ '',
945
+ ` if (!normalizedPath || normalizedPath === '.' || normalizedPath === './') {`,
946
+ ` candidatePairs.unshift({ root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, 'index.html') });`,
947
+ ` }`,
948
+ '',
949
+ ` // Asset serving for companion assets (e.g. /assets/body/<name>/model.model3.json)`,
950
+ ` if (decodedPathname.startsWith('/assets/')) {`,
951
+ ` const relativeAssetPath = path.normalize(decodedPathname.slice('/assets/'.length)).replace(/^[/\\\\]+/, '');`,
952
+ ` candidatePairs.unshift({ root: canonicalAssetsRoot, target: path.resolve(canonicalAssetsRoot, relativeAssetPath) });`,
953
+ ` }`,
954
+ '',
955
+ ` // Compatibility fallback for legacy /live2d/<modelName>/... -> ./assets/body/<modelName>/...`,
956
+ ` if (decodedPathname.startsWith('/live2d/')) {`,
957
+ ` const relativeAssetPath = path.normalize(decodedPathname.slice('/live2d/'.length)).replace(/^[/\\\\]+/, '');`,
958
+ ` candidatePairs.unshift({ root: canonicalAssetsRoot, target: path.resolve(canonicalAssetsRoot, 'body', relativeAssetPath) });`,
959
+ ` candidatePairs.unshift({ root: canonicalAssetsRoot, target: path.resolve(canonicalAssetsRoot, 'body', 'model', relativeAssetPath) });`,
960
+ ` }`,
961
+ '',
962
+ ` for (const { root, target } of candidatePairs) {`,
963
+ ` try {`,
964
+ ` // Strict path containment check: target must reside within configured root directory`,
965
+ ` const relative = path.relative(root, target);`,
966
+ ` if (relative.startsWith('..') || path.isAbsolute(relative)) {`,
967
+ ` continue;`,
968
+ ` }`,
969
+ ` const fileStat = await stat(target);`,
970
+ ` if (fileStat.isFile()) {`,
971
+ ` const ext = path.extname(target).toLowerCase();`,
972
+ ` const mimeTypes = {`,
973
+ ` '.html': 'text/html; charset=utf-8',`,
974
+ ` '.js': 'application/javascript; charset=utf-8',`,
975
+ ` '.css': 'text/css; charset=utf-8',`,
976
+ ` '.json': 'application/json',`,
977
+ ` '.png': 'image/png',`,
978
+ ` '.jpg': 'image/jpeg',`,
979
+ ` '.svg': 'image/svg+xml',`,
980
+ ` '.wav': 'audio/wav',`,
981
+ ` '.ico': 'image/x-icon',`,
982
+ ` '.txt': 'text/plain; charset=utf-8',`,
983
+ ` '.moc3': 'application/octet-stream',`,
984
+ ` };`,
985
+ ` const contentType = mimeTypes[ext] || 'application/octet-stream';`,
986
+ ` const content = await readFile(target);`,
987
+ ` res.writeHead(200, { 'Content-Type': contentType });`,
988
+ ` res.end(content);`,
989
+ ` return;`,
990
+ ` }`,
991
+ ` } catch (e) {}`,
992
+ ` }`,
993
+ '',
994
+ ` res.writeHead(404, { 'Content-Type': 'text/plain' });`,
995
+ ` res.end('Not Found');`,
996
+ `});`,
997
+ '',
998
+ `const PORT = process.env.PORT || 3000;`,
999
+ `server.listen(Number(PORT), '127.0.0.1', () => {`,
1000
+ ` console.log(\`✓ Siduri [\${config.name}] initialized with [${selectedDisplayNames}].\`);`,
1001
+ ` console.log(\`➜ Web Companion & Memory Console running at: http://127.0.0.1:\${PORT}\`);`,
1002
+ ` if (process.env.PORT === '0') {`,
1003
+ ` server.close();`,
1004
+ ` }`,
1005
+ `});`,
1006
+ '',
1007
+ ].join('\n');
1008
+ // 7. README.md
1009
+ const readmeLines = [
1010
+ `# ${instanceName}`,
1011
+ '',
1012
+ `Standalone Siduri AI companion instance generated with explicitly composed organs:`,
1013
+ '',
1014
+ ...manifests.map((m) => `- **${m.displayName}** (\`${m.name}\`)`),
1015
+ '',
1016
+ '## Prerequisites',
1017
+ '',
1018
+ '- **Node.js**: `v22.16.0` or higher',
1019
+ '- **Environment**: Valid `.env` file (configured from `.env.example`)',
1020
+ ];
1021
+ if (hasArchive) {
1022
+ readmeLines.push('- **Database**: Embedded SQLite (`siduri.sqlite`). Automatically initialized with WAL mode and FTS5 full-text indexing with zero external setup.');
1023
+ }
1024
+ if (isVoicevox) {
1025
+ readmeLines.push('- **VOICEVOX**: Note: The Voicevox engine executable will be securely auto-downloaded at runtime by Siduri if no local URL is provided.');
1026
+ }
1027
+ readmeLines.push('', '## Getting Started', '', '### 1. Install Dependencies', '```bash', 'npm install', '```', '', '### 2. Configure Environment', '```bash', 'cp .env.example .env', '```', 'Fill in your LLM API key (e.g. `OPENROUTER_API_KEY`) and any other service credentials in `.env`.');
1028
+ readmeLines.push('', '### 3. Diagnostics & Health Probe', 'Verify all environment variables, schema conformance, services, and organ connections:', '```bash', 'npm run doctor', '```', '', '### 4. Start Companion & Web Console', 'Launch your companion runtime and Web UI / Memory Control Panel:', '```bash', 'npm start', '```', 'Then open `http://localhost:3000` in your browser.');
1029
+ const createAssetsDirs = [];
1030
+ if (hasBody) {
1031
+ createAssetsDirs.push('assets/body/default');
1032
+ readmeLines.push('', '### Body & Avatar Models', 'Place your Live2D Cubism model assets into `./assets/body/default/`:', '- `model.model3.json`', '- `model.moc3`', '- textures directory');
1033
+ }
1034
+ if (hasVoice) {
1035
+ createAssetsDirs.push('assets/voice/default');
1036
+ readmeLines.push('', '### Voice & RVC Models', 'Place your character RVC voice models into `./assets/voice/default/`:', '- `default.pth` (Target voice weights)', '- `default.index` (Feature index file)');
1037
+ }
1038
+ const knowledgeConfig = options.organConfigs?.knowledge || options.organConfigs?.['@sidurijs/knowledge'];
1039
+ if (manifests.some((m) => m.organType === 'knowledge') && knowledgeConfig?.packPath) {
1040
+ const cleanPackDir = String(knowledgeConfig.packPath).replace(/^\.\//, '');
1041
+ createAssetsDirs.push(cleanPackDir);
1042
+ readmeLines.push('', '### Knowledge Pack Assets', `Local knowledge pack files reside in \`./${cleanPackDir}/\`.`);
1043
+ }
1044
+ const behaviorConfig = options.organConfigs?.behavior || options.organConfigs?.['@sidurijs/self'];
1045
+ let selfPersonaFile = null;
1046
+ if (manifests.some((m) => m.organType === 'behavior')) {
1047
+ createAssetsDirs.push('assets/self');
1048
+ if (behaviorConfig?.mode === 'custom' || behaviorConfig?.archetype) {
1049
+ const selfRelPath = 'assets/self/default.self';
1050
+ const selfContent = [
1051
+ `specVersion: "2.0.0"`,
1052
+ `kind: "self"`,
1053
+ `id: "default-self"`,
1054
+ `name: "Default Persona"`,
1055
+ `version: "1.0.0"`,
1056
+ `author:`,
1057
+ ` name: "Operator"`,
1058
+ `license: "MIT"`,
1059
+ ``,
1060
+ `identity:`,
1061
+ ` name: "${(behaviorConfig.name || behaviorConfig.companionName || options.name || '').replace(/"/g, '\\"')}"`,
1062
+ ` archetype: "${(behaviorConfig.archetype || 'Knowledge Assistant & Research Partner').replace(/"/g, '\\"')}"`,
1063
+ ` origin: "Constructed companion"`,
1064
+ ` ethos: "${(behaviorConfig.ethos || 'Direct technical candor, thoughtful, concise, and loyal').replace(/"/g, '\\"')}"`,
1065
+ ``,
1066
+ `directives:`,
1067
+ ` - id: "dir-01"`,
1068
+ ` category: "behavioral"`,
1069
+ ` directive: "${(behaviorConfig.directive || 'Speak concisely and stay in character without sycophantic filler').replace(/"/g, '\\"')}"`,
1070
+ ``,
1071
+ ].join('\n');
1072
+ selfPersonaFile = { path: selfRelPath, content: selfContent };
1073
+ readmeLines.push('', '### Self & Persona Assets', `Companion persona manifest resides in \`./${selfRelPath}\`.`);
1074
+ }
1075
+ }
1076
+ readmeLines.push('');
1077
+ const readmeMd = readmeLines.join('\n');
1078
+ const webHtml = (0, web_template_1.generateWebHtml)(instanceName, manifests);
1079
+ const result = {
1080
+ 'package.json': packageJson,
1081
+ 'siduri.config.json': siduriConfigJson,
1082
+ 'siduri.schema.json': siduriSchemaJson,
1083
+ '.env.example': envExample,
1084
+ 'README.md': readmeMd,
1085
+ 'src/index.js': srcIndexJs,
1086
+ 'public/index.html': webHtml,
1087
+ createAssetsBodyDir: hasBody,
1088
+ createAssetsDirs,
1089
+ };
1090
+ if (selfPersonaFile) {
1091
+ result[selfPersonaFile.path] = selfPersonaFile.content;
1092
+ }
1093
+ return result;
1094
+ }