@vxnus/siduri 0.1.8 → 0.1.10
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.
- package/dist/builtin-manifests.d.ts +2 -0
- package/dist/builtin-manifests.js +492 -0
- package/dist/clean-machine-e2e.test.d.ts +1 -0
- package/dist/clean-machine-e2e.test.js +236 -0
- package/dist/configurators/behavior.d.ts +2 -0
- package/dist/configurators/behavior.js +36 -0
- package/dist/configurators/body.d.ts +2 -0
- package/dist/configurators/body.js +60 -0
- package/dist/configurators/brain.d.ts +6 -0
- package/dist/configurators/brain.js +80 -0
- package/dist/configurators/ear.d.ts +2 -0
- package/dist/configurators/ear.js +28 -0
- package/dist/configurators/hands.d.ts +2 -0
- package/dist/configurators/hands.js +27 -0
- package/dist/configurators/index.d.ts +24 -0
- package/dist/configurators/index.js +78 -0
- package/dist/configurators/knowledge.d.ts +6 -0
- package/dist/configurators/knowledge.js +100 -0
- package/dist/configurators/memory.d.ts +2 -0
- package/dist/configurators/memory.js +44 -0
- package/dist/configurators/mouth.d.ts +2 -0
- package/dist/configurators/mouth.js +15 -0
- package/dist/configurators/observation.d.ts +2 -0
- package/dist/configurators/observation.js +12 -0
- package/dist/configurators/types.d.ts +12 -0
- package/dist/configurators/types.js +2 -0
- package/dist/configurators/vision.d.ts +2 -0
- package/dist/configurators/vision.js +39 -0
- package/dist/configurators/voice.d.ts +2 -0
- package/dist/configurators/voice.js +477 -0
- package/dist/configurators.test.d.ts +1 -0
- package/dist/configurators.test.js +331 -0
- package/dist/db.d.ts +25 -0
- package/dist/db.js +152 -0
- package/dist/discovery.d.ts +13 -0
- package/dist/discovery.js +87 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +69 -0
- package/dist/doctor-db.test.d.ts +1 -0
- package/dist/doctor-db.test.js +141 -0
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.js +225 -0
- package/dist/generator.d.ts +21 -0
- package/dist/generator.js +666 -0
- package/dist/generator.test.d.ts +1 -0
- package/dist/generator.test.js +197 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +401 -0
- package/dist/manifest.d.ts +33 -0
- package/dist/manifest.js +40 -0
- package/dist/providers/knowledge-hub.d.ts +37 -0
- package/dist/providers/knowledge-hub.js +141 -0
- package/dist/providers/openrouter.d.ts +32 -0
- package/dist/providers/openrouter.js +201 -0
- package/dist/release-check.d.ts +9 -0
- package/dist/release-check.js +133 -0
- package/dist/runtime-parity.test.d.ts +1 -0
- package/dist/runtime-parity.test.js +140 -0
- package/dist/web-template.d.ts +2 -0
- package/dist/web-template.js +557 -0
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
7
|
+
const discovery_1 = require("./discovery");
|
|
8
|
+
const generator_1 = require("./generator");
|
|
9
|
+
const index_1 = require("./index");
|
|
10
|
+
const openrouter_1 = require("./providers/openrouter");
|
|
11
|
+
const knowledge_hub_1 = require("./providers/knowledge-hub");
|
|
12
|
+
const brain_1 = require("./configurators/brain");
|
|
13
|
+
const knowledge_1 = require("./configurators/knowledge");
|
|
14
|
+
const memory_1 = require("./configurators/memory");
|
|
15
|
+
const voice_1 = require("./configurators/voice");
|
|
16
|
+
const body_1 = require("./configurators/body");
|
|
17
|
+
const hands_1 = require("./configurators/hands");
|
|
18
|
+
const behavior_1 = require("./configurators/behavior");
|
|
19
|
+
const vision_1 = require("./configurators/vision");
|
|
20
|
+
// Mock inquirer.prompt
|
|
21
|
+
jest.mock('inquirer', () => ({
|
|
22
|
+
prompt: jest.fn(),
|
|
23
|
+
Separator: jest.fn((label) => ({ type: 'separator', line: label })),
|
|
24
|
+
}));
|
|
25
|
+
describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
|
|
26
|
+
const registry = discovery_1.OrganRegistry.discover();
|
|
27
|
+
const brainManifest = registry.get('brain');
|
|
28
|
+
const knowledgeManifest = registry.get('knowledge');
|
|
29
|
+
const memoryManifest = registry.get('memory');
|
|
30
|
+
const voiceManifest = registry.get('voice');
|
|
31
|
+
const bodyManifest = registry.get('body');
|
|
32
|
+
const handsManifest = registry.get('hands');
|
|
33
|
+
const behaviorManifest = registry.get('behavior');
|
|
34
|
+
const visionManifest = registry.get('vision');
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
describe('Discrepancy #1: OpenRouter Model Discovery & Brain Configurator', () => {
|
|
39
|
+
test('OpenRouter model provider parses model catalog correctly', async () => {
|
|
40
|
+
const mockModels = [
|
|
41
|
+
{ id: 'openai/gpt-4o-mini', name: 'OpenAI: GPT-4o Mini', description: 'Fast model', context_length: 128000 },
|
|
42
|
+
{ id: 'anthropic/claude-3.5-sonnet', name: 'Anthropic: Claude 3.5 Sonnet', context_length: 200000 },
|
|
43
|
+
];
|
|
44
|
+
const globalFetch = global.fetch;
|
|
45
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: async () => ({ data: mockModels }),
|
|
48
|
+
});
|
|
49
|
+
const provider = new openrouter_1.OpenRouterModelProvider('https://mock-api.com/models');
|
|
50
|
+
const models = await provider.listModels();
|
|
51
|
+
expect(models.length).toBe(2);
|
|
52
|
+
expect(models[0].id).toBe('openai/gpt-4o-mini');
|
|
53
|
+
expect(models[0].name).toBe('OpenAI: GPT-4o Mini');
|
|
54
|
+
expect(models[1].id).toBe('anthropic/claude-3.5-sonnet');
|
|
55
|
+
global.fetch = globalFetch;
|
|
56
|
+
});
|
|
57
|
+
test('Brain configurator selects OpenRouter model and retains canonical model ID', async () => {
|
|
58
|
+
const mockProvider = {
|
|
59
|
+
listModels: jest.fn().mockResolvedValue([
|
|
60
|
+
{ id: 'openai/gpt-4o-mini', name: 'OpenAI: GPT-4o Mini' },
|
|
61
|
+
{ id: 'anthropic/claude-3.5-sonnet', name: 'Anthropic: Claude 3.5 Sonnet' },
|
|
62
|
+
]),
|
|
63
|
+
};
|
|
64
|
+
inquirer_1.default.prompt
|
|
65
|
+
.mockResolvedValueOnce({ provider: 'openrouter' }) // Brain provider
|
|
66
|
+
.mockResolvedValueOnce({ selectedValue: 'openai/gpt-4o-mini' }); // Select model
|
|
67
|
+
const result = await (0, brain_1.configureBrain)({ companionName: 'Sparkle', manifest: brainManifest }, { modelProvider: mockProvider });
|
|
68
|
+
expect(result.config.provider).toBe('openrouter');
|
|
69
|
+
expect(result.config.model).toBe('openai/gpt-4o-mini');
|
|
70
|
+
expect(result.summary?.Model).toBe('openai/gpt-4o-mini');
|
|
71
|
+
expect(result.summary?.Provider).toBe('OpenRouter');
|
|
72
|
+
});
|
|
73
|
+
test('Brain configurator handles discovery failure with curated fallback models', async () => {
|
|
74
|
+
const failingProvider = {
|
|
75
|
+
listModels: jest.fn().mockRejectedValue(new Error('Network request failed')),
|
|
76
|
+
};
|
|
77
|
+
inquirer_1.default.prompt
|
|
78
|
+
.mockResolvedValueOnce({ provider: 'openrouter' }) // Brain provider
|
|
79
|
+
.mockResolvedValueOnce({ failureAction: 'curated' }) // Pick curated fallback
|
|
80
|
+
.mockResolvedValueOnce({ selectedValue: 'anthropic/claude-3.5-sonnet' }); // Select model
|
|
81
|
+
const result = await (0, brain_1.configureBrain)({ companionName: 'Sparkle', manifest: brainManifest }, { modelProvider: failingProvider });
|
|
82
|
+
expect(result.config.provider).toBe('openrouter');
|
|
83
|
+
expect(result.config.model).toBe('anthropic/claude-3.5-sonnet');
|
|
84
|
+
});
|
|
85
|
+
test('Brain configurator handles discovery failure with manual fallback entry', async () => {
|
|
86
|
+
const failingProvider = {
|
|
87
|
+
listModels: jest.fn().mockRejectedValue(new Error('Network request failed')),
|
|
88
|
+
};
|
|
89
|
+
inquirer_1.default.prompt
|
|
90
|
+
.mockResolvedValueOnce({ provider: 'openrouter' })
|
|
91
|
+
.mockResolvedValueOnce({ failureAction: 'manual' })
|
|
92
|
+
.mockResolvedValueOnce({ manualId: 'mistralai/mistral-large' });
|
|
93
|
+
const result = await (0, brain_1.configureBrain)({ companionName: 'Sparkle', manifest: brainManifest }, { modelProvider: failingProvider });
|
|
94
|
+
expect(result.config.provider).toBe('openrouter');
|
|
95
|
+
expect(result.config.model).toBe('mistralai/mistral-large');
|
|
96
|
+
});
|
|
97
|
+
test('Brain configurator supports OpenAI-compatible / Custom endpoint', async () => {
|
|
98
|
+
inquirer_1.default.prompt
|
|
99
|
+
.mockResolvedValueOnce({ provider: 'openai-compatible' })
|
|
100
|
+
.mockResolvedValueOnce({
|
|
101
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
102
|
+
model: 'llama3:latest',
|
|
103
|
+
apiKeyEnv: 'OLLAMA_API_KEY',
|
|
104
|
+
});
|
|
105
|
+
const result = await (0, brain_1.configureBrain)({ companionName: 'Sparkle', manifest: brainManifest });
|
|
106
|
+
expect(result.config.provider).toBe('openai-compatible');
|
|
107
|
+
expect(result.config.model).toBe('llama3:latest');
|
|
108
|
+
expect(result.config.baseUrl).toBe('http://localhost:11434/v1');
|
|
109
|
+
expect(result.config.apiKeyEnv).toBe('OLLAMA_API_KEY');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
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' });
|
|
115
|
+
const result = await (0, knowledge_1.configureKnowledge)({
|
|
116
|
+
companionName: 'Sparkle',
|
|
117
|
+
manifest: knowledgeManifest,
|
|
118
|
+
});
|
|
119
|
+
expect(result.config.provider).toBe('none');
|
|
120
|
+
expect(result.summary?.Source).toBe('Do not use knowledge');
|
|
121
|
+
});
|
|
122
|
+
test('Selecting "E Knowledge Hub" discovers manifest, extracts capabilities, and confirms provider', async () => {
|
|
123
|
+
const mockClient = {
|
|
124
|
+
resolveProvider: jest.fn().mockResolvedValue({
|
|
125
|
+
name: 'e-teyvat',
|
|
126
|
+
displayName: 'E Teyvat',
|
|
127
|
+
package: '@vxnus/e-teyvat',
|
|
128
|
+
version: '1.2.0',
|
|
129
|
+
description: 'Teyvat knowledge provider for Siduri.',
|
|
130
|
+
capabilities: ['Search', 'Retrieval', 'Context injection'],
|
|
131
|
+
source: 'E Knowledge Hub',
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
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
|
|
138
|
+
const result = await (0, knowledge_1.configureKnowledge)({ companionName: 'Sparkle', manifest: knowledgeManifest }, { client: mockClient });
|
|
139
|
+
expect(result.config.provider).toBe('e-hub');
|
|
140
|
+
expect(result.config.packId).toBe('@vxnus/e-teyvat');
|
|
141
|
+
expect(result.summary?.Source).toBe('E Knowledge Hub');
|
|
142
|
+
expect(result.summary?.Provider).toBe('E Teyvat');
|
|
143
|
+
expect(result.summary?.Package).toBe('@vxnus/e-teyvat');
|
|
144
|
+
expect(result.summary?.Version).toBe('1.2.0');
|
|
145
|
+
});
|
|
146
|
+
test('Knowledge manifest validation enforces name and version', () => {
|
|
147
|
+
expect(() => (0, knowledge_hub_1.validateKnowledgeManifest)({})).toThrow('Invalid knowledge manifest');
|
|
148
|
+
expect(() => (0, knowledge_hub_1.validateKnowledgeManifest)({ name: 'test' })).toThrow('missing \'version\'');
|
|
149
|
+
const valid = (0, knowledge_hub_1.validateKnowledgeManifest)({
|
|
150
|
+
name: 'e-teyvat',
|
|
151
|
+
displayName: 'E Teyvat',
|
|
152
|
+
version: '1.0.0',
|
|
153
|
+
description: 'Teyvat lore',
|
|
154
|
+
}, '@vxnus/e-teyvat');
|
|
155
|
+
expect(valid.name).toBe('e-teyvat');
|
|
156
|
+
expect(valid.package).toBe('@vxnus/e-teyvat');
|
|
157
|
+
expect(valid.version).toBe('1.0.0');
|
|
158
|
+
});
|
|
159
|
+
test('extractCapabilitiesList formats object and array capabilities correctly', () => {
|
|
160
|
+
const arrayCaps = (0, knowledge_hub_1.extractCapabilitiesList)(['Search', 'Retrieval', 'Custom Tool']);
|
|
161
|
+
expect(arrayCaps).toEqual(['Search', 'Retrieval', 'Custom Tool']);
|
|
162
|
+
const objCaps = (0, knowledge_hub_1.extractCapabilitiesList)({
|
|
163
|
+
search: true,
|
|
164
|
+
retrieval: true,
|
|
165
|
+
contextInjection: true,
|
|
166
|
+
semanticSearch: true,
|
|
167
|
+
});
|
|
168
|
+
expect(objCaps).toContain('Search');
|
|
169
|
+
expect(objCaps).toContain('Retrieval');
|
|
170
|
+
expect(objCaps).toContain('Context injection');
|
|
171
|
+
expect(objCaps).toContain('Semantic search');
|
|
172
|
+
});
|
|
173
|
+
test('Knowledge Hub client fallback returns known pack for @vxnus/e-teyvat', async () => {
|
|
174
|
+
const client = new knowledge_hub_1.KnowledgeHubClient('https://invalid-non-existent-url.local/api');
|
|
175
|
+
const manifest = await client.resolveProvider('@vxnus/e-teyvat');
|
|
176
|
+
expect(manifest.package).toBe('@vxnus/e-teyvat');
|
|
177
|
+
expect(manifest.displayName).toBe('E Teyvat');
|
|
178
|
+
expect(manifest.version).toBe('1.2.0');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('Other Organ Configurators & Generic Router', () => {
|
|
182
|
+
test('Memory configurator configures PostgreSQL with Supabase deployment', async () => {
|
|
183
|
+
inquirer_1.default.prompt
|
|
184
|
+
.mockResolvedValueOnce({ database: 'postgres' })
|
|
185
|
+
.mockResolvedValueOnce({ deployment: 'supabase' });
|
|
186
|
+
const result = await (0, memory_1.configureMemory)({ companionName: 'Sparkle', manifest: memoryManifest });
|
|
187
|
+
expect(result.config.provider).toBe('postgres');
|
|
188
|
+
expect(result.config.deployment).toBe('supabase');
|
|
189
|
+
expect(result.summary?.Deploy).toBe('Supabase');
|
|
190
|
+
});
|
|
191
|
+
test('Voice configurator configures VOICEVOX engine and speaker ID', async () => {
|
|
192
|
+
inquirer_1.default.prompt
|
|
193
|
+
.mockResolvedValueOnce({ provider: 'voicevox' })
|
|
194
|
+
.mockResolvedValueOnce({ baseUrl: 'http://localhost:50021' })
|
|
195
|
+
.mockResolvedValueOnce({ speaker: { id: 2, label: '四国めたん (Shikoku Metan) - ノーマル (Normal) (ID: 2)' } });
|
|
196
|
+
const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
|
|
197
|
+
expect(result.config.provider).toBe('voicevox');
|
|
198
|
+
expect(result.config.speakerId).toBe(2);
|
|
199
|
+
expect(result.summary?.['Speaker ID']).toBe(2);
|
|
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
|
+
});
|
|
218
|
+
test('Voice configurator configures standalone Edge-TTS', async () => {
|
|
219
|
+
inquirer_1.default.prompt
|
|
220
|
+
.mockResolvedValueOnce({ provider: 'edge-tts' })
|
|
221
|
+
.mockResolvedValueOnce({ voice: 'en-US-AriaNeural' });
|
|
222
|
+
const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
|
|
223
|
+
expect(result.config.provider).toBe('edge-tts');
|
|
224
|
+
expect(result.config.voice).toBe('en-US-AriaNeural');
|
|
225
|
+
expect(result.summary?.Provider).toBe('Edge-TTS (Cloud Neural)');
|
|
226
|
+
expect(result.summary?.Voice).toBe('en-US-AriaNeural');
|
|
227
|
+
});
|
|
228
|
+
test('Voice configurator configures standalone Kokoro TTS', async () => {
|
|
229
|
+
inquirer_1.default.prompt
|
|
230
|
+
.mockResolvedValueOnce({ provider: 'kokoro' })
|
|
231
|
+
.mockResolvedValueOnce({ baseUrl: 'http://localhost:8880', voice: 'af_heart', speed: '1.2' });
|
|
232
|
+
const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
|
|
233
|
+
expect(result.config.provider).toBe('kokoro');
|
|
234
|
+
expect(result.config.baseUrl).toBe('http://localhost:8880');
|
|
235
|
+
expect(result.config.voice).toBe('af_heart');
|
|
236
|
+
expect(result.config.speed).toBe(1.2);
|
|
237
|
+
expect(result.summary?.Provider).toBe('Kokoro TTS (Local / Server)');
|
|
238
|
+
});
|
|
239
|
+
test('Voice configurator configures standalone Piper TTS', async () => {
|
|
240
|
+
inquirer_1.default.prompt
|
|
241
|
+
.mockResolvedValueOnce({ provider: 'piper' })
|
|
242
|
+
.mockResolvedValueOnce({ baseUrl: 'http://localhost:5000', model: 'en_US-lessac', speakerId: '2' });
|
|
243
|
+
const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
|
|
244
|
+
expect(result.config.provider).toBe('piper');
|
|
245
|
+
expect(result.config.baseUrl).toBe('http://localhost:5000');
|
|
246
|
+
expect(result.config.model).toBe('en_US-lessac');
|
|
247
|
+
expect(result.config.speakerId).toBe(2);
|
|
248
|
+
expect(result.summary?.Provider).toBe('Piper TTS (Local / Server)');
|
|
249
|
+
});
|
|
250
|
+
test('Body configurator configures Live2D, custom model path, and expression', async () => {
|
|
251
|
+
inquirer_1.default.prompt
|
|
252
|
+
.mockResolvedValueOnce({ provider: 'live2d' })
|
|
253
|
+
.mockResolvedValueOnce({
|
|
254
|
+
modelSource: './assets/body/sparkle/model.model3.json',
|
|
255
|
+
initialExpression: 'happy',
|
|
256
|
+
});
|
|
257
|
+
const result = await (0, body_1.configureBody)({ companionName: 'Sparkle', manifest: bodyManifest });
|
|
258
|
+
expect(result.config.provider).toBe('live2d');
|
|
259
|
+
expect(result.config.modelPath).toBe('./assets/body/sparkle/model.model3.json');
|
|
260
|
+
expect(result.config.modelUrl).toBe('/assets/body/sparkle/model.model3.json');
|
|
261
|
+
expect(result.config.initialExpression).toBe('happy');
|
|
262
|
+
expect(result.summary?.['Model Path']).toBe('./assets/body/sparkle/model.model3.json');
|
|
263
|
+
});
|
|
264
|
+
test('Hands configurator configures MCP tool execution timeout', async () => {
|
|
265
|
+
inquirer_1.default.prompt.mockResolvedValueOnce({ timeoutSeconds: '15' });
|
|
266
|
+
const result = await (0, hands_1.configureHands)({ companionName: 'Sparkle', manifest: handsManifest });
|
|
267
|
+
expect(result.config.defaultTimeoutMs).toBe(15000);
|
|
268
|
+
});
|
|
269
|
+
test('Behavior configurator configures active self personality preset', async () => {
|
|
270
|
+
inquirer_1.default.prompt.mockResolvedValueOnce({ preset: 'cheerful' });
|
|
271
|
+
const result = await (0, behavior_1.configureBehavior)({ companionName: 'Sparkle', manifest: behaviorManifest });
|
|
272
|
+
expect(result.config.provider).toBe('active_self');
|
|
273
|
+
expect(result.config.preset).toBe('cheerful');
|
|
274
|
+
});
|
|
275
|
+
test('Vision configurator configures OpenRouter vision model', async () => {
|
|
276
|
+
inquirer_1.default.prompt.mockResolvedValueOnce({ model: 'gpt-4-vision' });
|
|
277
|
+
const result = await (0, vision_1.configureVision)({ companionName: 'Sparkle', manifest: visionManifest });
|
|
278
|
+
expect(result.config.provider).toBe('openrouter');
|
|
279
|
+
expect(result.config.model).toBe('gpt-4-vision');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('Review Summary Formatting & Generator Integration', () => {
|
|
283
|
+
test('formatReviewSummary displays actual configuration values and metadata', () => {
|
|
284
|
+
const summaryOutput = (0, index_1.formatReviewSummary)('Sparkle', [brainManifest, memoryManifest, knowledgeManifest, voiceManifest], {
|
|
285
|
+
brain: { Provider: 'OpenRouter', Model: 'openai/gpt-4o-mini' },
|
|
286
|
+
memory: { Database: 'PostgreSQL', Deploy: 'Supabase' },
|
|
287
|
+
knowledge: { Source: 'E Knowledge Hub', Provider: 'E Teyvat', Package: '@vxnus/e-teyvat', Version: '1.2.0' },
|
|
288
|
+
voice: { Provider: 'VOICEVOX', 'Speaker ID': 1 },
|
|
289
|
+
});
|
|
290
|
+
expect(summaryOutput).toContain('Sparkle');
|
|
291
|
+
expect(summaryOutput).toContain('Brain');
|
|
292
|
+
expect(summaryOutput).toContain('OpenRouter');
|
|
293
|
+
expect(summaryOutput).toContain('openai/gpt-4o-mini');
|
|
294
|
+
expect(summaryOutput).toContain('Memory');
|
|
295
|
+
expect(summaryOutput).toContain('PostgreSQL');
|
|
296
|
+
expect(summaryOutput).toContain('Supabase');
|
|
297
|
+
expect(summaryOutput).toContain('Knowledge');
|
|
298
|
+
expect(summaryOutput).toContain('E Knowledge Hub');
|
|
299
|
+
expect(summaryOutput).toContain('E Teyvat');
|
|
300
|
+
expect(summaryOutput).toContain('@vxnus/e-teyvat');
|
|
301
|
+
expect(summaryOutput).toContain('Voice');
|
|
302
|
+
expect(summaryOutput).toContain('VOICEVOX');
|
|
303
|
+
});
|
|
304
|
+
test('generateInstanceFiles accurately embeds configured organ values in siduri.config.json', () => {
|
|
305
|
+
const organConfigs = {
|
|
306
|
+
brain: { provider: 'openrouter', model: 'openai/gpt-4o-mini', apiKeyEnv: 'OPENROUTER_API_KEY' },
|
|
307
|
+
memory: { provider: 'postgres', deployment: 'supabase' },
|
|
308
|
+
knowledge: { provider: 'e-hub', registryUrl: 'https://e.vxnus.xyz/api/v1/knowledge', packId: '@vxnus/e-teyvat' },
|
|
309
|
+
voice: { provider: 'voicevox', speakerId: 2, baseUrl: 'http://localhost:50021' },
|
|
310
|
+
};
|
|
311
|
+
const files = (0, generator_1.generateInstanceFiles)({
|
|
312
|
+
name: 'Sparkle',
|
|
313
|
+
selectedManifests: [brainManifest, memoryManifest, knowledgeManifest, voiceManifest],
|
|
314
|
+
organConfigs,
|
|
315
|
+
});
|
|
316
|
+
const config = JSON.parse(files['siduri.config.json']);
|
|
317
|
+
expect(config.name).toBe('Sparkle');
|
|
318
|
+
expect(config.organs.brain.provider).toBe('openrouter');
|
|
319
|
+
expect(config.organs.brain.model).toBe('openai/gpt-4o-mini');
|
|
320
|
+
expect(config.organs.memory.deployment).toBe('supabase');
|
|
321
|
+
expect(config.organs.knowledge.provider).toBe('e-hub');
|
|
322
|
+
expect(config.organs.knowledge.packId).toBe('@vxnus/e-teyvat');
|
|
323
|
+
expect(config.organs.voice.speakerId).toBe(2);
|
|
324
|
+
// Verify explicit imports in src/index.js
|
|
325
|
+
expect(files['src/index.js']).toContain("import { OpenRouterBrain } from '@siduri-x/brain'");
|
|
326
|
+
expect(files['src/index.js']).toContain("import { PostgresMemoryOrgan } from '@siduri-x/memory'");
|
|
327
|
+
expect(files['src/index.js']).toContain("import { EKnowledgeAdapter } from '@siduri-x/knowledge'");
|
|
328
|
+
expect(files['src/index.js']).toContain("import { VoiceAdapter } from '@siduri-x/voice'");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare function redactDatabaseUrl(url: string): string;
|
|
2
|
+
export interface MigrationFile {
|
|
3
|
+
name: string;
|
|
4
|
+
fullPath: string;
|
|
5
|
+
sql: string;
|
|
6
|
+
checksum: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MigrationRecord {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
checksum: string;
|
|
12
|
+
applied_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DbPushOptions {
|
|
15
|
+
projectDir?: string;
|
|
16
|
+
connectionString?: string;
|
|
17
|
+
env?: Record<string, string | undefined>;
|
|
18
|
+
}
|
|
19
|
+
export interface DbPushResult {
|
|
20
|
+
status: 'NOOP' | 'APPLIED' | 'UP_TO_DATE';
|
|
21
|
+
appliedMigrations: string[];
|
|
22
|
+
skippedMigrations: string[];
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function runDbPush(options?: DbPushOptions): Promise<DbPushResult>;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.redactDatabaseUrl = redactDatabaseUrl;
|
|
7
|
+
exports.runDbPush = runDbPush;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const pg_1 = require("pg");
|
|
12
|
+
const discovery_1 = require("./discovery");
|
|
13
|
+
function redactDatabaseUrl(url) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
if (parsed.password) {
|
|
17
|
+
parsed.password = '***';
|
|
18
|
+
}
|
|
19
|
+
return parsed.toString();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 'postgres://***:***@...';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function runDbPush(options = {}) {
|
|
26
|
+
const projectDir = options.projectDir ? node_path_1.default.resolve(options.projectDir) : process.cwd();
|
|
27
|
+
const configPath = node_path_1.default.join(projectDir, 'siduri.config.json');
|
|
28
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
29
|
+
throw new Error(`siduri.config.json not found at ${projectDir}. Make sure you are in a Siduri instance directory.`);
|
|
30
|
+
}
|
|
31
|
+
const config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
|
|
32
|
+
const configuredOrgans = config.organs || {};
|
|
33
|
+
// 1. Discover manifests of configured organs
|
|
34
|
+
const registry = discovery_1.OrganRegistry.discover([
|
|
35
|
+
node_path_1.default.join(projectDir, 'node_modules/@siduri-x'),
|
|
36
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs'),
|
|
37
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs'),
|
|
38
|
+
]);
|
|
39
|
+
const databaseOrgans = [];
|
|
40
|
+
for (const [key] of Object.entries(configuredOrgans)) {
|
|
41
|
+
const manifest = registry.get(key) || registry.getAll().find((m) => m.configKey === key || m.organType === key);
|
|
42
|
+
if (manifest && manifest.database && manifest.database.migrationsDir) {
|
|
43
|
+
// Resolve migrations directory relative to the organ package
|
|
44
|
+
const candidatePaths = [
|
|
45
|
+
node_path_1.default.resolve(projectDir, 'node_modules', manifest.name, manifest.database.migrationsDir),
|
|
46
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs', manifest.organType, manifest.database.migrationsDir),
|
|
47
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs', manifest.organType, manifest.database.migrationsDir),
|
|
48
|
+
];
|
|
49
|
+
const resolvedDir = candidatePaths.find((p) => node_fs_1.default.existsSync(p));
|
|
50
|
+
if (resolvedDir) {
|
|
51
|
+
databaseOrgans.push({
|
|
52
|
+
organType: manifest.organType,
|
|
53
|
+
manifest,
|
|
54
|
+
migrationsDir: resolvedDir,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (databaseOrgans.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'NOOP',
|
|
62
|
+
appliedMigrations: [],
|
|
63
|
+
skippedMigrations: [],
|
|
64
|
+
message: 'No database migrations are required by this instance.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// 2. Resolve DATABASE_URL
|
|
68
|
+
const connectionString = options.connectionString ||
|
|
69
|
+
options.env?.DATABASE_URL ||
|
|
70
|
+
process.env.DATABASE_URL;
|
|
71
|
+
if (!connectionString) {
|
|
72
|
+
throw new Error('DATABASE_URL is required to run database migrations. Set it in .env or environment.');
|
|
73
|
+
}
|
|
74
|
+
const pool = new pg_1.Pool({
|
|
75
|
+
connectionString,
|
|
76
|
+
connectionTimeoutMillis: 5000,
|
|
77
|
+
});
|
|
78
|
+
const client = await pool.connect();
|
|
79
|
+
try {
|
|
80
|
+
// 3. Ensure migrations table exists
|
|
81
|
+
await client.query(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS siduri_migrations (
|
|
83
|
+
id SERIAL PRIMARY KEY,
|
|
84
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
85
|
+
checksum VARCHAR(64) NOT NULL,
|
|
86
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
// 4. Fetch applied migrations
|
|
90
|
+
const existingRes = await client.query('SELECT name, checksum FROM siduri_migrations ORDER BY id ASC;');
|
|
91
|
+
const appliedMap = new Map();
|
|
92
|
+
for (const row of existingRes.rows) {
|
|
93
|
+
appliedMap.set(row.name, row.checksum);
|
|
94
|
+
}
|
|
95
|
+
// 5. Gather all migration files across database organs in deterministic order
|
|
96
|
+
const allMigrationFiles = [];
|
|
97
|
+
for (const item of databaseOrgans) {
|
|
98
|
+
const files = node_fs_1.default.readdirSync(item.migrationsDir).filter((f) => f.endsWith('.sql')).sort();
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const fullPath = node_path_1.default.join(item.migrationsDir, file);
|
|
101
|
+
const sql = node_fs_1.default.readFileSync(fullPath, 'utf8');
|
|
102
|
+
const checksum = (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex');
|
|
103
|
+
allMigrationFiles.push({
|
|
104
|
+
name: `${item.organType}/${file}`,
|
|
105
|
+
fullPath,
|
|
106
|
+
sql,
|
|
107
|
+
checksum,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
allMigrationFiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
112
|
+
const appliedList = [];
|
|
113
|
+
const skippedList = [];
|
|
114
|
+
for (const migration of allMigrationFiles) {
|
|
115
|
+
const recordedChecksum = appliedMap.get(migration.name);
|
|
116
|
+
if (recordedChecksum !== undefined) {
|
|
117
|
+
if (recordedChecksum !== migration.checksum) {
|
|
118
|
+
throw new Error(`Migration checksum mismatch for ${migration.name}! Recorded: ${recordedChecksum.slice(0, 8)}, Current: ${migration.checksum.slice(0, 8)}. Refusing to apply modified migration.`);
|
|
119
|
+
}
|
|
120
|
+
skippedList.push(migration.name);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Apply migration in transaction
|
|
124
|
+
await client.query('BEGIN');
|
|
125
|
+
try {
|
|
126
|
+
await client.query(migration.sql);
|
|
127
|
+
await client.query('INSERT INTO siduri_migrations (name, checksum) VALUES ($1, $2)', [migration.name, migration.checksum]);
|
|
128
|
+
await client.query('COMMIT');
|
|
129
|
+
appliedList.push(migration.name);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
await client.query('ROLLBACK');
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const redacted = redactDatabaseUrl(connectionString);
|
|
138
|
+
const message = appliedList.length > 0
|
|
139
|
+
? `Applied ${appliedList.length} migration(s) to ${redacted}`
|
|
140
|
+
: `Database schema up to date on ${redacted}`;
|
|
141
|
+
return {
|
|
142
|
+
status: appliedList.length > 0 ? 'APPLIED' : 'UP_TO_DATE',
|
|
143
|
+
appliedMigrations: appliedList,
|
|
144
|
+
skippedMigrations: skippedList,
|
|
145
|
+
message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
client.release();
|
|
150
|
+
await pool.end().catch(() => { });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OrganManifest } from './manifest';
|
|
2
|
+
export declare class OrganRegistry {
|
|
3
|
+
private manifests;
|
|
4
|
+
constructor(manifests?: OrganManifest[]);
|
|
5
|
+
register(manifest: OrganManifest): void;
|
|
6
|
+
get(organType: string): OrganManifest | undefined;
|
|
7
|
+
getAll(): OrganManifest[];
|
|
8
|
+
getAvailableOrganTypes(): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Discover installed or monorepo organ packages.
|
|
11
|
+
*/
|
|
12
|
+
static discover(searchRoots?: string[]): OrganRegistry;
|
|
13
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OrganRegistry = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const manifest_1 = require("./manifest");
|
|
10
|
+
const builtin_manifests_1 = require("./builtin-manifests");
|
|
11
|
+
class OrganRegistry {
|
|
12
|
+
manifests = new Map();
|
|
13
|
+
constructor(manifests) {
|
|
14
|
+
if (manifests) {
|
|
15
|
+
for (const m of manifests) {
|
|
16
|
+
this.manifests.set(m.organType, m);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
register(manifest) {
|
|
21
|
+
const validated = (0, manifest_1.validateOrganManifest)(manifest);
|
|
22
|
+
this.manifests.set(validated.organType, validated);
|
|
23
|
+
}
|
|
24
|
+
get(organType) {
|
|
25
|
+
return this.manifests.get(organType);
|
|
26
|
+
}
|
|
27
|
+
getAll() {
|
|
28
|
+
return Array.from(this.manifests.values());
|
|
29
|
+
}
|
|
30
|
+
getAvailableOrganTypes() {
|
|
31
|
+
return Array.from(this.manifests.keys());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discover installed or monorepo organ packages.
|
|
35
|
+
*/
|
|
36
|
+
static discover(searchRoots) {
|
|
37
|
+
const registry = new OrganRegistry();
|
|
38
|
+
const rootsToScan = searchRoots && searchRoots.length > 0
|
|
39
|
+
? searchRoots
|
|
40
|
+
: [
|
|
41
|
+
// 1. Monorepo organs folder relative to cli
|
|
42
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs'),
|
|
43
|
+
// 2. Monorepo organs folder relative to cwd
|
|
44
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs'),
|
|
45
|
+
// 3. Node modules of current directory or global resolution
|
|
46
|
+
node_path_1.default.resolve(process.cwd(), 'node_modules/@siduri-x'),
|
|
47
|
+
node_path_1.default.resolve(__dirname, '../node_modules/@siduri-x'),
|
|
48
|
+
];
|
|
49
|
+
const visitedDirs = new Set();
|
|
50
|
+
for (const root of rootsToScan) {
|
|
51
|
+
if (!node_fs_1.default.existsSync(root) || visitedDirs.has(root))
|
|
52
|
+
continue;
|
|
53
|
+
visitedDirs.add(root);
|
|
54
|
+
try {
|
|
55
|
+
const entries = node_fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
const candidateDir = node_path_1.default.join(root, entry.name);
|
|
60
|
+
const manifestPath = node_path_1.default.join(candidateDir, 'organ-manifest.json');
|
|
61
|
+
if (node_fs_1.default.existsSync(manifestPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
const manifest = (0, manifest_1.validateOrganManifest)(raw, manifestPath);
|
|
65
|
+
registry.register(manifest);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.warn(`[Siduri CLI] Warning: failed to parse manifest at ${manifestPath}: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// Continue scanning other roots
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// If running in a standalone environment without local package folders (e.g. via npx in an empty directory),
|
|
78
|
+
// register canonical built-in @siduri-x/* organ manifests as fallback.
|
|
79
|
+
if (registry.getAll().length === 0) {
|
|
80
|
+
for (const builtinManifest of builtin_manifests_1.BUILTIN_ORGAN_MANIFESTS) {
|
|
81
|
+
registry.register(builtinManifest);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return registry;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.OrganRegistry = OrganRegistry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|