@vxnus/siduri 0.1.10 → 2.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 (71) hide show
  1. package/README.md +3 -4
  2. package/dist/builtin-manifests.js +15 -30
  3. package/dist/clean-machine-e2e.test.js +13 -89
  4. package/dist/configurators/memory.js +1 -1
  5. package/dist/configurators.test.js +1 -1
  6. package/dist/db.d.ts +0 -12
  7. package/dist/db.js +7 -144
  8. package/dist/discovery.js +5 -7
  9. package/dist/discovery.test.js +5 -4
  10. package/dist/doctor-db.test.js +2 -91
  11. package/dist/doctor.d.ts +1 -1
  12. package/dist/doctor.js +32 -40
  13. package/dist/generator.d.ts +0 -1
  14. package/dist/generator.js +16 -67
  15. package/dist/generator.test.js +7 -7
  16. package/dist/index.js +0 -6
  17. package/dist/release-check.js +4 -8
  18. package/dist/runtime-parity.test.js +2 -1
  19. package/dist/schema-validator.d.ts +9 -0
  20. package/dist/schema-validator.js +107 -0
  21. package/dist/web-dist/404/index.html +1 -0
  22. package/dist/web-dist/404.html +1 -0
  23. package/dist/web-dist/__next.__PAGE__.txt +35 -0
  24. package/dist/web-dist/__next._full.txt +32 -0
  25. package/dist/web-dist/__next._tree.txt +4 -0
  26. package/dist/web-dist/_next/static/chunks/09y7khbmco_v4.js +1 -0
  27. package/dist/web-dist/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  28. package/dist/web-dist/_next/static/chunks/0uwih7sydv413.js +1 -0
  29. package/dist/web-dist/_next/static/chunks/0xhe1hasjan-9.css +1 -0
  30. package/dist/web-dist/_next/static/chunks/1_yae15ffa5f6.js +1 -0
  31. package/dist/web-dist/_next/static/chunks/2_duynll0m51m.js +30 -0
  32. package/dist/web-dist/_next/static/chunks/2kcqevsmv6kh2.js +31 -0
  33. package/dist/web-dist/_next/static/chunks/2s3d1bfr0l4b0.js +1 -0
  34. package/dist/web-dist/_next/static/chunks/32z87id9k6i2z.js +22 -0
  35. package/dist/web-dist/_next/static/chunks/3bx2gs3idjmo1.js +1 -0
  36. package/dist/web-dist/_next/static/chunks/turbopack-3eipe4c-imn5l.js +1 -0
  37. package/dist/web-dist/_next/static/media/apple-icon.2g7-yefdphkmy.png +0 -0
  38. package/dist/web-dist/_next/static/media/favicon.3rlors1ui1gw9.ico +0 -0
  39. package/dist/web-dist/_next/static/media/icon.0580nb9h-ugue.svg +4 -0
  40. package/dist/web-dist/_next/static/pWHUixFDDvANZeAf-shqT/_buildManifest.js +11 -0
  41. package/dist/web-dist/_next/static/pWHUixFDDvANZeAf-shqT/_clientMiddlewareManifest.js +1 -0
  42. package/dist/web-dist/_next/static/pWHUixFDDvANZeAf-shqT/_ssgManifest.js +1 -0
  43. package/dist/web-dist/_not-found/__next._full.txt +17 -0
  44. package/dist/web-dist/_not-found/__next._not-found.__PAGE__.txt +23 -0
  45. package/dist/web-dist/_not-found/__next._tree.txt +3 -0
  46. package/dist/web-dist/_not-found/index.html +1 -0
  47. package/dist/web-dist/_not-found/index.txt +17 -0
  48. package/dist/web-dist/apple-icon.png +0 -0
  49. package/dist/web-dist/apple-touch-icon.png +0 -0
  50. package/dist/web-dist/chat/__next._full.txt +18 -0
  51. package/dist/web-dist/chat/__next._tree.txt +3 -0
  52. package/dist/web-dist/chat/__next.chat.__PAGE__.txt +24 -0
  53. package/dist/web-dist/chat/index.html +1 -0
  54. package/dist/web-dist/chat/index.txt +18 -0
  55. package/dist/web-dist/favicon.ico +0 -0
  56. package/dist/web-dist/favicon.svg +4 -0
  57. package/dist/web-dist/icon.svg +4 -0
  58. package/dist/web-dist/index.html +1 -0
  59. package/dist/web-dist/index.txt +32 -0
  60. package/dist/web-dist/live2d/live2dcubismcore.min.js +9 -0
  61. package/dist/web-dist/logo-circle.png +0 -0
  62. package/dist/web-dist/logo-circle.svg +4 -0
  63. package/dist/web-dist/logo-white.svg +3 -0
  64. package/dist/web-dist/logo.png +0 -0
  65. package/dist/web-dist/logo.svg +3 -0
  66. package/dist/web-dist/operator/__next._full.txt +18 -0
  67. package/dist/web-dist/operator/__next._tree.txt +3 -0
  68. package/dist/web-dist/operator/__next.operator.__PAGE__.txt +24 -0
  69. package/dist/web-dist/operator/index.html +1 -0
  70. package/dist/web-dist/operator/index.txt +18 -0
  71. package/package.json +2 -4
@@ -11,10 +11,9 @@ const generator_1 = require("./generator");
11
11
  const discovery_1 = require("./discovery");
