hanuscode 1.0.0__py3-none-any.whl

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 (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. hanuscode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1750 @@
1
+ # plugins/cortex.py — Semantic Memory System with Knowledge Graph
2
+ """
3
+ Semantic memory system with knowledge graphs.
4
+
5
+ Allows long-term information retention and understanding relationships between concepts
6
+ (people, events, preferences, projects) rather than just text matching.
7
+
8
+ Commands:
9
+ /cortex remember <entity> <type> <attributes> — Save entity with attributes
10
+ /cortex relate <entity1> <relation> <entity2> — Create relation
11
+ /cortex recall <entity> — Get entity info
12
+ /cortex find <type> — Search entities by type
13
+ /cortex query <entity> <depth> — Query graph (relations)
14
+ /cortex forget <entity> — Delete entity
15
+ /cortex list — List entities
16
+ /cortex types — View entity types
17
+ /cortex relations — View relation types
18
+ /cortex graph [entity] — Visualize graph
19
+ /cortex timeline <entity> — View entity events
20
+ /cortex similar <entity> — Similar entities
21
+ /cortex path <entity1> <entity2> — Path between entities
22
+ """
23
+ import json
24
+ import re
25
+ from pathlib import Path
26
+ from datetime import datetime
27
+ from typing import Dict, List, Set, Optional, Tuple
28
+ from collections import defaultdict
29
+
30
+ NAME = "cortex"
31
+ DESCRIPTION = "Semantic memory system with knowledge graphs - USE THIS to store data you learn"
32
+ USAGE = "remember|relate|recall|find|query|forget|list|types|relations|graph|timeline|similar|path"
33
+
34
+ AGENT_DOC = """
35
+ ## āš ļø IMPORTANT: Cortex is for STORING DATA - DO NOT create files in .cortex/
36
+
37
+ Cortex is a KNOWLEDGE GRAPH MEMORY SYSTEM. When you learn or discover information,
38
+ STORE IT HERE using the commands below. DO NOT create markdown files in .cortex/ directory.
39
+
40
+ ## Why Cortex?
41
+
42
+ **Notes** = text notes with keyword search.
43
+ **Cortex** = STRUCTURED MEMORY with:
44
+ - Typed entities (person, target, vulnerability, codebase, etc.)
45
+ - Typed relations between entities (with automatic inverse relations)
46
+ - Graph queries (find related entities, paths, similar items)
47
+ - Episodic memory (events with temporal context)
48
+
49
+ ## MANDATORY - When to Use Cortex
50
+
51
+ **You MUST use Cortex after EVERY significant action:**
52
+
53
+ ### After Security Work (pentesting, CTF, recon):
54
+ 1. **Found target/host?** → `<run_plugin name="cortex" args="remember target1 target type=host ip=10.10.10.1 os=linux"/>`
55
+ 2. **Found open port?** → `<run_plugin name="cortex" args="remember port_80 service port=80 service=http host=target1"/>`
56
+ 3. **Found vulnerability?** → `<run_plugin name="cortex" args="remember vuln1 vulnerability cve=CVE-2024-1234 severity=high"/>`
57
+ 4. **Found credential?** → `<run_plugin name="cortex" args="remember admin_cred credential type=password user=admin source=target1"/>`
58
+ 5. **Exploited something?** → `<run_plugin name="cortex" args="remember exploit1 exploit type=rce target=target1 cve=CVE-2024-1234"/>`
59
+
60
+ ### After Creating/Modifying Code:
61
+ 1. **New project?** → `<run_plugin name="cortex" args="remember myproject project language=python framework=flask"/>`
62
+ 2. **New file?** → `<run_plugin name="cortex" args="remember auth_module module path=auth.py purpose=authentication"/>`
63
+ 3. **New API?** → `<run_plugin name="cortex" args="remember login_api api method=POST path=/login auth=jwt"/>`
64
+
65
+ ### Create Relations:
66
+ ```
67
+ <run_plugin name="cortex" args="relate target1 has_vuln vuln1"/>
68
+ <run_plugin name="cortex" args="relate vuln1 exploited_by exploit1"/>
69
+ <run_plugin name="cortex" args="relate auth_module defines login_api"/>
70
+ ```
71
+
72
+ ## Quick Reference Commands
73
+
74
+ ### remember - Save an entity
75
+ ```
76
+ <run_plugin name="cortex" args="remember <entity_name> <type> [key=value ...]"/>
77
+ <run_plugin name="cortex" args="remember webserver host ip=192.168.1.1 os=linux ports=22,80,443"/>
78
+ <run_plugin name="cortex" args="remember sqli_vuln vulnerability severity=high url=/search param=id"/>
79
+ ```
80
+
81
+ ### relate - Create relationship
82
+ ```
83
+ <run_plugin name="cortex" args="relate <entity1> <relation> <entity2>"/>
84
+ <run_plugin name="cortex" args="relate webserver has_vuln sqli_vuln"/>
85
+ ```
86
+
87
+ ### recall - Get entity info
88
+ ```
89
+ <run_plugin name="cortex" args="recall webserver"/>
90
+ ```
91
+
92
+ ### find - Search by type
93
+ ```
94
+ <run_plugin name="cortex" args="find host"/>
95
+ <run_plugin name="cortex" args="find vulnerability severity=high"/>
96
+ ```
97
+
98
+ ### query - Get related entities
99
+ ```
100
+ <run_plugin name="cortex" args="query webserver 2"/>
101
+ ```
102
+
103
+ ### list - List all entities
104
+ ```
105
+ <run_plugin name="cortex" args="list"/>
106
+ ```
107
+
108
+ ## Entity Types (you can use ANY type)
109
+
110
+ | Type | Use Case | Common Attributes |
111
+ |------|----------|-------------------|
112
+ | target | Security targets | type, scope, status |
113
+ | host | Servers/hosts | ip, hostname, os, ports, services |
114
+ | domain | Domain names | name, registrar, ip |
115
+ | port | Open ports | number, service, version |
116
+ | vulnerability | Vulnerabilities | cve, severity, cvss, affected |
117
+ | exploit | Exploits/PoCs | name, type, cve, reliability |
118
+ | credential | Found credentials | type, source, valid, scope |
119
+ | tool | Security tools | category, purpose, usage |
120
+ | finding | Security findings | severity, type, url, evidence |
121
+ | project | Software projects | status, language, framework |
122
+ | module | Code files | path, purpose, dependencies |
123
+ | api | APIs/endpoints | method, path, auth |
124
+ | person | People | role, team, email |
125
+
126
+ ## Relation Types (you can use ANY relation)
127
+
128
+ | Relation | Inverse | Meaning |
129
+ |----------|---------|---------|
130
+ | has_vuln | vuln_in | Has vulnerability |
131
+ | exploits | exploited_by | Exploits vulnerability |
132
+ | targets | targeted_by | Targets host/domain |
133
+ | hosts | hosted_on | Hosts service |
134
+ | contains | contained_in | Contains sub-component |
135
+ | uses | used_by | Uses technology/service |
136
+ | depends_on | depended_by | Depends on |
137
+ | depends_on | depended_by | Depends on |
138
+ | calls | called_by | Calls function |
139
+ | imports | imported_by | Imports module |
140
+ | defines | defined_in | Defines function/class |
141
+ | exposes | exposed_by | Exposes API |
142
+ | fixes | fixed_by | Fixes bug |
143
+ | has_vuln | vuln_in | Has vulnerability |
144
+ | exploits | exploited_by | Exploits vuln |
145
+ | targets | targeted_by | Targets host/domain |
146
+ | finds | found_by | Finds vulnerability |
147
+ | hosts | hosted_on | Hosts service |
148
+ | works_on | has_member | Works on project |
149
+
150
+ ## Commands
151
+
152
+ ### remember <entity> <type> [key=value...]
153
+ Save entity with attributes.
154
+
155
+ <run_plugin name="cortex" args="remember 'Auth API' project status=active language=python framework=flask"/>
156
+ <run_plugin name="cortex" args="remember auth.py module path=/auth purpose='authentication module'"/>
157
+ <run_plugin name="cortex" args="remember login function file=auth.py params='username,password'"/>
158
+
159
+ ### relate <entity1> <relation> <entity2>
160
+ Create relation (auto-creates inverse).
161
+
162
+ <run_plugin name="cortex" args="relate 'Auth API' contains auth.py"/>
163
+ <run_plugin name="cortex" args="relate auth.py defines login"/>
164
+ <run_plugin name="cortex" args="relate 'Auth API' uses Flask"/>
165
+
166
+ ### recall <entity>
167
+ Get all entity info.
168
+
169
+ <run_plugin name="cortex" args="recall 'Auth API'"/>
170
+
171
+ ### find <type> [key=value]
172
+ Search entities by type and/or attributes.
173
+
174
+ <run_plugin name="cortex" args="find module"/>
175
+ <run_plugin name="cortex" args="find bug severity=critical"/>
176
+
177
+ ### query <entity> [depth]
178
+ Get related entities up to N levels.
179
+
180
+ <run_plugin name="cortex" args="query 'Auth API' 2"/>
181
+
182
+ ### graph [entity]
183
+ Visualize the knowledge graph.
184
+
185
+ <run_plugin name="cortex" args="graph"/>
186
+
187
+ ### reset --force
188
+ Clear all data (use with caution).
189
+
190
+ <run_plugin name="cortex" args="reset --force"/>
191
+
192
+ <run_plugin name="cortex" args="path John Python"/>
193
+
194
+ ## Notes
195
+
196
+ - Entity types and relations are SUGGESTIONS, not restrictions
197
+ - You can create any type or relation you need
198
+ - Inverse relations are auto-created when defined
199
+ - Use /cortex types and /cortex relations to see all available
200
+ """
201
+
202
+ DATA_DIR = Path.home() / ".hanus" / "cortex"
203
+ ENTITIES_FILE = DATA_DIR / "entities.json"
204
+ RELATIONS_FILE = DATA_DIR / "relations.json"
205
+ EVENTS_FILE = DATA_DIR / "events.json"
206
+
207
+ # Suggested entity types with common attributes
208
+ # These are SUGGESTIONS - any type can be used
209
+ ENTITY_TYPES = {
210
+ # === PEOPLE & ORGANIZATIONS ===
211
+ "person": {
212
+ "description": "Person",
213
+ "default_attrs": ["role", "team", "email", "seniority", "timezone", "github", "skills"]
214
+ },
215
+ "organization": {
216
+ "description": "Company, team or group",
217
+ "default_attrs": ["type", "size", "industry", "website", "scope"]
218
+ },
219
+ "team": {
220
+ "description": "Work team",
221
+ "default_attrs": ["name", "project", "members", "tech_stack"]
222
+ },
223
+
224
+ # === PROJECTS & CODE ===
225
+ "project": {
226
+ "description": "Software project",
227
+ "default_attrs": ["status", "priority", "language", "framework", "repo", "branch"]
228
+ },
229
+ "codebase": {
230
+ "description": "Code repository",
231
+ "default_attrs": ["language", "framework", "size", "structure", "main_tech"]
232
+ },
233
+ "module": {
234
+ "description": "Code module or component",
235
+ "default_attrs": ["path", "language", "purpose", "dependencies"]
236
+ },
237
+ "function": {
238
+ "description": "Function or method",
239
+ "default_attrs": ["name", "file", "params", "returns", "purpose"]
240
+ },
241
+ "class": {
242
+ "description": "Class or type",
243
+ "default_attrs": ["name", "file", "inherits", "methods", "purpose"]
244
+ },
245
+ "api": {
246
+ "description": "API or service",
247
+ "default_attrs": ["method", "path", "auth", "rate_limit", "purpose"]
248
+ },
249
+ "endpoint": {
250
+ "description": "HTTP endpoint",
251
+ "default_attrs": ["url", "method", "auth_type", "params", "response_type"]
252
+ },
253
+ "database": {
254
+ "description": "Database",
255
+ "default_attrs": ["type", "host", "port", "name", "schema"]
256
+ },
257
+ "dependency": {
258
+ "description": "Dependency or library",
259
+ "default_attrs": ["name", "version", "source", "license", "purpose"]
260
+ },
261
+
262
+ # === DEVELOPMENT ===
263
+ "bug": {
264
+ "description": "Bug or issue",
265
+ "default_attrs": ["severity", "status", "component", "found_in", "fixed_in"]
266
+ },
267
+ "feature": {
268
+ "description": "Feature or functionality",
269
+ "default_attrs": ["status", "priority", "effort", "component", "milestone"]
270
+ },
271
+ "pr": {
272
+ "description": "Pull Request",
273
+ "default_attrs": ["number", "status", "author", "branch", "files_changed"]
274
+ },
275
+ "commit": {
276
+ "description": "Git commit",
277
+ "default_attrs": ["hash", "author", "date", "message", "files"]
278
+ },
279
+ "test": {
280
+ "description": "Test or test suite",
281
+ "default_attrs": ["type", "coverage", "status", "component"]
282
+ },
283
+ "config": {
284
+ "description": "Configuration",
285
+ "default_attrs": ["file", "environment", "purpose", "sensitive"]
286
+ },
287
+
288
+ # === SECURITY - INFRASTRUCTURE ===
289
+ "target": {
290
+ "description": "Security target (host, domain, app)",
291
+ "default_attrs": ["type", "scope", "status", "priority", "notes"]
292
+ },
293
+ "domain": {
294
+ "description": "Web domain",
295
+ "default_attrs": ["name", "registrar", "ip", "ports", "tech_stack"]
296
+ },
297
+ "host": {
298
+ "description": "Host or server",
299
+ "default_attrs": ["ip", "hostname", "os", "ports", "services"]
300
+ },
301
+ "ip": {
302
+ "description": "IP address",
303
+ "default_attrs": ["address", "type", "asn", "country", "hostname"]
304
+ },
305
+ "port": {
306
+ "description": "Open port",
307
+ "default_attrs": ["number", "protocol", "service", "version", "banner"]
308
+ },
309
+ "service": {
310
+ "description": "Detected service",
311
+ "default_attrs": ["name", "port", "version", "product", "vulnerabilities"]
312
+ },
313
+ "network": {
314
+ "description": "Network or subnet",
315
+ "default_attrs": ["cidr", "gateway", "range", "hosts", "purpose"]
316
+ },
317
+
318
+ # === SECURITY - VULNERABILITIES ===
319
+ "vulnerability": {
320
+ "description": "Vulnerability",
321
+ "default_attrs": ["cve", "severity", "cvss", "vector", "affected"]
322
+ },
323
+ "cve": {
324
+ "description": "Specific CVE",
325
+ "default_attrs": ["id", "severity", "product", "version", "exploit_available"]
326
+ },
327
+ "exploit": {
328
+ "description": "Exploit or PoC",
329
+ "default_attrs": ["name", "type", "cve", "reliability", "source"]
330
+ },
331
+ "payload": {
332
+ "description": "Exploit payload",
333
+ "default_attrs": ["type", "language", "target", "purpose", "detected_by"]
334
+ },
335
+ "finding": {
336
+ "description": "Security finding",
337
+ "default_attrs": ["severity", "type", "url", "parameter", "evidence"]
338
+ },
339
+
340
+ # === SECURITY - TOOLS & TECHNIQUES ===
341
+ "tool": {
342
+ "description": "Security tool",
343
+ "default_attrs": ["category", "language", "source", "purpose", "usage"]
344
+ },
345
+ "technique": {
346
+ "description": "Attack/defense technique",
347
+ "default_attrs": ["category", "mitre_id", "difficulty", "detection", "mitigation"]
348
+ },
349
+ "wordlist": {
350
+ "description": "Wordlist or dictionary",
351
+ "default_attrs": ["type", "size", "source", "purpose"]
352
+ },
353
+ "payload_type": {
354
+ "description": "Payload type",
355
+ "default_attrs": ["category", "context", "encoding", "bypasses"]
356
+ },
357
+ "scan": {
358
+ "description": "Scan performed",
359
+ "default_attrs": ["type", "tool", "target", "date", "findings"]
360
+ },
361
+
362
+ # === SECURITY - CREDENTIALS & ACCESS ===
363
+ "credential": {
364
+ "description": "Found credential",
365
+ "default_attrs": ["type", "source", "valid", "scope", "leaked_in"]
366
+ },
367
+ "session": {
368
+ "description": "Session or token",
369
+ "default_attrs": ["type", "valid_until", "scope", "user"]
370
+ },
371
+ "backdoor": {
372
+ "description": "Backdoor or access",
373
+ "default_attrs": ["type", "location", "persistence", "detected"]
374
+ },
375
+
376
+ # === GENERAL ===
377
+ "event": {
378
+ "description": "Event or meeting",
379
+ "default_attrs": ["date", "time", "location", "outcome", "attendees"]
380
+ },
381
+ "preference": {
382
+ "description": "Preference or setting",
383
+ "default_attrs": ["category", "value", "context", "reason"]
384
+ },
385
+ "technology": {
386
+ "description": "General technology",
387
+ "default_attrs": ["type", "version", "category", "maturity", "docs"]
388
+ },
389
+ "location": {
390
+ "description": "Location",
391
+ "default_attrs": ["type", "address", "timezone", "related_hosts"]
392
+ },
393
+ "task": {
394
+ "description": "Pending task",
395
+ "default_attrs": ["status", "priority", "due", "assignee", "context"]
396
+ },
397
+ "decision": {
398
+ "description": "Decision made",
399
+ "default_attrs": ["date", "context", "outcome", "rationale", "impact"]
400
+ },
401
+ "resource": {
402
+ "description": "External resource",
403
+ "default_attrs": ["type", "url", "access", "purpose"]
404
+ },
405
+ "note": {
406
+ "description": "Note or information",
407
+ "default_attrs": ["source", "date", "importance", "context"]
408
+ },
409
+ "knowledge": {
410
+ "description": "Learned knowledge",
411
+ "default_attrs": ["topic", "source", "confidence", "applicable_to"]
412
+ },
413
+ "concept": {
414
+ "description": "Concept or idea",
415
+ "default_attrs": ["definition", "related_to", "importance", "context"]
416
+ },
417
+ }
418
+
419
+ # Relation types with inverse mapping
420
+ # These are SUGGESTIONS - any relation can be used
421
+ # Format: relation_name -> {inverse: inverse_relation, description: meaning}
422
+ RELATION_TYPES = {
423
+ # === SOCIAL ===
424
+ "knows": {"inverse": "knows", "description": "Knows"},
425
+ "works_with": {"inverse": "works_with", "description": "Works with"},
426
+ "reports_to": {"inverse": "manages", "description": "Reports to"},
427
+ "manages": {"inverse": "reports_to", "description": "Manages"},
428
+ "collaborates_with": {"inverse": "collaborates_with", "description": "Collaborates with"},
429
+
430
+ # === PROJECTS ===
431
+ "works_on": {"inverse": "has_member", "description": "Works on"},
432
+ "has_member": {"inverse": "works_on", "description": "Has member"},
433
+ "leads": {"inverse": "led_by", "description": "Leads"},
434
+ "led_by": {"inverse": "leads", "description": "Led by"},
435
+ "owns": {"inverse": "owned_by", "description": "Owns"},
436
+ "owned_by": {"inverse": "owns", "description": "Owned by"},
437
+ "maintains": {"inverse": "maintained_by", "description": "Maintains"},
438
+ "maintained_by": {"inverse": "maintains", "description": "Maintained by"},
439
+
440
+ # === CODE ===
441
+ "imports": {"inverse": "imported_by", "description": "Imports"},
442
+ "imported_by": {"inverse": "imports", "description": "Imported by"},
443
+ "calls": {"inverse": "called_by", "description": "Calls"},
444
+ "called_by": {"inverse": "calls", "description": "Called by"},
445
+ "extends": {"inverse": "extended_by", "description": "Extends"},
446
+ "extended_by": {"inverse": "extends", "description": "Extended by"},
447
+ "implements": {"inverse": "implemented_by", "description": "Implements"},
448
+ "implemented_by": {"inverse": "implements", "description": "Implemented by"},
449
+ "overrides": {"inverse": "overridden_by", "description": "Overrides"},
450
+ "overridden_by": {"inverse": "overrides", "description": "Overridden by"},
451
+ "contains": {"inverse": "contained_in", "description": "Contains"},
452
+ "contained_in": {"inverse": "contains", "description": "Contained in"},
453
+ "defines": {"inverse": "defined_in", "description": "Defines"},
454
+ "defined_in": {"inverse": "defines", "description": "Defined in"},
455
+ "references": {"inverse": "referenced_by", "description": "References"},
456
+ "referenced_by": {"inverse": "references", "description": "Referenced by"},
457
+ "exposes": {"inverse": "exposed_by", "description": "Exposes"},
458
+ "exposed_by": {"inverse": "exposes", "description": "Exposed by"},
459
+
460
+ # === DEPENDENCIES ===
461
+ "depends_on": {"inverse": "depended_by", "description": "Depends on"},
462
+ "depended_by": {"inverse": "depends_on", "description": "Depended by"},
463
+ "requires": {"inverse": "required_by", "description": "Requires"},
464
+ "required_by": {"inverse": "requires", "description": "Required by"},
465
+ "uses": {"inverse": "used_by", "description": "Uses"},
466
+ "used_by": {"inverse": "uses", "description": "Used by"},
467
+ "includes": {"inverse": "included_in", "description": "Includes"},
468
+ "included_in": {"inverse": "includes", "description": "Included in"},
469
+
470
+ # === DEVELOPMENT ===
471
+ "fixes": {"inverse": "fixed_by", "description": "Fixes"},
472
+ "fixed_by": {"inverse": "fixes", "description": "Fixed by"},
473
+ "introduces": {"inverse": "introduced_by", "description": "Introduces"},
474
+ "introduced_by": {"inverse": "introduces", "description": "Introduced by"},
475
+ "causes": {"inverse": "caused_by", "description": "Causes"},
476
+ "caused_by": {"inverse": "causes", "description": "Caused by"},
477
+ "affects": {"inverse": "affected_by", "description": "Affects"},
478
+ "affected_by": {"inverse": "affects", "description": "Affected by"},
479
+ "tests": {"inverse": "tested_by", "description": "Tests"},
480
+ "tested_by": {"inverse": "tests", "description": "Tested by"},
481
+ "covers": {"inverse": "covered_by", "description": "Covers"},
482
+ "covered_by": {"inverse": "covers", "description": "Covered by"},
483
+ "merges": {"inverse": "merged_in", "description": "Merges"},
484
+ "merged_in": {"inverse": "merges", "description": "Merged in"},
485
+
486
+ # === SECURITY - INFRASTRUCTURE ===
487
+ "hosts": {"inverse": "hosted_on", "description": "Hosts"},
488
+ "hosted_on": {"inverse": "hosts", "description": "Hosted on"},
489
+ "runs_on": {"inverse": "runs", "description": "Runs on"},
490
+ "runs": {"inverse": "runs_on", "description": "Runs"},
491
+ "has_port": {"inverse": "port_of", "description": "Has port"},
492
+ "port_of": {"inverse": "has_port", "description": "Port of"},
493
+ "has_service": {"inverse": "service_of", "description": "Has service"},
494
+ "service_of": {"inverse": "has_service", "description": "Service of"},
495
+ "resolves_to": {"inverse": "resolves_from", "description": "Resolves to"},
496
+ "resolves_from": {"inverse": "resolves_to", "description": "Resolves from"},
497
+ "belongs_to": {"inverse": "owns", "description": "Belongs to"},
498
+ "part_of_network": {"inverse": "has_host", "description": "Part of network"},
499
+ "has_host": {"inverse": "part_of_network", "description": "Has host"},
500
+
501
+ # === SECURITY - ATTACKS ===
502
+ "has_vuln": {"inverse": "vuln_in", "description": "Has vulnerability"},
503
+ "vuln_in": {"inverse": "has_vuln", "description": "Vulnerability in"},
504
+ "exploits": {"inverse": "exploited_by", "description": "Exploits"},
505
+ "exploited_by": {"inverse": "exploits", "description": "Exploited by"},
506
+ "targets": {"inverse": "targeted_by", "description": "Targets"},
507
+ "targeted_by": {"inverse": "targets", "description": "Targeted by"},
508
+ "attacks": {"inverse": "attacked_by", "description": "Attacks"},
509
+ "attacked_by": {"inverse": "attacks", "description": "Attacked by"},
510
+ "scans": {"inverse": "scanned_by", "description": "Scans"},
511
+ "scanned_by": {"inverse": "scans", "description": "Scanned by"},
512
+ "finds": {"inverse": "found_by", "description": "Finds"},
513
+ "found_by": {"inverse": "finds", "description": "Found by"},
514
+ "detects": {"inverse": "detected_by", "description": "Detects"},
515
+ "detected_by": {"inverse": "detects", "description": "Detected by"},
516
+ "bypasses": {"inverse": "bypassed_by", "description": "Bypasses"},
517
+ "bypassed_by": {"inverse": "bypasses", "description": "Bypassed by"},
518
+ "mitigates": {"inverse": "mitigated_by", "description": "Mitigates"},
519
+ "mitigated_by": {"inverse": "mitigates", "description": "Mitigated by"},
520
+ "patches": {"inverse": "patched_by", "description": "Patches"},
521
+ "patched_by": {"inverse": "patches", "description": "Patched by"},
522
+
523
+ # === SECURITY - CREDENTIALS ===
524
+ "leaks": {"inverse": "leaked_in", "description": "Leaks"},
525
+ "leaked_in": {"inverse": "leaks", "description": "Leaked in"},
526
+ "uses_credential": {"inverse": "credential_for", "description": "Uses credential"},
527
+ "credential_for": {"inverse": "uses_credential", "description": "Credential for"},
528
+ "valid_for": {"inverse": "has_valid", "description": "Valid for"},
529
+ "has_valid": {"inverse": "valid_for", "description": "Has valid"},
530
+ "compromises": {"inverse": "compromised_by", "description": "Compromises"},
531
+ "compromised_by": {"inverse": "compromises", "description": "Compromised by"},
532
+
533
+ # === SECURITY - TOOLS ===
534
+ "runs_tool": {"inverse": "tool_runs_on", "description": "Runs tool"},
535
+ "tool_runs_on": {"inverse": "runs_tool", "description": "Tool runs on"},
536
+ "generates": {"inverse": "generated_by", "description": "Generates"},
537
+ "generated_by": {"inverse": "generates", "description": "Generated by"},
538
+ "produces": {"inverse": "produced_by", "description": "Produces"},
539
+ "produced_by": {"inverse": "produces", "description": "Produced by"},
540
+ "consumes": {"inverse": "consumed_by", "description": "Consumes"},
541
+ "consumed_by": {"inverse": "consumes", "description": "Consumed by"},
542
+
543
+ # === STRUCTURAL ===
544
+ "related_to": {"inverse": "related_to", "description": "Related to"},
545
+ "part_of": {"inverse": "has_part", "description": "Part of"},
546
+ "has_part": {"inverse": "part_of", "description": "Has part"},
547
+ "parent_of": {"inverse": "child_of", "description": "Parent of"},
548
+ "child_of": {"inverse": "parent_of", "description": "Child of"},
549
+ "similar_to": {"inverse": "similar_to", "description": "Similar to"},
550
+ "same_as": {"inverse": "same_as", "description": "Same as"},
551
+ "alternative_to": {"inverse": "alternative_to", "description": "Alternative to"},
552
+
553
+ # === CREATION ===
554
+ "created": {"inverse": "created_by", "description": "Created"},
555
+ "created_by": {"inverse": "created", "description": "Created by"},
556
+ "modified": {"inverse": "modified_by", "description": "Modified"},
557
+ "modified_by": {"inverse": "modified", "description": "Modified by"},
558
+ "discovered": {"inverse": "discovered_by", "description": "Discovered"},
559
+ "discovered_by": {"inverse": "discovered", "description": "Discovered by"},
560
+ "reported": {"inverse": "reported_by", "description": "Reported"},
561
+ "reported_by": {"inverse": "reported", "description": "Reported by"},
562
+
563
+ # === LOCATION ===
564
+ "located_at": {"inverse": "location_of", "description": "Located at"},
565
+ "location_of": {"inverse": "located_at", "description": "Location of"},
566
+ "based_in": {"inverse": "base_of", "description": "Based in"},
567
+ "base_of": {"inverse": "based_in", "description": "Base of"},
568
+ "stored_in": {"inverse": "stores", "description": "Stored in"},
569
+ "stores": {"inverse": "stored_in", "description": "Stores"},
570
+ "accessible_from": {"inverse": "provides_access_to", "description": "Accessible from"},
571
+ "provides_access_to": {"inverse": "accessible_from", "description": "Provides access to"},
572
+
573
+ # === TIME/EVENTS ===
574
+ "scheduled": {"inverse": "has_event", "description": "Scheduled"},
575
+ "has_event": {"inverse": "scheduled", "description": "Has event"},
576
+ "attended": {"inverse": "attendee", "description": "Attended"},
577
+ "attendee": {"inverse": "attended", "description": "Attendee of"},
578
+ "occurred_in": {"inverse": "has_occurrence", "description": "Occurred in"},
579
+ "has_occurrence": {"inverse": "occurred_in", "description": "Has occurrence"},
580
+ "before": {"inverse": "after", "description": "Before"},
581
+ "after": {"inverse": "before", "description": "After"},
582
+ "during": {"inverse": "includes_event", "description": "During"},
583
+ "includes_event": {"inverse": "during", "description": "Includes event"},
584
+
585
+ # === DECISIONS ===
586
+ "decided_in": {"inverse": "has_decision", "description": "Decided in"},
587
+ "has_decision": {"inverse": "decided_in", "description": "Has decision"},
588
+ "participated_in": {"inverse": "participant", "description": "Participated in"},
589
+ "participant": {"inverse": "participated_in", "description": "Participant of"},
590
+ "approved_by": {"inverse": "approves", "description": "Approved by"},
591
+ "approves": {"inverse": "approved_by", "description": "Approves"},
592
+ "rejected_by": {"inverse": "rejects", "description": "Rejected by"},
593
+ "rejects": {"inverse": "rejected_by", "description": "Rejects"},
594
+
595
+ # === REFERENCES ===
596
+ "mentions": {"inverse": "mentioned_in", "description": "Mentions"},
597
+ "mentioned_in": {"inverse": "mentions", "description": "Mentioned in"},
598
+ "documents": {"inverse": "documented_in", "description": "Documents"},
599
+ "documented_in": {"inverse": "documents", "description": "Documented in"},
600
+ "proves": {"inverse": "proven_by", "description": "Proves"},
601
+ "proven_by": {"inverse": "proves", "description": "Proven by"},
602
+ "evidences": {"inverse": "evidenced_by", "description": "Evidences"},
603
+ "evidenced_by": {"inverse": "evidences", "description": "Evidenced by"},
604
+
605
+ # === COMMUNICATION ===
606
+ "contacted": {"inverse": "contacted_by", "description": "Contacted"},
607
+ "contacted_by": {"inverse": "contacted", "description": "Contacted by"},
608
+ "emailed": {"inverse": "emailed_by", "description": "Emailed"},
609
+ "emailed_by": {"inverse": "emailed", "description": "Emailed by"},
610
+ "messaged": {"inverse": "messaged_by", "description": "Messaged"},
611
+ "messaged_by": {"inverse": "messaged", "description": "Messaged by"},
612
+ "notified": {"inverse": "notified_by", "description": "Notified"},
613
+ "notified_by": {"inverse": "notified", "description": "Notified by"},
614
+ "asked": {"inverse": "asked_by", "description": "Asked"},
615
+ "asked_by": {"inverse": "asked", "description": "Asked by"},
616
+ "answered": {"inverse": "answered_by", "description": "Answered"},
617
+ "answered_by": {"inverse": "answered", "description": "Answered by"},
618
+
619
+ # === KNOWLEDGE ===
620
+ "learns": {"inverse": "learned_by", "description": "Learns"},
621
+ "learned_by": {"inverse": "learns", "description": "Learned by"},
622
+ "teaches": {"inverse": "taught_by", "description": "Teaches"},
623
+ "taught_by": {"inverse": "teaches", "description": "Taught by"},
624
+ "applies_to": {"inverse": "applied_in", "description": "Applies to"},
625
+ "applied_in": {"inverse": "applies_to", "description": "Applied in"},
626
+ }
627
+
628
+
629
+ class CortexDB:
630
+ """Knowledge graph database."""
631
+
632
+ def __init__(self):
633
+ self.entities: Dict[str, dict] = {} # entity_name -> {type, attrs, created, modified}
634
+ self.relations: List[dict] = [] # [{from, relation, to, created, weight}]
635
+ self.events: List[dict] = [] # [{entity, event, timestamp, context}]
636
+ self._relation_index: Dict[str, List[int]] = defaultdict(list) # entity -> [relation_indices]
637
+ self.load()
638
+
639
+ def load(self):
640
+ """Load data from files."""
641
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
642
+
643
+ if ENTITIES_FILE.exists():
644
+ try:
645
+ self.entities = json.loads(ENTITIES_FILE.read_text(encoding="utf-8"))
646
+ except:
647
+ self.entities = {}
648
+
649
+ if RELATIONS_FILE.exists():
650
+ try:
651
+ self.relations = json.loads(RELATIONS_FILE.read_text(encoding="utf-8"))
652
+ # Rebuild index
653
+ for i, r in enumerate(self.relations):
654
+ self._relation_index[self._norm(r["from"])].append(i)
655
+ self._relation_index[self._norm(r["to"])].append(i)
656
+ except:
657
+ self.relations = []
658
+
659
+ if EVENTS_FILE.exists():
660
+ try:
661
+ self.events = json.loads(EVENTS_FILE.read_text(encoding="utf-8"))
662
+ except:
663
+ self.events = []
664
+
665
+ def save(self):
666
+ """Save data to files."""
667
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
668
+ ENTITIES_FILE.write_text(json.dumps(self.entities, indent=2, ensure_ascii=False), encoding="utf-8")
669
+ RELATIONS_FILE.write_text(json.dumps(self.relations, indent=2, ensure_ascii=False), encoding="utf-8")
670
+ EVENTS_FILE.write_text(json.dumps(self.events, indent=2, ensure_ascii=False), encoding="utf-8")
671
+
672
+ def _norm(self, name: str) -> str:
673
+ """Normalize entity name."""
674
+ return name.lower().strip()
675
+
676
+ def add_entity(self, name: str, entity_type: str, attrs: dict = None) -> bool:
677
+ """Add or update an entity."""
678
+ key = self._norm(name)
679
+ now = datetime.now().isoformat()
680
+
681
+ if key in self.entities:
682
+ # Update existing
683
+ self.entities[key]["type"] = entity_type
684
+ if attrs:
685
+ self.entities[key]["attrs"].update(attrs)
686
+ self.entities[key]["modified"] = now
687
+ return False # Updated
688
+
689
+ # Create new
690
+ self.entities[key] = {
691
+ "name": name,
692
+ "type": entity_type,
693
+ "attrs": attrs or {},
694
+ "created": now,
695
+ "modified": now
696
+ }
697
+ return True # Created
698
+
699
+ def get_entity(self, name: str) -> Optional[dict]:
700
+ """Get an entity."""
701
+ return self.entities.get(self._norm(name))
702
+
703
+ def delete_entity(self, name: str) -> bool:
704
+ """Delete an entity and its relations."""
705
+ key = self._norm(name)
706
+ if key not in self.entities:
707
+ return False
708
+
709
+ del self.entities[key]
710
+
711
+ # Remove relations
712
+ indices_to_remove = self._relation_index.get(key, [])
713
+ for i in sorted(indices_to_remove, reverse=True):
714
+ if i < len(self.relations):
715
+ del self.relations[i]
716
+
717
+ # Rebuild index
718
+ self._relation_index = defaultdict(list)
719
+ for i, r in enumerate(self.relations):
720
+ self._relation_index[self._norm(r["from"])].append(i)
721
+ self._relation_index[self._norm(r["to"])].append(i)
722
+
723
+ self.save()
724
+ return True
725
+
726
+ def add_relation(self, from_entity: str, relation: str, to_entity: str, weight: float = 1.0) -> bool:
727
+ """Add a relation between entities."""
728
+ from_key = self._norm(from_entity)
729
+ to_key = self._norm(to_entity)
730
+ relation = relation.lower()
731
+
732
+ if from_key not in self.entities:
733
+ return False
734
+ if to_key not in self.entities:
735
+ return False
736
+
737
+ # Check if relation already exists
738
+ for r in self.relations:
739
+ if (self._norm(r["from"]) == from_key and
740
+ r["relation"] == relation and
741
+ self._norm(r["to"]) == to_key):
742
+ return False # Already exists
743
+
744
+ # Add relation
745
+ rel_entry = {
746
+ "from": from_entity,
747
+ "relation": relation,
748
+ "to": to_entity,
749
+ "weight": weight,
750
+ "created": datetime.now().isoformat()
751
+ }
752
+ idx = len(self.relations)
753
+ self.relations.append(rel_entry)
754
+ self._relation_index[from_key].append(idx)
755
+ self._relation_index[to_key].append(idx)
756
+
757
+ # Add inverse relation if defined
758
+ if relation in RELATION_TYPES:
759
+ inverse = RELATION_TYPES[relation].get("inverse")
760
+ if inverse and inverse != relation:
761
+ inv_entry = {
762
+ "from": to_entity,
763
+ "relation": inverse,
764
+ "to": from_entity,
765
+ "weight": weight,
766
+ "created": datetime.now().isoformat(),
767
+ "inverse_of": True
768
+ }
769
+ idx2 = len(self.relations)
770
+ self.relations.append(inv_entry)
771
+ self._relation_index[to_key].append(idx2)
772
+ self._relation_index[from_key].append(idx2)
773
+
774
+ self.save()
775
+ return True
776
+
777
+ def get_relations(self, entity: str, direction: str = "both") -> List[dict]:
778
+ """Get relations for an entity."""
779
+ key = self._norm(entity)
780
+ result = []
781
+
782
+ for i in self._relation_index.get(key, []):
783
+ if i >= len(self.relations):
784
+ continue
785
+ r = self.relations[i]
786
+ is_from = self._norm(r["from"]) == key
787
+
788
+ if direction == "out" and not is_from:
789
+ continue
790
+ if direction == "in" and is_from:
791
+ continue
792
+
793
+ result.append(r)
794
+
795
+ return result
796
+
797
+ def get_related_entities(self, entity: str, depth: int = 1) -> Set[str]:
798
+ """Get related entities up to certain depth."""
799
+ key = self._norm(entity)
800
+ visited = {key}
801
+ frontier = {key}
802
+
803
+ for _ in range(depth):
804
+ next_frontier = set()
805
+ for e in frontier:
806
+ for r in self.get_relations(e):
807
+ other = self._norm(r["to"]) if self._norm(r["from"]) == e else self._norm(r["from"])
808
+ if other not in visited:
809
+ visited.add(other)
810
+ next_frontier.add(other)
811
+ frontier = next_frontier
812
+ if not frontier:
813
+ break
814
+
815
+ return visited - {key}
816
+
817
+ def find_path(self, from_entity: str, to_entity: str) -> Optional[List[Tuple[str, str, str]]]:
818
+ """Find shortest path between two entities (BFS)."""
819
+ from_key = self._norm(from_entity)
820
+ to_key = self._norm(to_entity)
821
+
822
+ if from_key not in self.entities or to_key not in self.entities:
823
+ return None
824
+
825
+ if from_key == to_key:
826
+ return []
827
+
828
+ # BFS
829
+ queue = [(from_key, [])]
830
+ visited = {from_key}
831
+
832
+ while queue:
833
+ current, path = queue.pop(0)
834
+
835
+ for r in self.get_relations(current, "out"):
836
+ other = self._norm(r["to"])
837
+ new_path = path + [(r["from"], r["relation"], r["to"])]
838
+
839
+ if other == to_key:
840
+ return new_path
841
+
842
+ if other not in visited:
843
+ visited.add(other)
844
+ queue.append((other, new_path))
845
+
846
+ return None
847
+
848
+ def find_similar(self, entity: str) -> List[Tuple[str, float]]:
849
+ """Find similar entities based on type and shared relations."""
850
+ key = self._norm(entity)
851
+ if key not in self.entities:
852
+ return []
853
+
854
+ entity_type = self.entities[key]["type"]
855
+ entity_relations = set()
856
+
857
+ for r in self.get_relations(key):
858
+ other = self._norm(r["to"]) if self._norm(r["from"]) == key else self._norm(r["from"])
859
+ entity_relations.add(other)
860
+
861
+ scores = []
862
+ for other_key, other_data in self.entities.items():
863
+ if other_key == key:
864
+ continue
865
+
866
+ score = 0.0
867
+
868
+ # Same type
869
+ if other_data["type"] == entity_type:
870
+ score += 0.5
871
+
872
+ # Shared relations
873
+ other_relations = set()
874
+ for r in self.get_relations(other_key):
875
+ third = self._norm(r["to"]) if self._norm(r["from"]) == other_key else self._norm(r["from"])
876
+ other_relations.add(third)
877
+
878
+ shared = entity_relations & other_relations
879
+ if entity_relations or other_relations:
880
+ jaccard = len(shared) / len(entity_relations | other_relations)
881
+ score += jaccard * 0.5
882
+
883
+ if score > 0:
884
+ scores.append((other_data["name"], score))
885
+
886
+ return sorted(scores, key=lambda x: -x[1])
887
+
888
+ def search_by_attrs(self, entity_type: str = None, attrs: dict = None) -> List[dict]:
889
+ """Search entities by type and/or attributes."""
890
+ results = []
891
+
892
+ for key, data in self.entities.items():
893
+ if entity_type and data["type"] != entity_type:
894
+ continue
895
+
896
+ if attrs:
897
+ match = True
898
+ for k, v in attrs.items():
899
+ if data["attrs"].get(k) != v:
900
+ match = False
901
+ break
902
+ if not match:
903
+ continue
904
+
905
+ results.append(data)
906
+
907
+ return results
908
+
909
+ def add_event(self, entity: str, event: str, context: str = ""):
910
+ """Add an episodic event."""
911
+ key = self._norm(entity)
912
+ if key not in self.entities:
913
+ return False
914
+
915
+ self.events.append({
916
+ "entity": entity,
917
+ "event": event,
918
+ "context": context,
919
+ "timestamp": datetime.now().isoformat()
920
+ })
921
+ self.save()
922
+ return True
923
+
924
+ def get_timeline(self, entity: str = None) -> List[dict]:
925
+ """Get events ordered by time."""
926
+ if entity:
927
+ key = self._norm(entity)
928
+ events = [e for e in self.events if self._norm(e["entity"]) == key]
929
+ else:
930
+ events = self.events
931
+
932
+ return sorted(events, key=lambda x: x["timestamp"], reverse=True)
933
+
934
+ def get_stats(self) -> dict:
935
+ """Get system statistics."""
936
+ type_counts = defaultdict(int)
937
+ for data in self.entities.values():
938
+ type_counts[data["type"]] += 1
939
+
940
+ rel_counts = defaultdict(int)
941
+ for r in self.relations:
942
+ rel_counts[r["relation"]] += 1
943
+
944
+ return {
945
+ "total_entities": len(self.entities),
946
+ "total_relations": len(self.relations),
947
+ "total_events": len(self.events),
948
+ "entity_types": dict(type_counts),
949
+ "relation_types": dict(rel_counts)
950
+ }
951
+
952
+
953
+ # Global instance
954
+ _db: Optional[CortexDB] = None
955
+
956
+
957
+ def _get_db() -> CortexDB:
958
+ global _db
959
+ if _db is None:
960
+ _db = CortexDB()
961
+ return _db
962
+
963
+
964
+ def run(args: str = "") -> str:
965
+ """Plugin entry point."""
966
+ if not args.strip():
967
+ return _cmd_list("")
968
+
969
+ parts = args.strip().split(maxsplit=2)
970
+ cmd = parts[0].lower()
971
+ rest = parts[1] if len(parts) > 1 else ""
972
+ rest2 = parts[2] if len(parts) > 2 else ""
973
+
974
+ commands = {
975
+ "remember": _cmd_remember,
976
+ "memorize": _cmd_remember,
977
+ "learn": _cmd_remember,
978
+ "relate": _cmd_relate,
979
+ "link": _cmd_relate,
980
+ "connect": _cmd_relate,
981
+ "recall": _cmd_recall,
982
+ "get": _cmd_recall,
983
+ "show": _cmd_recall,
984
+ "find": _cmd_find,
985
+ "search": _cmd_find,
986
+ "query": _cmd_query,
987
+ "explore": _cmd_query,
988
+ "forget": _cmd_forget,
989
+ "delete": _cmd_forget,
990
+ "remove": _cmd_forget,
991
+ "list": _cmd_list,
992
+ "ls": _cmd_list,
993
+ "types": _cmd_types,
994
+ "relations": _cmd_relations,
995
+ "graph": _cmd_graph,
996
+ "visualize": _cmd_graph,
997
+ "timeline": _cmd_timeline,
998
+ "history": _cmd_timeline,
999
+ "similar": _cmd_similar,
1000
+ "related": _cmd_similar,
1001
+ "path": _cmd_path,
1002
+ "route": _cmd_path,
1003
+ "stats": _cmd_stats,
1004
+ "info": _cmd_stats,
1005
+ "demo": _cmd_demo,
1006
+ "init": _cmd_demo,
1007
+ "example": _cmd_demo,
1008
+ "reset": _cmd_reset,
1009
+ "clear": _cmd_reset,
1010
+ "wipe": _cmd_reset,
1011
+ }
1012
+
1013
+ handler = commands.get(cmd)
1014
+ if not handler:
1015
+ return f"Unknown command: {cmd}\n\nCommands: remember, relate, recall, find, query, forget, list, types, relations, graph, timeline, similar, path, stats, demo, reset"
1016
+
1017
+ return handler(rest + (" " + rest2 if rest2 else ""))
1018
+
1019
+
1020
+ def _parse_attrs(attr_str: str) -> dict:
1021
+ """Parse attributes in key=value format."""
1022
+ attrs = {}
1023
+ # Match key=value or key="value with spaces"
1024
+ pattern = r'(\w+)=(?:"([^"]*)"|(\S+))'
1025
+ for match in re.finditer(pattern, attr_str):
1026
+ key = match.group(1)
1027
+ value = match.group(2) if match.group(2) is not None else match.group(3)
1028
+ attrs[key] = value
1029
+ return attrs
1030
+
1031
+
1032
+ def _cmd_remember(args: str) -> str:
1033
+ """Save an entity with attributes."""
1034
+ if not args.strip():
1035
+ return """Usage: cortex remember <name> <type> [key=value ...]
1036
+
1037
+ Types: person, project, target, host, vulnerability, codebase, module, function, api, bug, technology, event...
1038
+
1039
+ Examples:
1040
+ cortex remember John person role=developer team=backend
1041
+ cortex remember 'Auth API' project status=active language=python
1042
+ cortex remember target.example.com domain ip=1.2.3.4
1043
+ """
1044
+
1045
+ parts = args.strip().split(maxsplit=2)
1046
+ if len(parts) < 2:
1047
+ return "Usage: cortex remember <name> <type> [key=value ...]"
1048
+
1049
+ name = parts[0].strip('"\'')
1050
+ entity_type = parts[1].lower()
1051
+ attr_str = parts[2] if len(parts) > 2 else ""
1052
+
1053
+ # Note: Type is NOT validated - any type can be used
1054
+ # Show warning only if type is not in suggested types
1055
+ type_warning = ""
1056
+ if entity_type not in ENTITY_TYPES:
1057
+ type_warning = f"\n (Note: '{entity_type}' is a custom type - that's fine!)"
1058
+
1059
+ attrs = _parse_attrs(attr_str)
1060
+
1061
+ db = _get_db()
1062
+ created = db.add_entity(name, entity_type, attrs)
1063
+ db.save()
1064
+
1065
+ action = "Created" if created else "Updated"
1066
+ type_info = ENTITY_TYPES.get(entity_type, {"description": "custom entity"})
1067
+
1068
+ lines = [
1069
+ f"āœ“ {action} entity: {name}",
1070
+ f" Type: {entity_type} ({type_info['description']})"
1071
+ ]
1072
+
1073
+ if attrs:
1074
+ lines.append(" Attributes:")
1075
+ for k, v in attrs.items():
1076
+ lines.append(f" {k} = {v}")
1077
+
1078
+ if type_warning:
1079
+ lines.append(type_warning)
1080
+
1081
+ return "\n".join(lines)
1082
+
1083
+
1084
+ def _cmd_relate(args: str) -> str:
1085
+ """Create a relation between entities."""
1086
+ if not args.strip():
1087
+ # Show available relations
1088
+ lines = ["Usage: cortex relate <entity1> <relation> <entity2>", "", "Common relations:"]
1089
+ shown = set()
1090
+ for rel, info in list(RELATION_TYPES.items())[:20]:
1091
+ if rel in shown or info.get("inverse") in shown:
1092
+ continue
1093
+ shown.add(rel)
1094
+ inv = f" ↔ {info['inverse']}" if info.get("inverse") and info["inverse"] != rel else ""
1095
+ lines.append(f" {rel}: {info['description']}{inv}")
1096
+ lines.append("\n (Use any relation - these are just suggestions)")
1097
+ return "\n".join(lines)
1098
+
1099
+ parts = args.strip().split(maxsplit=2)
1100
+ if len(parts) < 3:
1101
+ return "Usage: cortex relate <entity1> <relation> <entity2>"
1102
+
1103
+ from_entity = parts[0].strip('"\'')
1104
+ relation = parts[1].lower()
1105
+ to_entity = parts[2].strip('"\'')
1106
+
1107
+ db = _get_db()
1108
+
1109
+ # Check entities exist
1110
+ if not db.get_entity(from_entity):
1111
+ return f"Entity not found: {from_entity}"
1112
+ if not db.get_entity(to_entity):
1113
+ return f"Entity not found: {to_entity}"
1114
+
1115
+ # Note: Relation is NOT validated - any relation can be used
1116
+ # Show warning only if relation is not in suggested types
1117
+ rel_warning = ""
1118
+ if relation not in RELATION_TYPES:
1119
+ rel_warning = f"\n (Note: '{relation}' is a custom relation - that's fine!)"
1120
+
1121
+ if db.add_relation(from_entity, relation, to_entity):
1122
+ inv_info = ""
1123
+ if relation in RELATION_TYPES:
1124
+ inv = RELATION_TYPES[relation].get("inverse")
1125
+ if inv and inv != relation:
1126
+ inv_info = f"\n (Auto-created inverse: {to_entity} {inv} {from_entity})"
1127
+
1128
+ return f"āœ“ Relation created:\n {from_entity} → {relation} → {to_entity}{inv_info}{rel_warning}"
1129
+ else:
1130
+ return f"Relation already exists: {from_entity} → {relation} → {to_entity}"
1131
+
1132
+
1133
+ def _cmd_recall(args: str) -> str:
1134
+ """Get information about an entity."""
1135
+ if not args.strip():
1136
+ return "Usage: cortex recall <entity>"
1137
+
1138
+ name = args.strip().strip('"\'')
1139
+ db = _get_db()
1140
+ entity = db.get_entity(name)
1141
+
1142
+ if not entity:
1143
+ # Try fuzzy match
1144
+ matches = [e for e in db.entities if name.lower() in e]
1145
+ if matches:
1146
+ return f"Entity not found.\nSimilar: {', '.join(db.entities[m]['name'] for m in matches[:5])}"
1147
+ return f"Entity not found: {name}"
1148
+
1149
+ type_info = ENTITY_TYPES.get(entity["type"], {"description": "custom entity"})
1150
+
1151
+ lines = [
1152
+ f"═══ {entity['name']} ═══",
1153
+ f"Type: {entity['type']} ({type_info.get('description', 'unknown')})"
1154
+ ]
1155
+
1156
+ if entity["attrs"]:
1157
+ lines.append("\nAttributes:")
1158
+ for k, v in entity["attrs"].items():
1159
+ lines.append(f" {k}: {v}")
1160
+
1161
+ # Outgoing relations
1162
+ out_rels = db.get_relations(name, "out")
1163
+ if out_rels:
1164
+ lines.append("\nOutgoing relations:")
1165
+ for r in out_rels[:10]:
1166
+ rel_info = RELATION_TYPES.get(r["relation"], {})
1167
+ desc = rel_info.get("description", r["relation"])
1168
+ lines.append(f" → {desc} → {r['to']}")
1169
+
1170
+ # Incoming relations
1171
+ in_rels = db.get_relations(name, "in")
1172
+ if in_rels:
1173
+ lines.append("\nIncoming relations:")
1174
+ for r in in_rels[:10]:
1175
+ rel_info = RELATION_TYPES.get(r["relation"], {})
1176
+ desc = rel_info.get("description", r["relation"])
1177
+ lines.append(f" ← {desc} ← {r['from']}")
1178
+
1179
+ # Recent events
1180
+ events = [e for e in db.events if db._norm(e["entity"]) == db._norm(name)]
1181
+ if events:
1182
+ lines.append(f"\nEvents ({len(events)}):")
1183
+ for e in events[-5:]:
1184
+ ts = e["timestamp"][:10]
1185
+ lines.append(f" [{ts}] {e['event']}")
1186
+
1187
+ lines.append(f"\nCreated: {entity['created'][:10]}")
1188
+ lines.append(f"Modified: {entity['modified'][:10]}")
1189
+
1190
+ return "\n".join(lines)
1191
+
1192
+
1193
+ def _cmd_find(args: str) -> str:
1194
+ """Search entities by type and/or attributes."""
1195
+ if not args.strip():
1196
+ db = _get_db()
1197
+ stats = db.get_stats()
1198
+ lines = ["Usage: cortex find <type> [key=value ...]", "", "Entities by type:"]
1199
+ for t, count in sorted(stats["entity_types"].items()):
1200
+ type_info = ENTITY_TYPES.get(t, {})
1201
+ desc = type_info.get("description", "custom")
1202
+ lines.append(f" {t}: {count} ({desc})")
1203
+ return "\n".join(lines)
1204
+
1205
+ parts = args.strip().split(maxsplit=1)
1206
+ entity_type = parts[0].lower()
1207
+ attrs = _parse_attrs(parts[1]) if len(parts) > 1 else {}
1208
+
1209
+ db = _get_db()
1210
+
1211
+ # If type is not valid, search in all types
1212
+ if entity_type not in ENTITY_TYPES:
1213
+ # Treat as attribute search
1214
+ attrs = _parse_attrs(args)
1215
+ entity_type = None
1216
+
1217
+ results = db.search_by_attrs(entity_type, attrs)
1218
+
1219
+ if not results:
1220
+ return "No entities found."
1221
+
1222
+ lines = [f"Found {len(results)} entities:"]
1223
+ for e in results[:20]:
1224
+ attrs_str = ", ".join(f"{k}={v}" for k, v in list(e["attrs"].items())[:3])
1225
+ if attrs_str:
1226
+ attrs_str = f" ({attrs_str})"
1227
+ lines.append(f" • [{e['type']}] {e['name']}{attrs_str}")
1228
+
1229
+ return "\n".join(lines)
1230
+
1231
+
1232
+ def _cmd_query(args: str) -> str:
1233
+ """Graph query with depth."""
1234
+ if not args.strip():
1235
+ return "Usage: cortex query <entity> [depth]"
1236
+
1237
+ parts = args.strip().split()
1238
+ name = parts[0].strip('"\'')
1239
+ depth = int(parts[1]) if len(parts) > 1 else 2
1240
+
1241
+ db = _get_db()
1242
+ entity = db.get_entity(name)
1243
+
1244
+ if not entity:
1245
+ return f"Entity not found: {name}"
1246
+
1247
+ related = db.get_related_entities(name, depth)
1248
+
1249
+ if not related:
1250
+ return f"No entities related to '{name}'"
1251
+
1252
+ lines = [f"Entities related to '{name}' (depth {depth}):"]
1253
+ lines.append("=" * 50)
1254
+
1255
+ # Group by distance
1256
+ visited = {db._norm(name)}
1257
+ current_level = {db._norm(name)}
1258
+
1259
+ for d in range(1, depth + 1):
1260
+ next_level = set()
1261
+ for e in current_level:
1262
+ for r in db.get_relations(e, "out"):
1263
+ other = db._norm(r["to"])
1264
+ if other not in visited:
1265
+ visited.add(other)
1266
+ next_level.add(other)
1267
+ e_name = db.entities.get(e, {}).get("name", e)
1268
+ other_name = db.entities.get(other, {}).get("name", other)
1269
+ rel_desc = RELATION_TYPES.get(r["relation"], {}).get("description", r["relation"])
1270
+ lines.append(f" {' ' * (d-1)}{e_name} → {rel_desc} → {other_name}")
1271
+
1272
+ current_level = next_level
1273
+ if not current_level:
1274
+ break
1275
+
1276
+ lines.append(f"\nTotal: {len(related)} related entities")
1277
+
1278
+ return "\n".join(lines)
1279
+
1280
+
1281
+ def _cmd_forget(args: str) -> str:
1282
+ """Delete an entity."""
1283
+ if not args.strip():
1284
+ return "Usage: cortex forget <entity>"
1285
+
1286
+ name = args.strip().strip('"\'')
1287
+ db = _get_db()
1288
+
1289
+ entity = db.get_entity(name)
1290
+ if not entity:
1291
+ return f"Entity not found: {name}"
1292
+
1293
+ # Count relations
1294
+ rels = db.get_relations(name)
1295
+
1296
+ db.delete_entity(name)
1297
+
1298
+ return f"āœ“ Deleted entity: {name}\n {len(rels)} relations removed"
1299
+
1300
+
1301
+ def _cmd_list(args: str) -> str:
1302
+ """List all entities."""
1303
+ db = _get_db()
1304
+ stats = db.get_stats()
1305
+
1306
+ if not db.entities:
1307
+ return "No entities saved.\n\nUse: cortex remember <name> <type> [attrs]"
1308
+
1309
+ lines = [f"Entities ({len(db.entities)}):"]
1310
+ lines.append("=" * 50)
1311
+
1312
+ # Group by type
1313
+ by_type = defaultdict(list)
1314
+ for key, data in db.entities.items():
1315
+ by_type[data["type"]].append(data)
1316
+
1317
+ for etype in sorted(by_type.keys()):
1318
+ type_info = ENTITY_TYPES.get(etype, {})
1319
+ lines.append(f"\n[{etype}] {type_info.get('description', 'custom')}:")
1320
+
1321
+ for e in sorted(by_type[etype], key=lambda x: x["name"]):
1322
+ rels = db.get_relations(e["name"])
1323
+ attrs_str = ""
1324
+ if e["attrs"]:
1325
+ attrs_str = " — " + ", ".join(f"{k}={v}" for k, v in list(e["attrs"].items())[:2])
1326
+ lines.append(f" • {e['name']}{attrs_str} ({len(rels)} rels)")
1327
+
1328
+ lines.append(f"\n" + "=" * 50)
1329
+ lines.append(f"Total relations: {len(db.relations)}")
1330
+ lines.append(f"Total events: {len(db.events)}")
1331
+
1332
+ return "\n".join(lines)
1333
+
1334
+
1335
+ def _cmd_types(args: str) -> str:
1336
+ """Show entity types."""
1337
+ lines = ["Entity types (suggestions - you can use any type):", "=" * 50]
1338
+
1339
+ for etype, info in sorted(ENTITY_TYPES.items()):
1340
+ attrs = ", ".join(info["default_attrs"][:5])
1341
+ lines.append(f"\n{etype}:")
1342
+ lines.append(f" {info['description']}")
1343
+ if attrs:
1344
+ lines.append(f" Common attrs: {attrs}")
1345
+
1346
+ return "\n".join(lines)
1347
+
1348
+
1349
+ def _cmd_relations(args: str) -> str:
1350
+ """Show relation types."""
1351
+ lines = ["Relation types (suggestions - you can use any relation):", "=" * 50]
1352
+
1353
+ # Group by category
1354
+ categories = {
1355
+ "Social": ["knows", "works_with", "reports_to", "manages", "collaborates_with"],
1356
+ "Projects": ["works_on", "has_member", "leads", "led_by", "owns", "owned_by", "maintains"],
1357
+ "Code": ["imports", "calls", "extends", "implements", "contains", "defines", "references"],
1358
+ "Dependencies": ["depends_on", "requires", "uses", "includes"],
1359
+ "Development": ["fixes", "introduces", "causes", "affects", "tests", "covers", "merges"],
1360
+ "Security - Infrastructure": ["hosts", "runs_on", "has_port", "has_service", "resolves_to"],
1361
+ "Security - Attacks": ["has_vuln", "exploits", "targets", "attacks", "scans", "finds", "detects", "bypasses", "mitigates"],
1362
+ "Security - Credentials": ["leaks", "uses_credential", "valid_for", "compromises"],
1363
+ "Security - Tools": ["runs_tool", "generates", "produces", "consumes"],
1364
+ "Structural": ["related_to", "part_of", "parent_of", "child_of", "similar_to", "same_as"],
1365
+ "Creation": ["created", "created_by", "modified", "modified_by", "discovered", "reported"],
1366
+ "Location": ["located_at", "based_in", "stored_in", "accessible_from"],
1367
+ "Events": ["scheduled", "attended", "occurred_in", "before", "after", "during"],
1368
+ "Decisions": ["decided_in", "participated_in", "approved_by", "rejected_by"],
1369
+ "References": ["mentions", "documents", "proves", "evidences"],
1370
+ "Communication": ["contacted", "emailed", "messaged", "notified", "asked", "answered"],
1371
+ "Knowledge": ["learns", "teaches", "applies_to"],
1372
+ }
1373
+
1374
+ for cat, rels in categories.items():
1375
+ cat_lines = []
1376
+ for rel in rels:
1377
+ if rel in RELATION_TYPES:
1378
+ info = RELATION_TYPES[rel]
1379
+ inv = f" ↔ {info['inverse']}" if info.get("inverse") and info["inverse"] != rel else ""
1380
+ cat_lines.append(f" {rel}: {info['description']}{inv}")
1381
+ if cat_lines:
1382
+ lines.append(f"\n{cat}:")
1383
+ lines.extend(cat_lines)
1384
+
1385
+ return "\n".join(lines)
1386
+
1387
+
1388
+ def _cmd_graph(args: str) -> str:
1389
+ """Visualize the graph."""
1390
+ db = _get_db()
1391
+
1392
+ if not db.entities:
1393
+ return "No entities to visualize."
1394
+
1395
+ if args.strip():
1396
+ # Graph centered on entity
1397
+ name = args.strip().strip('"\'')
1398
+ entity = db.get_entity(name)
1399
+ if not entity:
1400
+ return f"Entity not found: {name}"
1401
+
1402
+ related = db.get_related_entities(name, 2)
1403
+ entities_to_show = {db._norm(name)} | related
1404
+ else:
1405
+ entities_to_show = set(db.entities.keys())
1406
+
1407
+ lines = ["Knowledge graph:", "=" * 50]
1408
+
1409
+ # Build adjacency for display
1410
+ for key in sorted(entities_to_show):
1411
+ if key not in db.entities:
1412
+ continue
1413
+
1414
+ data = db.entities[key]
1415
+ type_emoji = {
1416
+ "person": "šŸ‘¤",
1417
+ "project": "šŸ“",
1418
+ "concept": "šŸ’”",
1419
+ "event": "šŸ“…",
1420
+ "preference": "⭐",
1421
+ "organization": "šŸ¢",
1422
+ "technology": "āš™ļø",
1423
+ "location": "šŸ“",
1424
+ "task": "āœ…",
1425
+ "decision": "šŸŽÆ",
1426
+ "target": "šŸŽÆ",
1427
+ "host": "šŸ–„ļø",
1428
+ "domain": "🌐",
1429
+ "vulnerability": "šŸ”“",
1430
+ "exploit": "šŸ’„",
1431
+ "credential": "šŸ”‘",
1432
+ "tool": "šŸ”§",
1433
+ "codebase": "šŸ“¦",
1434
+ "module": "šŸ“¦",
1435
+ "api": "šŸ”Œ",
1436
+ }.get(data["type"], "šŸ“Œ")
1437
+
1438
+ lines.append(f"\n{type_emoji} {data['name']} [{data['type']}]")
1439
+
1440
+ # Show outgoing relations
1441
+ for r in db.get_relations(key, "out"):
1442
+ other_key = db._norm(r["to"])
1443
+ if other_key in entities_to_show:
1444
+ rel_desc = RELATION_TYPES.get(r["relation"], {}).get("description", r["relation"])
1445
+ other_name = db.entities.get(other_key, {}).get("name", other_key)
1446
+ lines.append(f" → {rel_desc} → {other_name}")
1447
+
1448
+ lines.append("\n" + "=" * 50)
1449
+ lines.append(f"Nodes: {len(entities_to_show)}")
1450
+
1451
+ return "\n".join(lines)
1452
+
1453
+
1454
+ def _cmd_timeline(args: str) -> str:
1455
+ """Show events in temporal order."""
1456
+ db = _get_db()
1457
+
1458
+ entity = args.strip().strip('"\'') if args.strip() else None
1459
+
1460
+ events = db.get_timeline(entity)
1461
+
1462
+ if not events:
1463
+ if entity:
1464
+ return f"No events for: {entity}"
1465
+ return "No events recorded."
1466
+
1467
+ lines = [f"Timeline{' for ' + entity if entity else ''}:", "=" * 50]
1468
+
1469
+ for e in events[:30]:
1470
+ ts = e["timestamp"][:16]
1471
+ entity_name = db.entities.get(db._norm(e["entity"]), {}).get("name", e["entity"])
1472
+ lines.append(f" [{ts}] {entity_name}: {e['event']}")
1473
+ if e.get("context"):
1474
+ lines.append(f" {e['context'][:50]}")
1475
+
1476
+ if len(events) > 30:
1477
+ lines.append(f"\n ... and {len(events) - 30} more events")
1478
+
1479
+ return "\n".join(lines)
1480
+
1481
+
1482
+ def _cmd_similar(args: str) -> str:
1483
+ """Find similar entities."""
1484
+ if not args.strip():
1485
+ return "Usage: cortex similar <entity>"
1486
+
1487
+ name = args.strip().strip('"\'')
1488
+ db = _get_db()
1489
+
1490
+ entity = db.get_entity(name)
1491
+ if not entity:
1492
+ return f"Entity not found: {name}"
1493
+
1494
+ similar = db.find_similar(name)
1495
+
1496
+ if not similar:
1497
+ return f"No entities similar to '{name}'"
1498
+
1499
+ lines = [f"Entities similar to '{name}':", "=" * 50]
1500
+
1501
+ for other_name, score in similar[:10]:
1502
+ other = db.get_entity(other_name)
1503
+ if other:
1504
+ lines.append(f" • {other_name} [{other['type']}] - {score:.0%} similar")
1505
+ # Show why
1506
+ shared_rels = set()
1507
+ for r in db.get_relations(name):
1508
+ other_rel = db._norm(r["to"]) if db._norm(r["from"]) == db._norm(name) else db._norm(r["from"])
1509
+ shared_rels.add(other_rel)
1510
+ for r in db.get_relations(other_name):
1511
+ third = db._norm(r["to"]) if db._norm(r["from"]) == db._norm(other_name) else db._norm(r["from"])
1512
+ if third in shared_rels:
1513
+ lines.append(f" Common connection: {db.entities.get(third, {}).get('name', third)}")
1514
+
1515
+ return "\n".join(lines)
1516
+
1517
+
1518
+ def _cmd_path(args: str) -> str:
1519
+ """Find path between entities."""
1520
+ if not args.strip():
1521
+ return "Usage: cortex path <entity1> <entity2>"
1522
+
1523
+ parts = args.strip().split()
1524
+ if len(parts) < 2:
1525
+ return "Usage: cortex path <entity1> <entity2>"
1526
+
1527
+ from_name = parts[0].strip('"\'')
1528
+ to_name = parts[1].strip('"\'')
1529
+
1530
+ db = _get_db()
1531
+
1532
+ path = db.find_path(from_name, to_name)
1533
+
1534
+ if path is None:
1535
+ return f"No path between '{from_name}' and '{to_name}'"
1536
+
1537
+ if not path:
1538
+ return f"Same entity: {from_name}"
1539
+
1540
+ lines = [f"Path from '{from_name}' to '{to_name}':", "=" * 50]
1541
+
1542
+ for from_e, rel, to_e in path:
1543
+ rel_desc = RELATION_TYPES.get(rel, {}).get("description", rel)
1544
+ lines.append(f" {from_e} → {rel_desc} → {to_e}")
1545
+
1546
+ lines.append(f"\nDistance: {len(path)} steps")
1547
+
1548
+ return "\n".join(lines)
1549
+
1550
+
1551
+ def _cmd_stats(args: str) -> str:
1552
+ """Show statistics."""
1553
+ db = _get_db()
1554
+ stats = db.get_stats()
1555
+
1556
+ lines = [
1557
+ "════════════════════════════════════",
1558
+ " CORTEX - STATISTICS",
1559
+ "════════════════════════════════════",
1560
+ "",
1561
+ f"Total entities: {stats['total_entities']}",
1562
+ f"Total relations: {stats['total_relations']}",
1563
+ f"Total events: {stats['total_events']}",
1564
+ "",
1565
+ "Entities by type:",
1566
+ ]
1567
+
1568
+ for t, count in sorted(stats["entity_types"].items(), key=lambda x: -x[1]):
1569
+ type_info = ENTITY_TYPES.get(t, {})
1570
+ desc = type_info.get("description", "custom")
1571
+ bar = "ā–ˆ" * min(count, 20)
1572
+ lines.append(f" {t:<12} {bar} {count}")
1573
+
1574
+ lines.append("\nMost used relations:")
1575
+
1576
+ for r, count in sorted(stats["relation_types"].items(), key=lambda x: -x[1])[:10]:
1577
+ rel_info = RELATION_TYPES.get(r, {})
1578
+ desc = rel_info.get("description", r)
1579
+ lines.append(f" {r:<15} {count:>3} ({desc})")
1580
+
1581
+ return "\n".join(lines)
1582
+
1583
+
1584
+ def _cmd_demo(args: str) -> str:
1585
+ """Generate demo data for testing."""
1586
+ db = _get_db()
1587
+
1588
+ # Check if already has data
1589
+ if db.entities:
1590
+ return f"Database already has {len(db.entities)} entities.\nUse '/cortex forget <name>' to remove or '/cortex list' to see them.\n\nTo reset and add demo data: /cortex demo --force"
1591
+
1592
+ demo_type = args.strip().lower() if args.strip() else "all"
1593
+
1594
+ lines = ["Generating demo data...", "=" * 50]
1595
+
1596
+ # === PROGRAMMING DEMO DATA ===
1597
+ if demo_type in ["all", "programming", "code"]:
1598
+ lines.append("\nšŸ“ Programming Demo:")
1599
+
1600
+ # People
1601
+ db.add_entity("Alice", "person", {"role": "lead developer", "team": "backend", "email": "alice@example.com"})
1602
+ db.add_entity("Bob", "person", {"role": "developer", "team": "backend", "email": "bob@example.com"})
1603
+ db.add_entity("Carol", "person", {"role": "devops", "team": "infrastructure", "email": "carol@example.com"})
1604
+
1605
+ # Projects
1606
+ db.add_entity("HanusCode", "project", {"status": "active", "language": "python", "priority": "high"})
1607
+ db.add_entity("WebUI", "project", {"status": "active", "language": "javascript", "priority": "medium"})
1608
+
1609
+ # Codebase
1610
+ db.add_entity("hanus-core", "codebase", {"language": "python", "framework": "none", "size": "15k LOC"})
1611
+ db.add_entity("hanus-plugins", "module", {"path": "hanus/plugins/", "purpose": "plugin system"})
1612
+ db.add_entity("cortex", "module", {"path": "hanus/plugins/cortex.py", "purpose": "semantic memory"})
1613
+
1614
+ # Technologies
1615
+ db.add_entity("Python", "technology", {"type": "language", "version": "3.11"})
1616
+ db.add_entity("FastAPI", "technology", {"type": "framework", "category": "web"})
1617
+ db.add_entity("SQLite", "technology", {"type": "database", "category": "storage"})
1618
+
1619
+ # Relations
1620
+ db.add_relation("Alice", "leads", "HanusCode")
1621
+ db.add_relation("Alice", "works_on", "hanus-core")
1622
+ db.add_relation("Bob", "works_on", "hanus-plugins")
1623
+ db.add_relation("Bob", "works_on", "WebUI")
1624
+ db.add_relation("Carol", "maintains", "HanusCode")
1625
+ db.add_relation("HanusCode", "uses", "Python")
1626
+ db.add_relation("HanusCode", "uses", "FastAPI")
1627
+ db.add_relation("HanusCode", "uses", "SQLite")
1628
+ db.add_relation("hanus-core", "contains", "hanus-plugins")
1629
+ db.add_relation("hanus-plugins", "contains", "cortex")
1630
+ db.add_relation("Alice", "knows", "Bob")
1631
+ db.add_relation("Bob", "knows", "Carol")
1632
+
1633
+ lines.append(" āœ“ Added programming entities (people, projects, code)")
1634
+
1635
+ # === SECURITY DEMO DATA ===
1636
+ if demo_type in ["all", "security", "pentest"]:
1637
+ lines.append("\nšŸ”’ Security Demo:")
1638
+
1639
+ # Targets
1640
+ db.add_entity("target.local", "target", {"type": "network", "scope": "internal", "status": "in_scope"})
1641
+ db.add_entity("web.target.local", "domain", {"ip": "192.168.1.100", "ports": "80,443,8080"})
1642
+ db.add_entity("api.target.local", "domain", {"ip": "192.168.1.101", "ports": "443,8443"})
1643
+
1644
+ # Hosts
1645
+ db.add_entity("192.168.1.100", "host", {"hostname": "web.target.local", "os": "Ubuntu 22.04"})
1646
+ db.add_entity("192.168.1.101", "host", {"hostname": "api.target.local", "os": "Debian 11"})
1647
+ db.add_entity("192.168.1.1", "host", {"hostname": "gateway.local", "os": "Linux"})
1648
+
1649
+ # Services
1650
+ db.add_entity("nginx-80", "service", {"name": "nginx", "port": "80", "version": "1.18.0"})
1651
+ db.add_entity("ssh-22", "service", {"name": "openssh", "port": "22", "version": "8.9"})
1652
+
1653
+ # Vulnerabilities
1654
+ db.add_entity("CVE-2023-1234", "cve", {"severity": "high", "cvss": "8.5", "product": "nginx"})
1655
+ db.add_entity("CVE-2024-5678", "cve", {"severity": "critical", "cvss": "9.8", "product": "openssh"})
1656
+
1657
+ # Findings
1658
+ db.add_entity("SQLi-login", "finding", {"severity": "critical", "type": "sqli", "url": "/login"})
1659
+ db.add_entity("XSS-search", "finding", {"severity": "medium", "type": "xss", "url": "/search"})
1660
+
1661
+ # Tools
1662
+ db.add_entity("nmap", "tool", {"category": "recon", "purpose": "port scanning"})
1663
+ db.add_entity("sqlmap", "tool", {"category": "exploitation", "purpose": "SQL injection"})
1664
+ db.add_entity("burpsuite", "tool", {"category": "web", "purpose": "web testing"})
1665
+
1666
+ # Credentials
1667
+ db.add_entity("admin-creds", "credential", {"type": "password", "source": "phishing", "valid": "true"})
1668
+
1669
+ # Exploits
1670
+ db.add_entity("exploit-db-12345", "exploit", {"name": "nginx_rce", "cve": "CVE-2023-1234", "reliability": "high"})
1671
+
1672
+ # Relations
1673
+ db.add_relation("target.local", "has_host", "192.168.1.100")
1674
+ db.add_relation("target.local", "has_host", "192.168.1.101")
1675
+ db.add_relation("web.target.local", "resolves_to", "192.168.1.100")
1676
+ db.add_relation("api.target.local", "resolves_to", "192.168.1.101")
1677
+ db.add_relation("192.168.1.100", "has_service", "nginx-80")
1678
+ db.add_relation("192.168.1.100", "has_service", "ssh-22")
1679
+ db.add_relation("nginx-80", "has_vuln", "CVE-2023-1234")
1680
+ db.add_relation("ssh-22", "has_vuln", "CVE-2024-5678")
1681
+ db.add_relation("web.target.local", "has_vuln", "SQLi-login")
1682
+ db.add_relation("web.target.local", "has_vuln", "XSS-search")
1683
+ db.add_relation("exploit-db-12345", "exploits", "CVE-2023-1234")
1684
+ db.add_relation("sqlmap", "finds", "SQLi-login")
1685
+ db.add_relation("admin-creds", "valid_for", "web.target.local")
1686
+ db.add_relation("192.168.1.1", "part_of_network", "target.local")
1687
+
1688
+ lines.append(" āœ“ Added security entities (targets, hosts, vulns, tools)")
1689
+
1690
+ db.save()
1691
+
1692
+ stats = db.get_stats()
1693
+ lines.append(f"\nāœ“ Demo complete!")
1694
+ lines.append(f" Total entities: {stats['total_entities']}")
1695
+ lines.append(f" Total relations: {stats['total_relations']}")
1696
+ lines.append(f"\nTry: /cortex graph")
1697
+ lines.append(f" /cortex query HanusCode 2")
1698
+ lines.append(f" /cortex path Alice Python")
1699
+
1700
+ return "\n".join(lines)
1701
+
1702
+
1703
+ def _cmd_reset(args: str) -> str:
1704
+ """Reset/clear all cortex data."""
1705
+ db = _get_db()
1706
+
1707
+ if args.strip().lower() == "--force":
1708
+ # Skip confirmation
1709
+ pass
1710
+ elif db.entities:
1711
+ return f"āš ļø This will delete ALL data:\n - {len(db.entities)} entities\n - {len(db.relations)} relations\n - {len(db.events)} events\n\nUse: /cortex reset --force"
1712
+
1713
+ # Clear all data
1714
+ db.entities.clear()
1715
+ db.relations.clear()
1716
+ db.events.clear()
1717
+ db._relation_index.clear()
1718
+ db.save()
1719
+
1720
+ # Clear global nodes in webui
1721
+ return "āœ“ All cortex data has been cleared.\n\nUse '/cortex demo' to generate sample data."
1722
+
1723
+
1724
+ # API for integration with other plugins
1725
+ def get_entity(name: str) -> Optional[dict]:
1726
+ """API: Get an entity."""
1727
+ return _get_db().get_entity(name)
1728
+
1729
+
1730
+ def add_entity(name: str, entity_type: str, attrs: dict = None) -> bool:
1731
+ """API: Add an entity."""
1732
+ db = _get_db()
1733
+ result = db.add_entity(name, entity_type, attrs)
1734
+ db.save()
1735
+ return result
1736
+
1737
+
1738
+ def add_relation(from_entity: str, relation: str, to_entity: str) -> bool:
1739
+ """API: Add a relation."""
1740
+ return _get_db().add_relation(from_entity, relation, to_entity)
1741
+
1742
+
1743
+ def get_related(entity: str, depth: int = 1) -> Set[str]:
1744
+ """API: Get related entities."""
1745
+ return _get_db().get_related_entities(entity, depth)
1746
+
1747
+
1748
+ def find_path(from_entity: str, to_entity: str) -> Optional[List[Tuple[str, str, str]]]:
1749
+ """API: Find path between entities."""
1750
+ return _get_db().find_path(from_entity, to_entity)