@vxnus/siduri 0.1.5 → 0.1.7

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 (43) hide show
  1. package/dist/builtin-manifests.js +4 -4
  2. package/dist/clean-machine-e2e.test.js +14 -14
  3. package/dist/configurators/voice.js +11 -8
  4. package/dist/configurators.test.js +18 -1
  5. package/dist/generator.js +86 -72
  6. package/dist/generator.test.js +20 -3
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/release-check.js +6 -6
  10. package/dist/runtime-parity.test.d.ts +1 -0
  11. package/dist/runtime-parity.test.js +140 -0
  12. package/dist/web-dist/404/index.html +1 -1
  13. package/dist/web-dist/404.html +1 -1
  14. package/dist/web-dist/__next.__PAGE__.txt +22 -22
  15. package/dist/web-dist/__next._full.txt +26 -22
  16. package/dist/web-dist/__next._tree.txt +2 -2
  17. package/dist/web-dist/_next/static/chunks/0bncolc2nuzmo.css +1 -0
  18. package/dist/web-dist/_next/static/chunks/{1eusi7dzngpy3.js → 0xe6aaej0xsj8.js} +1 -1
  19. package/dist/web-dist/_next/static/chunks/10rmct1ax-opq.js +1 -0
  20. package/dist/web-dist/_not-found/__next._full.txt +2 -2
  21. package/dist/web-dist/_not-found/__next._not-found.__PAGE__.txt +2 -2
  22. package/dist/web-dist/_not-found/__next._tree.txt +2 -2
  23. package/dist/web-dist/_not-found/index.html +1 -1
  24. package/dist/web-dist/_not-found/index.txt +2 -2
  25. package/dist/web-dist/chat/__next._full.txt +3 -3
  26. package/dist/web-dist/chat/__next._tree.txt +2 -2
  27. package/dist/web-dist/chat/__next.chat.__PAGE__.txt +3 -3
  28. package/dist/web-dist/chat/index.html +1 -1
  29. package/dist/web-dist/chat/index.txt +3 -3
  30. package/dist/web-dist/index.html +1 -1
  31. package/dist/web-dist/index.txt +26 -22
  32. package/dist/web-dist/operator/__next._full.txt +3 -3
  33. package/dist/web-dist/operator/__next._tree.txt +2 -2
  34. package/dist/web-dist/operator/__next.operator.__PAGE__.txt +3 -3
  35. package/dist/web-dist/operator/index.html +1 -1
  36. package/dist/web-dist/operator/index.txt +3 -3
  37. package/dist/web-template.js +15 -6
  38. package/package.json +1 -1
  39. package/dist/web-dist/_next/static/chunks/1dc9n0p39yo65.css +0 -1
  40. package/dist/web-dist/_next/static/chunks/1hz3jco387c-k.js +0 -1
  41. /package/dist/web-dist/_next/static/{fySnDRfSgiBzStXXyXjTp → uvsK8ULhL7fHQMR8sr_JL}/_buildManifest.js +0 -0
  42. /package/dist/web-dist/_next/static/{fySnDRfSgiBzStXXyXjTp → uvsK8ULhL7fHQMR8sr_JL}/_clientMiddlewareManifest.js +0 -0
  43. /package/dist/web-dist/_next/static/{fySnDRfSgiBzStXXyXjTp → uvsK8ULhL7fHQMR8sr_JL}/_ssgManifest.js +0 -0
