@drdeeks/character-kit 1.0.1

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/deploy/ack_monitor.py +140 -0
  4. package/deploy/ack_watchdog.py +110 -0
  5. package/deploy/agent-character-monitor.service +21 -0
  6. package/deploy/agent-character-watchdog.service +21 -0
  7. package/deploy/agent-enforcer-proof.service +18 -0
  8. package/deploy/agent-enforcer.service +53 -0
  9. package/deploy/deploy-ack-services.sh +47 -0
  10. package/deploy/deploy-agent-enforcer.sh +133 -0
  11. package/deploy/proof-self-respawn.sh +59 -0
  12. package/node/bin/ack.js +383 -0
  13. package/node/bin/aik.js +520 -0
  14. package/node/bin/install.js +409 -0
  15. package/node/enforcer/agent_enforcer_daemon.js +690 -0
  16. package/node/examples/.agent/constitution.yaml +38 -0
  17. package/node/examples/.agent/enforcer.yaml +33 -0
  18. package/node/examples/.agent/habits/affirm_character_each_action.yaml +9 -0
  19. package/node/examples/.agent/habits/audit_not_silent.yaml +9 -0
  20. package/node/examples/.agent/habits/binding_map_one_core.yaml +9 -0
  21. package/node/examples/.agent/habits/character_hash_visible.yaml +9 -0
  22. package/node/examples/.agent/habits/check_duplication_before_debug.yaml +9 -0
  23. package/node/examples/.agent/habits/complete-thoroughly.yaml +16 -0
  24. package/node/examples/.agent/habits/consistent-info-across-files.yaml +18 -0
  25. package/node/examples/.agent/habits/document-for-next-agent.yaml +15 -0
  26. package/node/examples/.agent/habits/documented_rollback.yaml +9 -0
  27. package/node/examples/.agent/habits/drift_signal_detection.yaml +9 -0
  28. package/node/examples/.agent/habits/due-diligence.yaml +16 -0
  29. package/node/examples/.agent/habits/enterprise-grade-modular.yaml +16 -0
  30. package/node/examples/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  31. package/node/examples/.agent/habits/forever_one_idea.yaml +9 -0
  32. package/node/examples/.agent/habits/graceful_degradation.yaml +9 -0
  33. package/node/examples/.agent/habits/idempotent_operations.yaml +9 -0
  34. package/node/examples/.agent/habits/interactive-no-stalls.yaml +16 -0
  35. package/node/examples/.agent/habits/layered_not_rewritten.yaml +9 -0
  36. package/node/examples/.agent/habits/lossless_consolidation.yaml +9 -0
  37. package/node/examples/.agent/habits/no-credential-leak.yaml +41 -0
  38. package/node/examples/.agent/habits/no-deception.yaml +14 -0
  39. package/node/examples/.agent/habits/no_hold_narration.yaml +9 -0
  40. package/node/examples/.agent/habits/one_concern_per_file.yaml +9 -0
  41. package/node/examples/.agent/habits/optimized-robust.yaml +16 -0
  42. package/node/examples/.agent/habits/registered_plugin_not_string.yaml +9 -0
  43. package/node/examples/.agent/habits/rename_as_layer_op.yaml +9 -0
  44. package/node/examples/.agent/habits/resolve-root-cause.yaml +15 -0
  45. package/node/examples/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  46. package/node/examples/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  47. package/node/examples/.agent/habits/safe_file_permissions.yaml +9 -0
  48. package/node/examples/.agent/habits/self-healing-portability.yaml +14 -0
  49. package/node/examples/.agent/habits/shippable-pride.yaml +15 -0
  50. package/node/examples/.agent/habits/single_source_of_truth.yaml +9 -0
  51. package/node/examples/.agent/habits/test_of_forever.yaml +9 -0
  52. package/node/examples/.agent/habits/timeout_and_retry.yaml +9 -0
  53. package/node/examples/.agent/habits/track_defects_openly.yaml +9 -0
  54. package/node/examples/.agent/habits/validate-against-real-source.yaml +14 -0
  55. package/node/examples/.agent/habits/verify-against-docs.yaml +15 -0
  56. package/node/examples/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  57. package/node/examples/.agent/habits/versioning-discipline.yaml +18 -0
  58. package/node/package.json +53 -0
  59. package/node/src/enforcer/client.js +149 -0
  60. package/node/src/hooks/character.js +286 -0
  61. package/node/src/index.js +24 -0
  62. package/node/src/knowledge/indexer.js +486 -0
  63. package/node/src/knowledge/semantic.js +154 -0
  64. package/node/src/memory/index.js +355 -0
  65. package/node/tests/character.test.js +80 -0
  66. package/node/tests/habit-create.test.js +67 -0
  67. package/node/tests/habits-default.test.js +68 -0
  68. package/node/tests/install-chain.test.js +19 -0
  69. package/node/tests/install.test.js +104 -0
  70. package/package.json +65 -0
  71. package/python/agent_character_kit/__init__.py +388 -0
  72. package/python/agent_character_kit/__main__.py +333 -0
  73. package/python/agent_character_kit/_yaml.py +33 -0
  74. package/python/agent_character_kit/enforcer.py +379 -0
  75. package/python/agent_character_kit/memory.py +257 -0
  76. package/python/agent_character_kit/semantic.py +126 -0
  77. package/python/example_workspace/.agent/habits/affirm_character_each_action.yaml +9 -0
  78. package/python/example_workspace/.agent/habits/audit_not_silent.yaml +9 -0
  79. package/python/example_workspace/.agent/habits/binding_map_one_core.yaml +9 -0
  80. package/python/example_workspace/.agent/habits/character_hash_visible.yaml +9 -0
  81. package/python/example_workspace/.agent/habits/check_duplication_before_debug.yaml +9 -0
  82. package/python/example_workspace/.agent/habits/complete-thoroughly.yaml +16 -0
  83. package/python/example_workspace/.agent/habits/consistent-info-across-files.yaml +18 -0
  84. package/python/example_workspace/.agent/habits/document-for-next-agent.yaml +15 -0
  85. package/python/example_workspace/.agent/habits/documented_rollback.yaml +9 -0
  86. package/python/example_workspace/.agent/habits/drift_signal_detection.yaml +9 -0
  87. package/python/example_workspace/.agent/habits/due-diligence.yaml +16 -0
  88. package/python/example_workspace/.agent/habits/enterprise-grade-modular.yaml +16 -0
  89. package/python/example_workspace/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  90. package/python/example_workspace/.agent/habits/forever_one_idea.yaml +9 -0
  91. package/python/example_workspace/.agent/habits/graceful_degradation.yaml +9 -0
  92. package/python/example_workspace/.agent/habits/idempotent_operations.yaml +9 -0
  93. package/python/example_workspace/.agent/habits/interactive-no-stalls.yaml +16 -0
  94. package/python/example_workspace/.agent/habits/layered_not_rewritten.yaml +9 -0
  95. package/python/example_workspace/.agent/habits/lossless_consolidation.yaml +9 -0
  96. package/python/example_workspace/.agent/habits/no-credential-leak.yaml +41 -0
  97. package/python/example_workspace/.agent/habits/no-deception.yaml +14 -0
  98. package/python/example_workspace/.agent/habits/no_hold_narration.yaml +9 -0
  99. package/python/example_workspace/.agent/habits/one_concern_per_file.yaml +9 -0
  100. package/python/example_workspace/.agent/habits/optimized-robust.yaml +16 -0
  101. package/python/example_workspace/.agent/habits/registered_plugin_not_string.yaml +9 -0
  102. package/python/example_workspace/.agent/habits/rename_as_layer_op.yaml +9 -0
  103. package/python/example_workspace/.agent/habits/resolve-root-cause.yaml +15 -0
  104. package/python/example_workspace/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  105. package/python/example_workspace/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  106. package/python/example_workspace/.agent/habits/safe_file_permissions.yaml +9 -0
  107. package/python/example_workspace/.agent/habits/self-healing-portability.yaml +14 -0
  108. package/python/example_workspace/.agent/habits/shippable-pride.yaml +15 -0
  109. package/python/example_workspace/.agent/habits/single_source_of_truth.yaml +9 -0
  110. package/python/example_workspace/.agent/habits/test_of_forever.yaml +9 -0
  111. package/python/example_workspace/.agent/habits/timeout_and_retry.yaml +9 -0
  112. package/python/example_workspace/.agent/habits/track_defects_openly.yaml +9 -0
  113. package/python/example_workspace/.agent/habits/validate-against-real-source.yaml +14 -0
  114. package/python/example_workspace/.agent/habits/verify-against-docs.yaml +15 -0
  115. package/python/example_workspace/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  116. package/python/example_workspace/.agent/habits/versioning-discipline.yaml +18 -0
  117. package/python/hermes_plugin/README.md +63 -0
  118. package/python/hermes_plugin/__init__.py +367 -0
  119. package/python/hermes_plugin/config.yaml +20 -0
  120. package/python/hermes_plugin/plugin.yaml +7 -0
  121. package/python/hermes_plugin/test_plugin.py +166 -0
  122. package/python/pyproject.toml +19 -0
  123. package/python/tests/python_parity_test.py +84 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Agent Character Kit — Python CLI
