@sidurijs/api 1.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 (66) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +78 -0
  3. package/dist/app.d.ts +11 -0
  4. package/dist/app.js +1190 -0
  5. package/dist/auth.d.ts +34 -0
  6. package/dist/auth.js +84 -0
  7. package/dist/auth.test.d.ts +1 -0
  8. package/dist/auth.test.js +74 -0
  9. package/dist/b0-b6.test.d.ts +1 -0
  10. package/dist/b0-b6.test.js +121 -0
  11. package/dist/boot.d.ts +91 -0
  12. package/dist/boot.js +154 -0
  13. package/dist/boot.test.d.ts +1 -0
  14. package/dist/boot.test.js +91 -0
  15. package/dist/context-mapper.d.ts +20 -0
  16. package/dist/context-mapper.js +233 -0
  17. package/dist/context-mapper.test.d.ts +1 -0
  18. package/dist/context-mapper.test.js +287 -0
  19. package/dist/cors.d.ts +3 -0
  20. package/dist/cors.js +42 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +116 -0
  23. package/dist/index.test.d.ts +1 -0
  24. package/dist/index.test.js +151 -0
  25. package/dist/knowledge.test.d.ts +1 -0
  26. package/dist/knowledge.test.js +236 -0
  27. package/dist/runtime.d.ts +1 -0
  28. package/dist/runtime.js +17 -0
  29. package/dist/runtime.test.d.ts +1 -0
  30. package/dist/runtime.test.js +282 -0
  31. package/dist/smoke.test.d.ts +0 -0
  32. package/dist/smoke.test.js +6 -0
  33. package/dist/t4-gating.test.d.ts +1 -0
  34. package/dist/t4-gating.test.js +193 -0
  35. package/dist/t5-experience.test.d.ts +1 -0
  36. package/dist/t5-experience.test.js +155 -0
  37. package/dist/t6-security.test.d.ts +1 -0
  38. package/dist/t6-security.test.js +535 -0
  39. package/dist/t7-release.test.d.ts +1 -0
  40. package/dist/t7-release.test.js +119 -0
  41. package/dist/teach-mode.test.d.ts +1 -0
  42. package/dist/teach-mode.test.js +277 -0
  43. package/jest.config.json +5 -0
  44. package/package.json +44 -0
  45. package/siduri.sqlite +0 -0
  46. package/src/app.ts +1209 -0
  47. package/src/auth.test.ts +90 -0
  48. package/src/auth.ts +101 -0
  49. package/src/b0-b6.test.ts +137 -0
  50. package/src/boot.test.ts +112 -0
  51. package/src/boot.ts +197 -0
  52. package/src/context-mapper.test.ts +319 -0
  53. package/src/context-mapper.ts +279 -0
  54. package/src/cors.ts +44 -0
  55. package/src/index.test.ts +178 -0
  56. package/src/index.ts +101 -0
  57. package/src/knowledge.test.ts +267 -0
  58. package/src/runtime.test.ts +336 -0
  59. package/src/runtime.ts +1 -0
  60. package/src/smoke.test.ts +5 -0
  61. package/src/t4-gating.test.ts +219 -0
  62. package/src/t5-experience.test.ts +174 -0
  63. package/src/t6-security.test.ts +589 -0
  64. package/src/t7-release.test.ts +131 -0
  65. package/src/teach-mode.test.ts +312 -0
  66. package/tsconfig.json +16 -0