@@ -5,7 +5,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
5
5
  {
6
6
  name: '@siduri-x/behavior',
7
7
  organType: 'behavior',
8
- version: '1.0.2',
8
+ version: '1.0.5',
9
9
  displayName: 'Behavior (Active Self Directives)',
10
10
  description: 'Atomic directive state machine and personality projection compiler',
11
11
  entrypoint: './dist/index.js',
@@ -66,7 +66,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
66
66
  {
67
67
  name: '@siduri-x/brain',
68
68
  organType: 'brain',
69
- version: '1.0.2',
69
+ version: '1.0.3',
70
70
  displayName: 'Brain (Cognition & Planning)',
71
71
  description: 'Provider-neutral LLM reasoning, response planning, and proposal generation',
72
72
  entrypoint: './dist/index.js',
@@ -272,7 +272,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
272
272
  {
273
273
  name: '@siduri-x/memory',
274
274
  organType: 'memory',
275
- version: '1.0.2',
275
+ version: '1.0.3',
276
276
  displayName: 'Memory (PostgreSQL FTS Claims)',
277
277
  description: 'Relational semantic claims with Full-Text Search and companion isolation',
278
278
  entrypoint: './dist/index.js',
@@ -389,7 +389,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
389
389
  {
390
390
  name: '@siduri-x/voice',
391
391
  organType: 'voice',
392
- version: '1.0.3',
392
+ version: '1.0.5',
393
393
  displayName: 'Voice (VOICEVOX Speech Synthesis)',
394
394
  description: 'Queued speech synthesis and audio rendering lifecycle adapter',
395
395
  entrypoint: './dist/index.js',
@@ -13,18 +13,18 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
13
13
  const tempPackDir = node_path_1.default.resolve(__dirname, '../temp-packs-e2e');
14
14
  const cleanMachineRoot = node_path_1.default.resolve(__dirname, '../temp-clean-machine-e2e');
15
15
  const ALL_CANONICAL_PACKAGES = [
16
- { filter: '@siduri-x/core', tarName: 'siduri-x-core-1.0.2.tgz', isOrgan: false },
17
- { filter: '@siduri-x/brain', tarName: 'siduri-x-brain-1.0.2.tgz', isOrgan: true },
18
- { filter: '@siduri-x/memory', tarName: 'siduri-x-memory-1.0.2.tgz', isOrgan: true },
16
+ { filter: '@siduri-x/core', tarName: 'siduri-x-core-1.0.4.tgz', isOrgan: false },
17
+ { filter: '@siduri-x/brain', tarName: 'siduri-x-brain-1.0.3.tgz', isOrgan: true },
18
+ { filter: '@siduri-x/memory', tarName: 'siduri-x-memory-1.0.3.tgz', isOrgan: true },
19
19
  { filter: '@siduri-x/knowledge', tarName: 'siduri-x-knowledge-1.0.2.tgz', isOrgan: true },
20
- { filter: '@siduri-x/behavior', tarName: 'siduri-x-behavior-1.0.2.tgz', isOrgan: true },
20
+ { filter: '@siduri-x/behavior', tarName: 'siduri-x-behavior-1.0.5.tgz', isOrgan: true },
21
21
  { filter: '@siduri-x/ear', tarName: 'siduri-x-ear-1.0.2.tgz', isOrgan: true },
22
22
  { filter: '@siduri-x/vision', tarName: 'siduri-x-vision-1.0.2.tgz', isOrgan: true },
23
23
  { filter: '@siduri-x/hands', tarName: 'siduri-x-hands-1.0.2.tgz', isOrgan: true },
24
24
  { filter: '@siduri-x/body', tarName: 'siduri-x-body-1.0.4.tgz', isOrgan: true },
25
- { filter: '@siduri-x/voice', tarName: 'siduri-x-voice-1.0.3.tgz', isOrgan: true },
25
+ { filter: '@siduri-x/voice', tarName: 'siduri-x-voice-1.0.5.tgz', isOrgan: true },
26
26
  { filter: '@siduri-x/observation', tarName: 'siduri-x-observation-1.0.2.tgz', isOrgan: true },
27
- { filter: '@vxnus/siduri', tarName: 'vxnus-siduri-0.1.5.tgz', isOrgan: false },
27
+ { filter: '@vxnus/siduri', tarName: 'vxnus-siduri-0.1.7.tgz', isOrgan: false },
28
28
  ];
29
29
  beforeAll(() => {
30
30
  // 1. Prepare clean directories
@@ -86,7 +86,7 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
86
86
  }
87
87
  });
88
88
  test('memory organ tarball packages migrations/001_initial_schema.sql', () => {
89
- const memoryTarPath = node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.2.tgz');
89
+ const memoryTarPath = node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.3.tgz');
90
90
  const listing = (0, node_child_process_1.execSync)(`tar -tzf ${memoryTarPath}`, { encoding: 'utf8' });
91
91
  expect(listing).toContain('package/migrations/001_initial_schema.sql');
92
92
  });
@@ -106,8 +106,8 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
106
106
  // Write instance files referencing packed tarballs directly for true clean machine install
107
107
  const pkgObj = JSON.parse(files['package.json']);
108
108
  pkgObj.dependencies = {
109
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.2.tgz')}`,
110
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.2.tgz')}`,
109
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.4.tgz')}`,
110
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.3.tgz')}`,
111
111
  };
112
112
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
113
113
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -154,8 +154,8 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
154
154
  });
155
155
  const pkgObj = JSON.parse(files['package.json']);
156
156
  pkgObj.dependencies = {
157
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.2.tgz')}`,
158
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.2.tgz')}`,
157
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.4.tgz')}`,
158
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.3.tgz')}`,
159
159
  '@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.2.tgz')}`,
160
160
  };
161
161
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
@@ -193,9 +193,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
193
193
  });
194
194
  const pkgObj = JSON.parse(files['package.json']);
195
195
  pkgObj.dependencies = {
196
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.2.tgz')}`,
197
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.2.tgz')}`,
198
- '@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.2.tgz')}`,
196
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.4.tgz')}`,
197
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.3.tgz')}`,
198
+ '@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.3.tgz')}`,
199
199
  };
200
200
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
201
201
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -61,10 +61,15 @@ async function configureVoice(_context) {
61
61
  default: 'rmvpe',
62
62
  },
63
63
  {
64
- type: 'input',
65
- name: 'baseUrl',
66
- message: 'Base TTS Synthesis Engine URL (VOICEVOX):',
67
- default: 'http://localhost:50021',
64
+ type: 'list',
65
+ name: 'baseTts',
66
+ message: 'Base TTS Engine for RVC (Generates initial audio):',
67
+ choices: [
68
+ { name: 'Edge-TTS (Cloud API, 0MB)', value: 'edge-tts' },
69
+ { name: 'Kokoro TTS (Local, ~80MB)', value: 'kokoro' },
70
+ { name: 'Piper TTS (Local, ~20MB)', value: 'piper' },
71
+ ],
72
+ default: 'edge-tts',
68
73
  },
69
74
  {
70
75
  type: 'input',
@@ -75,9 +80,7 @@ async function configureVoice(_context) {
75
80
  ]);
76
81
  const pitchShift = parseInt(rvcAnswers.pitchShift, 10) || 0;
77
82
  const config = {
78
- provider: 'voicevox',
79
- baseUrl: rvcAnswers.baseUrl.trim(),
80
- speakerId: 1,
83
+ provider: rvcAnswers.baseTts,
81
84
  maxQueueDepth: 50,
82
85
  rvc: {
83
86
  enabled: true,
@@ -92,11 +95,11 @@ async function configureVoice(_context) {
92
95
  };
93
96
  const summary = {
94
97
  Provider: 'RVC (Custom Voice Model)',
98
+ 'Base TTS': rvcAnswers.baseTts,
95
99
  'Model Path': rvcAnswers.modelPath.trim(),
96
100
  'Pitch Shift': pitchShift,
97
101
  'F0 Method': rvcAnswers.f0Method,
98
102
  'RVC Service': rvcAnswers.serviceUrl.trim(),
99
- 'Base TTS': rvcAnswers.baseUrl.trim(),
100
103
  };
101
104
  return {
102
105
  config,
@@ -198,6 +198,23 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
198
198
  expect(result.config.speakerId).toBe(2);
199
199
  expect(result.summary?.['Speaker ID']).toBe(2);
200
200
  });
201
+ test('Voice configurator configures RVC with Base TTS Engine', async () => {
202
+ inquirer_1.default.prompt
203
+ .mockResolvedValueOnce({ provider: 'rvc' })
204
+ .mockResolvedValueOnce({
205
+ modelPath: './assets/voice/sparkle/sparkle.pth',
206
+ indexPath: './assets/voice/sparkle/sparkle.index',
207
+ pitchShift: '12',
208
+ f0Method: 'rmvpe',
209
+ baseTts: 'edge-tts',
210
+ serviceUrl: 'http://localhost:50055',
211
+ });
212
+ const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
213
+ expect(result.config.provider).toBe('edge-tts');
214
+ expect(result.config.rvc.enabled).toBe(true);
215
+ expect(result.config.rvc.pitchShift).toBe(12);
216
+ expect(result.summary?.['Base TTS']).toBe('edge-tts');
217
+ });
201
218
  test('Body configurator configures Live2D, custom model path, and expression', async () => {
202
219
  inquirer_1.default.prompt
203
220
  .mockResolvedValueOnce({ provider: 'live2d' })
@@ -276,7 +293,7 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
276
293
  expect(files['src/index.js']).toContain("import { OpenRouterBrain } from '@siduri-x/brain'");
277
294
  expect(files['src/index.js']).toContain("import { PostgresMemoryOrgan } from '@siduri-x/memory'");
278
295
  expect(files['src/index.js']).toContain("import { EKnowledgeAdapter } from '@siduri-x/knowledge'");
279
- expect(files['src/index.js']).toContain("import { VoicevoxAdapter } from '@siduri-x/voice'");
296
+ expect(files['src/index.js']).toContain("import { VoiceAdapter } from '@siduri-x/voice'");
280
297
  });
281
298
  });
282
299
  });
package/dist/generator.js CHANGED
@@ -68,7 +68,7 @@ function getDefaultConfigForManifest(manifest) {
68
68
  function generateInstanceFiles(options) {
69
69
  const instanceName = options.name || 'my-siduri';
70
70
  const instanceId = options.id || 'default';
71
- const coreVersion = options.coreVersion || '^1.0.2';
71
+ const coreVersion = options.coreVersion || '^1.0.4';
72
72
  const manifests = options.selectedManifests;
73
73
  const hasMemory = manifests.some((m) => m.organType === 'memory');
74
74
  const hasVoice = manifests.some((m) => m.organType === 'voice');
@@ -101,14 +101,6 @@ function generateInstanceFiles(options) {
101
101
  ].join('\n'));
102
102
  dockerVolumes.push(' postgres_data:');
103
103
  }
104
- if (isVoicevox) {
105
- dockerServices.push([
106
- ' voicevox:',
107
- ' image: voicevox/voicevox_engine:cpu-latest',
108
- ' ports:',
109
- ' - "50021:50021"',
110
- ].join('\n'));
111
- }
112
104
  if (hasVoice && voiceConfig?.rvc?.enabled) {
113
105
  dockerServices.push([
114
106
  ' rvc:',
@@ -218,7 +210,7 @@ function generateInstanceFiles(options) {
218
210
  `import { readFile, stat, readdir } from 'node:fs/promises';`,
219
211
  `import path from 'node:path';`,
220
212
  `import { fileURLToPath } from 'node:url';`,
221
- `import { SiduriRuntime } from '@siduri-x/core';`,
213
+ `import { SiduriRuntime, dispatchCompanionChat } from '@siduri-x/core';`,
222
214
  ];
223
215
  for (const m of manifests) {
224
216
  importLines.push(`import { ${m.factory} } from '${m.name}';`);
@@ -256,14 +248,11 @@ function generateInstanceFiles(options) {
256
248
  ` const parsedUrl = new URL(req.url, \`http://\${req.headers.host || 'localhost'}\`);`,
257
249
  ` const pathname = parsedUrl.pathname;`,
258
250
  '',
259
- ` // API: Status & Health Probes`,
251
+ ` // API: Status & Health Probes (minimal safe operational state, no secrets/credentials/config.organs)`,
260
252
  ` if ((pathname === '/health' || pathname === '/api/status') && req.method === 'GET') {`,
261
253
  ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
262
254
  ` res.end(JSON.stringify({`,
263
255
  ` status: 'ok',`,
264
- ` name: config.name,`,
265
- ` id: config.id,`,
266
- ` organs: config.organs,`,
267
256
  ` uptime: process.uptime(),`,
268
257
  ` }));`,
269
258
  ` return;`,
@@ -324,8 +313,35 @@ function generateInstanceFiles(options) {
324
313
  '',
325
314
  ` // API: Memory Proposal Approval / Rejection`,
326
315
  ` if ((pathname === '/memory/proposals/approve' || pathname === '/memory/proposals/reject') && req.method === 'POST') {`,
327
- ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
328
- ` res.end(JSON.stringify({ success: true, approved: pathname.endsWith('approve') }));`,
316
+ ` let body = '';`,
317
+ ` req.on('data', (chunk) => { body += chunk; });`,
318
+ ` req.on('end', async () => {`,
319
+ ` try {`,
320
+ ` const payload = JSON.parse(body || '{}');`,
321
+ ` const claimId = payload.id || payload.claimId;`,
322
+ ` if (!claimId) {`,
323
+ ` res.writeHead(400, { 'Content-Type': 'application/json' });`,
324
+ ` res.end(JSON.stringify({ error: 'Missing required claim id' }));`,
325
+ ` return;`,
326
+ ` }`,
327
+ ` if (pathname.endsWith('approve')) {`,
328
+ ` if (typeof memory?.approveClaim === 'function') {`,
329
+ ` await memory.approveClaim(claimId);`,
330
+ ` }`,
331
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
332
+ ` res.end(JSON.stringify({ approved: true, id: claimId }));`,
333
+ ` } else {`,
334
+ ` if (typeof memory?.rejectClaim === 'function') {`,
335
+ ` await memory.rejectClaim(claimId);`,
336
+ ` }`,
337
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
338
+ ` res.end(JSON.stringify({ rejected: true, id: claimId }));`,
339
+ ` }`,
340
+ ` } catch (err) {`,
341
+ ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
342
+ ` res.end(JSON.stringify({ error: err.message }));`,
343
+ ` }`,
344
+ ` });`,
329
345
  ` return;`,
330
346
  ` }`,
331
347
  '',
@@ -336,43 +352,23 @@ function generateInstanceFiles(options) {
336
352
  ` return;`,
337
353
  ` }`,
338
354
  '',
339
- ` // API: Chat interaction (supports both /chat and /api/chat)`,
355
+ ` // API: Chat interaction (routed through canonical dispatchCompanionChat from @siduri-x/core)`,
340
356
  ` if ((pathname === '/chat' || pathname === '/api/chat') && req.method === 'POST') {`,
341
357
  ` let body = '';`,
342
358
  ` req.on('data', (chunk) => { body += chunk; });`,
343
359
  ` req.on('end', async () => {`,
344
360
  ` try {`,
345
361
  ` const payload = JSON.parse(body || '{}');`,
346
- ` const userMessage = payload.message || payload.text || '';`,
347
- ` let reply = \`Hello! I received: "\${userMessage}"\`;`,
348
- ` let expression = 'neutral';`,
349
- ` let audioUrl = undefined;`,
350
- '',
351
- ` if (typeof brain?.generatePlan === 'function') {`,
352
- ` try {`,
353
- ` const plan = await brain.generatePlan({ prompt: userMessage });`,
354
- ` reply = plan.speech || plan.text || reply;`,
355
- ` } catch (err) {`,
356
- ` reply = \`Thinking about "\${userMessage}"... (Brain active)\`;`,
357
- ` }`,
358
- ` }`,
359
- '',
360
- ` if (typeof voice?.synthesize === 'function') {`,
361
- ` try {`,
362
- ` const wavData = await voice.synthesize(reply);`,
363
- ` const audioId = \`audio_\${Date.now()}\`;`,
364
- ` audioCache.set(audioId, wavData);`,
365
- ` audioUrl = \`/api/audio/\${audioId}\`;`,
366
- ` } catch (e) {}`,
367
- ` }`,
368
- '',
369
- ` if (typeof body?.setExpression === 'function') {`,
370
- ` expression = reply.includes('!') ? 'happy' : 'calm';`,
371
- ` body.setExpression(expression);`,
372
- ` }`,
362
+ ` const response = await dispatchCompanionChat(runtime, {`,
363
+ ` id: config.id,`,
364
+ ` companionId: config.id,`,
365
+ ` message: payload.message || payload.text || '',`,
366
+ ` role: payload.role || 'OWNER',`,
367
+ ` history: Array.isArray(payload.history) ? payload.history : [],`,
368
+ ` });`,
373
369
  '',
374
370
  ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
375
- ` res.end(JSON.stringify({ reply, text: reply, expression, audioUrl, status: 'ok' }));`,
371
+ ` res.end(JSON.stringify(response));`,
376
372
  ` } catch (err) {`,
377
373
  ` res.writeHead(500, { 'Content-Type': 'application/json' });`,
378
374
  ` res.end(JSON.stringify({ error: err.message }));`,
@@ -408,32 +404,49 @@ function generateInstanceFiles(options) {
408
404
  ` return;`,
409
405
  ` }`,
410
406
  '',
411
- ` // Static files & Next.js apps/web export routing`,
412
- ` const decodedPathname = decodeURIComponent(pathname);`,
413
- ` const normalizedPath = decodedPathname.replace(/^[/]+|[/]+$/g, '');`,
414
- ` const candidatePaths = [`,
415
- ` path.join(rootDir, 'public', normalizedPath, 'index.html'),`,
416
- ` path.join(rootDir, 'public', normalizedPath + '.html'),`,
417
- ` path.join(rootDir, 'public', normalizedPath),`,
418
- ` path.join(rootDir, normalizedPath),`,
407
+ ` // Static files & Next.js apps/web export routing with strict boundary verification`,
408
+ ` let decodedPathname = pathname;`,
409
+ ` try {`,
410
+ ` while (decodedPathname.includes('%')) {`,
411
+ ` const next = decodeURIComponent(decodedPathname);`,
412
+ ` if (next === decodedPathname) break;`,
413
+ ` decodedPathname = next;`,
414
+ ` }`,
415
+ ` } catch {`,
416
+ ` res.writeHead(400, { 'Content-Type': 'text/plain' });`,
417
+ ` res.end('Bad Request');`,
418
+ ` return;`,
419
+ ` }`,
420
+ ` const normalizedPath = path.normalize(decodedPathname).replace(/^[/\\\\]+/, '');`,
421
+ ` const canonicalPublicRoot = path.resolve(rootDir, 'public');`,
422
+ ` const canonicalAssetsRoot = path.resolve(rootDir, 'assets');`,
423
+ ` const candidatePairs = [`,
424
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath, 'index.html') },`,
425
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath + '.html') },`,
426
+ ` { root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, normalizedPath) },`,
419
427
  ` ];`,
420
428
  '',
421
- ` if (!normalizedPath) {`,
422
- ` candidatePaths.unshift(path.join(rootDir, 'public', 'index.html'));`,
429
+ ` if (!normalizedPath || normalizedPath === '.' || normalizedPath === './') {`,
430
+ ` candidatePairs.unshift({ root: canonicalPublicRoot, target: path.resolve(canonicalPublicRoot, 'index.html') });`,
423
431
  ` }`,
424
432
  '',
425
433
  ` // Compatibility fallback for legacy /live2d/<modelName>/... -> ./assets/body/<modelName>/...`,
426
- ` if (pathname.startsWith('/live2d/')) {`,
427
- ` const relativeAssetPath = pathname.slice('/live2d/'.length);`,
428
- ` candidatePaths.unshift(path.join(rootDir, 'assets', 'body', relativeAssetPath));`,
429
- ` candidatePaths.unshift(path.join(rootDir, 'assets', 'body', 'model', relativeAssetPath));`,
434
+ ` if (decodedPathname.startsWith('/live2d/')) {`,
435
+ ` const relativeAssetPath = path.normalize(decodedPathname.slice('/live2d/'.length)).replace(/^[/\\\\]+/, '');`,
436
+ ` candidatePairs.unshift({ root: canonicalAssetsRoot, target: path.resolve(canonicalAssetsRoot, 'body', relativeAssetPath) });`,
437
+ ` candidatePairs.unshift({ root: canonicalAssetsRoot, target: path.resolve(canonicalAssetsRoot, 'body', 'model', relativeAssetPath) });`,
430
438
  ` }`,
431
439
  '',
432
- ` for (const candidate of candidatePaths) {`,
440
+ ` for (const { root, target } of candidatePairs) {`,
433
441
  ` try {`,
434
- ` const fileStat = await stat(candidate);`,
442
+ ` // Strict path containment check: target must reside within configured root directory`,
443
+ ` const relative = path.relative(root, target);`,
444
+ ` if (relative.startsWith('..') || path.isAbsolute(relative)) {`,
445
+ ` continue;`,
446
+ ` }`,
447
+ ` const fileStat = await stat(target);`,
435
448
  ` if (fileStat.isFile()) {`,
436
- ` const ext = path.extname(candidate).toLowerCase();`,
449
+ ` const ext = path.extname(target).toLowerCase();`,
437
450
  ` const mimeTypes = {`,
438
451
  ` '.html': 'text/html; charset=utf-8',`,
439
452
  ` '.js': 'application/javascript; charset=utf-8',`,
@@ -448,7 +461,7 @@ function generateInstanceFiles(options) {
448
461
  ` '.moc3': 'application/octet-stream',`,
449
462
  ` };`,
450
463
  ` const contentType = mimeTypes[ext] || 'application/octet-stream';`,
451
- ` const content = await readFile(candidate);`,
464
+ ` const content = await readFile(target);`,
452
465
  ` res.writeHead(200, { 'Content-Type': contentType });`,
453
466
  ` res.end(content);`,
454
467
  ` return;`,
@@ -461,9 +474,12 @@ function generateInstanceFiles(options) {
461
474
  `});`,
462
475
  '',
463
476
  `const PORT = process.env.PORT || 3000;`,
464
- `server.listen(PORT, () => {`,
477
+ `server.listen(Number(PORT), '127.0.0.1', () => {`,
465
478
  ` console.log(\`✓ Siduri [\${config.name}] initialized with [${selectedDisplayNames}].\`);`,
466
- ` console.log(\`➜ Web Companion & Memory Console running at: http://localhost:\${PORT}\`);`,
479
+ ` console.log(\`➜ Web Companion & Memory Console running at: http://127.0.0.1:\${PORT}\`);`,
480
+ ` if (process.env.PORT === '0') {`,
481
+ ` server.close();`,
482
+ ` }`,
467
483
  `});`,
468
484
  '',
469
485
  ].join('\n');
@@ -480,14 +496,12 @@ function generateInstanceFiles(options) {
480
496
  '- **Node.js**: `v20.0.0` or higher',
481
497
  '- **Environment**: Valid `.env` file (configured from `.env.example`)',
482
498
  ];
483
- if (hasMemory || isVoicevox) {
499
+ if (hasMemory) {
484
500
  readmeLines.push('- **Local Services (Optional)**: Docker (or standalone alternatives):');
485
- if (hasMemory) {
486
- readmeLines.push(' - **PostgreSQL**: Required for memory claims & durable state (or use cloud Supabase/Neon)');
487
- }
488
- if (isVoicevox) {
489
- readmeLines.push(' - **VOICEVOX**: Required for voice synthesis (or run the official desktop app from [voicevox.hiroshiba.jp](https://voicevox.hiroshiba.jp/))');
490
- }
501
+ readmeLines.push(' - **PostgreSQL**: Required for memory claims & durable state (or use cloud Supabase/Neon)');
502
+ }
503
+ if (isVoicevox) {
504
+ readmeLines.push('- **VOICEVOX**: Note: The Voicevox engine executable will be securely auto-downloaded at runtime by Siduri if no local URL is provided.');
491
505
  }
492
506
  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`.');
493
507
  if (dockerComposeYaml) {
@@ -58,7 +58,7 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
58
58
  version: '1.0.0',
59
59
  displayName: 'Voice (VOICEVOX Speech Synthesis)',
60
60
  entrypoint: './dist/index.js',
61
- factory: 'VoicevoxAdapter',
61
+ factory: 'VoiceAdapter',
62
62
  configKey: 'voice',
63
63
  configSchema: { type: 'object', properties: { provider: { type: 'string' } } },
64
64
  environment: [{ name: 'VOICEVOX_URL', default: 'http://localhost:50021' }],
@@ -158,10 +158,10 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
158
158
  expect(files['README.md']).toContain('assets/body/companion-full');
159
159
  expect(files['README.md']).toContain('assets/voice/companion-full');
160
160
  expect(files['README.md']).toContain('Prerequisites');
161
- // Docker compose generated for memory & voice
161
+ // Docker compose generated for memory (voicevox is auto-downloaded at runtime)
162
162
  expect(files['docker-compose.yml']).toBeDefined();
163
163
  expect(files['docker-compose.yml']).toContain('postgres:15');
164
- expect(files['docker-compose.yml']).toContain('voicevox/voicevox_engine');
164
+ expect(files['README.md']).toContain('Voicevox engine executable will be securely auto-downloaded');
165
165
  expect(pkg.scripts['services:up']).toBe('docker compose up -d');
166
166
  expect(pkg.scripts['services:down']).toBe('docker compose down');
167
167
  });
@@ -177,4 +177,21 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
177
177
  expect(files['public/index.html']).toContain('Memory Console');
178
178
  expect(files['public/index.html']).toContain('Companion Chat');
179
179
  });
180
+ test('Generated runtime server template enforces loopback binding, safe health, and path containment', () => {
181
+ const files = (0, generator_1.generateInstanceFiles)({
182
+ name: 'secure-companion',
183
+ selectedManifests: [MOCK_MANIFESTS.brain, MOCK_MANIFESTS.memory],
184
+ });
185
+ const srcIndexJs = files['src/index.js'];
186
+ // 1. Explicit loopback binding 127.0.0.1
187
+ expect(srcIndexJs).toContain("server.listen(Number(PORT), '127.0.0.1'");
188
+ expect(srcIndexJs).toContain("http://127.0.0.1:");
189
+ // 2. Minimal /health endpoint without config.organs or secrets
190
+ expect(srcIndexJs).toContain("pathname === '/health'");
191
+ expect(srcIndexJs).not.toContain('organs: config.organs');
192
+ // 3. Strict path traversal containment
193
+ expect(srcIndexJs).toContain('relative.startsWith(\'..\')');
194
+ expect(srcIndexJs).toContain('path.isAbsolute(relative)');
195
+ expect(srcIndexJs).toContain('decodeURIComponent');
196
+ });
180
197
  });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { OrganManifest } from './manifest';
3
- export declare const CLI_VERSION = "0.1.5";
3
+ export declare const CLI_VERSION = "0.1.7";
4
4
  export declare const colors: {
5
5
  cyan: string;
6
6
  dim: string;
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ const doctor_1 = require("./doctor");
26
26
  const db_1 = require("./db");
27
27
  const configurators_1 = require("./configurators");
28
28
  const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile);
29
- exports.CLI_VERSION = '0.1.5';
29
+ exports.CLI_VERSION = '0.1.7';
30
30
  exports.colors = {
31
31
  cyan: '\u001b[36m',
32
32
  dim: '\u001b[2m',
@@ -17,18 +17,18 @@ function runReleaseCheck(repoRoot = node_path_1.default.resolve(__dirname, '../.
17
17
  node_fs_1.default.rmSync(tempPackDir, { recursive: true, force: true });
18
18
  node_fs_1.default.mkdirSync(tempPackDir, { recursive: true });
19
19
  const canonicalPackages = [
20
- { name: '@siduri-x/core', dir: 'packages/core', isOrgan: false, tarName: 'siduri-x-core-1.0.2.tgz' },
21
- { name: '@siduri-x/brain', dir: 'packages/organs/brain', isOrgan: true, tarName: 'siduri-x-brain-1.0.2.tgz' },
22
- { name: '@siduri-x/memory', dir: 'packages/organs/memory', isOrgan: true, tarName: 'siduri-x-memory-1.0.2.tgz' },
20
+ { name: '@siduri-x/core', dir: 'packages/core', isOrgan: false, tarName: 'siduri-x-core-1.0.4.tgz' },
21
+ { name: '@siduri-x/brain', dir: 'packages/organs/brain', isOrgan: true, tarName: 'siduri-x-brain-1.0.3.tgz' },
22
+ { name: '@siduri-x/memory', dir: 'packages/organs/memory', isOrgan: true, tarName: 'siduri-x-memory-1.0.3.tgz' },
23
23
  { name: '@siduri-x/knowledge', dir: 'packages/organs/knowledge', isOrgan: true, tarName: 'siduri-x-knowledge-1.0.2.tgz' },
24
- { name: '@siduri-x/behavior', dir: 'packages/organs/behavior', isOrgan: true, tarName: 'siduri-x-behavior-1.0.2.tgz' },
24
+ { name: '@siduri-x/behavior', dir: 'packages/organs/behavior', isOrgan: true, tarName: 'siduri-x-behavior-1.0.5.tgz' },
25
25
  { name: '@siduri-x/ear', dir: 'packages/organs/ear', isOrgan: true, tarName: 'siduri-x-ear-1.0.2.tgz' },
26
26
  { name: '@siduri-x/vision', dir: 'packages/organs/vision', isOrgan: true, tarName: 'siduri-x-vision-1.0.2.tgz' },
27
27
  { name: '@siduri-x/hands', dir: 'packages/organs/hands', isOrgan: true, tarName: 'siduri-x-hands-1.0.2.tgz' },
28
28
  { name: '@siduri-x/body', dir: 'packages/organs/body', isOrgan: true, tarName: 'siduri-x-body-1.0.4.tgz' },
29
- { name: '@siduri-x/voice', dir: 'packages/organs/voice', isOrgan: true, tarName: 'siduri-x-voice-1.0.3.tgz' },
29
+ { name: '@siduri-x/voice', dir: 'packages/organs/voice', isOrgan: true, tarName: 'siduri-x-voice-1.0.5.tgz' },
30
30
  { name: '@siduri-x/observation', dir: 'packages/organs/observation', isOrgan: true, tarName: 'siduri-x-observation-1.0.2.tgz' },
31
- { name: '@vxnus/siduri', dir: 'cli', isOrgan: false, tarName: 'vxnus-siduri-0.1.5.tgz' },
31
+ { name: '@vxnus/siduri', dir: 'cli', isOrgan: false, tarName: 'vxnus-siduri-0.1.7.tgz' },
32
32
  ];
33
33
  let packagesChecked = 0;
34
34
  let tarballsInspected = 0;
@@ -0,0 +1 @@
1
+ export {};