12
12
  describe('Phase 4: Diagnostics and Database Provisioning Tests', () => {
13
13
  const rootOrgansDir = node_path_1.default.resolve(__dirname, '../../packages/organs');
14
- const registry = discovery_1.OrganRegistry.discover([rootOrgansDir]);
14
+ const registry = discovery_1.OrganRegistry.discover();
15
15
  const brain = registry.get('brain');
16
16
  const hands = registry.get('hands');
17
- const memory = registry.get('memory');
18
17
  const testTempRoot = node_path_1.default.resolve(__dirname, '../temp-phase4-test');
19
18
  beforeAll(() => {
20
19
  if (node_fs_1.default.existsSync(testTempRoot)) {
@@ -27,14 +26,6 @@ describe('Phase 4: Diagnostics and Database Provisioning Tests', () => {
27
26
  node_fs_1.default.rmSync(testTempRoot, { recursive: true, force: true });
28
27
  }
29
28
  });
30
- test('redactDatabaseUrl redacts password and sensitive credentials', () => {
31
- const raw = 'postgresql://myuser:supersecretpassword@db.example.com:5432/siduri_db';
32
- const redacted = (0, db_1.redactDatabaseUrl)(raw);
33
- expect(redacted).not.toContain('supersecretpassword');
34
- expect(redacted).toContain('***');
35
- expect(redacted).toContain('myuser');
36
- expect(redacted).toContain('db.example.com:5432/siduri_db');
37
- });
38
29
  test('Test Matrix A: Brain only instance doctor and db push', async () => {
39
30
  const instanceDir = node_path_1.default.join(testTempRoot, 'brain-only');
40
31
  node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
@@ -49,93 +40,13 @@ describe('Phase 4: Diagnostics and Database Provisioning Tests', () => {
49
40
  node_fs_1.default.writeFileSync(fullPath, content);
50
41
  }
51
42
  }
52
- // 1. Doctor without OPENROUTER_API_KEY -> FAIL (Brain Health Probe fails)
53
- const doctorFail = await (0, doctor_1.runDoctor)({ projectDir: instanceDir, env: { OPENROUTER_API_KEY: '' } });
54
- expect(doctorFail.passed).toBe(false);
55
- expect(doctorFail.results.some((r) => r.category === 'Health Probe' && r.status === 'FAIL')).toBe(true);
56
- // Database check must be SKIPPED
57
- expect(doctorFail.results.some((r) => r.category === 'Database' && r.status === 'SKIPPED')).toBe(true);
58
- // 2. Doctor with OPENROUTER_API_KEY -> PASS
59
43
  const doctorPass = await (0, doctor_1.runDoctor)({
60
44
  projectDir: instanceDir,
61
45
  env: { OPENROUTER_API_KEY: 'test-key-123' },
62
46
  });
63
47
  expect(doctorPass.passed).toBe(true);
64
- // 3. db push -> NOOP ("No database migrations are required...")
65
- const dbResult = await (0, db_1.runDbPush)({ projectDir: instanceDir });
66
- expect(dbResult.status).toBe('NOOP');
67
- expect(dbResult.message).toContain('No database migrations are required');
68
- });
69
- test('Test Matrix B: Brain + Hands instance doctor and db push', async () => {
70
- const instanceDir = node_path_1.default.join(testTempRoot, 'brain-hands');
71
- node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
72
- const files = (0, generator_1.generateInstanceFiles)({
73
- name: 'BrainHandsInstance',
74
- selectedManifests: [brain, hands],
75
- });
76
- for (const [name, content] of Object.entries(files)) {
77
- if (typeof content === 'string') {
78
- const fullPath = node_path_1.default.join(instanceDir, name);
79
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(fullPath), { recursive: true });
80
- node_fs_1.default.writeFileSync(fullPath, content);
81
- }
82
- }
83
- // Doctor checks both keys
84
- const doctorRes = await (0, doctor_1.runDoctor)({
85
- projectDir: instanceDir,
86
- env: {
87
- OPENROUTER_API_KEY: 'test-openrouter-key',
88
- ACTION_POLICY_SECRET: 'test-policy-secret',
89
- },
90
- });
91
- expect(doctorRes.passed).toBe(true);
92
- expect(doctorRes.results.some((r) => r.name === 'OPENROUTER_API_KEY' && r.status === 'PASS')).toBe(true);
93
- expect(doctorRes.results.some((r) => r.name === 'ACTION_POLICY_SECRET' && r.status === 'PASS')).toBe(true);
94
- // Database check must be SKIPPED
95
- expect(doctorRes.results.some((r) => r.category === 'Database' && r.status === 'SKIPPED')).toBe(true);
96
- // db push returns NOOP
97
48
  const dbResult = await (0, db_1.runDbPush)({ projectDir: instanceDir });
98
49
  expect(dbResult.status).toBe('NOOP');
99
- });
100
- test('Test Matrix C: Brain + Memory instance requires DATABASE_URL', async () => {
101
- const instanceDir = node_path_1.default.join(testTempRoot, 'brain-memory');
102
- node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
103
- const files = (0, generator_1.generateInstanceFiles)({
104
- name: 'BrainMemoryInstance',
105
- selectedManifests: [brain, memory],
106
- });
107
- for (const [name, content] of Object.entries(files)) {
108
- if (typeof content === 'string') {
109
- const fullPath = node_path_1.default.join(instanceDir, name);
110
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(fullPath), { recursive: true });
111
- node_fs_1.default.writeFileSync(fullPath, content);
112
- }
113
- }
114
- // 1. Doctor fails without DATABASE_URL
115
- const doctorMissingDb = await (0, doctor_1.runDoctor)({
116
- projectDir: instanceDir,
117
- env: { OPENROUTER_API_KEY: 'test-key' },
118
- });
119
- expect(doctorMissingDb.passed).toBe(false);
120
- expect(doctorMissingDb.results.some((r) => r.name === 'DATABASE_URL' && r.status === 'FAIL')).toBe(true);
121
- // 2. db push fails clearly without DATABASE_URL
122
- await expect((0, db_1.runDbPush)({ projectDir: instanceDir, env: {} })).rejects.toThrow(/DATABASE_URL is required/);
123
- });
124
- test('Test Matrix D: Diagnostics and Error Handling for invalid or missing configs', async () => {
125
- // 1. Missing siduri.config.json
126
- const nonExistentDir = node_path_1.default.join(testTempRoot, 'non-existent-dir');
127
- await expect((0, doctor_1.runDoctor)({ projectDir: nonExistentDir })).rejects.toThrow(/siduri.config.json not found/);
128
- await expect((0, db_1.runDbPush)({ projectDir: nonExistentDir })).rejects.toThrow(/siduri.config.json not found/);
129
- // 2. Unreachable PostgreSQL host reported gracefully by doctor
130
- const instDir = node_path_1.default.join(testTempRoot, 'brain-memory');
131
- const doctorUnreachable = await (0, doctor_1.runDoctor)({
132
- projectDir: instDir,
133
- env: {
134
- OPENROUTER_API_KEY: 'test-key',
135
- DATABASE_URL: 'postgresql://postgres:postgres@127.0.0.1:59999/non_existent_db',
136
- },
137
- });
138
- expect(doctorUnreachable.passed).toBe(false);
139
- expect(doctorUnreachable.results.some((r) => r.category === 'Database' && r.status === 'FAIL')).toBe(true);
50
+ expect(dbResult.message).toContain('SQLite manages schema natively.');
140
51
  });
141
52
  });