4
+
5
+ Companion to the Node.js package. Provides:
6
+ - Hook execution for Python-based frameworks (Hermes, OpenCode)
7
+ - Character validation via enforcer daemon
8
+ - Knowledge/memory operations
9
+
10
+ Usage:
11
+ python3 -m agent_character_kit hook --framework hermes
12
+ python3 -m agent_character_kit enforcer --status
13
+ python3 -m agent_character_kit index --path ./docs
14
+ python3 -m agent_character_kit memory log "entry" --tags "tag1,tag2"
15
+ """
16
+
17
+ import asyncio
18
+ import json
19
+ import os
20
+ import sys
21
+ import argparse
22
+ from pathlib import Path
23
+ from datetime import datetime, timezone
24
+
25
+ # ─── Self-resolving paths ────────────────────────────────────────────────────
26
+
27
+ import importlib.util
28
+
29
+ def _load(modname):
30
+ """Lazily import a sub-module so unused components are never loaded."""
31
+ here = Path(__file__).resolve().parent
32
+ spec = importlib.util.spec_from_file_location(f"agent_character_kit.{modname}", here / f"{modname}.py")
33
+ mod = importlib.util.module_from_spec(spec)
34
+ spec.loader.exec_module(mod)
35
+ return mod
36
+
37
+ def _load_indexer():
38
+ # DocumentIndexer lives in the package __init__ (kept lean — no heavy deps).
39
+ from . import DocumentIndexer
40
+ return DocumentIndexer
41
+
42
+ HOME = Path(os.environ.get("HOME", "/root"))
43
+ # Harness-neutral default: the kit lives under the user's home, not a specific
44
+ # agent harness's directory. Override with AGENT_WORKSPACE (the user designates
45
+ # where their agent + habits reside).
46
+ WORKSPACE = Path(os.environ.get("AGENT_WORKSPACE", HOME / ".agent-character-kit" / "workspace"))
47
+ AUDIT_DIR = WORKSPACE / ".agent" / "audit"
48
+
49
+ # ─── Enforcer Client ─────────────────────────────────────────────────────────
50
+
51
+ def _enforcer():
52
+ """Lazily build an EnforcerClient (keeps the enforcer component optional)."""
53
+ return _load("enforcer").EnforcerClient()
54
+
55
+ # ─── Framework Detection ─────────────────────────────────────────────────────
56
+
57
+ def detect_framework(payload):
58
+ if "hook_event_name" in payload:
59
+ evt = payload["hook_event_name"]
60
+ if evt in ("PreToolUse", "PostToolUse", "SessionStart", "Stop"):
61
+ return "claude"
62
+ if evt in ("BeforeTool", "AfterTool", "BeforeToolSelection"):
63
+ return "gemini"
64
+ if evt in ("preToolUse", "postToolUse", "beforeShellExecution"):
65
+ return "cursor"
66
+ if "tool_name" in payload and "args" in payload:
67
+ return "hermes"
68
+ if "tool" in payload and "args" in payload:
69
+ return "opencode"
70
+ return "generic"
71
+
72
+ def normalize_input(payload, framework):
73
+ if framework == "claude":
74
+ return {"tool": payload.get("tool_name", "unknown"), "params": payload.get("tool_input", {}),
75
+ "event": payload.get("hook_event_name", "PreToolUse").lower(),
76
+ "session_id": payload.get("session_id"), "cwd": payload.get("cwd")}
77
+ elif framework == "cursor":
78
+ return {"tool": payload.get("tool_name", payload.get("tool", "unknown")),
79
+ "params": payload.get("tool_input", payload.get("args", {})),
80
+ "event": payload.get("hook_event_name", "preToolUse").lower(),
81
+ "session_id": payload.get("session_id"), "cwd": payload.get("cwd")}
82
+ elif framework == "gemini":
83
+ return {"tool": payload.get("tool_name", "unknown"), "params": payload.get("tool_input", {}),
84
+ "event": payload.get("hook_event_name", "BeforeTool").lower(),
85
+ "session_id": payload.get("session_id"), "cwd": payload.get("cwd")}
86
+ elif framework == "hermes":
87
+ return {"tool": payload.get("tool_name", "unknown"), "params": payload.get("args", {}),
88
+ "event": "pre_tool_call", "session_id": payload.get("task_id")}
89
+ elif framework == "opencode":
90
+ return {"tool": payload.get("tool", payload.get("tool_name", "unknown")),
91
+ "params": payload.get("args", payload.get("tool_input", {})),
92
+ "event": payload.get("event", "tool.execute.before"),
93
+ "session_id": payload.get("session_id")}
94
+ else:
95
+ return {"tool": payload.get("tool", payload.get("tool_name", "unknown")),
96
+ "params": payload.get("params", payload.get("args", payload.get("tool_input", {}))),
97
+ "event": payload.get("event", "pre_tool_use"),
98
+ "session_id": payload.get("session_id")}
99
+
100
+ def format_output(result, framework, original):
101
+ allowed = result.get("allow", True)
102
+ reason = result.get("reason", "")
103
+
104
+ if framework == "claude":
105
+ if not allowed:
106
+ return {"hookSpecificOutput": {"hookEventName": original.get("hook_event_name", "PreToolUse"),
107
+ "permissionDecision": "deny", "permissionDecisionReason": reason}}
108
+ return {"hookSpecificOutput": {"hookEventName": original.get("hook_event_name", "PreToolUse"),
109
+ "permissionDecision": "allow"}}
110
+ elif framework == "cursor":
111
+ return {"permission": "deny" if not allowed else "allow", "reason": reason if not allowed else None}
112
+ elif framework == "gemini":
113
+ return {"decision": "deny" if not allowed else "allow", "reason": reason if not allowed else None}
114
+ elif framework == "hermes":
115
+ return {"action": "block", "message": reason} if not allowed else {}
116
+ elif framework == "opencode":
117
+ return {"block": not allowed, "reason": reason if not allowed else None}
118
+ else:
119
+ return {"allow": allowed, "reason": reason if not allowed else None,
120
+ "reflection": result.get("reflection") if not allowed else None}
121
+
122
+ def audit_log(event, tool, params, result=None):
123
+ AUDIT_DIR.mkdir(parents=True, exist_ok=True)
124
+ entry = {"ts": datetime.now(timezone.utc).isoformat(), "event": event, "tool": tool,
125
+ "params": params, "result": result}
126
+ with open(AUDIT_DIR / "tool-audit.jsonl", "a") as f:
127
+ f.write(json.dumps(entry) + "\n")
128
+
129
+ # ─── Commands ────────────────────────────────────────────────────────────────
130
+
131
+ async def cmd_hook(args):
132
+ payload = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
133
+ framework = args.framework if args.framework != "auto" else detect_framework(payload)
134
+ normalized = normalize_input(payload, framework)
135
+
136
+ if normalized["tool"] in ("validate_workspace", "heartbeat", "execute_tool"):
137
+ output = format_output({"allow": True}, framework, payload)
138
+ print(json.dumps(output))
139
+ return
140
+
141
+ is_pre = normalized["event"] in ("pre_tool_use", "pretooluse", "beforetool", "pre_tool_call", "tool.execute.before")
142
+
143
+ if is_pre:
144
+ client = _load("enforcer").EnforcerClient()
145
+ response = await client.validate_tool(normalized["tool"], normalized["params"],
146
+ normalized.get("session_id", "unknown"))
147
+ audit_log("pre_tool_use", normalized["tool"], normalized["params"], response)
148
+
149
+ # Fail-closed by default: if the enforcer can't be reached, block.
150
+ # Opt out only in development with ACK_FAIL_OPEN=1.
151
+ fail_closed = not os.environ.get("ACK_FAIL_OPEN")
152
+ if response.get("error") and fail_closed:
153
+ result = {"allow": False,
154
+ "reason": response.get("reason", "Enforcer unavailable"),
155
+ "reflection": response.get("reflection",
156
+ "The enforcer could not be reached. character cannot be "
157
+ "verified, so the action is blocked. A guard that fails "
158
+ "open is no guard.")}
159
+ elif response.get("denied"):
160
+ result = {"allow": False, "reason": response.get("reason", "Denied by enforcer"),
161
+ "reflection": response.get("reflection", "")}
162
+ else:
163
+ result = {"allow": True}
164
+ else:
165
+ audit_log("post_tool_use", normalized["tool"], normalized["params"], payload.get("result"))
166
+ result = {"allow": True}
167
+
168
+ output = format_output(result, framework, payload)
169
+ print(json.dumps(output))
170
+ if not result.get("allow", True) and framework in ("claude", "cursor", "gemini"):
171
+ sys.exit(2)
172
+
173
+ async def cmd_enforcer(args):
174
+ client = _enforcer()
175
+ if args.status:
176
+ result = await client.validate_workspace()
177
+ print(json.dumps(result, indent=2))
178
+ elif args.heartbeat:
179
+ result = await client.heartbeat()
180
+ print(json.dumps(result, indent=2))
181
+
182
+ # ─── Index Command (Python companion to Node package) ───────────────────────
183
+
184
+ def cmd_index(args):
185
+ DocumentIndexer = _load_indexer()
186
+ idx = DocumentIndexer(str(WORKSPACE))
187
+ if args.path:
188
+ target = args.path
189
+ else:
190
+ corpus = WORKSPACE / "corpus"
191
+ corpus.mkdir(parents=True, exist_ok=True)
192
+ target = str(corpus)
193
+ res = idx.index_directory(target, {"followLinks": not args.no_follow})
194
+ if args.search:
195
+ print(json.dumps(idx.search(args.search), indent=2))
196
+ else:
197
+ print(json.dumps(res, indent=2))
198
+ if args.status:
199
+ print(json.dumps(idx.status(), indent=2))
200
+
201
+ # ─── Memory / Knowledge / Semantic commands (full parity with Node) ──────────
202
+
203
+ def cmd_memory(args):
204
+ mem = _load("memory").Memory(str(WORKSPACE))
205
+ mem.init()
206
+ a = args.action
207
+ if a == "log":
208
+ print(json.dumps(mem.daily.log(args.entry, args.tags.split(",") if args.tags else [], args.category), indent=2))
209
+ elif a == "today":
210
+ print(json.dumps(mem.daily.get_today(), indent=2))
211
+ elif a == "lesson":
212
+ print(json.dumps(mem.longterm.add_lesson(args.title, args.content, args.tags.split(",") if args.tags else []), indent=2))
213
+ elif a == "pattern":
214
+ print(json.dumps(mem.longterm.add_pattern(args.name, args.description), indent=2))
215
+ elif a == "decision":
216
+ print(json.dumps(mem.longterm.add_decision(args.title, args.context, args.decision, args.rationale), indent=2))
217
+ elif a == "search":
218
+ print(json.dumps(mem.search(args.query), indent=2))
219
+ elif a == "status":
220
+ print(json.dumps(mem.status(), indent=2))
221
+
222
+
223
+ def cmd_knowledge(args):
224
+ mem = _load("memory").Memory(str(WORKSPACE))
225
+ mem.init()
226
+ a = args.action
227
+ if a == "add":
228
+ import ast
229
+ facts = ast.literal_eval(args.facts) if args.facts else {}
230
+ print(json.dumps(mem.knowledge.add_entity(args.name, args.type or "general", facts, args.tags.split(",") if args.tags else []), indent=2))
231
+ elif a == "get":
232
+ print(json.dumps(mem.knowledge.get_entity(args.name), indent=2))
233
+ elif a == "search":
234
+ print(json.dumps(mem.knowledge.search_entities(args.query), indent=2))
235
+ elif a == "list":
236
+ print(json.dumps(mem.knowledge.list_entities(args.type), indent=2))
237
+
238
+
239
+ def cmd_semantic(args):
240
+ sem = _load("semantic").SemanticSearch(str(WORKSPACE))
241
+ sem.init()
242
+ a = args.action
243
+ if a == "index":
244
+ idx = _load_indexer()(str(WORKSPACE))
245
+ idx.init()
246
+ count = 0
247
+ for doc in idx.list_documents():
248
+ doc_data = idx.index.documents[doc["id"]]
249
+ for ch in doc_data.get("chunks", []):
250
+ sem.index_document(ch["id"], ch["content"], {"parent": doc["id"],
251
+ "category": doc["category"], "tags": doc["tags"]})
252
+ count += 1
253
+ sem.save()
254
+ print(json.dumps({"indexed": count, "status": "ok"}, indent=2))
255
+ elif a == "search":
256
+ print(json.dumps(sem.search(args.query, args.limit), indent=2))
257
+ elif a == "hybrid":
258
+ idx = _load_indexer()(str(WORKSPACE))
259
+ idx.init()
260
+ kw = idx.search(args.query, {"limit": args.limit})
261
+ print(json.dumps(sem.hybrid_search(args.query, kw, args.limit), indent=2))
262
+ elif a == "status":
263
+ print(json.dumps(sem.status(), indent=2))
264
+
265
+
266
+ # ─── Main ────────────────────────────────────────────────────────────────────
267
+
268
+ def main():
269
+ parser = argparse.ArgumentParser(description="Agent Character Kit (Python)")
270
+ sub = parser.add_subparsers(dest="command")
271
+
272
+ hook_p = sub.add_parser("hook", help="Run character hook")
273
+ hook_p.add_argument("--framework", default="auto", choices=["claude", "cursor", "gemini", "hermes", "opencode", "generic", "auto"])
274
+
275
+ enf_p = sub.add_parser("enforcer", help="Enforcer operations")
276
+ enf_p.add_argument("--status", action="store_true")
277
+ enf_p.add_argument("--heartbeat", action="store_true")
278
+
279
+ idx_p = sub.add_parser("index", help="Index documents (file discovery + links + llms.txt)")
280
+ idx_p.add_argument("--path", default=None, help="Directory or file to index")
281
+ idx_p.add_argument("--search", default=None, help="Search indexed documents")
282
+ idx_p.add_argument("--status", action="store_true", help="Show index status")
283
+ idx_p.add_argument("--no-follow", action="store_true", help="Do not follow local links")
284
+
285
+ mem_p = sub.add_parser("memory", help="Memory: daily / weekly / long-term")
286
+ mem_sp = mem_p.add_subparsers(dest="action", required=True)
287
+ m_log = mem_sp.add_parser("log"); m_log.add_argument("entry"); m_log.add_argument("-t", "--tags"); m_log.add_argument("-c", "--category", default="general")
288
+ mem_sp.add_parser("today")
289
+ m_lesson = mem_sp.add_parser("lesson"); m_lesson.add_argument("title"); m_lesson.add_argument("content"); m_lesson.add_argument("-t", "--tags")
290
+ m_pat = mem_sp.add_parser("pattern"); m_pat.add_argument("name"); m_pat.add_argument("description")
291
+ m_dec = mem_sp.add_parser("decision"); m_dec.add_argument("title"); m_dec.add_argument("context"); m_dec.add_argument("decision"); m_dec.add_argument("rationale")
292
+ m_search = mem_sp.add_parser("search"); m_search.add_argument("query")
293
+ mem_sp.add_parser("status")
294
+
295
+ kg_p = sub.add_parser("knowledge", help="Knowledge graph (entities)")
296
+ kg_sp = kg_p.add_subparsers(dest="action", required=True)
297
+ k_add = kg_sp.add_parser("add"); k_add.add_argument("name"); k_add.add_argument("--type", default="general"); k_add.add_argument("--facts", default=None); k_add.add_argument("-t", "--tags")
298
+ k_get = kg_sp.add_parser("get"); k_get.add_argument("name")
299
+ k_search = kg_sp.add_parser("search"); k_search.add_argument("query")
300
+ k_list = kg_sp.add_parser("list"); k_list.add_argument("--type", default=None)
301
+
302
+ sem_p = sub.add_parser("semantic", help="Semantic (vector) search")
303
+ sem_sp = sem_p.add_subparsers(dest="action", required=True)
304
+ sem_sp.add_parser("index")
305
+ s_search = sem_sp.add_parser("search"); s_search.add_argument("query"); s_search.add_argument("-l", "--limit", type=int, default=10)
306
+ s_hybrid = sem_sp.add_parser("hybrid"); s_hybrid.add_argument("query"); s_hybrid.add_argument("-l", "--limit", type=int, default=10)
307
+ sem_sp.add_parser("status")
308
+
309
+ args = parser.parse_args()
310
+ if not args.command:
311
+ parser.print_help()
312
+ sys.exit(1)
313
+
314
+ if args.command == "hook":
315
+ asyncio.run(cmd_hook(args))
316
+ elif args.command == "enforcer":
317
+ asyncio.run(cmd_enforcer(args))
318
+ elif args.command == "index":
319
+ cmd_index(args)
320
+ elif args.command == "memory":
321
+ cmd_memory(args)
322
+ elif args.command == "knowledge":
323
+ cmd_knowledge(args)
324
+ elif args.command == "semantic":
325
+ asyncio.run(_run_semantic(args))
326
+
327
+
328
+ async def _run_semantic(args):
329
+ cmd_semantic(args)
330
+
331
+
332
+ if __name__ == "__main__":
333
+ main()
@@ -0,0 +1,33 @@
1
+ """
2
+ Thin YAML helper — keeps YAML handling optional so the package stays modular.
3
+ If PyYAML is unavailable, it transparently falls back to JSON. Imported lazily by
4
+ the memory/knowledge modules so depending on YAML is never forced.
5
+ """
6
+
7
+
8
+ def _yaml():
9
+ try:
10
+ import yaml # PyYAML
11
+ return yaml, False
12
+ except ImportError:
13
+ import json
14
+ return json, True
15
+
16
+
17
+ def load_yaml(path):
18
+ from pathlib import Path
19
+ try:
20
+ text = Path(path).read_text(encoding="utf-8")
21
+ except FileNotFoundError:
22
+ return {}
23
+ mod, is_json = _yaml()
24
+ if is_json:
25
+ return mod.loads(text)
26
+ return mod.safe_load(text) or {}
27
+
28
+
29
+ def dump_yaml(data):
30
+ mod, is_json = _yaml()
31
+ if is_json:
32
+ return mod.dumps(data, indent=2)
33
+ return mod.safe_dump(data, sort_keys=False, allow_unicode=True, width=100000)