pacs-core 0.1.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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * PACS Phase 2 — Quick Test
3
+ * Run from pacs root: node phase2-test.mjs
4
+ */
5
+
6
+ import AgentRegistry from './src/agent-registry.js';
7
+ import InterAgentBus from './src/inter-agent-bus.js';
8
+ import AgentCreator from './src/agent-creator.js';
9
+
10
+ console.log('=== Test 1: Agent Registry ===');
11
+ const a1 = AgentRegistry.registerAgent('test-agent-1', { domain: 'dev', capabilities: ['coding', 'analysis'] });
12
+ console.log('Registered:', a1.name, '| domain:', a1.domain);
13
+
14
+ const a2 = AgentRegistry.registerAgent('test-agent-2', { domain: 'finance', capabilities: ['analysis'] });
15
+ console.log('Registered:', a2.name);
16
+
17
+ const list = AgentRegistry.listAgents();
18
+ console.log('Total agents:', list.length, '| Names:', list.map(a => a.name).join(', '));
19
+
20
+ const found = AgentRegistry.getAgent('test-agent-1');
21
+ console.log('Get test-agent-1:', found ? found.domain : 'NOT FOUND');
22
+
23
+ console.log('\n=== Test 2: Agent Creator (text) ===');
24
+ const a3 = AgentCreator.createFromText('Erstelle einen Agenten für Finanzen mit Analyse und Planung');
25
+ console.log('Created from text:', a3.name, '| domain:', a3.domain, '| caps:', a3.capabilities.join(', '));
26
+
27
+ console.log('\n=== Test 3: Agent Creator (MD) ===');
28
+ const md = `---
29
+ name: marketing-test
30
+ domain: marketing
31
+ status: active
32
+ ---
33
+ Marketing agent for campaigns`;
34
+ const a4 = AgentCreator.createFromMD(md);
35
+ console.log('Created from MD:', a4.name, '| domain:', a4.domain);
36
+
37
+ console.log('\n=== Test 4: Inter-Agent Bus ===');
38
+ const msg = InterAgentBus.emit('orchestrator', 'test-agent-1', 'Hello agent!');
39
+ console.log('Emit:', msg.id.slice(0,8), '... from:', msg.from, 'to:', msg.to, 'type:', msg.type);
40
+
41
+ const bcast = InterAgentBus.broadcast('orchestrator', 'System check');
42
+ console.log('Broadcast:', bcast.id.slice(0,8), '... to:', bcast.to);
43
+
44
+ const msgs = InterAgentBus.getMessages('test-agent-1');
45
+ console.log('Messages for test-agent-1:', msgs.length);
46
+
47
+ // Cleanup
48
+ AgentRegistry.removeAgent('test-agent-1');
49
+ AgentRegistry.removeAgent('test-agent-2');
50
+ AgentRegistry.removeAgent(a3.name);
51
+ AgentRegistry.removeAgent(a4.name);
52
+ console.log('\nCleanup done.');
53
+ console.log('\n✅ All tests passed!');
@@ -0,0 +1,178 @@
1
+ /**
2
+ * PACS Core — Agent Creator Module
3
+ * Creates agents from natural language text or MD definitions
4
+ */
5
+
6
+ import { registerAgent } from './agent-registry.js';
7
+ import triggerParser from './trigger-parser.js';
8
+
9
+ /**
10
+ * Map of domain keywords to domain names.
11
+ * @type {Record<string, string>}
12
+ */
13
+ const DOMAIN_KEYWORDS = {
14
+ finance: ['finanz', 'geld', 'budget', 'invest', 'kosten', 'rechnung', 'bank', 'steuer', 'bilanz', 'cashflow', 'buchhalt'],
15
+ marketing: ['marketing', 'werbung', 'seo', 'social media', 'content', 'kampagne', 'crm', 'kunde', 'conversion'],
16
+ dev: ['code', 'programmier', 'entwickl', 'api', 'database', 'backend', 'frontend', 'test', 'deploy', 'git'],
17
+ design: ['design', 'ui', 'ux', 'grafik', 'logo', 'farbe', 'layout', 'figma'],
18
+ hr: ['personal', 'mitarbeiter', 'recruit', 'lohn', 'urlaub', 'team'],
19
+ legal: ['recht', 'vertrag', 'agb', 'dsgvo', 'gesetz', 'jurist'],
20
+ operations: ['oper', 'prozess', 'workflow', 'automatis', 'integration'],
21
+ strategy: ['strateg', 'vision', 'planning', 'roadmap', 'ziele'],
22
+ };
23
+
24
+ /**
25
+ * Capability keywords mapping.
26
+ * @type {Record<string, string[]>}
27
+ */
28
+ const CAPABILITY_KEYWORDS = {
29
+ research: ['recherchier', 'such', 'analysier', 'find'],
30
+ writing: ['schreib', 'text', 'dokument', 'artikel', 'blog'],
31
+ coding: ['code', 'programmier', 'implementier', 'entwickl'],
32
+ analysis: ['analysier', 'auswert', 'bewert', 'pruef'],
33
+ planning: ['plan', 'organisier', 'koordinier'],
34
+ communication: ['kommunizier', 'schreib', 'antworte', 'pr\u00e4sentier'],
35
+ automation: ['automatisier', 'skript', 'workflow'],
36
+ data: ['daten', 'tabelle', 'statistik', 'csv', 'excel'],
37
+ };
38
+
39
+ /**
40
+ * Extract domain from natural language text.
41
+ * @param {string} text
42
+ * @returns {string}
43
+ */
44
+ function extractDomain(text) {
45
+ const lower = text.toLowerCase();
46
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
47
+ if (keywords.some((kw) => lower.includes(kw))) {
48
+ return domain;
49
+ }
50
+ }
51
+ return 'general';
52
+ }
53
+
54
+ /**
55
+ * Extract capabilities from natural language text.
56
+ * @param {string} text
57
+ * @returns {string[]}
58
+ */
59
+ function extractCapabilities(text) {
60
+ const lower = text.toLowerCase();
61
+ const found = [];
62
+ for (const [cap, keywords] of Object.entries(CAPABILITY_KEYWORDS)) {
63
+ if (keywords.some((kw) => lower.includes(kw))) {
64
+ found.push(cap);
65
+ }
66
+ }
67
+ return found;
68
+ }
69
+
70
+ /**
71
+ * Infer agent name from text.
72
+ * @param {string} text
73
+ * @returns {string}
74
+ */
75
+ function inferName(text) {
76
+ // Try to find "Agent name" or similar patterns
77
+ const namePatterns = [
78
+ /(?:agent[ey]?|bot|helfer)\s+(?:f[ü|u]r|named?|called?)\s+"?([^",\n.]+)"?/i,
79
+ /(?:erstelle|kreiere|baue)\s+(?:einen|eine|ein)?\s*(?:agent|bot|helfer)\s*(?:f[ü|u]r|named?)?\s*"?([^",\n.]+)"?/i,
80
+ /(?:finanz|marketing|dev|hr|legal)\s*(?:agent|bot|helfer)/i,
81
+ ];
82
+
83
+ for (const pattern of namePatterns) {
84
+ const match = text.match(pattern);
85
+ if (match) {
86
+ return match[1].trim().replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
87
+ }
88
+ }
89
+
90
+ // Fall back to domain-based name
91
+ const domain = extractDomain(text);
92
+ return `${domain}-agent-${Date.now().toString(36).slice(-4)}`;
93
+ }
94
+
95
+ /**
96
+ * Create an agent from natural language text.
97
+ * @param {string} text - e.g. "Erstelle einen Agenten für Finanzen"
98
+ * @returns {object} The registered agent
99
+ */
100
+ export function createFromText(text) {
101
+ const domain = extractDomain(text);
102
+ const capabilities = extractCapabilities(text);
103
+ const name = inferName(text);
104
+
105
+ // Check if trigger parser finds any intent
106
+ const parsed = triggerParser.parse(text);
107
+
108
+ const agent = registerAgent(name, {
109
+ domain,
110
+ capabilities,
111
+ description: text.slice(0, 200),
112
+ metadata: {
113
+ source: 'text',
114
+ originalText: text,
115
+ parsedTrigger: parsed ? parsed.trigger.id : null,
116
+ },
117
+ });
118
+
119
+ return agent;
120
+ }
121
+
122
+ /**
123
+ * Parse a markdown agent definition and register it.
124
+ * Supports a simple structured MD format:
125
+ * ---
126
+ * name: agent-name
127
+ * domain: finance
128
+ * status: active
129
+ * ---
130
+ * Optional description text here.
131
+ * @param {string} mdContent
132
+ * @returns {object} The registered agent
133
+ */
134
+ export function createFromMD(mdContent) {
135
+ // Split on --- frontmatter
136
+ const fmMatch = mdContent.match(/^---\s*\n([\s\S]*?)\n---/);
137
+ let metadata = {};
138
+ let description = '';
139
+
140
+ if (fmMatch) {
141
+ // Parse frontmatter
142
+ const fmLines = fmMatch[1].split('\n');
143
+ for (const line of fmLines) {
144
+ const colonIdx = line.indexOf(':');
145
+ if (colonIdx > 0) {
146
+ const key = line.slice(0, colonIdx).trim();
147
+ const value = line.slice(colonIdx + 1).trim();
148
+ metadata[key] = value;
149
+ }
150
+ }
151
+ description = mdContent.slice(fmMatch[0].length).trim();
152
+ } else {
153
+ description = mdContent;
154
+ }
155
+
156
+ const name = metadata.name || inferName(description);
157
+
158
+ const agent = registerAgent(name, {
159
+ domain: metadata.domain || extractDomain(description),
160
+ capabilities: metadata.capabilities
161
+ ? metadata.capabilities.split(',').map((c) => c.trim())
162
+ : extractCapabilities(description),
163
+ status: metadata.status || 'active',
164
+ description: description.slice(0, 500),
165
+ metadata: {
166
+ source: 'markdown',
167
+ originalMD: mdContent,
168
+ ...metadata,
169
+ },
170
+ });
171
+
172
+ return agent;
173
+ }
174
+
175
+ export default {
176
+ createFromText,
177
+ createFromMD,
178
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * PACS Core — Agent Registry Module
3
+ * Manages agent definitions with encrypted persistence
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import crypto from './crypto.js';
10
+ import memoryStore from './memory-store.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const AGENTS_FILE = 'agents.enc';
14
+
15
+ /**
16
+ * Generate a short unique ID.
17
+ * @returns {string}
18
+ */
19
+ function generateId() {
20
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
21
+ }
22
+
23
+ /**
24
+ * Get the agents store path.
25
+ * @returns {string}
26
+ */
27
+ function getAgentsPath() {
28
+ return memoryStore.getFilePath(AGENTS_FILE);
29
+ }
30
+
31
+ /**
32
+ * Read all agents from encrypted store.
33
+ * @returns {object} Agent store
34
+ */
35
+ function readAgentsStore() {
36
+ try {
37
+ return memoryStore.read(AGENTS_FILE) || { agents: [] };
38
+ } catch {
39
+ return { agents: [] };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Write agents to encrypted store.
45
+ * @param {object} store - { agents: [] }
46
+ */
47
+ function writeAgentsStore(store) {
48
+ memoryStore.write(AGENTS_FILE, store);
49
+ }
50
+
51
+ /**
52
+ * Register a new agent or update an existing one.
53
+ * @param {string} name - Unique agent name
54
+ * @param {object} definition - Agent definition
55
+ * @returns {object} Registered agent
56
+ */
57
+ export function registerAgent(name, definition = {}) {
58
+ const store = readAgentsStore();
59
+
60
+ const now = new Date().toISOString();
61
+ const existing = store.agents.find((a) => a.name === name);
62
+
63
+ const agent = {
64
+ id: existing ? existing.id : generateId(),
65
+ name,
66
+ domain: definition.domain || 'general',
67
+ capabilities: definition.capabilities || [],
68
+ status: definition.status || 'active',
69
+ lastActive: now,
70
+ createdAt: existing ? existing.createdAt : now,
71
+ description: definition.description || '',
72
+ metadata: definition.metadata || {},
73
+ };
74
+
75
+ // Replace or add
76
+ store.agents = store.agents.filter((a) => a.name !== name);
77
+ store.agents.push(agent);
78
+ writeAgentsStore(store);
79
+
80
+ return agent;
81
+ }
82
+
83
+ /**
84
+ * Get an agent by name.
85
+ * @param {string} name
86
+ * @returns {object|null}
87
+ */
88
+ export function getAgent(name) {
89
+ const store = readAgentsStore();
90
+ const agent = store.agents.find((a) => a.name === name);
91
+ if (agent) {
92
+ // Touch lastActive on access
93
+ agent.lastActive = new Date().toISOString();
94
+ writeAgentsStore(store);
95
+ }
96
+ return agent || null;
97
+ }
98
+
99
+ /**
100
+ * List all agents, optionally filtered.
101
+ * @param {object} [filter] - { status, domain }
102
+ * @returns {object[]}
103
+ */
104
+ export function listAgents(filter = {}) {
105
+ const store = readAgentsStore();
106
+ let agents = store.agents;
107
+
108
+ if (filter.status) {
109
+ agents = agents.filter((a) => a.status === filter.status);
110
+ }
111
+ if (filter.domain) {
112
+ agents = agents.filter((a) => a.domain === filter.domain);
113
+ }
114
+
115
+ return agents;
116
+ }
117
+
118
+ /**
119
+ * Remove an agent by name.
120
+ * @param {string} name
121
+ * @returns {boolean} True if removed
122
+ */
123
+ export function removeAgent(name) {
124
+ const store = readAgentsStore();
125
+ const before = store.agents.length;
126
+ store.agents = store.agents.filter((a) => a.name !== name);
127
+ if (store.agents.length < before) {
128
+ writeAgentsStore(store);
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ /**
135
+ * Update agent fields.
136
+ * @param {string} name
137
+ * @param {object} updates
138
+ * @returns {object|null}
139
+ */
140
+ export function updateAgent(name, updates) {
141
+ const store = readAgentsStore();
142
+ const idx = store.agents.findIndex((a) => a.name === name);
143
+ if (idx === -1) return null;
144
+
145
+ const agent = store.agents[idx];
146
+ const allowed = ['domain', 'capabilities', 'status', 'description', 'metadata'];
147
+ for (const key of allowed) {
148
+ if (updates[key] !== undefined) {
149
+ agent[key] = updates[key];
150
+ }
151
+ }
152
+ agent.lastActive = new Date().toISOString();
153
+
154
+ store.agents[idx] = agent;
155
+ writeAgentsStore(store);
156
+ return agent;
157
+ }
158
+
159
+ export default {
160
+ registerAgent,
161
+ getAgent,
162
+ listAgents,
163
+ removeAgent,
164
+ updateAgent,
165
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * PACS Core — Bootstrap Module
3
+ * Creates ~/.openclaw/pacs/ directory structure with sample encrypted files.
4
+ * Run with: npm run bootstrap
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const crypto = require('./crypto');
10
+ const memoryStore = require('./memory-store');
11
+ const triggerParser = require('./trigger-parser');
12
+
13
+ const PACS_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw', 'pacs');
14
+
15
+ /**
16
+ * Print colored console output.
17
+ */
18
+ function log(msg, color = 'INFO') {
19
+ const colors = {
20
+ INFO: '\x1b[36m',
21
+ OK: '\x1b[32m',
22
+ WARN: '\x1b[33m',
23
+ ERROR: '\x1b[31m',
24
+ RESET: '\x1b[0m',
25
+ };
26
+ console.log(`${colors[color] || ''}[PACS Bootstrap]${colors.RESET} ${msg}`);
27
+ }
28
+
29
+ /**
30
+ * Check if PACS_ENCRYPTION_KEY is set.
31
+ */
32
+ function checkEncryptionKey() {
33
+ if (!process.env.PACS_ENCRYPTION_KEY) {
34
+ log('PACS_ENCRYPTION_KEY is not set.', 'WARN');
35
+ log('Generating a temporary key for bootstrap only.', 'WARN');
36
+ log('IMPORTANT: Set your own key for production use:', 'WARN');
37
+ const key = crypto.generateKey();
38
+ console.log(` export PACS_ENCRYPTION_KEY=\"${key}\"`);
39
+ console.log('');
40
+ // Set for this session only
41
+ process.env.PACS_ENCRYPTION_KEY = key;
42
+ } else {
43
+ log(`Encryption key: ✓ (${process.env.PACS_ENCRYPTION_KEY.slice(0, 8)}...)`, 'OK');
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Create the PACS directory structure.
49
+ */
50
+ function createDirectories() {
51
+ if (!fs.existsSync(PACS_DIR)) {
52
+ fs.mkdirSync(PACS_DIR, { recursive: true });
53
+ log(`Created: ${PACS_DIR}`, 'OK');
54
+ } else {
55
+ log(`Directory exists: ${PACS_DIR}`, 'INFO');
56
+ }
57
+
58
+ // Create subdirectories
59
+ const subdirs = ['triggers', 'backups'];
60
+ for (const subdir of subdirs) {
61
+ const dir = path.join(PACS_DIR, subdir);
62
+ if (!fs.existsSync(dir)) {
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ log(`Created: ${dir}`, 'OK');
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Copy triggers.json to PACS directory.
71
+ */
72
+ function installTriggers() {
73
+ const source = path.join(__dirname, 'triggers.json');
74
+ const target = path.join(PACS_DIR, 'triggers', 'triggers.json');
75
+
76
+ if (fs.existsSync(target)) {
77
+ log(`triggers.json already installed`, 'INFO');
78
+ } else {
79
+ fs.copyFileSync(source, target);
80
+ log(`Installed: ${target}`, 'OK');
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Create sample encrypted memory file.
86
+ */
87
+ function createSampleMemory() {
88
+ const memory = {
89
+ facts: [
90
+ 'Ich spreche am liebsten Deutsch',
91
+ 'Arbeite gerne mit klaren Strukturen',
92
+ 'Wichtig: Datenschutz bei privaten Daten',
93
+ ],
94
+ private: [],
95
+ learnedAt: {},
96
+ _bootstrap: true,
97
+ _created: new Date().toISOString(),
98
+ };
99
+
100
+ try {
101
+ memoryStore.writeMemory(memory);
102
+ log(`Created sample memory`, 'OK');
103
+ } catch (err) {
104
+ if (err.message.includes('environment variable')) {
105
+ log(`Skipped memory creation (no encryption key)`, 'WARN');
106
+ } else {
107
+ throw err;
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Create sample routing file.
114
+ */
115
+ function createSampleRouting() {
116
+ const routing = {
117
+ hints: [
118
+ 'finance → finance-agent',
119
+ 'code → code-agent',
120
+ 'quick → fast-agent',
121
+ ],
122
+ agents: [],
123
+ _bootstrap: true,
124
+ _created: new Date().toISOString(),
125
+ };
126
+
127
+ try {
128
+ memoryStore.writeRouting(routing);
129
+ log(`Created sample routing`, 'OK');
130
+ } catch (err) {
131
+ if (err.message.includes('environment variable')) {
132
+ log(`Skipped routing creation (no encryption key)`, 'WARN');
133
+ } else {
134
+ throw err;
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Create sample core rules file.
141
+ */
142
+ function createSampleCoreRules() {
143
+ const rules = {
144
+ rules: [
145
+ 'Antworte in der Sprache des Users',
146
+ ' Sei präzise und strukturiert',
147
+ 'Private Daten niemals weitergeben oder exportieren',
148
+ ],
149
+ _bootstrap: true,
150
+ _created: new Date().toISOString(),
151
+ };
152
+
153
+ try {
154
+ memoryStore.writeCoreRules(rules);
155
+ log(`Created sample core rules`, 'OK');
156
+ } catch (err) {
157
+ if (err.message.includes('environment variable')) {
158
+ log(`Skipped rules creation (no encryption key)`, 'WARN');
159
+ } else {
160
+ throw err;
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Create a README in the PACS directory.
167
+ */
168
+ function createPacsReadme() {
169
+ const readme = `# PACS — Personal AI Control System
170
+
171
+ ## Directory Structure
172
+ \`\`\`
173
+ ~/.openclaw/pacs/
174
+ ├── memory.enc # Encrypted user facts
175
+ ├── routing.enc # Encrypted routing hints
176
+ ├── core-rules.enc # Encrypted AI behavior rules
177
+ └── triggers/
178
+ └── triggers.json # Plaintext trigger definitions
179
+ \`\`\`
180
+
181
+ ## Security
182
+ - All .enc files are encrypted with AES-256-GCM
183
+ - Key is in PACS_ENCRYPTION_KEY env variable only
184
+ - Never share your encryption key
185
+ - Private facts are excluded from exports
186
+
187
+ ## Commands
188
+ Run: \`node src/trigger-parser.js\` to test parsing.
189
+ Run: \`node src/memory-store.js\` to test storage.
190
+
191
+ ## Learn More
192
+ See: README.md (project root)
193
+ `;
194
+
195
+ const readmePath = path.join(PACS_DIR, 'README.md');
196
+ fs.writeFileSync(readmePath, readme, 'utf8');
197
+ log(`Created: ${readmePath}`, 'OK');
198
+ }
199
+
200
+ /**
201
+ * Print setup instructions.
202
+ */
203
+ function printInstructions() {
204
+ console.log('');
205
+ log('=== Bootstrap Complete ===', 'OK');
206
+ console.log('');
207
+ console.log('Next steps:');
208
+ console.log(' 1. Set your encryption key:');
209
+ console.log(` export PACS_ENCRYPTION_KEY="$(openssl rand -hex 32)"`);
210
+ console.log('');
211
+ console.log(' 2. Test the module:');
212
+ console.log(' npm test');
213
+ console.log('');
214
+ console.log(` 3. PACS home: ${PACS_DIR}`);
215
+ console.log('');
216
+ console.log('Phase 1 complete. Phase 2 will add:');
217
+ console.log(' - Agent routing engine');
218
+ console.log(' - Learning loop with confirmation prompts');
219
+ console.log(' - OpenClaw integration bridge');
220
+ }
221
+
222
+ /**
223
+ * Main bootstrap function.
224
+ */
225
+ function bootstrap() {
226
+ console.log('');
227
+ log('PACS Core — Bootstrap (Phase 1)', 'INFO');
228
+ console.log('');
229
+
230
+ checkEncryptionKey();
231
+ createDirectories();
232
+ installTriggers();
233
+ createSampleMemory();
234
+ createSampleRouting();
235
+ createSampleCoreRules();
236
+ createPacsReadme();
237
+ printInstructions();
238
+ }
239
+
240
+ // Run if called directly
241
+ if (require.main === module) {
242
+ bootstrap();
243
+ }
244
+
245
+ module.exports = { bootstrap, PACS_DIR };