package/dist/doctor.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface DoctorCheckResult {
2
- category: 'Environment' | 'Services' | 'Database' | 'Health Probe';
2
+ category: 'Environment' | 'Services' | 'Database' | 'Health Probe' | 'Configuration';
3
3
  name: string;
4
4
  status: 'PASS' | 'FAIL' | 'OPTIONAL_MISSING' | 'SKIPPED';
5
5
  organName?: string;
package/dist/doctor.js CHANGED
@@ -7,7 +7,7 @@ exports.runDoctor = runDoctor;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const discovery_1 = require("./discovery");
10
- const pg_1 = require("pg");
10
+ const schema_validator_1 = require("./schema-validator");
11
11
  async function runDoctor(options = {}) {
12
12
  const projectDir = options.projectDir ? node_path_1.default.resolve(options.projectDir) : process.cwd();
13
13
  const configPath = node_path_1.default.join(projectDir, 'siduri.config.json');
@@ -46,6 +46,29 @@ async function runDoctor(options = {}) {
46
46
  ]);
47
47
  const selectedManifests = [];
48
48
  const results = [];
49
+ // 0. Schema Validation Check
50
+ const schemaPath = node_path_1.default.join(projectDir, 'siduri.schema.json');
51
+ if (node_fs_1.default.existsSync(schemaPath)) {
52
+ try {
53
+ const schema = JSON.parse(node_fs_1.default.readFileSync(schemaPath, 'utf8'));
54
+ (0, schema_validator_1.validateCompanionConfig)(config, schema);
55
+ results.push({
56
+ category: 'Configuration',
57
+ name: 'Schema Conformance',
58
+ status: 'PASS',
59
+ message: 'siduri.config.json conforms to siduri.schema.json',
60
+ });
61
+ }
62
+ catch (err) {
63
+ results.push({
64
+ category: 'Configuration',
65
+ name: 'Schema Conformance',
66
+ status: 'FAIL',
67
+ message: err.message,
68
+ remediation: 'Update siduri.config.json to conform to the generated schema.',
69
+ });
70
+ }
71
+ }
49
72
  // 1. Resolve manifests for selected organs
50
73
  for (const organKey of organKeys) {
51
74
  const m = registry.get(organKey) || registry.getAll().find((item) => item.configKey === organKey || item.organType === organKey);
@@ -118,49 +141,18 @@ async function runDoctor(options = {}) {
118
141
  // 4. Database Check (only if an organ declares database requirement)
119
142
  const dbOrgan = selectedManifests.find((m) => m.database !== null && m.database !== undefined);
120
143
  if (dbOrgan) {
121
- const dbUrl = effectiveEnv.DATABASE_URL;
122
- if (!dbUrl) {
123
- results.push({
124
- category: 'Database',
125
- name: 'PostgreSQL Connection',
126
- status: 'FAIL',
127
- organName: dbOrgan.name,
128
- message: 'DATABASE_URL is not configured.',
129
- remediation: 'Provide DATABASE_URL in .env (e.g. postgresql://postgres:postgres@localhost:5432/siduri)',
130
- });
131
- }
132
- else {
133
- const pool = new pg_1.Pool({ connectionString: dbUrl, connectionTimeoutMillis: 3000 });
134
- try {
135
- const client = await pool.connect();
136
- await client.query('SELECT 1');
137
- client.release();
138
- await pool.end();
139
- results.push({
140
- category: 'Database',
141
- name: 'PostgreSQL Connection',
142
- status: 'PASS',
143
- organName: dbOrgan.name,
144
- message: 'Connection successful',
145
- });
146
- }
147
- catch (err) {
148
- await pool.end().catch(() => { });
149
- results.push({
150
- category: 'Database',
151
- name: 'PostgreSQL Connection',
152
- status: 'FAIL',
153
- organName: dbOrgan.name,
154
- message: `Connection failed: ${err.message}`,
155
- remediation: 'Check database host availability, port, credentials, and network accessibility.',
156
- });
157
- }
158
- }
144
+ results.push({
145
+ category: 'Database',
146
+ name: 'SQLite Database',
147
+ status: 'PASS',
148
+ organName: dbOrgan.name,
149
+ message: 'SQLite operates natively.',
150
+ });
159
151
  }
160
152
  else {
161
153
  results.push({
162
154
  category: 'Database',
163
- name: 'PostgreSQL Database',
155
+ name: 'SQLite Database',
164
156
  status: 'SKIPPED',
165
157
  message: 'Not required by current composition',
166
158
  });
@@ -7,7 +7,6 @@ export interface GeneratedInstanceFiles {
7
7
  'README.md': string;
8
8
  'src/index.js': string;
9
9
  'public/index.html': string;
10
- 'docker-compose.yml'?: string;
11
10
  createAssetsBodyDir?: boolean;
12
11
  createAssetsDirs?: string[];
13
12
  }
package/dist/generator.js CHANGED
@@ -72,7 +72,7 @@ function getDefaultConfigForManifest(manifest) {
72
72
  function generateInstanceFiles(options) {
73
73
  const instanceName = options.name || 'my-siduri';
74
74
  const instanceId = options.id || 'default';
75
- const coreVersion = options.coreVersion || '^1.0.8';
75
+ const coreVersion = options.coreVersion || '^1.0.9';
76
76
  const manifests = options.selectedManifests;
77
77
  const hasMemory = manifests.some((m) => m.organType === 'memory');
78
78
  const hasVoice = manifests.some((m) => m.organType === 'voice');
@@ -82,56 +82,7 @@ function generateInstanceFiles(options) {
82
82
  const isVoicevox = hasVoice && (!voiceConfig || voiceConfig.provider === 'voicevox');
83
83
  const memoryConfig = options.organConfigs?.memory || options.organConfigs?.['@siduri-x/memory'];
84
84
  const isPostgresLocal = hasMemory && (!memoryConfig || memoryConfig.deployment === 'local' || memoryConfig.provider === 'postgres');
85
- // 1. Optional docker-compose.yml
86
- let dockerComposeYaml;
87
- const dockerServices = [];
88
- const dockerVolumes = [];
89
- if (hasMemory && isPostgresLocal) {
90
- dockerServices.push([
91
- ' db:',
92
- ' image: postgres:15',
93
- ' environment:',
94
- ' POSTGRES_USER: postgres',
95
- ' POSTGRES_PASSWORD: password',
96
- ' POSTGRES_DB: siduri',
97
- ' ports:',
98
- ' - "5432:5432"',
99
- ' volumes:',
100
- ' - postgres_data:/var/lib/postgresql/data',
101
- ' healthcheck:',
102
- ' test: ["CMD-SHELL", "pg_isready -U postgres"]',
103
- ' interval: 2s',
104
- ' timeout: 5s',
105
- ' retries: 5',
106
- ].join('\n'));
107
- dockerVolumes.push(' postgres_data:');
108
- }
109
- if (hasVoice && voiceConfig?.rvc?.enabled) {
110
- dockerServices.push([
111
- ' rvc:',
112
- ' image: ghcr.io/vxnus-studio/rvc-headless:latest',
113
- ' ports:',
114
- ' - "50055:50055"',
115
- ' volumes:',
116
- ' - ./assets/voice:/app/models',
117
- ' environment:',
118
- ' - RVC_MODELS_DIR=/app/models',
119
- ].join('\n'));
120
- }
121
- if (dockerServices.length > 0) {
122
- const composeLines = [
123
- 'version: "3.8"',
124
- '',
125
- 'services:',
126
- ...dockerServices,
127
- ];
128
- if (dockerVolumes.length > 0) {
129
- composeLines.push('', 'volumes:', ...dockerVolumes);
130
- }
131
- composeLines.push('');
132
- dockerComposeYaml = composeLines.join('\n');
133
- }
134
- // 2. package.json
85
+ // 1. package.json
135
86
  const dependencies = {
136
87
  '@siduri-x/core': coreVersion,
137
88
  };
@@ -144,11 +95,6 @@ function generateInstanceFiles(options) {
144
95
  doctor: 'siduri doctor',
145
96
  db: 'siduri db',
146
97
  };
147
- if (dockerComposeYaml) {
148
- scripts['services:up'] = 'docker compose up -d';
149
- scripts['services:down'] = 'docker compose down';
150
- scripts['services:logs'] = 'docker compose logs -f';
151
- }
152
98
  const packageJsonObj = {
153
99
  name: instanceName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'my-siduri',
154
100
  private: true,
@@ -215,7 +161,7 @@ function generateInstanceFiles(options) {
215
161
  `import { readFile, stat, readdir } from 'node:fs/promises';`,
216
162
  `import path from 'node:path';`,
217
163
  `import { fileURLToPath } from 'node:url';`,
218
- `import { SiduriRuntime, dispatchCompanionChat } from '@siduri-x/core';`,
164
+ `import { SiduriRuntime, dispatchCompanionChat, validateCompanionConfig } from '@siduri-x/core';`,
219
165
  ];
220
166
  for (const m of manifests) {
221
167
  importLines.push(`import { ${m.factory} } from '${m.name}';`);
@@ -241,6 +187,16 @@ function generateInstanceFiles(options) {
241
187
  `const config = JSON.parse(`,
242
188
  ` await readFile(path.join(rootDir, 'siduri.config.json'), 'utf8')`,
243
189
  `);`,
190
+ `const schemaPath = path.join(rootDir, 'siduri.schema.json');`,
191
+ `try {`,
192
+ ` const schema = JSON.parse(await readFile(schemaPath, 'utf8'));`,
193
+ ` validateCompanionConfig(config, schema);`,
194
+ `} catch (err) {`,
195
+ ` if (err?.code !== 'ENOENT') {`,
196
+ ` console.error('Config validation failed:', err.message);`,
197
+ ` throw err;`,
198
+ ` }`,
199
+ `}`,
244
200
  '',
245
201
  ...instantiationLines,
246
202
  '',
@@ -621,20 +577,16 @@ function generateInstanceFiles(options) {
621
577
  '- **Environment**: Valid `.env` file (configured from `.env.example`)',
622
578
  ];
623
579
  if (hasMemory) {
624
- readmeLines.push('- **Local Services (Optional)**: Docker (or standalone alternatives):');
625
- readmeLines.push(' - **PostgreSQL**: Required for memory claims & durable state (or use cloud Supabase/Neon)');
580
+ readmeLines.push('- **PostgreSQL (Optional)**: Required if PostgreSQL memory organ is enabled (local PostgreSQL server or cloud Supabase/Neon).');
626
581
  }
627
582
  if (isVoicevox) {
628
583
  readmeLines.push('- **VOICEVOX**: Note: The Voicevox engine executable will be securely auto-downloaded at runtime by Siduri if no local URL is provided.');
629
584
  }
630
585
  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`.');
631
- if (dockerComposeYaml) {
632
- readmeLines.push('', '### 3. Start Local Services (Docker)', '```bash', 'npm run services:up', '```', '*(To stop services later, run `npm run services:down`)*');
633
- }
634
586
  if (hasMemory) {
635
- readmeLines.push('', `### ${dockerComposeYaml ? '4' : '3'}. Database Migrations`, 'Ensure `DATABASE_URL` in `.env` is reachable, then push the memory organ PostgreSQL schema:', '```bash', 'npx @vxnus/siduri db push', '```');
587
+ readmeLines.push('', '### 3. Database Migrations', 'Ensure `DATABASE_URL` in `.env` is reachable, then push the memory organ PostgreSQL schema:', '```bash', 'npx @vxnus/siduri db push', '```');
636
588
  }
637
- readmeLines.push('', `### ${hasMemory ? (dockerComposeYaml ? '5' : '4') : (dockerComposeYaml ? '4' : '3')}. Diagnostics & Health Probe`, 'Verify all environment variables, services, and organ connections:', '```bash', 'npm run doctor', '```', '', `### ${hasMemory ? (dockerComposeYaml ? '6' : '5') : (dockerComposeYaml ? '5' : '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.');
589
+ readmeLines.push('', `### ${hasMemory ? '4' : '3'}. Diagnostics & Health Probe`, 'Verify all environment variables, schema conformance, services, and organ connections:', '```bash', 'npm run doctor', '```', '', `### ${hasMemory ? '5' : '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.');
638
590
  const companionSlug = instanceName.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'default';
639
591
  const createAssetsDirs = [];
640
592
  if (hasBody) {
@@ -659,8 +611,5 @@ function generateInstanceFiles(options) {
659
611
  createAssetsBodyDir: hasBody,
660
612
  createAssetsDirs,
661
613
  };
662
- if (dockerComposeYaml) {
663
- result['docker-compose.yml'] = dockerComposeYaml;
664
- }
665
614
  return result;
666
615
  }
@@ -33,7 +33,7 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
33
33
  version: '1.0.0',
34
34
  displayName: 'Memory (PostgreSQL FTS Claims)',
35
35
  entrypoint: './dist/index.js',
36
- factory: 'PostgresMemoryOrgan',
36
+ factory: 'SqliteMemoryStore',
37
37
  configKey: 'memory',
38
38
  configSchema: { type: 'object', properties: { provider: { type: 'string' } } },
39
39
  environment: [{ name: 'DATABASE_URL', required: true, secret: true }],
@@ -158,14 +158,13 @@ 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 (voicevox is auto-downloaded at runtime)
162
- expect(files['docker-compose.yml']).toBeDefined();
163
- expect(files['docker-compose.yml']).toContain('postgres:15');
161
+ // No docker compose generated (Docker completely removed, host-native runtime)
162
+ expect(files['docker-compose.yml']).toBeUndefined();
164
163
  expect(files['README.md']).toContain('Voicevox engine executable will be securely auto-downloaded');
165
- expect(pkg.scripts['services:up']).toBe('docker compose up -d');
166
- expect(pkg.scripts['services:down']).toBe('docker compose down');
164
+ expect(pkg.scripts['services:up']).toBeUndefined();
165
+ expect(pkg.scripts['services:down']).toBeUndefined();
167
166
  });
168
- test('Composition E: Brain only has no docker-compose.yml', () => {
167
+ test('Composition E: Standalone companion never generates docker-compose.yml and validates config', () => {
169
168
  const files = (0, generator_1.generateInstanceFiles)({
170
169
  name: 'brain-solo',
171
170
  selectedManifests: [MOCK_MANIFESTS.brain],
@@ -173,6 +172,7 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
173
172
  expect(files['docker-compose.yml']).toBeUndefined();
174
173
  const pkg = JSON.parse(files['package.json']);
175
174
  expect(pkg.scripts['services:up']).toBeUndefined();
175
+ expect(files['src/index.js']).toContain('validateCompanionConfig');
176
176
  expect(files['public/index.html']).toBeDefined();
177
177
  expect(files['public/index.html']).toContain('Memory Console');
178
178
  expect(files['public/index.html']).toContain('Companion Chat');
package/dist/index.js CHANGED
@@ -241,9 +241,6 @@ async function runCreateWizard(targetDir) {
241
241
  else {
242
242
  await (0, promises_1.writeFile)(node_path_1.default.join(publicDir, 'index.html'), files['public/index.html'], 'utf8');
243
243
  }
244
- if (files['docker-compose.yml']) {
245
- await (0, promises_1.writeFile)(node_path_1.default.join(projectDir, 'docker-compose.yml'), files['docker-compose.yml'], 'utf8');
246
- }
247
244
  if (files.createAssetsDirs && files.createAssetsDirs.length > 0) {
248
245
  for (const dir of files.createAssetsDirs) {
249
246
  await (0, promises_1.mkdir)(node_path_1.default.join(projectDir, dir), { recursive: true });
@@ -267,9 +264,6 @@ async function runCreateWizard(targetDir) {
267
264
  console.log(`Your Siduri companion is ready! Next steps:\n`);
268
265
  console.log(` cd ${node_path_1.default.relative(process.cwd(), projectDir) || '.'}`);
269
266
  console.log(` cp .env.example .env ${exports.colors.dim}# Fill in required API keys/credentials${exports.colors.reset}`);
270
- if (files['docker-compose.yml']) {
271
- console.log(` npm run services:up ${exports.colors.dim}# Start local Docker services (PostgreSQL / VOICEVOX)${exports.colors.reset}`);
272
- }
273
267
  const hasMemory = selectedManifests.some((m) => m.organType === 'memory');
274
268
  if (hasMemory) {
275
269
  console.log(` npx @vxnus/siduri db push ${exports.colors.dim}# Push PostgreSQL memory schema${exports.colors.reset}`);
@@ -19,11 +19,12 @@ function runReleaseCheck(repoRoot = node_path_1.default.resolve(__dirname, '../.
19
19
  const getPkgVer = (dir) => JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.resolve(repoRoot, dir, 'package.json'), 'utf8')).version;
20
20
  const canonicalPackages = [
21
21
  { name: '@siduri-x/core', dir: 'packages/core', isOrgan: false, tarName: `siduri-x-core-${getPkgVer('packages/core')}.tgz` },
22
+ { name: '@siduri-x/self', dir: 'packages/self', isOrgan: false, tarName: `siduri-x-self-${getPkgVer('packages/self')}.tgz` },
23
+ { name: '@siduri-x/knowledge', dir: 'packages/knowledge', isOrgan: false, tarName: `siduri-x-knowledge-${getPkgVer('packages/knowledge')}.tgz` },
24
+ { name: '@siduri-x/memory', dir: 'packages/memory', isOrgan: false, tarName: `siduri-x-memory-${getPkgVer('packages/memory')}.tgz` },
22
25
  { name: '@siduri-x/brain', dir: 'packages/organs/brain', isOrgan: true, tarName: `siduri-x-brain-${getPkgVer('packages/organs/brain')}.tgz` },
23
- { name: '@siduri-x/memory', dir: 'packages/organs/memory', isOrgan: true, tarName: `siduri-x-memory-${getPkgVer('packages/organs/memory')}.tgz` },
24
- { name: '@siduri-x/knowledge', dir: 'packages/organs/knowledge', isOrgan: true, tarName: `siduri-x-knowledge-${getPkgVer('packages/organs/knowledge')}.tgz` },
25
- { name: '@siduri-x/behavior', dir: 'packages/organs/behavior', isOrgan: true, tarName: `siduri-x-behavior-${getPkgVer('packages/organs/behavior')}.tgz` },
26
26
  { name: '@siduri-x/ear', dir: 'packages/organs/ear', isOrgan: true, tarName: `siduri-x-ear-${getPkgVer('packages/organs/ear')}.tgz` },
27
+ { name: '@siduri-x/eknowledge', dir: 'packages/organs/eknowledge', isOrgan: true, tarName: `siduri-x-eknowledge-${getPkgVer('packages/organs/eknowledge')}.tgz` },
27
28
  { name: '@siduri-x/vision', dir: 'packages/organs/vision', isOrgan: true, tarName: `siduri-x-vision-${getPkgVer('packages/organs/vision')}.tgz` },
28
29
  { name: '@siduri-x/hands', dir: 'packages/organs/hands', isOrgan: true, tarName: `siduri-x-hands-${getPkgVer('packages/organs/hands')}.tgz` },
29
30
  { name: '@siduri-x/body', dir: 'packages/organs/body', isOrgan: true, tarName: `siduri-x-body-${getPkgVer('packages/organs/body')}.tgz` },
@@ -88,11 +89,6 @@ function runReleaseCheck(repoRoot = node_path_1.default.resolve(__dirname, '../.
88
89
  manifestsValidated++;
89
90
  }
90
91
  }
91
- if (pkg.name === '@siduri-x/memory') {
92
- if (!listing.some((l) => l.includes('package/migrations/001_initial_schema.sql'))) {
93
- errors.push(`${pkg.name}: migrations/001_initial_schema.sql missing in tarball`);
94
- }
95
- }
96
92
  if (pkg.name === '@vxnus/siduri') {
97
93
  if (!pkgJson.bin || !pkgJson.bin.siduri) {
98
94
  errors.push(`${pkg.name}: bin.siduri field missing in package.json`);
@@ -131,7 +131,8 @@ describe('Localweb and CLI Standalone 1:1 Runtime & Chat Parity', () => {
131
131
  });
132
132
  const srcIndexJs = files['src/index.js'];
133
133
  // Ensures single source of truth import
134
- expect(srcIndexJs).toContain("import { SiduriRuntime, dispatchCompanionChat } from '@siduri-x/core';");
134
+ expect(srcIndexJs).toContain("import { SiduriRuntime, dispatchCompanionChat, validateCompanionConfig } from '@siduri-x/core';");
135
+ expect(srcIndexJs).toContain('validateCompanionConfig(config, schema);');
135
136
  // Ensures /chat uses dispatchCompanionChat directly without ad-hoc runtimeResult envelope
136
137
  expect(srcIndexJs).toContain('const response = await dispatchCompanionChat(runtime,');
137
138
  expect(srcIndexJs).toContain('res.end(JSON.stringify(response));');
@@ -0,0 +1,9 @@
1
+ export declare class ConfigValidationError extends Error {
2
+ errors: string[];
3
+ constructor(errors: string[]);
4
+ }
5
+ /**
6
+ * Validates a companion configuration object against a JSON schema (draft-07 compatible).
7
+ * Throws ConfigValidationError if validation errors are detected.
8
+ */
9
+ export declare function validateCompanionConfig(config: unknown, schema: any, path?: string): void;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigValidationError = void 0;
4
+ exports.validateCompanionConfig = validateCompanionConfig;
5
+ class ConfigValidationError extends Error {
6
+ errors;
7
+ constructor(errors) {
8
+ super(`Siduri configuration validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
9
+ this.name = 'ConfigValidationError';
10
+ this.errors = errors;
11
+ }
12
+ }
13
+ exports.ConfigValidationError = ConfigValidationError;
14
+ /**
15
+ * Validates a companion configuration object against a JSON schema (draft-07 compatible).
16
+ * Throws ConfigValidationError if validation errors are detected.
17
+ */
18
+ function validateCompanionConfig(config, schema, path = '$') {
19
+ const errors = [];
20
+ function validateNode(value, nodeSchema, curPath) {
21
+ if (!nodeSchema || typeof nodeSchema !== 'object')
22
+ return;
23
+ // Type validation
24
+ if (nodeSchema.type !== undefined) {
25
+ const type = nodeSchema.type;
26
+ if (type === 'object') {
27
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
28
+ errors.push(`${curPath}: expected object, received ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`);
29
+ return;
30
+ }
31
+ }
32
+ else if (type === 'array') {
33
+ if (!Array.isArray(value)) {
34
+ errors.push(`${curPath}: expected array, received ${typeof value}`);
35
+ return;
36
+ }
37
+ }
38
+ else if (type === 'string') {
39
+ if (typeof value !== 'string') {
40
+ errors.push(`${curPath}: expected string, received ${typeof value}`);
41
+ return;
42
+ }
43
+ }
44
+ else if (type === 'number') {
45
+ if (typeof value !== 'number' || Number.isNaN(value)) {
46
+ errors.push(`${curPath}: expected number, received ${typeof value}`);
47
+ return;
48
+ }
49
+ }
50
+ else if (type === 'integer') {
51
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
52
+ errors.push(`${curPath}: expected integer, received ${typeof value}`);
53
+ return;
54
+ }
55
+ }
56
+ else if (type === 'boolean') {
57
+ if (typeof value !== 'boolean') {
58
+ errors.push(`${curPath}: expected boolean, received ${typeof value}`);
59
+ return;
60
+ }
61
+ }
62
+ }
63
+ // Enum validation
64
+ if (Array.isArray(nodeSchema.enum)) {
65
+ if (!nodeSchema.enum.includes(value)) {
66
+ errors.push(`${curPath}: invalid value ${JSON.stringify(value)}, expected one of: ${nodeSchema.enum.map((v) => JSON.stringify(v)).join(', ')}`);
67
+ return;
68
+ }
69
+ }
70
+ // Object properties validation
71
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
72
+ if (Array.isArray(nodeSchema.required)) {
73
+ for (const reqKey of nodeSchema.required) {
74
+ if (value[reqKey] === undefined) {
75
+ errors.push(`${curPath}.${reqKey}: is required`);
76
+ }
77
+ }
78
+ }
79
+ const definedProps = nodeSchema.properties || {};
80
+ if (nodeSchema.additionalProperties === false) {
81
+ for (const key of Object.keys(value)) {
82
+ // Allow $schema property at root level
83
+ if (curPath === '$' && key === '$schema')
84
+ continue;
85
+ if (!(key in definedProps)) {
86
+ errors.push(`${curPath}.${key}: unexpected property is not allowed`);
87
+ }
88
+ }
89
+ }
90
+ for (const [propName, propSchema] of Object.entries(definedProps)) {
91
+ if (value[propName] !== undefined) {
92
+ validateNode(value[propName], propSchema, `${curPath}.${propName}`);
93
+ }
94
+ }
95
+ }
96
+ // Array items validation
97
+ if (Array.isArray(value) && nodeSchema.items) {
98
+ value.forEach((item, index) => {
99
+ validateNode(item, nodeSchema.items, `${curPath}[${index}]`);
100
+ });
101
+ }
102
+ }
103
+ validateNode(config, schema, path);
104
+ if (errors.length > 0) {
105
+ throw new ConfigValidationError(errors);
106
+ }
107
+ }
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><html lang="en" class="h-full antialiased dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/chunks/0xhe1hasjan-9.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/2kcqevsmv6kh2.js"/><script src="/_next/static/chunks/1_yae15ffa5f6.js" async=""></script><script src="/_next/static/chunks/3bx2gs3idjmo1.js" async=""></script><script src="/_next/static/chunks/32z87id9k6i2z.js" async=""></script><script src="/_next/static/chunks/turbopack-3eipe4c-imn5l.js" async=""></script><script src="/_next/static/chunks/0uwih7sydv413.js" async="" crossorigin=""></script><link rel="preload" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;family=Noto+Sans+JP:wght@400;500;700&amp;family=Noto+Serif+JP:wght@400;500;700&amp;display=swap" as="style"/><meta name="robots" content="noindex"/><link rel="preconnect" href="https://fonts.googleapis.com"/><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/><title>404: This page could not be found.</title><title>Siduri — Local-First Companion</title><meta name="description" content="Siduri — a local-first companion with explicit memory, grounded knowledge, and optional presence."/><link rel="icon" href="/favicon.ico?favicon.3rlors1ui1gw9.ico" sizes="256x256" type="image/x-icon"/><link rel="icon" href="/favicon.ico"/><link rel="icon" href="/favicon.svg" type="image/svg+xml"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;family=Noto+Sans+JP:wght@400;500;700&amp;family=Noto+Serif+JP:wght@400;500;700&amp;display=swap" rel="stylesheet"/><script src="/_next/static/chunks/0cz1d0mv5g_q7.js" noModule=""></script></head><body class="min-h-full flex flex-col bg-[#0b0b0e] text-[#eee8df] selection:bg-[#d99a68]/30 selection:text-[#f4c28d]"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/2kcqevsmv6kh2.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[81869,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"default\"]\n3:I[57830,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"default\"]\n4:I[44101,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"OutletBoundary\"]\n5:\"$Sreact.suspense\"\n8:I[44101,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"ViewportBoundary\"]\na:I[44101,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"MetadataBoundary\"]\nc:I[4626,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"default\",1]\n:HL[\"/_next/static/chunks/0xhe1hasjan-9.css\",\"style\"]\n:HL[\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500\u0026family=Noto+Sans+JP:wght@400;500;700\u0026family=Noto+Serif+JP:wght@400;500;700\u0026display=swap\",\"style\"]\n7:X\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"_not-found\",\"\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},\"$undefined\",\"$undefined\",4608]},\"$undefined\",\"$undefined\",4608]},\"$undefined\",\"$undefined\",4624],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/0xhe1hasjan-9.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/0uwih7sydv413.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"h-full antialiased dark\",\"children\":[[\"$\",\"head\",null,{\"children\":[[\"$\",\"link\",null,{\"rel\":\"preconnect\",\"href\":\"https://fonts.googleapis.com\"}],[\"$\",\"link\",null,{\"rel\":\"preconnect\",\"href\":\"https://fonts.gstatic.com\",\"crossOrigin\":\"anonymous\"}],[\"$\",\"link\",null,{\"href\":\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500\u0026family=Noto+Sans+JP:wght@400;500;700\u0026family=Noto+Serif+JP:wght@400;500;700\u0026display=swap\",\"rel\":\"stylesheet\"}]]}],[\"$\",\"body\",null,{\"className\":\"min-h-full flex flex-col bg-[#0b0b0e] text-[#eee8df] selection:bg-[#d99a68]/30 selection:text-[#f4c28d]\",\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:1:props:children:props:notFound:0:1:props:style\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$0:f:0:1:0:props:children:1:props:children:1:props:children:props:notFound:0:1:props:children:props:children:1:props:style\",\"children\":404}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:1:props:children:props:notFound:0:1:props:children:props:children:2:props:style\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:1:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style\",\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$5\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@6\"}]}]]}],{},null,false,null]},null,false,\"$7\"]},null,false,null],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$La\",null,{\"children\":[\"$\",\"$5\",null,{\"name\":\"Next.Metadata\",\"children\":\"$Lb\"}]}]}],null]}],false]],\"m\":\"$undefined\",\"G\":[\"$c\",[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/0xhe1hasjan-9.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]]],\"S\":true,\"h\":null,\"r\":\"$undefined\",\"s\":\"$undefined\",\"a\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"pWHUixFDDvANZeAf-shqT\"}\n"])</script><script>self.__next_f.push([1,"7:C\n9:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"d:I[89236,[\"/_next/static/chunks/0uwih7sydv413.js\"],\"IconMark\"]\n6:null\nb:[[\"$\",\"title\",\"0\",{\"children\":\"Siduri — Local-First Companion\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Siduri — a local-first companion with explicit memory, grounded knowledge, and optional presence.\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.3rlors1ui1gw9.ico\",\"sizes\":\"256x256\",\"type\":\"image/x-icon\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/favicon.svg\",\"type\":\"image/svg+xml\"}],[\"$\",\"link\",\"5\",{\"rel\":\"apple-touch-icon\",\"href\":\"/apple-touch-icon.png\"}],[\"$\",\"$Ld\",\"6\",{}]]\n"])</script></body></html>