@vxnus/siduri 2.0.3 → 2.0.5

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.
@@ -6,7 +6,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
6
6
  name: '@siduri-x/self',
7
7
  organType: 'behavior',
8
8
  version: '2.0.2',
9
- displayName: 'Behavior / Self (Active Self Directives)',
9
+ displayName: 'Behavior & Self (Personality Directives)',
10
10
  description: 'Atomic directive state machine and personality projection compiler',
11
11
  entrypoint: './dist/index.js',
12
12
  factory: 'ActiveSelfCompiler',
@@ -207,8 +207,8 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
207
207
  {
208
208
  name: '@siduri-x/knowledge',
209
209
  organType: 'knowledge',
210
- version: '2.0.1',
211
- displayName: 'Knowledge (Life DB & Portable E-Packs)',
210
+ version: '2.0.2',
211
+ displayName: 'Knowledge (Sovereign Life DB & Knowledge Packs)',
212
212
  description: 'Sovereign Life Database (finances, inventory, schedule, preferences) and portable E knowledge packs',
213
213
  entrypoint: './dist/index.js',
214
214
  factory: 'UnifiedKnowledgeOrgan',
@@ -286,7 +286,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
286
286
  name: '@siduri-x/memory',
287
287
  organType: 'memory',
288
288
  version: '2.0.1',
289
- displayName: 'Memory (SQLite FTS5 Claims)',
289
+ displayName: 'Memory (Long-term Recall & State)',
290
290
  description: 'Relational semantic claims with Full-Text Search (FTS5) and companion isolation',
291
291
  entrypoint: './dist/index.js',
292
292
  factory: 'SqliteMemoryStore',
@@ -380,9 +380,9 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
380
380
  {
381
381
  name: '@siduri-x/voice',
382
382
  organType: 'voice',
383
- version: '2.0.1',
384
- displayName: 'Voice (VOICEVOX Speech Synthesis)',
385
- description: 'Queued speech synthesis and audio rendering lifecycle adapter',
383
+ version: '2.0.2',
384
+ displayName: 'Voice (Speech Synthesis & Voice Conversion)',
385
+ description: 'Queued speech synthesis and audio rendering lifecycle adapter (VOICEVOX, RVC)',
386
386
  entrypoint: './dist/index.js',
387
387
  factory: 'VoicevoxAdapter',
388
388
  configKey: 'voice',
@@ -392,7 +392,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
392
392
  properties: {
393
393
  provider: {
394
394
  type: 'string',
395
- enum: ['voicevox', 'none']
395
+ enum: ['voicevox', 'edge-tts', 'kokoro', 'piper', 'none']
396
396
  },
397
397
  speakerId: {
398
398
  type: 'number',
@@ -5,90 +5,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.configureKnowledge = configureKnowledge;
7
7
  const inquirer_1 = __importDefault(require("inquirer"));
8
- const node_fs_1 = __importDefault(require("node:fs"));
9
8
  const knowledge_hub_1 = require("../providers/knowledge-hub");
10
9
  async function configureKnowledge(_context, options = {}) {
11
10
  const client = options.client || new knowledge_hub_1.KnowledgeHubClient();
12
- // 1. Ask Knowledge source / mode
13
- const { source } = await inquirer_1.default.prompt({
14
- type: 'list',
15
- name: 'source',
16
- message: 'Select knowledge configuration:',
17
- choices: [
18
- { name: 'E Knowledge Hub / Portable Pack (with Life Database)', value: 'e-hub' },
19
- { name: 'Life Database Only (SQLite personal state, schedule, preferences)', value: 'life-only' },
20
- { name: 'Do not use knowledge', value: 'none' },
21
- ],
22
- });
23
- if (source === 'none') {
24
- return {
25
- config: {
26
- provider: 'none',
27
- lifeDatabase: false,
28
- },
29
- summary: {
30
- Source: 'Do not use knowledge',
31
- Knowledge: 'Disabled',
32
- },
33
- };
34
- }
35
- const summary = {};
36
11
  const config = {
12
+ provider: 'unified',
37
13
  dbPath: 'siduri.sqlite',
38
14
  lifeDatabase: true,
39
15
  };
40
- if (source === 'life-only') {
41
- config.provider = 'unified';
42
- summary.Source = 'Life Database';
43
- summary['Life Database'] = 'Enabled (siduri.sqlite)';
16
+ const summary = {
17
+ 'Life Database': 'Enabled (siduri.sqlite)',
18
+ };
19
+ // 1. Life Database is the sovereign default. Ask whether to attach an external knowledge pack
20
+ const { useExternalPack } = await inquirer_1.default.prompt({
21
+ type: 'confirm',
22
+ name: 'useExternalPack',
23
+ message: 'Attach an external knowledge pack from E Knowledge Hub?',
24
+ default: false,
25
+ });
26
+ if (!useExternalPack) {
44
27
  summary['E-Pack'] = 'None';
45
28
  return { config, summary };
46
29
  }
47
- // 2. Discovery-First E-Knowledge Resolution Flow
30
+ // 2. Discover and resolve E Knowledge Hub package
48
31
  while (true) {
49
32
  const promptRes = await inquirer_1.default.prompt({
50
33
  type: 'input',
51
34
  name: 'packId',
52
- message: 'Enter package ID (@publisher/name), local pack path, or remote URL:',
35
+ message: 'Enter package ID (@publisher/name):',
53
36
  default: '@vxnus/e-teyvat',
54
- validate: (val) => val.trim().length > 0 || 'Please enter a package ID, path, or URL.',
37
+ validate: (val) => val.trim().length > 0 || 'Please enter a package ID.',
55
38
  });
56
39
  const target = (promptRes.packId || promptRes.query || '').trim();
57
- // Case A: Remote HTTP endpoint
58
- if (target.startsWith('http://') || target.startsWith('https://')) {
59
- console.log(`\n\x1b[36m── Knowledge Provider ────────────────────────\x1b[0m\n`);
60
- console.log(` \x1b[2mType:\x1b[0m Remote E-Provider`);
61
- console.log(` \x1b[2mEndpoint:\x1b[0m ${target}`);
62
- console.log(` \x1b[2mConnectors:\x1b[0m Remote only (HTTP stream)\n`);
63
- config.provider = 'e-remote';
64
- config.baseUrl = target;
65
- config.pack = {
66
- mode: 'remote',
67
- baseUrl: target,
68
- };
69
- summary.Source = 'Remote E-Provider';
70
- summary.Provider = target;
71
- summary['E-Pack'] = `Remote (${target})`;
72
- return { config, summary };
73
- }
74
- // Case B: Local path / file
75
- if (target.startsWith('./') || target.startsWith('/') || node_fs_1.default.existsSync(target)) {
76
- console.log(`\n\x1b[36m── Knowledge Provider ────────────────────────\x1b[0m\n`);
77
- console.log(` \x1b[2mType:\x1b[0m Local Knowledge Pack`);
78
- console.log(` \x1b[2mPath:\x1b[0m ${target}`);
79
- console.log(` \x1b[2mConnectors:\x1b[0m Local only (Filesystem)\n`);
80
- config.provider = 'e-knowledge';
81
- config.packPath = target;
82
- config.pack = {
83
- mode: 'local',
84
- packPath: target,
85
- };
86
- summary.Source = 'Local Knowledge Pack';
87
- summary.Provider = target;
88
- summary['E-Pack'] = `Local (${target})`;
89
- return { config, summary };
90
- }
91
- // Case C: E Knowledge Hub package ID (@publisher/name)
92
40
  let manifest;
93
41
  try {
94
42
  process.stdout.write('\x1b[2mResolving knowledge metadata...\x1b[0m');
@@ -103,82 +51,90 @@ async function configureKnowledge(_context, options = {}) {
103
51
  name: 'failureAction',
104
52
  message: 'What would you like to do?',
105
53
  choices: [
106
- { name: 'Try again / Enter another package ID or path', value: 'retry' },
107
- { name: 'Skip attaching E-pack (keep Life DB only)', value: 'skip' },
54
+ { name: 'Try again / Enter another package ID', value: 'retry' },
55
+ { name: 'Continue without external pack (Life DB only)', value: 'skip' },
108
56
  { name: 'Cancel', value: 'cancel' },
109
57
  ],
110
58
  });
111
59
  if (failureAction === 'retry')
112
60
  continue;
113
61
  if (failureAction === 'skip') {
114
- config.provider = 'unified';
115
- summary.Source = 'Life Database';
116
62
  summary['E-Pack'] = 'None';
117
63
  return { config, summary };
118
64
  }
119
65
  throw new Error('Knowledge configuration cancelled.');
120
66
  }
121
- // Display metadata
67
+ // Display provider summary
122
68
  (0, knowledge_hub_1.displayKnowledgeProviderSummary)(manifest, target);
123
- // Inspect supported connectors
124
- const hasRemoteUrl = Boolean(manifest.distribution?.url);
125
- const connectorChoices = [];
126
- if (hasRemoteUrl) {
127
- connectorChoices.push({
128
- name: `Remote (Stream directly from ${manifest.distribution?.url} without local storage)`,
129
- value: 'remote',
130
- });
131
- connectorChoices.push({
132
- name: 'Local (Download and run offline pack files on this machine)',
133
- value: 'local',
134
- });
135
- connectorChoices.push({
136
- name: 'Hybrid (Local pack cache with remote provider fallback/sync)',
137
- value: 'hybrid',
69
+ // Determine connection type: User selects strictly 1 mode (Remote OR Local)
70
+ let connectionType;
71
+ const isHybrid = manifest.distributionType === 'both';
72
+ const isLocalOnly = manifest.distributionType === 'local' || manifest.distribution?.kind === 'archive';
73
+ if (isHybrid) {
74
+ const { selectedMode } = await inquirer_1.default.prompt({
75
+ type: 'list',
76
+ name: 'selectedMode',
77
+ message: 'This pack offers both remote and local options. Select connection type:',
78
+ choices: [
79
+ {
80
+ name: `Remote (Stream directly from ${manifest.distribution?.url || 'provider'} without local storage)`,
81
+ value: 'remote',
82
+ },
83
+ {
84
+ name: 'Local (Download archive and run offline on this machine)',
85
+ value: 'local',
86
+ },
87
+ ],
138
88
  });
89
+ connectionType = selectedMode;
90
+ }
91
+ else if (isLocalOnly) {
92
+ connectionType = 'local';
139
93
  }
140
94
  else {
141
- connectorChoices.push({
142
- name: 'Local (Offline pack files on this machine)',
143
- value: 'local',
144
- });
95
+ connectionType = 'remote';
145
96
  }
146
- connectorChoices.push({
147
- name: 'Choose another provider',
148
- value: 'retry',
149
- });
150
- const choiceRes = await inquirer_1.default.prompt({
151
- type: 'list',
152
- name: 'connectorMode',
153
- message: 'Choose connection type for this pack:',
154
- choices: connectorChoices,
155
- });
156
- const rawChoice = choiceRes.connectorMode ?? choiceRes.confirmProvider;
157
- if (rawChoice === 'retry') {
158
- continue;
97
+ if (connectionType === 'remote') {
98
+ config.provider = 'e-hub';
99
+ config.registryUrl = 'https://e.vxnus.xyz/api/v1/knowledge';
100
+ config.packId = target;
101
+ config.pack = {
102
+ mode: 'remote',
103
+ packId: target,
104
+ registryUrl: 'https://e.vxnus.xyz/api/v1/knowledge',
105
+ baseUrl: manifest.distribution?.url,
106
+ preferredMode: 'lexical',
107
+ };
108
+ if (manifest.distribution?.url) {
109
+ config.baseUrl = manifest.distribution.url;
110
+ }
111
+ summary.Source = 'E Knowledge Hub';
112
+ summary.Provider = manifest.displayName || manifest.name;
113
+ summary.Package = target;
114
+ summary.Version = manifest.version;
115
+ summary['E-Pack'] = `${manifest.displayName || manifest.name} (Remote)`;
159
116
  }
160
- const connectorMode = (rawChoice === 'yes'
161
- ? (hasRemoteUrl ? 'remote' : 'local')
162
- : (['remote', 'local', 'hybrid'].includes(rawChoice || '') ? rawChoice : 'local'));
163
- config.provider = 'e-hub';
164
- config.registryUrl = 'https://e.vxnus.xyz/api/v1/knowledge';
165
- config.packId = target;
166
- config.pack = {
167
- mode: connectorMode,
168
- packId: target,
169
- registryUrl: 'https://e.vxnus.xyz/api/v1/knowledge',
170
- baseUrl: manifest.distribution?.url,
171
- preferredMode: 'lexical',
172
- };
173
- if (manifest.distribution?.url) {
174
- config.baseUrl = manifest.distribution.url;
117
+ else {
118
+ // Local archive: stored in our defined path inside companion assets (user doesn't type path)
119
+ const packSlug = target.replace(/^@/, '').replace(/[/_]/g, '-');
120
+ const packDir = `./assets/knowledge/${packSlug}`;
121
+ config.provider = 'e-knowledge';
122
+ config.packPath = packDir;
123
+ config.packId = target;
124
+ config.pack = {
125
+ mode: 'local',
126
+ packId: target,
127
+ packPath: packDir,
128
+ archiveUrl: manifest.distribution?.url,
129
+ checksum: manifest.distribution?.checksum,
130
+ preferredMode: 'lexical',
131
+ };
132
+ summary.Source = 'E Knowledge Hub';
133
+ summary.Provider = manifest.displayName || manifest.name;
134
+ summary.Package = target;
135
+ summary.Version = manifest.version;
136
+ summary['E-Pack'] = `${manifest.displayName || manifest.name} (Local @ ${packDir})`;
175
137
  }
176
- summary.Source = 'E Knowledge Hub';
177
- summary.Provider = manifest.displayName || manifest.name;
178
- summary.Package = target;
179
- summary.Version = manifest.version;
180
- summary['Life Database'] = 'Enabled (siduri.sqlite)';
181
- summary['E-Pack'] = `${manifest.displayName || manifest.name} (${connectorMode})`;
182
138
  return {
183
139
  config,
184
140
  summary,
@@ -10,26 +10,14 @@ async function configureVoice(_context) {
10
10
  const { provider } = await inquirer_1.default.prompt({
11
11
  type: 'list',
12
12
  name: 'provider',
13
- message: 'Voice provider?',
13
+ message: 'Voice engine?',
14
14
  choices: [
15
15
  {
16
- name: 'Edge-TTS (Zero-setup cloud TTS, no downloads/no Python required)',
17
- value: 'edge-tts',
18
- },
19
- {
20
- name: 'VOICEVOX (Standard VOICEVOX stock character voice banks)',
16
+ name: 'VOICEVOX (Pre-trained character voice banks: Zundamon, Shikoku Metan, etc.)',
21
17
  value: 'voicevox',
22
18
  },
23
19
  {
24
- name: 'Kokoro TTS (Fast lightweight neural TTS via HTTP/CLI)',
25
- value: 'kokoro',
26
- },
27
- {
28
- name: 'Piper TTS (Fast local neural TTS via HTTP/CLI)',
29
- value: 'piper',
30
- },
31
- {
32
- name: 'RVC (Custom user-owned voice model via .pth / .index weights)',
20
+ name: 'RVC (Custom character voice model via .pth / .index weights + Base TTS)',
33
21
  value: 'rvc',
34
22
  },
35
23
  {
@@ -44,108 +32,6 @@ async function configureVoice(_context) {
44
32
  summary: { Provider: 'None (Voice disabled)' },
45
33
  };
46
34
  }
47
- if (provider === 'edge-tts') {
48
- const edgeAnswers = await inquirer_1.default.prompt([
49
- {
50
- type: 'input',
51
- name: 'voice',
52
- message: 'Edge-TTS Voice Identifier (e.g. ja-JP-NanamiNeural, en-US-AriaNeural):',
53
- default: 'ja-JP-NanamiNeural',
54
- },
55
- ]);
56
- const voice = edgeAnswers.voice.trim() || 'ja-JP-NanamiNeural';
57
- return {
58
- config: {
59
- provider: 'edge-tts',
60
- voice,
61
- maxQueueDepth: 50,
62
- },
63
- summary: {
64
- Provider: 'Edge-TTS (Cloud Neural)',
65
- Voice: voice,
66
- },
67
- };
68
- }
69
- if (provider === 'kokoro') {
70
- const kokoroAnswers = await inquirer_1.default.prompt([
71
- {
72
- type: 'input',
73
- name: 'baseUrl',
74
- message: 'Kokoro HTTP Base URL (leave empty if using local kokoro-tts CLI):',
75
- default: 'http://localhost:8880',
76
- },
77
- {
78
- type: 'input',
79
- name: 'voice',
80
- message: 'Kokoro Voice Name (e.g. af_bella, af_heart):',
81
- default: 'af_bella',
82
- },
83
- {
84
- type: 'input',
85
- name: 'speed',
86
- message: 'Speech Speed multiplier (e.g. 1.0):',
87
- default: '1.0',
88
- },
89
- ]);
90
- const baseUrl = kokoroAnswers.baseUrl.trim() || undefined;
91
- const voice = kokoroAnswers.voice.trim() || 'af_bella';
92
- const speed = parseFloat(kokoroAnswers.speed) || 1.0;
93
- return {
94
- config: {
95
- provider: 'kokoro',
96
- baseUrl,
97
- voice,
98
- speed,
99
- maxQueueDepth: 50,
100
- },
101
- summary: {
102
- Provider: 'Kokoro TTS (Local / Server)',
103
- Mode: baseUrl ? `HTTP (${baseUrl})` : 'CLI (kokoro-tts)',
104
- Voice: voice,
105
- Speed: speed,
106
- },
107
- };
108
- }
109
- if (provider === 'piper') {
110
- const piperAnswers = await inquirer_1.default.prompt([
111
- {
112
- type: 'input',
113
- name: 'baseUrl',
114
- message: 'Piper HTTP Base URL (leave empty if using local piper CLI):',
115
- default: 'http://localhost:5000',
116
- },
117
- {
118
- type: 'input',
119
- name: 'model',
120
- message: 'Piper Model Path or Name (.onnx, optional):',
121
- default: '',
122
- },
123
- {
124
- type: 'input',
125
- name: 'speakerId',
126
- message: 'Piper Speaker ID (optional):',
127
- default: '',
128
- },
129
- ]);
130
- const baseUrl = piperAnswers.baseUrl.trim() || undefined;
131
- const model = piperAnswers.model.trim() || undefined;
132
- const speakerId = piperAnswers.speakerId.trim() ? parseInt(piperAnswers.speakerId.trim(), 10) : undefined;
133
- return {
134
- config: {
135
- provider: 'piper',
136
- baseUrl,
137
- model,
138
- speakerId,
139
- maxQueueDepth: 50,
140
- },
141
- summary: {
142
- Provider: 'Piper TTS (Local / Server)',
143
- Mode: baseUrl ? `HTTP (${baseUrl})` : 'CLI (piper)',
144
- Model: model || 'default',
145
- 'Speaker ID': speakerId !== undefined ? speakerId : 'default',
146
- },
147
- };
148
- }
149
35
  if (provider === 'rvc') {
150
36
  const rvcAnswers = await inquirer_1.default.prompt([
151
37
  {
@@ -110,16 +110,18 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
110
110
  });
111
111
  });
112
112
  describe('Discrepancy #2 & #3: Knowledge Choices & Manifest Metadata Confirmation', () => {
113
- test('Selecting "Do not use knowledge" sets provider: none and skips discovery', async () => {
114
- inquirer_1.default.prompt.mockResolvedValueOnce({ source: 'none' });
113
+ test('Declining external pack uses sovereign Life Database by default and skips discovery', async () => {
114
+ inquirer_1.default.prompt.mockResolvedValueOnce({ useExternalPack: false });
115
115
  const result = await (0, knowledge_1.configureKnowledge)({
116
116
  companionName: 'Sparkle',
117
117
  manifest: knowledgeManifest,
118
118
  });
119
- expect(result.config.provider).toBe('none');
120
- expect(result.summary?.Source).toBe('Do not use knowledge');
119
+ expect(result.config.provider).toBe('unified');
120
+ expect(result.config.lifeDatabase).toBe(true);
121
+ expect(result.summary?.['Life Database']).toBe('Enabled (siduri.sqlite)');
122
+ expect(result.summary?.['E-Pack']).toBe('None');
121
123
  });
122
- test('Selecting "E Knowledge Hub" discovers manifest, extracts capabilities, and confirms provider', async () => {
124
+ test('Attaching E Knowledge Hub pack discovers manifest and configures remote provider automatically', async () => {
123
125
  const mockClient = {
124
126
  resolveProvider: jest.fn().mockResolvedValue({
125
127
  name: 'e-teyvat',
@@ -128,13 +130,17 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
128
130
  version: '1.2.0',
129
131
  description: 'Teyvat knowledge provider for Siduri.',
130
132
  capabilities: ['Search', 'Retrieval', 'Context injection'],
133
+ distributionType: 'remote',
134
+ distribution: {
135
+ kind: 'provider',
136
+ url: 'https://teyvat.e.vxnus.xyz',
137
+ },
131
138
  source: 'E Knowledge Hub',
132
139
  }),
133
140
  };
134
141
  inquirer_1.default.prompt
135
- .mockResolvedValueOnce({ source: 'e-hub' }) // Knowledge source
136
- .mockResolvedValueOnce({ packId: '@vxnus/e-teyvat' }) // Package ID
137
- .mockResolvedValueOnce({ confirmProvider: 'yes' }); // Confirm provider
142
+ .mockResolvedValueOnce({ useExternalPack: true }) // Attach external pack
143
+ .mockResolvedValueOnce({ packId: '@vxnus/e-teyvat' }); // Package ID
138
144
  const result = await (0, knowledge_1.configureKnowledge)({ companionName: 'Sparkle', manifest: knowledgeManifest }, { client: mockClient });
139
145
  expect(result.config.provider).toBe('e-hub');
140
146
  expect(result.config.packId).toBe('@vxnus/e-teyvat');
@@ -142,6 +148,33 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
142
148
  expect(result.summary?.Provider).toBe('E Teyvat');
143
149
  expect(result.summary?.Package).toBe('@vxnus/e-teyvat');
144
150
  expect(result.summary?.Version).toBe('1.2.0');
151
+ expect(result.summary?.['E-Pack']).toContain('Remote');
152
+ });
153
+ test('Attaching hybrid pack allows choosing between remote and local with automated path', async () => {
154
+ const mockClient = {
155
+ resolveProvider: jest.fn().mockResolvedValue({
156
+ name: 'e-teyvat',
157
+ displayName: 'E Teyvat',
158
+ package: '@vxnus/e-teyvat',
159
+ version: '1.2.0',
160
+ description: 'Teyvat knowledge provider for Siduri.',
161
+ capabilities: ['Search', 'Retrieval', 'Context injection'],
162
+ distributionType: 'both',
163
+ distribution: {
164
+ kind: 'archive',
165
+ url: 'https://knowledge.e.vxnus.xyz/archives/teyvat.tar.gz',
166
+ },
167
+ source: 'E Knowledge Hub',
168
+ }),
169
+ };
170
+ inquirer_1.default.prompt
171
+ .mockResolvedValueOnce({ useExternalPack: true })
172
+ .mockResolvedValueOnce({ packId: '@vxnus/e-teyvat' })
173
+ .mockResolvedValueOnce({ selectedMode: 'local' });
174
+ const result = await (0, knowledge_1.configureKnowledge)({ companionName: 'Sparkle', manifest: knowledgeManifest }, { client: mockClient });
175
+ expect(result.config.provider).toBe('e-knowledge');
176
+ expect(result.config.packPath).toBe('./assets/knowledge/vxnus-e-teyvat');
177
+ expect(result.summary?.['E-Pack']).toContain('Local @ ./assets/knowledge/vxnus-e-teyvat');
145
178
  });
146
179
  test('Knowledge manifest validation enforces name and version', () => {
147
180
  expect(() => (0, knowledge_hub_1.validateKnowledgeManifest)({})).toThrow('Invalid knowledge manifest');
@@ -214,37 +247,45 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
214
247
  expect(result.config.rvc.pitchShift).toBe(12);
215
248
  expect(result.summary?.['Base TTS']).toBe('edge-tts');
216
249
  });
217
- test('Voice configurator configures standalone Edge-TTS', async () => {
250
+ test('Voice configurator disables voice when none is selected', async () => {
218
251
  inquirer_1.default.prompt
219
- .mockResolvedValueOnce({ provider: 'edge-tts' })
220
- .mockResolvedValueOnce({ voice: 'en-US-AriaNeural' });
252
+ .mockResolvedValueOnce({ provider: 'none' });
221
253
  const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
222
- expect(result.config.provider).toBe('edge-tts');
223
- expect(result.config.voice).toBe('en-US-AriaNeural');
224
- expect(result.summary?.Provider).toBe('Edge-TTS (Cloud Neural)');
225
- expect(result.summary?.Voice).toBe('en-US-AriaNeural');
254
+ expect(result.config.provider).toBe('none');
255
+ expect(result.summary?.Provider).toBe('None (Voice disabled)');
226
256
  });
227
- test('Voice configurator configures standalone Kokoro TTS', async () => {
257
+ test('Voice configurator configures RVC with Kokoro as Base TTS Engine', async () => {
228
258
  inquirer_1.default.prompt
229
- .mockResolvedValueOnce({ provider: 'kokoro' })
230
- .mockResolvedValueOnce({ baseUrl: 'http://localhost:8880', voice: 'af_heart', speed: '1.2' });
259
+ .mockResolvedValueOnce({ provider: 'rvc' })
260
+ .mockResolvedValueOnce({
261
+ modelPath: './assets/voice/sparkle/sparkle.pth',
262
+ indexPath: './assets/voice/sparkle/sparkle.index',
263
+ pitchShift: '0',
264
+ f0Method: 'pm',
265
+ baseTts: 'kokoro',
266
+ serviceUrl: 'http://localhost:50055',
267
+ });
231
268
  const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
232
269
  expect(result.config.provider).toBe('kokoro');
233
- expect(result.config.baseUrl).toBe('http://localhost:8880');
234
- expect(result.config.voice).toBe('af_heart');
235
- expect(result.config.speed).toBe(1.2);
236
- expect(result.summary?.Provider).toBe('Kokoro TTS (Local / Server)');
270
+ expect(result.config.rvc.enabled).toBe(true);
271
+ expect(result.summary?.['Base TTS']).toBe('kokoro');
237
272
  });
238
- test('Voice configurator configures standalone Piper TTS', async () => {
273
+ test('Voice configurator configures RVC with Piper as Base TTS Engine', async () => {
239
274
  inquirer_1.default.prompt
240
- .mockResolvedValueOnce({ provider: 'piper' })
241
- .mockResolvedValueOnce({ baseUrl: 'http://localhost:5000', model: 'en_US-lessac', speakerId: '2' });
275
+ .mockResolvedValueOnce({ provider: 'rvc' })
276
+ .mockResolvedValueOnce({
277
+ modelPath: './assets/voice/sparkle/sparkle.pth',
278
+ indexPath: '',
279
+ pitchShift: '-6',
280
+ f0Method: 'harvest',
281
+ baseTts: 'piper',
282
+ serviceUrl: 'http://localhost:50055',
283
+ });
242
284
  const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
243
285
  expect(result.config.provider).toBe('piper');
244
- expect(result.config.baseUrl).toBe('http://localhost:5000');
245
- expect(result.config.model).toBe('en_US-lessac');
246
- expect(result.config.speakerId).toBe(2);
247
- expect(result.summary?.Provider).toBe('Piper TTS (Local / Server)');
286
+ expect(result.config.rvc.enabled).toBe(true);
287
+ expect(result.config.rvc.pitchShift).toBe(-6);
288
+ expect(result.summary?.['Base TTS']).toBe('piper');
248
289
  });
249
290
  test('Body configurator configures Live2D, custom model path, and expression', async () => {
250
291
  inquirer_1.default.prompt
package/dist/generator.js CHANGED
@@ -54,7 +54,9 @@ function getDefaultConfigForManifest(manifest) {
54
54
  config.providers = config.providers || [];
55
55
  }
56
56
  else if (manifest.organType === 'knowledge') {
57
- config.provider = config.provider || 'none';
57
+ config.provider = config.provider || 'unified';
58
+ config.lifeDatabase = config.lifeDatabase ?? true;
59
+ config.dbPath = config.dbPath || 'siduri.sqlite';
58
60
  }
59
61
  else if (manifest.organType === 'behavior') {
60
62
  config.provider = config.provider || 'active_self';
@@ -673,6 +675,12 @@ function generateInstanceFiles(options) {
673
675
  createAssetsDirs.push(`assets/voice/${companionSlug}`);
674
676
  readmeLines.push('', '### Voice & RVC Models', `Place your character RVC voice models into \`./assets/voice/${companionSlug}/\`:`, `- \`${companionSlug}.pth\` (Target voice weights)`, `- \`${companionSlug}.index\` (Feature index file)`);
675
677
  }
678
+ const knowledgeConfig = options.organConfigs?.knowledge || options.organConfigs?.['@siduri-x/knowledge'];
679
+ if (manifests.some((m) => m.organType === 'knowledge') && knowledgeConfig?.packPath) {
680
+ const cleanPackDir = String(knowledgeConfig.packPath).replace(/^\.\//, '');
681
+ createAssetsDirs.push(cleanPackDir);
682
+ readmeLines.push('', '### Knowledge Pack Assets', `Local knowledge pack files reside in \`./${cleanPackDir}/\`.`);
683
+ }
676
684
  readmeLines.push('');
677
685
  const readmeMd = readmeLines.join('\n');
678
686
  const webHtml = (0, web_template_1.generateWebHtml)(instanceName, manifests);
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 = "2.0.3";
3
+ export declare const CLI_VERSION = "2.0.4";
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 = '2.0.3';
29
+ exports.CLI_VERSION = '2.0.4';
30
30
  exports.colors = {
31
31
  cyan: '\u001b[36m',
32
32
  dim: '\u001b[2m',
@@ -120,22 +120,10 @@ async function runCreateWizard(targetDir) {
120
120
  ]);
121
121
  const companionName = basicAnswers.name;
122
122
  const projectDir = targetDir ? node_path_1.default.resolve(process.cwd(), targetDir) : node_path_1.default.resolve(process.cwd(), projectDirectoryName(companionName));
123
- printSection('Organ Configuration');
124
- console.log(`${exports.colors.dim}Configure each capability organ for your standalone companion instance.${exports.colors.reset}\n`);
125
- const selectedManifests = [];
126
- const organConfigs = {};
127
- const organSummaries = {};
128
- // Brain is required by architecture contract for cognition
129
- const brainManifest = registry.get('brain') || availableManifests.find((m) => m.organType === 'brain');
130
- if (brainManifest) {
131
- selectedManifests.push(brainManifest);
132
- printSection('Brain · required');
133
- const res = await (0, configurators_1.configureOrgan)(brainManifest, {
134
- companionName,
135
- });
136
- organConfigs[brainManifest.configKey] = res.config;
137
- organSummaries[brainManifest.configKey] = res.summary || {};
138
- }
123
+ printSection('Organ Selection');
124
+ console.log(`${exports.colors.dim}In Siduri, a companion is assembled from modular capability organs:${exports.colors.reset}`);
125
+ console.log(`${exports.colors.dim}• Brain provides central cognition and LLM reasoning (required).${exports.colors.reset}`);
126
+ console.log(`${exports.colors.dim}• Select any capability organs below to attach to ${companionName}.${exports.colors.reset}\n`);
139
127
  // Canonical organ presentation order
140
128
  const canonicalOrder = ['memory', 'knowledge', 'behavior', 'voice', 'body', 'mouth', 'hands', 'vision', 'ear', 'observation'];
141
129
  const nonBrainManifests = availableManifests
@@ -147,19 +135,37 @@ async function runCreateWizard(targetDir) {
147
135
  const valB = idxB === -1 ? 99 : idxB;
148
136
  return valA - valB;
149
137
  });
150
- for (const m of nonBrainManifests) {
151
- const isRecommended = ['memory', 'knowledge', 'behavior', 'voice', 'body', 'mouth'].includes(m.organType);
152
- const { enableOrgan } = await inquirer_1.default.prompt({
153
- type: 'confirm',
154
- name: 'enableOrgan',
155
- message: `Enable ${m.displayName}?`,
156
- default: isRecommended,
157
- });
158
- if (!enableOrgan) {
159
- continue;
160
- }
161
- selectedManifests.push(m);
162
- const sectionTitle = m.displayName.split(' ')[0] || m.organType;
138
+ const isRecommendedType = (type) => ['memory', 'knowledge', 'behavior', 'voice', 'body', 'mouth'].includes(type);
139
+ const organChoices = nonBrainManifests.map((m) => ({
140
+ name: `${m.displayName} ${exports.colors.dim} ${m.description}${exports.colors.reset}`,
141
+ value: m.organType,
142
+ checked: isRecommendedType(m.organType),
143
+ }));
144
+ const { selectedOrganTypes } = await inquirer_1.default.prompt({
145
+ type: 'checkbox',
146
+ name: 'selectedOrganTypes',
147
+ message: 'Select capability organs to enable:',
148
+ choices: organChoices,
149
+ });
150
+ const selectedManifests = [];
151
+ const organConfigs = {};
152
+ const organSummaries = {};
153
+ // Brain is required by architecture contract for cognition
154
+ const brainManifest = registry.get('brain') || availableManifests.find((m) => m.organType === 'brain');
155
+ if (brainManifest) {
156
+ selectedManifests.push(brainManifest);
157
+ }
158
+ for (const organType of selectedOrganTypes) {
159
+ const m = registry.get(organType) || availableManifests.find((item) => item.organType === organType);
160
+ if (m)
161
+ selectedManifests.push(m);
162
+ }
163
+ // 2. Interactive Organ Configuration Stage
164
+ printSection('Organ Configuration');
165
+ console.log(`${exports.colors.dim}Configure each enabled organ for ${companionName}.${exports.colors.reset}\n`);
166
+ for (const m of selectedManifests) {
167
+ const isRequired = m.organType === 'brain';
168
+ const sectionTitle = isRequired ? `${m.displayName.split(' ')[0] || m.organType} · required` : m.displayName.split(' ')[0] || m.organType;
163
169
  printSection(sectionTitle);
164
170
  const res = await (0, configurators_1.configureOrgan)(m, {
165
171
  companionName,
@@ -258,6 +264,27 @@ async function runCreateWizard(targetDir) {
258
264
  else if (files.createAssetsBodyDir) {
259
265
  await (0, promises_1.mkdir)(node_path_1.default.join(projectDir, 'assets/body'), { recursive: true });
260
266
  }
267
+ // If local knowledge archive is configured with a download URL, automatically fetch and unpack it
268
+ const knowledgeConfig = organConfigs.knowledge || organConfigs['@siduri-x/knowledge'];
269
+ if (knowledgeConfig?.pack?.mode === 'local' && knowledgeConfig?.pack?.archiveUrl) {
270
+ const packRelDir = (knowledgeConfig.packPath || 'assets/knowledge/pack').replace(/^\.\//, '');
271
+ const packDest = node_path_1.default.join(projectDir, packRelDir);
272
+ try {
273
+ await withTask(`Downloading knowledge archive (${knowledgeConfig.pack.packId || 'pack'})`, async () => {
274
+ const res = await fetch(knowledgeConfig.pack.archiveUrl);
275
+ if (res.ok) {
276
+ const tarPath = node_path_1.default.join(packDest, 'pack.tar.gz');
277
+ const buffer = Buffer.from(await res.arrayBuffer());
278
+ await (0, promises_1.writeFile)(tarPath, buffer);
279
+ await execFile('tar', ['-xzf', tarPath, '-C', packDest]);
280
+ await node_fs_1.default.promises.unlink(tarPath).catch(() => { });
281
+ }
282
+ });
283
+ }
284
+ catch (err) {
285
+ console.warn(`${exports.colors.yellow}!${exports.colors.reset} Notice: Could not download or unpack knowledge archive: ${err.message}`);
286
+ }
287
+ }
261
288
  printSuccess(`Generated standalone files at ${projectDir}`);
262
289
  // 5. Install dependencies from registry
263
290
  try {
@@ -14,10 +14,17 @@ export interface KnowledgeHubManifest {
14
14
  version: string;
15
15
  description?: string;
16
16
  capabilities?: KnowledgeHubCapabilities | string[];
17
+ distributionType?: 'local' | 'remote' | 'both';
17
18
  distribution?: {
18
- kind?: string;
19
+ kind?: 'archive' | 'provider' | string;
19
20
  url?: string;
21
+ checksum?: string;
20
22
  };
23
+ distributions?: Array<{
24
+ kind?: 'archive' | 'provider' | string;
25
+ url?: string;
26
+ checksum?: string;
27
+ }>;
21
28
  source?: string;
22
29
  environment?: Array<{
23
30
  name: string;
@@ -13,6 +13,7 @@ exports.KNOWN_KNOWLEDGE_PACKS = {
13
13
  version: '1.2.0',
14
14
  description: 'Teyvat knowledge provider for Siduri.',
15
15
  capabilities: ['Search', 'Retrieval', 'Context injection'],
16
+ distributionType: 'remote',
16
17
  distribution: {
17
18
  kind: 'provider',
18
19
  url: 'https://teyvat.e.vxnus.xyz',
@@ -31,6 +32,18 @@ function validateKnowledgeManifest(raw, packId) {
31
32
  if (!m.version || typeof m.version !== 'string') {
32
33
  throw new Error(`Invalid knowledge manifest: missing 'version' field.`);
33
34
  }
35
+ const distributions = m.distributions || (m.distribution ? [m.distribution] : []);
36
+ let distributionType = m.distributionType;
37
+ if (!distributionType) {
38
+ const hasArchive = distributions.some((d) => d.kind === 'archive');
39
+ const hasProvider = distributions.some((d) => d.kind === 'provider');
40
+ if (hasArchive && hasProvider)
41
+ distributionType = 'both';
42
+ else if (hasProvider)
43
+ distributionType = 'remote';
44
+ else
45
+ distributionType = 'local';
46
+ }
34
47
  return {
35
48
  name: m.name,
36
49
  displayName: m.displayName || m.name,
@@ -39,7 +52,9 @@ function validateKnowledgeManifest(raw, packId) {
39
52
  version: m.version,
40
53
  description: m.description || 'Knowledge pack provider',
41
54
  capabilities: m.capabilities || ['Search', 'Retrieval'],
55
+ distributionType,
42
56
  distribution: m.distribution,
57
+ distributions: m.distributions,
43
58
  source: m.source || 'E Knowledge Hub',
44
59
  environment: m.environment || [],
45
60
  };
@@ -127,6 +142,12 @@ function displayKnowledgeProviderSummary(manifest, packId) {
127
142
  console.log(` ${dim}Name:${reset} ${manifest.displayName || manifest.name}`);
128
143
  console.log(` ${dim}Package:${reset} ${packId}`);
129
144
  console.log(` ${dim}Version:${reset} ${manifest.version}`);
145
+ const typeLabel = manifest.distributionType === 'both'
146
+ ? 'Hybrid (Available as Remote Provider & Local Archive)'
147
+ : manifest.distributionType === 'remote'
148
+ ? 'Remote Provider'
149
+ : 'Local Archive';
150
+ console.log(` ${dim}Distribution:${reset} ${typeLabel}`);
130
151
  if (manifest.description) {
131
152
  console.log(`\n ${dim}Description:${reset}`);
132
153
  console.log(` ${manifest.description}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxnus/siduri",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Experimental CLI for installing and configuring Siduri companions",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -23,6 +23,11 @@
23
23
  "siduri": "./dist/index.js"
24
24
  },
25
25
  "main": "dist/index.js",
26
+ "scripts": {
27
+ "build": "tsc && node ./scripts/copy-web-dist.cjs",
28
+ "dev": "tsc -w",
29
+ "test": "jest --config jest.config.json"
30
+ },
26
31
  "dependencies": {
27
32
  "@vxnus/e": "^0.1.5",
28
33
  "@vxnus/e-knowledge": "^0.1.5",
@@ -35,10 +40,5 @@
35
40
  "ts-jest": "^29.4.12",
36
41
  "typescript": "^5.9.3",
37
42
  "@types/inquirer": "^9.0.10"
38
- },
39
- "scripts": {
40
- "build": "tsc && node ./scripts/copy-web-dist.cjs",
41
- "dev": "tsc -w",
42
- "test": "jest --config jest.config.json"
43
43
  }
44
- }
44
+ }