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.
- package/LICENSE +21 -0
- package/README.md +602 -0
- package/bin/pacs-cli.js +7 -0
- package/package.json +31 -0
- package/phase2-test.mjs +53 -0
- package/src/agent-creator.js +178 -0
- package/src/agent-registry.js +165 -0
- package/src/bootstrap.js +245 -0
- package/src/cli.js +384 -0
- package/src/crypto.js +118 -0
- package/src/index.js +74 -0
- package/src/inter-agent-bus.js +187 -0
- package/src/learn-mode.js +97 -0
- package/src/learning-loop.js +254 -0
- package/src/memory-store.js +226 -0
- package/src/openclaw-bridge.cjs +214 -0
- package/src/openclaw-bridge.js +304 -0
- package/src/routing-engine.js +221 -0
- package/src/test.js +298 -0
- package/src/trigger-parser.js +187 -0
- package/src/triggers.json +122 -0
package/phase2-test.mjs
ADDED
|
@@ -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
|
+
};
|
package/src/bootstrap.js
ADDED
|
@@ -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 };
|