@vxnus/siduri 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2026 VXNUS Creative Technology Studio. All rights reserved.
2
+
3
+ This is an experimental prerelease of Siduri. It is provided for evaluation
4
+ and development only. No production use, redistribution, or sublicensing is
5
+ permitted without written authorization from VXNUS Creative Technology
6
+ Studio.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @vxnus/siduri
2
+
3
+ Experimental CLI for creating and configuring Siduri companions.
4
+
5
+ ```bash
6
+ npx @vxnus/siduri@0.0.1 create
7
+ ```
8
+
9
+ The wizard configures the required Brain and Memory organs, then lets you
10
+ enable or disable Voice, Knowledge, Behavior, Body, and Vision. Knowledge can
11
+ come from an installed E pack, an E Hub distribution, or a hosted provider.
12
+
13
+ This release is experimental and is not intended for production use.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_child_process_1 = require("node:child_process");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_util_1 = require("node:util");
13
+ const inquirer_1 = __importDefault(require("inquirer"));
14
+ const e_knowledge_1 = require("@vxnus/e-knowledge");
15
+ const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile);
16
+ const DEFAULT_REGISTRY_URL = 'https://e.vxnus.xyz/api/packs';
17
+ const CLI_VERSION = '0.0.1';
18
+ const colors = {
19
+ cyan: '\u001b[36m',
20
+ dim: '\u001b[2m',
21
+ green: '\u001b[32m',
22
+ yellow: '\u001b[33m',
23
+ reset: '\u001b[0m',
24
+ };
25
+ function printHeader() {
26
+ console.log(`\n${colors.cyan}◈ SIDURI${colors.reset} ${colors.dim}companion setup${colors.reset}`);
27
+ console.log(`${colors.yellow}Experimental release 0.0.1${colors.reset} · configuration may change\n`);
28
+ }
29
+ function printSection(title) {
30
+ console.log(`\n${colors.cyan}── ${title} ${'─'.repeat(Math.max(2, 42 - title.length))}${colors.reset}`);
31
+ }
32
+ function printSuccess(message) {
33
+ console.log(`${colors.green}✓${colors.reset} ${message}`);
34
+ }
35
+ async function withTask(label, task) {
36
+ process.stdout.write(`${colors.dim}${label}${colors.reset}`);
37
+ const frames = ['·', '•', '●', '•'];
38
+ let index = 0;
39
+ const timer = setInterval(() => {
40
+ process.stdout.write(`\r${colors.dim}${label} ${frames[index++ % frames.length]}${colors.reset}`);
41
+ }, 120);
42
+ try {
43
+ const result = await task();
44
+ clearInterval(timer);
45
+ process.stdout.write(`\r${colors.green}✓${colors.reset} ${label}\n`);
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ clearInterval(timer);
50
+ process.stdout.write(`\r${colors.yellow}!${colors.reset} ${label}\n`);
51
+ throw error;
52
+ }
53
+ }
54
+ function nonEmpty(value) {
55
+ return value.trim().length > 0 || 'Please enter a value.';
56
+ }
57
+ function urlValue(value) {
58
+ try {
59
+ const url = new URL(value);
60
+ return ['http:', 'https:'].includes(url.protocol) || 'Use an HTTP(S) URL.';
61
+ }
62
+ catch {
63
+ return 'Use a valid HTTP(S) URL.';
64
+ }
65
+ }
66
+ function safePart(value) {
67
+ return value.replace(/[^a-zA-Z0-9._-]/g, '_');
68
+ }
69
+ async function getJson(url) {
70
+ const controller = new AbortController();
71
+ const timeout = setTimeout(() => controller.abort(), 15000);
72
+ let response;
73
+ try {
74
+ response = await fetch(url, { headers: { accept: 'application/json' }, signal: controller.signal });
75
+ }
76
+ finally {
77
+ clearTimeout(timeout);
78
+ }
79
+ if (!response.ok)
80
+ throw new Error(`${url} returned HTTP ${response.status}`);
81
+ return response.json();
82
+ }
83
+ async function findManifest(root) {
84
+ try {
85
+ await (0, promises_1.readFile)(node_path_1.default.join(root, 'manifest.json'), 'utf8');
86
+ return root;
87
+ }
88
+ catch {
89
+ // Archives may contain one top-level directory.
90
+ }
91
+ for (const entry of await (0, promises_1.readdir)(root, { withFileTypes: true })) {
92
+ if (!entry.isDirectory())
93
+ continue;
94
+ try {
95
+ await (0, promises_1.readFile)(node_path_1.default.join(root, entry.name, 'manifest.json'), 'utf8');
96
+ return node_path_1.default.join(root, entry.name);
97
+ }
98
+ catch {
99
+ // Continue searching immediate children.
100
+ }
101
+ }
102
+ throw new Error('Downloaded archive does not contain a manifest.json at its root');
103
+ }
104
+ async function installArchive(pack) {
105
+ const response = await fetch(pack.distribution.url);
106
+ if (!response.ok)
107
+ throw new Error(`Pack archive returned HTTP ${response.status}`);
108
+ const archive = Buffer.from(await response.arrayBuffer());
109
+ if (pack.distribution.checksum) {
110
+ const checksum = (0, node_crypto_1.createHash)('sha256').update(archive).digest('hex');
111
+ if (checksum !== pack.distribution.checksum) {
112
+ throw new Error(`Pack archive checksum mismatch for ${pack.id}@${pack.version}`);
113
+ }
114
+ }
115
+ const work = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'siduri-pack-'));
116
+ try {
117
+ const archivePath = node_path_1.default.join(work, 'pack.tar.gz');
118
+ const extractedPath = node_path_1.default.join(work, 'extracted');
119
+ await (0, promises_1.writeFile)(archivePath, archive);
120
+ await (0, promises_1.mkdir)(extractedPath);
121
+ const { stdout: listing } = await execFile('tar', ['-tzf', archivePath]);
122
+ if (listing.split('\n').some((entry) => entry.startsWith('/') || entry.split('/').includes('..'))) {
123
+ throw new Error('Pack archive contains an unsafe path');
124
+ }
125
+ await execFile('tar', ['-xzf', archivePath, '-C', extractedPath, '--no-same-owner', '--no-same-permissions', '--no-absolute-names']);
126
+ const sourcePath = await findManifest(extractedPath);
127
+ const destination = node_path_1.default.join((0, node_os_1.homedir)(), '.siduri', 'knowledge', safePart(pack.publisher), safePart(pack.name), safePart(pack.version));
128
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
129
+ await (0, promises_1.rm)(destination, { recursive: true, force: true });
130
+ await (0, promises_1.cp)(sourcePath, destination, { recursive: true });
131
+ return destination;
132
+ }
133
+ finally {
134
+ await (0, promises_1.rm)(work, { recursive: true, force: true });
135
+ }
136
+ }
137
+ async function chooseHubPack(registryUrl) {
138
+ const { query } = await inquirer_1.default.prompt({
139
+ type: 'input',
140
+ name: 'query',
141
+ message: 'Search the Knowledge Hub or enter a package ID:',
142
+ });
143
+ const normalized = query.trim().replace(/^@/, '');
144
+ const [publisher, name] = normalized.split('/');
145
+ const result = publisher && name && !query.includes(' ')
146
+ ? await withTask('Searching Knowledge Hub', () => getJson(`${registryUrl}/${encodeURIComponent(publisher)}/${encodeURIComponent(name)}`))
147
+ : await withTask('Searching Knowledge Hub', () => getJson(`${registryUrl}?q=${encodeURIComponent(query)}&limit=20`));
148
+ const packs = 'packs' in result ? result.packs : [result];
149
+ if (packs.length === 0)
150
+ throw new Error(`No knowledge packs found for '${query}'`);
151
+ if (packs.length === 1)
152
+ return packs[0];
153
+ const { selected } = await inquirer_1.default.prompt({
154
+ type: 'list',
155
+ name: 'selected',
156
+ message: 'Select a knowledge pack:',
157
+ choices: packs.map((pack) => ({ name: `${pack.id} v${pack.version}`, value: pack.id })),
158
+ });
159
+ return packs.find((pack) => pack.id === selected);
160
+ }
161
+ async function configureKnowledge() {
162
+ const { mode } = await inquirer_1.default.prompt({
163
+ type: 'list',
164
+ name: 'mode',
165
+ message: 'Knowledge source?',
166
+ choices: [
167
+ { name: 'Knowledge Hub', value: 'hub' },
168
+ { name: 'Installed local pack', value: 'local' },
169
+ { name: 'Hosted provider URL', value: 'remote' },
170
+ { name: 'Do not use knowledge', value: 'none' },
171
+ ],
172
+ });
173
+ if (mode === 'none')
174
+ return { provider: 'none' };
175
+ if (mode === 'local') {
176
+ const { packPath } = await inquirer_1.default.prompt({
177
+ type: 'input',
178
+ name: 'packPath',
179
+ message: 'Path to the installed E knowledge pack:',
180
+ default: './knowledge-pack',
181
+ });
182
+ const resolved = node_path_1.default.resolve(packPath);
183
+ const loaded = await withTask('Validating local knowledge pack', () => (0, e_knowledge_1.loadPack)(resolved));
184
+ printSuccess(`Knowledge ready · ${loaded.manifest.id} · revision ${loaded.revision.id}`);
185
+ return { provider: 'e-knowledge', packPath: resolved };
186
+ }
187
+ if (mode === 'remote') {
188
+ const { baseUrl } = await inquirer_1.default.prompt({
189
+ type: 'input',
190
+ name: 'baseUrl',
191
+ message: 'Remote knowledge provider URL:',
192
+ });
193
+ const provider = (0, e_knowledge_1.createRemoteProvider)({ baseUrl, timeoutMs: 5000 });
194
+ const manifest = await withTask('Checking provider manifest', async () => provider.manifest());
195
+ printSuccess(`Provider ready · ${manifest.id}`);
196
+ return { provider: 'e-remote', baseUrl: baseUrl.replace(/\/+$/, ''), timeoutMs: 5000 };
197
+ }
198
+ const { registryUrl: enteredRegistryUrl } = await inquirer_1.default.prompt({
199
+ type: 'input',
200
+ name: 'registryUrl',
201
+ message: 'Knowledge Hub registry URL:',
202
+ default: process.env.SIDURI_KNOWLEDGE_REGISTRY_URL || DEFAULT_REGISTRY_URL,
203
+ validate: urlValue,
204
+ });
205
+ const registryUrl = enteredRegistryUrl.replace(/\/+$/, '');
206
+ const pack = await chooseHubPack(registryUrl);
207
+ if (pack.distribution.kind === 'archive') {
208
+ const { install } = await inquirer_1.default.prompt({
209
+ type: 'confirm',
210
+ name: 'install',
211
+ message: `Install ${pack.id} v${pack.version} locally?`,
212
+ default: true,
213
+ });
214
+ if (install) {
215
+ const packPath = await withTask(`Installing ${pack.id}@${pack.version}`, () => installArchive(pack));
216
+ const loaded = await withTask('Validating installed knowledge pack', () => (0, e_knowledge_1.loadPack)(packPath));
217
+ printSuccess(`Knowledge ready · ${loaded.manifest.id} · revision ${loaded.revision.id}`);
218
+ return { provider: 'e-knowledge', packPath };
219
+ }
220
+ }
221
+ if (pack.distribution.kind !== 'provider') {
222
+ throw new Error('Archive installation was declined and no hosted provider is available');
223
+ }
224
+ const provider = (0, e_knowledge_1.createRemoteProvider)({ baseUrl: pack.distribution.url, timeoutMs: 5000 });
225
+ await withTask('Checking provider manifest', async () => provider.manifest());
226
+ printSuccess(`Provider ready · ${pack.id}`);
227
+ return { provider: 'e-hub', registryUrl, packId: pack.id, timeoutMs: 5000 };
228
+ }
229
+ async function main() {
230
+ const command = process.argv[2];
231
+ if (command === '--version' || command === '-v') {
232
+ console.log(CLI_VERSION);
233
+ return;
234
+ }
235
+ if (command !== 'create') {
236
+ printHeader();
237
+ console.log('Usage: npx @vxnus/siduri create');
238
+ console.log(' npx @vxnus/siduri --version');
239
+ return;
240
+ }
241
+ printHeader();
242
+ printSection('Companion');
243
+ const answers = await inquirer_1.default.prompt([
244
+ { type: 'input', name: 'name', message: 'Companion name:', default: 'Siduri', validate: nonEmpty },
245
+ { type: 'list', name: 'memory', message: 'Memory provider?', choices: [{ name: 'PostgreSQL', value: 'postgres' }] },
246
+ ]);
247
+ printSuccess(`${answers.name} · PostgreSQL memory`);
248
+ printSection('Brain · required');
249
+ const { brainProvider } = await inquirer_1.default.prompt({
250
+ type: 'list',
251
+ name: 'brainProvider',
252
+ message: 'Brain provider?',
253
+ choices: [
254
+ { name: 'OpenRouter (managed model routing)', value: 'openrouter' },
255
+ { name: 'OpenAI-compatible API (custom endpoint)', value: 'openai-compatible' },
256
+ ],
257
+ });
258
+ const brain = brainProvider === 'openrouter'
259
+ ? {
260
+ provider: 'openrouter',
261
+ model: (await inquirer_1.default.prompt({ type: 'input', name: 'model', message: 'Model ID:', default: 'openai/gpt-4o-mini', validate: nonEmpty })).model,
262
+ apiKeyEnv: 'OPENROUTER_API_KEY',
263
+ }
264
+ : await (async () => {
265
+ const values = await inquirer_1.default.prompt([
266
+ { type: 'input', name: 'baseUrl', message: 'OpenAI-compatible API base URL:', default: 'http://127.0.0.1:1234/v1', validate: urlValue },
267
+ { type: 'input', name: 'model', message: 'Model ID:', default: 'local-model', validate: nonEmpty },
268
+ { type: 'input', name: 'apiKeyEnv', message: 'API key environment variable:', default: 'OPENAI_COMPATIBLE_API_KEY', validate: nonEmpty },
269
+ ]);
270
+ return { provider: 'openai-compatible', ...values };
271
+ })();
272
+ printSuccess(`${brain.provider} · ${brain.model}`);
273
+ printSection('Optional organs');
274
+ const { voice } = await inquirer_1.default.prompt({
275
+ type: 'list',
276
+ name: 'voice',
277
+ message: 'Voice provider?',
278
+ choices: [
279
+ { name: 'VOICEVOX', value: 'voicevox' },
280
+ { name: 'Do not use voice', value: 'none' },
281
+ ],
282
+ });
283
+ const knowledge = await configureKnowledge();
284
+ const remaining = await inquirer_1.default.prompt([
285
+ { type: 'list', name: 'behavior', message: 'Behavior preset?', choices: [{ name: 'Calm', value: 'Calm' }, { name: 'Cheerful, Encouraging', value: 'Cheerful, Encouraging' }, { name: 'Do not use custom behavior', value: 'none' }] },
286
+ { type: 'list', name: 'body', message: 'Body provider?', choices: [{ name: 'VTube Studio / Live2D', value: 'live2d' }, { name: 'Do not use body', value: 'none' }] },
287
+ { type: 'list', name: 'vision', message: 'Vision provider?', choices: [{ name: 'OpenRouter vision', value: 'openrouter' }, { name: 'Do not use vision', value: 'none' }] },
288
+ ]);
289
+ const config = {
290
+ id: 'default',
291
+ name: answers.name,
292
+ brain,
293
+ voice: { provider: voice, speakerId: 1 },
294
+ memory: { provider: answers.memory },
295
+ knowledge,
296
+ behavior: { provider: remaining.behavior === 'none' ? 'none' : 'active_self', preset: remaining.behavior },
297
+ body: { provider: remaining.body, vtsUrl: process.env.VTS_URL || 'ws://127.0.0.1:8001' },
298
+ vision: { provider: remaining.vision, model: 'gpt-4-vision' },
299
+ };
300
+ const configPath = node_path_1.default.join(process.cwd(), 'siduri.config.json');
301
+ try {
302
+ await (0, promises_1.readFile)(configPath, 'utf8');
303
+ const { overwrite } = await inquirer_1.default.prompt({
304
+ type: 'confirm',
305
+ name: 'overwrite',
306
+ message: `${node_path_1.default.basename(configPath)} already exists. Replace it?`,
307
+ default: false,
308
+ });
309
+ if (!overwrite) {
310
+ console.log('Configuration left unchanged.');
311
+ return;
312
+ }
313
+ }
314
+ catch {
315
+ // New configuration.
316
+ }
317
+ printSection('Review');
318
+ console.log(` ${colors.dim}companion${colors.reset} ${config.name}`);
319
+ console.log(` ${colors.dim}brain${colors.reset} ${config.brain.provider} · ${config.brain.model}`);
320
+ console.log(` ${colors.dim}memory${colors.reset} ${config.memory.provider}`);
321
+ console.log(` ${colors.dim}voice${colors.reset} ${config.voice.provider}`);
322
+ console.log(` ${colors.dim}knowledge${colors.reset} ${config.knowledge.provider}`);
323
+ console.log(` ${colors.dim}behavior${colors.reset} ${config.behavior.provider}`);
324
+ console.log(` ${colors.dim}body${colors.reset} ${config.body.provider}`);
325
+ console.log(` ${colors.dim}vision${colors.reset} ${config.vision.provider}`);
326
+ const { confirm } = await inquirer_1.default.prompt({
327
+ type: 'confirm',
328
+ name: 'confirm',
329
+ message: 'Write this Siduri configuration?',
330
+ default: true,
331
+ });
332
+ if (!confirm) {
333
+ console.log('Configuration cancelled.');
334
+ return;
335
+ }
336
+ await (0, promises_1.writeFile)(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
337
+ printSuccess(`Configuration written · ${configPath}`);
338
+ console.log(`${colors.dim}Next: start Siduri with pnpm --filter @siduri-y/api dev${colors.reset}\n`);
339
+ }
340
+ main().catch((error) => {
341
+ if (error && typeof error === 'object' && 'name' in error && error.name === 'ExitPromptError') {
342
+ console.log('\nConfiguration cancelled.');
343
+ return;
344
+ }
345
+ console.error(`\n${colors.yellow}!${colors.reset} ${error instanceof Error ? error.message : error}`);
346
+ process.exitCode = 1;
347
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@vxnus/siduri",
3
+ "version": "0.0.1",
4
+ "description": "Experimental CLI for installing and configuring Siduri companions",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/vxnuslabs/siduri-y",
9
+ "directory": "cli"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "bin": {
23
+ "siduri": "./dist/index.js"
24
+ },
25
+ "main": "dist/index.js",
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsc -w"
29
+ },
30
+ "dependencies": {
31
+ "@vxnus/e": "^0.1.3",
32
+ "@vxnus/e-knowledge": "^0.1.2",
33
+ "inquirer": "^9.2.12"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.3.3",
37
+ "@types/inquirer": "^9.0.7"
38
+ }
39
+ }