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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PACS Core — Memory Store Module
|
|
3
|
+
* Encrypted read/write for memory.enc, routing.enc, core-rules.enc
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('./crypto');
|
|
9
|
+
|
|
10
|
+
const STORAGE_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw', 'pacs');
|
|
11
|
+
|
|
12
|
+
// Encrypted file names
|
|
13
|
+
const FILES = {
|
|
14
|
+
MEMORY: 'memory.enc',
|
|
15
|
+
ROUTING: 'routing.enc',
|
|
16
|
+
CORE_RULES: 'core-rules.enc',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure the PACS storage directory exists.
|
|
21
|
+
*/
|
|
22
|
+
function ensureStorageDir() {
|
|
23
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
24
|
+
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
return STORAGE_DIR;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the full path to a storage file.
|
|
31
|
+
* @param {string} filename - One of the FILES constants
|
|
32
|
+
* @returns {string} Full path
|
|
33
|
+
*/
|
|
34
|
+
function getFilePath(filename) {
|
|
35
|
+
return path.join(STORAGE_DIR, filename);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read and decrypt a storage file.
|
|
40
|
+
* Returns null if file doesn't exist.
|
|
41
|
+
* @param {string} filename - Storage file name (from FILES constants)
|
|
42
|
+
* @returns {object|null} Parsed JSON or null
|
|
43
|
+
*/
|
|
44
|
+
function read(filename) {
|
|
45
|
+
const filePath = getFilePath(filename);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const encrypted = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
const decrypted = crypto.decrypt(encrypted);
|
|
54
|
+
return JSON.parse(decrypted);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code === 'ENOENT') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Failed to read encrypted file ${filename}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encrypt and write data to a storage file.
|
|
65
|
+
* @param {string} filename - Storage file name
|
|
66
|
+
* @param {object} data - Data to encrypt and save (will be JSON-stringified)
|
|
67
|
+
*/
|
|
68
|
+
function write(filename, data) {
|
|
69
|
+
ensureStorageDir();
|
|
70
|
+
const filePath = getFilePath(filename);
|
|
71
|
+
const json = JSON.stringify(data, null, 2);
|
|
72
|
+
const encrypted = crypto.encrypt(json);
|
|
73
|
+
fs.writeFileSync(filePath, encrypted, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read memory store.
|
|
78
|
+
* @returns {object} Memory object or empty structure
|
|
79
|
+
*/
|
|
80
|
+
function readMemory() {
|
|
81
|
+
return read(FILES.MEMORY) || { facts: [], private: [], learnedAt: {} };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write memory store.
|
|
86
|
+
* @param {object} memory - Memory object
|
|
87
|
+
*/
|
|
88
|
+
function writeMemory(memory) {
|
|
89
|
+
write(FILES.MEMORY, memory);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read routing store.
|
|
94
|
+
* @returns {object} Routing object or empty structure
|
|
95
|
+
*/
|
|
96
|
+
function readRouting() {
|
|
97
|
+
return read(FILES.ROUTING) || { hints: [], agents: [] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Write routing store.
|
|
102
|
+
* @param {object} routing - Routing object
|
|
103
|
+
*/
|
|
104
|
+
function writeRouting(routing) {
|
|
105
|
+
write(FILES.ROUTING, routing);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read core rules store.
|
|
110
|
+
* @returns {object} Core rules or empty structure
|
|
111
|
+
*/
|
|
112
|
+
function readCoreRules() {
|
|
113
|
+
return read(FILES.CORE_RULES) || { rules: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write core rules store.
|
|
118
|
+
* @param {object} rules - Core rules object
|
|
119
|
+
*/
|
|
120
|
+
function writeCoreRules(rules) {
|
|
121
|
+
write(FILES.CORE_RULES, rules);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add a fact to memory.
|
|
126
|
+
* @param {string} fact - The fact to store
|
|
127
|
+
* @param {boolean} isPrivate - Whether to mark as private
|
|
128
|
+
* @returns {object} Updated memory
|
|
129
|
+
*/
|
|
130
|
+
function addFact(fact, isPrivate = false) {
|
|
131
|
+
const memory = readMemory();
|
|
132
|
+
|
|
133
|
+
// Avoid duplicates
|
|
134
|
+
if (!memory.facts.includes(fact)) {
|
|
135
|
+
memory.facts.push(fact);
|
|
136
|
+
memory.learnedAt[fact] = new Date().toISOString();
|
|
137
|
+
if (isPrivate) {
|
|
138
|
+
memory.private.push(fact);
|
|
139
|
+
}
|
|
140
|
+
writeMemory(memory);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return memory;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Remove a fact from memory.
|
|
148
|
+
* @param {string} fact - The fact to forget
|
|
149
|
+
* @returns {object} Updated memory
|
|
150
|
+
*/
|
|
151
|
+
function removeFact(fact) {
|
|
152
|
+
const memory = readMemory();
|
|
153
|
+
|
|
154
|
+
memory.facts = memory.facts.filter((f) => f !== fact);
|
|
155
|
+
delete memory.learnedAt[fact];
|
|
156
|
+
memory.private = memory.private.filter((f) => f !== fact);
|
|
157
|
+
|
|
158
|
+
writeMemory(memory);
|
|
159
|
+
return memory;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Search facts in memory (simple substring search).
|
|
164
|
+
* @param {string} query - Search query
|
|
165
|
+
* @returns {string[]} Matching facts
|
|
166
|
+
*/
|
|
167
|
+
function searchFacts(query) {
|
|
168
|
+
const memory = readMemory();
|
|
169
|
+
const q = query.toLowerCase();
|
|
170
|
+
return memory.facts.filter((f) => f.toLowerCase().includes(q));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add a routing hint.
|
|
175
|
+
* @param {string} hint - Routing hint (e.g. "finance → finance-agent")
|
|
176
|
+
*/
|
|
177
|
+
function addRoutingHint(hint) {
|
|
178
|
+
const routing = readRouting();
|
|
179
|
+
if (!routing.hints.includes(hint)) {
|
|
180
|
+
routing.hints.push(hint);
|
|
181
|
+
writeRouting(routing);
|
|
182
|
+
}
|
|
183
|
+
return routing;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Add a core rule.
|
|
188
|
+
* @param {string} rule - Rule text
|
|
189
|
+
*/
|
|
190
|
+
function addRule(rule) {
|
|
191
|
+
const rules = readCoreRules();
|
|
192
|
+
if (!rules.rules.includes(rule)) {
|
|
193
|
+
rules.rules.push(rule);
|
|
194
|
+
writeCoreRules(rules);
|
|
195
|
+
}
|
|
196
|
+
return rules;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get the storage directory path.
|
|
201
|
+
* @returns {string} Storage directory
|
|
202
|
+
*/
|
|
203
|
+
function getStorageDir() {
|
|
204
|
+
return STORAGE_DIR;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
FILES,
|
|
209
|
+
STORAGE_DIR,
|
|
210
|
+
ensureStorageDir,
|
|
211
|
+
getFilePath,
|
|
212
|
+
read,
|
|
213
|
+
write,
|
|
214
|
+
readMemory,
|
|
215
|
+
writeMemory,
|
|
216
|
+
readRouting,
|
|
217
|
+
writeRouting,
|
|
218
|
+
readCoreRules,
|
|
219
|
+
writeCoreRules,
|
|
220
|
+
addFact,
|
|
221
|
+
removeFact,
|
|
222
|
+
searchFacts,
|
|
223
|
+
addRoutingHint,
|
|
224
|
+
addRule,
|
|
225
|
+
getStorageDir,
|
|
226
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PACS Core — OpenClaw Bridge Module (CJS wrapper)
|
|
3
|
+
* Integrates PACS with the OpenClaw agent framework
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* - "standalone": PACS runs independently, OpenClaw is just the UI
|
|
7
|
+
* - "integrated": PACS and OpenClaw agents coexist, PACS gets first pick
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { containsTrigger, detectLearnMode } = require('./trigger-parser.js');
|
|
13
|
+
const memoryStore = require('./memory-store.js');
|
|
14
|
+
|
|
15
|
+
/** @type {object|null} */
|
|
16
|
+
let _openClawConfig = null;
|
|
17
|
+
|
|
18
|
+
/** @type {object|null} OpenClaw agent API (when available) */
|
|
19
|
+
let _openClawAgents = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* PACS keywords / command prefixes that indicate a PACS query.
|
|
23
|
+
* @type {string[]}
|
|
24
|
+
*/
|
|
25
|
+
const PACS_TRIGGERS = ['MERKE', 'SUCHE', 'LERNE', 'AGENT', 'ROUTING', 'REGELN', 'MEMORY'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect whether a user message is a PACS command or a general OpenClaw task.
|
|
29
|
+
* @param {string} message
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isPACSQuery(message) {
|
|
33
|
+
if (!message || typeof message !== 'string') return false;
|
|
34
|
+
const upper = message.trim().toUpperCase();
|
|
35
|
+
if (PACS_TRIGGERS.some((t) => upper.startsWith(t))) return true;
|
|
36
|
+
if (containsTrigger(message)) return true;
|
|
37
|
+
if (detectLearnMode(message)) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize PACS inside OpenClaw.
|
|
43
|
+
* @param {object} openClawConfig - { pacs: { enabled, mode } }
|
|
44
|
+
* @param {object} [openClawAgentApi] - Optional OpenClaw agent API for fallback
|
|
45
|
+
*/
|
|
46
|
+
function init(openClawConfig = {}, openClawAgentApi = null) {
|
|
47
|
+
_openClawConfig = openClawConfig;
|
|
48
|
+
_openClawAgents = openClawAgentApi;
|
|
49
|
+
const pacsConfig = openClawConfig.pacs || {};
|
|
50
|
+
const mode = pacsConfig.mode || 'integrated';
|
|
51
|
+
const enabled = pacsConfig.enabled !== false;
|
|
52
|
+
if (!enabled) {
|
|
53
|
+
console.warn('[PACS Bridge] PACS is disabled in config.');
|
|
54
|
+
return { ok: false, reason: 'disabled' };
|
|
55
|
+
}
|
|
56
|
+
console.log(`[PACS Bridge] Initialized in "${mode}" mode.`);
|
|
57
|
+
return { ok: true, mode };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Main entry — PACS processes the user message, routes to agents, returns response.
|
|
62
|
+
* @param {string} userMessage
|
|
63
|
+
* @returns {Promise<object>} { response, routedTo, fallback }
|
|
64
|
+
*/
|
|
65
|
+
async function query(userMessage) {
|
|
66
|
+
if (!isPACSQuery(userMessage)) {
|
|
67
|
+
return fallbackToOpenClawAgents(userMessage);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mode = (_openClawConfig?.pacs?.mode) || 'integrated';
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const upper = userMessage.trim().toUpperCase();
|
|
74
|
+
|
|
75
|
+
// --- MERKE / learn a fact ---
|
|
76
|
+
if (upper.startsWith('MERKE') || upper.startsWith('LERNE')) {
|
|
77
|
+
const fact = userMessage
|
|
78
|
+
.replace(/^(MERKE|LERNE)\s*/i, '')
|
|
79
|
+
.trim();
|
|
80
|
+
if (fact) {
|
|
81
|
+
memoryStore.addFact(fact);
|
|
82
|
+
return { response: `✓ Merke: ${fact}`, routedTo: 'PACS-memory', fallback: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- SUCHE / search memory ---
|
|
87
|
+
if (upper.startsWith('SUCHE')) {
|
|
88
|
+
const q = userMessage.replace(/^(SUCHE|SUCH)\s*/i, '').trim();
|
|
89
|
+
const results = memoryStore.searchFacts(q);
|
|
90
|
+
if (results.length === 0) {
|
|
91
|
+
return { response: `Keine Ergebnisse für "${q}".`, routedTo: 'PACS-memory', fallback: false };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
response: `Gefunden:\n${results.map((r, i) => `${i + 1}. ${r}`).join('\n')}`,
|
|
95
|
+
routedTo: 'PACS-memory',
|
|
96
|
+
fallback: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- AGENT commands (basic) ---
|
|
101
|
+
if (upper.startsWith('AGENT')) {
|
|
102
|
+
return handleAgentCommand(userMessage);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Learn mode ---
|
|
106
|
+
if (detectLearnMode(userMessage)) {
|
|
107
|
+
return handleLearnModeCommand(userMessage);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
response: 'PACS verstanden, aber keine passende Aktion gefunden.',
|
|
112
|
+
routedTo: null,
|
|
113
|
+
fallback: false,
|
|
114
|
+
};
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error('[PACS Bridge] query error:', err);
|
|
117
|
+
return { response: `Fehler: ${err.message}`, routedTo: null, fallback: false };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fallback: delegate to OpenClaw's built-in agents when PACS has no match.
|
|
123
|
+
* @param {string} message
|
|
124
|
+
* @returns {Promise<object>}
|
|
125
|
+
*/
|
|
126
|
+
async function fallbackToOpenClawAgents(message) {
|
|
127
|
+
if (!_openClawAgents) {
|
|
128
|
+
return {
|
|
129
|
+
response: 'Kein OpenClaw-Agent verfügbar (Bridge nicht mit OpenClaw-API verbunden).',
|
|
130
|
+
routedTo: null,
|
|
131
|
+
fallback: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const result = await _openClawAgents.dispatch(message);
|
|
136
|
+
return {
|
|
137
|
+
response: result.response || result,
|
|
138
|
+
routedTo: result.agent || 'openclaw-default',
|
|
139
|
+
fallback: true,
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { response: `OpenClaw-Fallback fehlgeschlagen: ${err.message}`, routedTo: null, fallback: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function handleAgentCommand(message) {
|
|
147
|
+
const upper = message.trim().toUpperCase();
|
|
148
|
+
const AgentRegistry = (await import('./agent-registry.js')).default;
|
|
149
|
+
|
|
150
|
+
if (upper === 'AGENT LIST' || upper === 'AGENT LISTE') {
|
|
151
|
+
const agents = AgentRegistry.listAgents();
|
|
152
|
+
if (agents.length === 0) {
|
|
153
|
+
return { response: 'Keine Agenten registriert.', routedTo: 'PACS-registry', fallback: false };
|
|
154
|
+
}
|
|
155
|
+
const lines = agents.map((a) => `• ${a.name} (${a.domain}) — ${a.status}`);
|
|
156
|
+
return { response: `Agenten:\n${lines.join('\n')}`, routedTo: 'PACS-registry', fallback: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const addMatch = message.match(/^AGENT\s+ADD\s+(\w+)\s*(.*)/i);
|
|
160
|
+
if (addMatch) {
|
|
161
|
+
const [, name, rest] = addMatch;
|
|
162
|
+
const agent = AgentRegistry.registerAgent(name, { domain: 'custom', description: rest.trim() });
|
|
163
|
+
return { response: `✓ Agent "${name}" registriert.`, routedTo: 'PACS-registry', fallback: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const removeMatch = message.match(/^AGENT\s+REMOVE\s+(\w+)/i);
|
|
167
|
+
if (removeMatch) {
|
|
168
|
+
const removed = AgentRegistry.removeAgent(removeMatch[1]);
|
|
169
|
+
return {
|
|
170
|
+
response: removed ? `✓ Agent "${removeMatch[1]}" entfernt.` : `Agent "${removeMatch[1]}" nicht gefunden.`,
|
|
171
|
+
routedTo: 'PACS-registry',
|
|
172
|
+
fallback: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
response: 'Unbekannter AGENT-Befehl. Verfügbar: AGENT LIST, AGENT ADD <name>, AGENT REMOVE <name>',
|
|
178
|
+
routedTo: 'PACS-registry',
|
|
179
|
+
fallback: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleLearnModeCommand(message) {
|
|
184
|
+
const LearnMode = (await import('./learn-mode.js')).default;
|
|
185
|
+
const upper = message.trim().toUpperCase();
|
|
186
|
+
|
|
187
|
+
if (upper.includes('GET') || upper === 'LERNE MODUS') {
|
|
188
|
+
return { response: `Learn-Mode: ${LearnMode.getLearnMode()}`, routedTo: 'PACS-learn-mode', fallback: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const setMatch = message.match(/SET\s+(SAFE|EXPLICIT|AUTO)/i);
|
|
192
|
+
if (setMatch) {
|
|
193
|
+
const newMode = setMatch[1].toLowerCase();
|
|
194
|
+
LearnMode.setLearnMode(newMode);
|
|
195
|
+
return { response: `✓ Learn-Mode gesetzt auf: ${newMode}`, routedTo: 'PACS-learn-mode', fallback: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
response: 'Unbekannter LERNE-Befehl. Nutze: LERNE MODUS GET oder LERNE MODUS SET <safe|explicit|auto>',
|
|
200
|
+
routedTo: 'PACS-learn-mode',
|
|
201
|
+
fallback: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function status() {
|
|
206
|
+
return {
|
|
207
|
+
initialized: _openClawConfig !== null,
|
|
208
|
+
mode: _openClawConfig?.pacs?.mode || 'integrated',
|
|
209
|
+
enabled: _openClawConfig?.pacs?.enabled !== false,
|
|
210
|
+
openClawConnected: _openClawAgents !== null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { init, query, isPACSQuery, fallbackToOpenClawAgents, status };
|