package/dist/auth.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export type Role = 'OWNER' | 'OPERATOR' | 'VIEWER' | 'user' | string;
3
+ export interface Identity {
4
+ authenticated: boolean;
5
+ source: 'local' | 'external';
6
+ actorId?: string;
7
+ role?: Role;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * Checks whether an incoming request originates from the local machine loopback interface.
12
+ */
13
+ export declare function isLocalRequest(req: Request): boolean;
14
+ /**
15
+ * Resolves request identity according to the single-owner external machine boundary security model.
16
+ *
17
+ * - Security is enforced at the external machine boundary, NOT internally.
18
+ * - External network requests require a matching token (AUTH_TOKEN / OWNER_TOKEN).
19
+ * - There are no internal viewer/operator/owner role privileges inside the machine.
20
+ */
21
+ export declare function resolveIdentity(req: Request): Identity;
22
+ /**
23
+ * External boundary authentication middleware.
24
+ * Verifies that requests crossing the external boundary are authorized.
25
+ */
26
+ export declare function requireAuth(req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>> | undefined;
27
+ /**
28
+ * Attaches the resolved machine identity to the request.
29
+ */
30
+ export declare function attachIdentity(req: Request, res: Response, next: NextFunction): void;
31
+ /**
32
+ * Compatibility alias: in single-owner model, all authenticated callers have full access.
33
+ */
34
+ export declare function requireRole(allowedRoles?: Role[]): (req: Request, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
package/dist/auth.js ADDED
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLocalRequest = isLocalRequest;
4
+ exports.resolveIdentity = resolveIdentity;
5
+ exports.requireAuth = requireAuth;
6
+ exports.attachIdentity = attachIdentity;
7
+ exports.requireRole = requireRole;
8
+ /**
9
+ * Checks whether an incoming request originates from the local machine loopback interface.
10
+ */
11
+ function isLocalRequest(req) {
12
+ const ip = req.ip || req.socket?.remoteAddress || '';
13
+ return (ip === '127.0.0.1' ||
14
+ ip === '::1' ||
15
+ ip === '::ffff:127.0.0.1' ||
16
+ ip.endsWith('127.0.0.1') ||
17
+ ip === 'localhost');
18
+ }
19
+ /**
20
+ * Resolves request identity according to the single-owner external machine boundary security model.
21
+ *
22
+ * - Security is enforced at the external machine boundary, NOT internally.
23
+ * - External network requests require a matching token (AUTH_TOKEN / OWNER_TOKEN).
24
+ * - There are no internal viewer/operator/owner role privileges inside the machine.
25
+ */
26
+ function resolveIdentity(req) {
27
+ const authHeader = req.headers?.authorization;
28
+ const token = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : undefined;
29
+ const ownerToken = process.env.AUTH_TOKEN || process.env.API_TOKEN || process.env.OWNER_TOKEN;
30
+ const operatorToken = process.env.OPERATOR_TOKEN;
31
+ const configuredToken = ownerToken || operatorToken;
32
+ const local = isLocalRequest(req);
33
+ // 1. Explicit token matches
34
+ if (ownerToken && token === ownerToken) {
35
+ return { authenticated: true, source: local ? 'local' : 'external', role: 'OWNER' };
36
+ }
37
+ if (operatorToken && token === operatorToken) {
38
+ return { authenticated: true, source: local ? 'local' : 'external', role: 'OPERATOR' };
39
+ }
40
+ // 2. Dev local auth role fallback
41
+ const isDev = process.env.NODE_ENV !== 'production';
42
+ if (isDev && process.env.DEV_LOCAL_AUTH_ROLE) {
43
+ const fallbackRole = process.env.DEV_LOCAL_AUTH_ROLE.toUpperCase();
44
+ return {
45
+ authenticated: fallbackRole === 'OWNER' || fallbackRole === 'OPERATOR',
46
+ source: 'local',
47
+ role: fallbackRole,
48
+ };
49
+ }
50
+ // 3. Default unauthenticated / visitor
51
+ return { authenticated: false, source: local ? 'local' : 'external', role: 'VIEWER' };
52
+ }
53
+ /**
54
+ * External boundary authentication middleware.
55
+ * Verifies that requests crossing the external boundary are authorized.
56
+ */
57
+ function requireAuth(req, res, next) {
58
+ const identity = resolveIdentity(req);
59
+ req.identity = identity;
60
+ if (!identity.authenticated) {
61
+ return res.status(401).json({ error: 'Unauthorized: missing or invalid authentication token' });
62
+ }
63
+ next();
64
+ }
65
+ /**
66
+ * Attaches the resolved machine identity to the request.
67
+ */
68
+ function attachIdentity(req, res, next) {
69
+ req.identity = resolveIdentity(req);
70
+ next();
71
+ }
72
+ /**
73
+ * Compatibility alias: in single-owner model, all authenticated callers have full access.
74
+ */
75
+ function requireRole(allowedRoles) {
76
+ return (req, res, next) => {
77
+ const identity = resolveIdentity(req);
78
+ req.identity = identity;
79
+ if (!identity.authenticated) {
80
+ return res.status(403).json({ error: `Forbidden: requires one of ${(allowedRoles || []).join(', ')}` });
81
+ }
82
+ next();
83
+ };
84
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const auth_1 = require("./auth");
4
+ describe('Auth Identity Resolution (Single-Owner External Boundary)', () => {
5
+ const originalEnv = process.env;
6
+ beforeEach(() => {
7
+ jest.resetModules();
8
+ process.env = { ...originalEnv };
9
+ });
10
+ afterAll(() => {
11
+ process.env = originalEnv;
12
+ });
13
+ const mockReq = (token, ip = '192.168.1.50') => ({
14
+ headers: {
15
+ authorization: token ? `Bearer ${token}` : undefined,
16
+ },
17
+ ip,
18
+ socket: { remoteAddress: ip },
19
+ });
20
+ test('authenticates external request when token matches configured AUTH_TOKEN', () => {
21
+ process.env.AUTH_TOKEN = 'secret-auth-key';
22
+ process.env.NODE_ENV = 'production';
23
+ const identity = (0, auth_1.resolveIdentity)(mockReq('secret-auth-key'));
24
+ expect(identity.authenticated).toBe(true);
25
+ expect(identity.source).toBe('external');
26
+ });
27
+ test('authenticates external request when token matches legacy OWNER_TOKEN', () => {
28
+ process.env.OWNER_TOKEN = 'owner-secret';
29
+ process.env.NODE_ENV = 'production';
30
+ const identity = (0, auth_1.resolveIdentity)(mockReq('owner-secret'));
31
+ expect(identity.authenticated).toBe(true);
32
+ expect(identity.source).toBe('external');
33
+ });
34
+ test('rejects external request when token is missing in production with configured token', () => {
35
+ process.env.AUTH_TOKEN = 'secret-auth-key';
36
+ process.env.NODE_ENV = 'production';
37
+ const identity = (0, auth_1.resolveIdentity)(mockReq());
38
+ expect(identity.authenticated).toBe(false);
39
+ });
40
+ test('rejects external request for invalid token in production', () => {
41
+ process.env.AUTH_TOKEN = 'secret-auth-key';
42
+ process.env.NODE_ENV = 'production';
43
+ const identity = (0, auth_1.resolveIdentity)(mockReq('invalid-token'));
44
+ expect(identity.authenticated).toBe(false);
45
+ });
46
+ test('authenticates local loopback request in development mode', () => {
47
+ process.env.NODE_ENV = 'development';
48
+ process.env.DEV_LOCAL_AUTH_ROLE = 'OWNER';
49
+ process.env.AUTH_TOKEN = 'secret-key';
50
+ const devIdentity = (0, auth_1.resolveIdentity)(mockReq(undefined, '127.0.0.1'));
51
+ expect(devIdentity.authenticated).toBe(true);
52
+ expect(devIdentity.source).toBe('local');
53
+ });
54
+ test('requireAuth middleware accepts authenticated request and rejects unauthenticated with 401', () => {
55
+ process.env.AUTH_TOKEN = 'secret-key';
56
+ process.env.NODE_ENV = 'production';
57
+ const nextFn = jest.fn();
58
+ const resUnauthorized = {
59
+ status: jest.fn().mockReturnThis(),
60
+ json: jest.fn().mockReturnThis(),
61
+ };
62
+ // Unauthorized call
63
+ (0, auth_1.requireAuth)(mockReq('wrong-token'), resUnauthorized, nextFn);
64
+ expect(resUnauthorized.status).toHaveBeenCalledWith(401);
65
+ expect(nextFn).not.toHaveBeenCalled();
66
+ // Authorized call
67
+ const resAuthorized = {
68
+ status: jest.fn().mockReturnThis(),
69
+ json: jest.fn().mockReturnThis(),
70
+ };
71
+ (0, auth_1.requireAuth)(mockReq('secret-key'), resAuthorized, nextFn);
72
+ expect(nextFn).toHaveBeenCalled();
73
+ });
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
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 supertest_1 = __importDefault(require("supertest"));
7
+ const app_1 = require("./app");
8
+ const runtime_1 = require("./runtime");
9
+ describe('T0 B0 & B6 Runtime Proof Suite', () => {
10
+ let mockBrain;
11
+ let mockMemory;
12
+ let mockKnowledge;
13
+ let mockBehavior;
14
+ let runtime;
15
+ let app;
16
+ beforeEach(async () => {
17
+ mockBrain = {
18
+ generatePlan: jest.fn().mockImplementation(async (ctx) => {
19
+ return {
20
+ speech: 'Hello. I am a neutral companion.',
21
+ language: 'en',
22
+ };
23
+ }),
24
+ };
25
+ mockMemory = {
26
+ initialize: jest.fn().mockResolvedValue(undefined),
27
+ searchClaims: jest.fn().mockResolvedValue([]),
28
+ getClaims: jest.fn().mockResolvedValue([]),
29
+ getDirectives: jest.fn().mockResolvedValue([]),
30
+ getPendingClaims: jest.fn().mockResolvedValue([]),
31
+ proposeClaim: jest.fn().mockResolvedValue({}),
32
+ approveClaim: jest.fn().mockResolvedValue(undefined),
33
+ rejectClaim: jest.fn().mockResolvedValue(undefined),
34
+ };
35
+ mockKnowledge = {
36
+ search: jest.fn().mockResolvedValue([]),
37
+ };
38
+ mockBehavior = {
39
+ compile: jest.fn().mockResolvedValue(''),
40
+ };
41
+ const config = {
42
+ name: 'NeutralCompanion',
43
+ brain: { provider: 'openrouter' },
44
+ memory: { provider: 'sqlite' },
45
+ knowledge: { provider: 'e-knowledge' },
46
+ behavior: { provider: 'active-self' },
47
+ voice: { provider: 'none' },
48
+ vision: { provider: 'none' },
49
+ body: { provider: 'none' },
50
+ };
51
+ runtime = new runtime_1.SiduriRuntime('companion-a', config, {
52
+ brain: mockBrain,
53
+ memory: mockMemory,
54
+ knowledge: mockKnowledge,
55
+ behavior: mockBehavior,
56
+ });
57
+ await runtime.initialize();
58
+ const runtimes = new Map([['companion-a', runtime]]);
59
+ const created = (0, app_1.createApp)(runtimes);
60
+ app = created.app;
61
+ });
62
+ // B0: Fresh companion is empty (no prior claims, no user relationship, no knowledge search on greeting)
63
+ describe('B0 — Fresh companion is empty', () => {
64
+ test('initial state has empty memory and empty directives', async () => {
65
+ const claims = await runtime.memory?.getClaims();
66
+ const directives = await runtime.memory?.getDirectives();
67
+ expect(claims).toEqual([]);
68
+ expect(directives).toEqual([]);
69
+ });
70
+ test('greeting does not query knowledge or inject prior personal knowledge', async () => {
71
+ const res = await (0, supertest_1.default)(app)
72
+ .post('/chat')
73
+ .send({
74
+ companionId: 'companion-a',
75
+ message: 'Hello.',
76
+ history: [],
77
+ });
78
+ expect(res.status).toBe(200);
79
+ expect(mockKnowledge.search).not.toHaveBeenCalled();
80
+ expect(mockBrain.generatePlan).toHaveBeenCalledWith(expect.objectContaining({
81
+ contextPrompt: '',
82
+ recipient: 'OWNER',
83
+ }));
84
+ });
85
+ });
86
+ // B6: Identity and relationship are learned, not inferred (self identity questions do not query external knowledge)
87
+ describe('B6 — Identity and relationship are learned, not inferred', () => {
88
+ test('asking "Who are you?" suppresses knowledge query and asserts self identity without external search', async () => {
89
+ mockBrain.generatePlan.mockResolvedValueOnce({
90
+ speech: 'I am NeutralCompanion.',
91
+ language: 'en',
92
+ });
93
+ const res = await (0, supertest_1.default)(app)
94
+ .post('/chat')
95
+ .send({
96
+ companionId: 'companion-a',
97
+ message: 'Who are you?',
98
+ history: [],
99
+ });
100
+ expect(res.status).toBe(200);
101
+ // B6 oracle: self identity chat does not query external knowledge
102
+ expect(mockKnowledge.search).not.toHaveBeenCalled();
103
+ expect(mockMemory.searchClaims).toHaveBeenCalled();
104
+ expect(mockBrain.generatePlan).toHaveBeenCalledWith(expect.objectContaining({
105
+ contextPrompt: '',
106
+ }));
107
+ expect(res.body.response.subtitle_en).toBe('I am NeutralCompanion.');
108
+ });
109
+ test('asking "Tell me about yourself" suppresses knowledge query', async () => {
110
+ const res = await (0, supertest_1.default)(app)
111
+ .post('/chat')
112
+ .send({
113
+ companionId: 'companion-a',
114
+ message: 'Tell me about yourself',
115
+ history: [],
116
+ });
117
+ expect(res.status).toBe(200);
118
+ expect(mockKnowledge.search).not.toHaveBeenCalled();
119
+ });
120
+ });
121
+ });
package/dist/boot.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { OpenAICompatibleBrain } from '@sidurijs/brain';
2
+ import { SqliteMemoryStore } from '@sidurijs/memory';
3
+ import { VoiceAdapter, VoiceConfig } from '@sidurijs/voice';
4
+ import { UnifiedKnowledgeOrgan, UnifiedKnowledgeConfig } from '@sidurijs/knowledge';
5
+ import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@sidurijs/vision';
6
+ import { ActiveSelfCompiler, SqliteSelfRepository } from '@sidurijs/self';
7
+ import { NeutralBodyOrganConfig } from '@sidurijs/body';
8
+ import { FixtureObservationOrgan } from '@sidurijs/observation';
9
+ import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@sidurijs/hands';
10
+ import { DefaultEarOrgan, EarOrganConfig } from '@sidurijs/ear';
11
+ import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@sidurijs/mouth';
12
+ import { SiduriRuntime } from './runtime';
13
+ export interface AppBrainConfig {
14
+ provider?: 'openrouter' | 'openai-compatible' | string;
15
+ model?: string;
16
+ apiKey?: string;
17
+ apiKeyEnv?: string;
18
+ baseUrl?: string;
19
+ timeoutMs?: number;
20
+ [key: string]: unknown;
21
+ }
22
+ export interface AppBehaviorConfig {
23
+ provider?: 'active_self' | 'none' | string;
24
+ preset?: string;
25
+ [key: string]: unknown;
26
+ }
27
+ export interface AppBootCompanionConfig {
28
+ name: string;
29
+ id?: string;
30
+ brain?: AppBrainConfig;
31
+ voice?: VoiceConfig;
32
+ memory?: {
33
+ provider?: string;
34
+ connectionString?: string;
35
+ maxConnections?: number;
36
+ dbPath?: string;
37
+ [key: string]: unknown;
38
+ };
39
+ knowledge?: UnifiedKnowledgeConfig;
40
+ vision?: OpenRouterVisionConfig;
41
+ behavior?: AppBehaviorConfig;
42
+ body?: NeutralBodyOrganConfig;
43
+ hands?: DefaultHandsOrganConfig;
44
+ ear?: EarOrganConfig;
45
+ mouth?: DefaultMouthOrganConfig;
46
+ self?: {
47
+ dbPath?: string;
48
+ [key: string]: unknown;
49
+ };
50
+ organs?: Record<string, any>;
51
+ [key: string]: unknown;
52
+ }
53
+ export interface BootCompanionOptions {
54
+ observationOrgan?: FixtureObservationOrgan;
55
+ }
56
+ export declare function isDisabled(config?: {
57
+ provider?: string;
58
+ }): boolean;
59
+ export declare function createBrain(config?: AppBrainConfig): OpenAICompatibleBrain;
60
+ export declare function createVoice(config?: VoiceConfig): VoiceAdapter | undefined;
61
+ export declare function createKnowledge(config?: UnifiedKnowledgeConfig): UnifiedKnowledgeOrgan | undefined;
62
+ export declare function createVision(config?: OpenRouterVisionConfig & {
63
+ provider?: string;
64
+ }): OpenRouterVisionAdapter | undefined;
65
+ export declare function createBehavior(config?: AppBehaviorConfig): ActiveSelfCompiler | undefined;
66
+ export declare function createBody(config?: NeutralBodyOrganConfig): import("@sidurijs/body").NeutralBodyOrgan | undefined;
67
+ export declare function createHands(config?: DefaultHandsOrganConfig & {
68
+ provider?: string;
69
+ }): DefaultHandsOrgan;
70
+ export declare function createEar(config?: EarOrganConfig & {
71
+ provider?: string;
72
+ }): DefaultEarOrgan;
73
+ export declare function createMouth(config?: DefaultMouthOrganConfig & {
74
+ provider?: string;
75
+ }, voice?: any): DefaultMouthOrgan;
76
+ export declare function createMemory(config?: {
77
+ provider?: string;
78
+ connectionString?: string;
79
+ maxConnections?: number;
80
+ dbPath?: string;
81
+ }): SqliteMemoryStore | undefined;
82
+ export declare function createSelf(config?: {
83
+ dbPath?: string;
84
+ provider?: string;
85
+ }): SqliteSelfRepository | undefined;
86
+ export declare function createObservation(vision?: any): FixtureObservationOrgan;
87
+ /**
88
+ * Canonical companion bootstrapper.
89
+ * Wires all 10 organs, runs memory migrations, and initializes the SiduriRuntime.
90
+ */
91
+ export declare function bootCompanion(id: string, config: AppBootCompanionConfig, options?: BootCompanionOptions): Promise<SiduriRuntime>;
package/dist/boot.js ADDED
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isDisabled = isDisabled;
4
+ exports.createBrain = createBrain;
5
+ exports.createVoice = createVoice;
6
+ exports.createKnowledge = createKnowledge;
7
+ exports.createVision = createVision;
8
+ exports.createBehavior = createBehavior;
9
+ exports.createBody = createBody;
10
+ exports.createHands = createHands;
11
+ exports.createEar = createEar;
12
+ exports.createMouth = createMouth;
13
+ exports.createMemory = createMemory;
14
+ exports.createSelf = createSelf;
15
+ exports.createObservation = createObservation;
16
+ exports.bootCompanion = bootCompanion;
17
+ const brain_1 = require("@sidurijs/brain");
18
+ const memory_1 = require("@sidurijs/memory");
19
+ const voice_1 = require("@sidurijs/voice");
20
+ const knowledge_1 = require("@sidurijs/knowledge");
21
+ const vision_1 = require("@sidurijs/vision");
22
+ const self_1 = require("@sidurijs/self");
23
+ const body_1 = require("@sidurijs/body");
24
+ const observation_1 = require("@sidurijs/observation");
25
+ const hands_1 = require("@sidurijs/hands");
26
+ const ear_1 = require("@sidurijs/ear");
27
+ const mouth_1 = require("@sidurijs/mouth");
28
+ const runtime_1 = require("./runtime");
29
+ function isDisabled(config) {
30
+ return !config || config.provider === 'none';
31
+ }
32
+ function createBrain(config) {
33
+ const provider = config?.provider || 'openrouter';
34
+ const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
35
+ const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
36
+ if (provider === 'openai-compatible') {
37
+ return new brain_1.OpenAICompatibleBrain({
38
+ apiKey,
39
+ model: config?.model || 'local-model',
40
+ baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
41
+ });
42
+ }
43
+ return new brain_1.OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
44
+ }
45
+ function createVoice(config) {
46
+ return isDisabled(config)
47
+ ? undefined
48
+ : new voice_1.VoiceAdapter({
49
+ provider: config?.provider || 'voicevox',
50
+ baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
51
+ speakerId: config?.speakerId || 1,
52
+ ...config,
53
+ });
54
+ }
55
+ function createKnowledge(config) {
56
+ if (isDisabled(config))
57
+ return undefined;
58
+ const dbPath = config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite';
59
+ return new knowledge_1.UnifiedKnowledgeOrgan({
60
+ ...config,
61
+ dbPath,
62
+ lifeDatabase: config?.lifeDatabase ?? true,
63
+ });
64
+ }
65
+ function createVision(config) {
66
+ return isDisabled(config)
67
+ ? undefined
68
+ : new vision_1.OpenRouterVisionAdapter({
69
+ apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
70
+ model: config?.model || 'gpt-4-vision',
71
+ ...config,
72
+ });
73
+ }
74
+ function createBehavior(config) {
75
+ return isDisabled(config) ? undefined : new self_1.ActiveSelfCompiler();
76
+ }
77
+ function createBody(config) {
78
+ if (isDisabled(config)) {
79
+ return undefined;
80
+ }
81
+ if (config?.provider === 'vrm' || config?.format === 'vrm') {
82
+ return new body_1.VRMAdapter(config);
83
+ }
84
+ return new body_1.Live2DAdapter(config);
85
+ }
86
+ function createHands(config) {
87
+ return isDisabled(config)
88
+ ? new hands_1.DefaultHandsOrgan()
89
+ : new hands_1.DefaultHandsOrgan(config);
90
+ }
91
+ function createEar(config) {
92
+ return isDisabled(config)
93
+ ? new ear_1.DefaultEarOrgan()
94
+ : new ear_1.DefaultEarOrgan(config);
95
+ }
96
+ function createMouth(config, voice) {
97
+ return isDisabled(config)
98
+ ? new mouth_1.DefaultMouthOrgan({ voice })
99
+ : new mouth_1.DefaultMouthOrgan({ ...config, voice });
100
+ }
101
+ function createMemory(config) {
102
+ if (isDisabled(config))
103
+ return undefined;
104
+ const defaultPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'siduri.sqlite';
105
+ return new memory_1.SqliteMemoryStore({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || defaultPath });
106
+ }
107
+ function createSelf(config) {
108
+ if (isDisabled(config))
109
+ return undefined;
110
+ const defaultPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'siduri.sqlite';
111
+ return new self_1.SqliteSelfRepository({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || defaultPath });
112
+ }
113
+ function createObservation(vision) {
114
+ return new observation_1.FixtureObservationOrgan(vision ?? { analyze: async () => JSON.stringify({ readings: [] }) });
115
+ }
116
+ /**
117
+ * Canonical companion bootstrapper.
118
+ * Wires all 10 organs, runs memory migrations, and initializes the SiduriRuntime.
119
+ */
120
+ async function bootCompanion(id, config, options) {
121
+ const organs = config?.organs || {};
122
+ const brain = createBrain(organs.brain || config?.brain);
123
+ const memory = createMemory(organs.memory || config?.memory);
124
+ const selfRepo = createSelf(organs.self || config?.self);
125
+ const voice = createVoice(organs.voice || config?.voice);
126
+ const knowledge = createKnowledge(organs.knowledge || config?.knowledge);
127
+ const vision = createVision(organs.vision || config?.vision);
128
+ const behavior = createBehavior(organs.behavior || config?.behavior);
129
+ const body = createBody(organs.body || config?.body);
130
+ const hands = createHands({ ...(organs.hands || config?.hands), knowledge });
131
+ const ear = createEar(organs.ear || config?.ear);
132
+ const mouth = createMouth(organs.mouth || config?.mouth, voice);
133
+ const observation = options?.observationOrgan;
134
+ if (memory && typeof memory.runMigrations === 'function') {
135
+ await memory.runMigrations().catch((e) => console.warn("Migrations warning:", e.message));
136
+ }
137
+ const runtime = new runtime_1.SiduriRuntime(id, config, {
138
+ brain,
139
+ memory,
140
+ voice,
141
+ knowledge,
142
+ vision,
143
+ behavior,
144
+ body,
145
+ hands,
146
+ ear,
147
+ mouth,
148
+ observation,
149
+ self: selfRepo,
150
+ externalKnowledge: knowledge?.eAdapter ?? knowledge,
151
+ });
152
+ await runtime.initialize();
153
+ return runtime;
154
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const boot_1 = require("./boot");
4
+ const observation_1 = require("@sidurijs/observation");
5
+ describe('Canonical bootCompanion & Organ Factory Suite', () => {
6
+ const originalEnv = process.env;
7
+ beforeEach(() => {
8
+ process.env = { ...originalEnv };
9
+ });
10
+ afterAll(() => {
11
+ process.env = originalEnv;
12
+ });
13
+ test('isDisabled correctly identifies undefined or none provider', () => {
14
+ expect((0, boot_1.isDisabled)(undefined)).toBe(true);
15
+ expect((0, boot_1.isDisabled)({ provider: 'none' })).toBe(true);
16
+ expect((0, boot_1.isDisabled)({ provider: 'openrouter' })).toBe(false);
17
+ expect((0, boot_1.isDisabled)({ provider: 'sqlite' })).toBe(false);
18
+ });
19
+ test('creates all standard organs with fallback configurations', () => {
20
+ const brain = (0, boot_1.createBrain)({ provider: 'openai-compatible', baseUrl: 'http://localhost:11434/v1', apiKey: 'test' });
21
+ expect(brain).toBeDefined();
22
+ const voice = (0, boot_1.createVoice)({ provider: 'none' });
23
+ expect(voice).toBeUndefined();
24
+ const memory = (0, boot_1.createMemory)({ provider: 'sqlite', dbPath: ':memory:' });
25
+ expect(memory).toBeDefined();
26
+ const selfRepo = (0, boot_1.createSelf)({ dbPath: ':memory:' });
27
+ expect(selfRepo).toBeDefined();
28
+ const vision = (0, boot_1.createVision)({ provider: 'none' });
29
+ expect(vision).toBeUndefined();
30
+ const behavior = (0, boot_1.createBehavior)({ provider: 'active_self' });
31
+ expect(behavior).toBeDefined();
32
+ const bodyNone = (0, boot_1.createBody)({ provider: 'none' });
33
+ expect(bodyNone).toBeUndefined();
34
+ const bodyLive2d = (0, boot_1.createBody)({ provider: 'live2d', modelPath: './assets/avatar.model3.json' });
35
+ expect(bodyLive2d).toBeDefined();
36
+ expect(bodyLive2d.format).toBe('live2d');
37
+ const bodyVrm = (0, boot_1.createBody)({ provider: 'vrm', modelPath: './assets/avatar.vrm', vrm: { lookAtMode: 'camera' } });
38
+ expect(bodyVrm).toBeDefined();
39
+ expect(bodyVrm.format).toBe('vrm');
40
+ expect(bodyVrm.vrmOptions?.lookAtMode).toBe('camera');
41
+ const hands = (0, boot_1.createHands)();
42
+ expect(hands).toBeDefined();
43
+ const ear = (0, boot_1.createEar)();
44
+ expect(ear).toBeDefined();
45
+ const mouth = (0, boot_1.createMouth)(undefined, voice);
46
+ expect(mouth).toBeDefined();
47
+ const observation = (0, boot_1.createObservation)();
48
+ expect(observation).toBeInstanceOf(observation_1.FixtureObservationOrgan);
49
+ });
50
+ test('bootCompanion wires all organs and runs migrations', async () => {
51
+ let migrationsRun = false;
52
+ const testConfig = {
53
+ name: 'Test Companion',
54
+ brain: { provider: 'openrouter', apiKey: 'mock-key', model: 'mock-model' },
55
+ memory: { provider: 'none' }, // will use custom mock
56
+ voice: { provider: 'none' },
57
+ vision: { provider: 'none' },
58
+ behavior: { provider: 'none' },
59
+ body: { provider: 'none' },
60
+ hands: { provider: 'none' },
61
+ ear: { provider: 'none' },
62
+ mouth: { provider: 'none' },
63
+ knowledge: { provider: 'none' },
64
+ };
65
+ const mockObservation = new observation_1.FixtureObservationOrgan({ analyze: async () => JSON.stringify({ readings: [] }) });
66
+ const runtime = await (0, boot_1.bootCompanion)('test-comp-1', testConfig, {
67
+ observationOrgan: mockObservation,
68
+ });
69
+ expect(runtime).toBeDefined();
70
+ expect(runtime.id).toBe('test-comp-1');
71
+ expect(runtime.observation).toBe(mockObservation);
72
+ expect(runtime.hands).toBeDefined();
73
+ expect(runtime.ear).toBeDefined();
74
+ expect(runtime.mouth).toBeDefined();
75
+ });
76
+ test('bootCompanion handles nested organs configuration structure', async () => {
77
+ const nestedConfig = {
78
+ name: 'Nested Companion',
79
+ organs: {
80
+ brain: { provider: 'openrouter', apiKey: 'mock' },
81
+ memory: { provider: 'none' },
82
+ knowledge: { provider: 'none' },
83
+ voice: { provider: 'none' },
84
+ },
85
+ };
86
+ const runtime = await (0, boot_1.bootCompanion)('nested-comp', nestedConfig);
87
+ expect(runtime).toBeDefined();
88
+ expect(runtime.id).toBe('nested-comp');
89
+ expect(runtime.brain).toBeDefined();
90
+ });
91
+ });