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,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 };