@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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/deploy/ack_monitor.py +140 -0
- package/deploy/ack_watchdog.py +110 -0
- package/deploy/agent-character-monitor.service +21 -0
- package/deploy/agent-character-watchdog.service +21 -0
- package/deploy/agent-enforcer-proof.service +18 -0
- package/deploy/agent-enforcer.service +53 -0
- package/deploy/deploy-ack-services.sh +47 -0
- package/deploy/deploy-agent-enforcer.sh +133 -0
- package/deploy/proof-self-respawn.sh +59 -0
- package/node/bin/ack.js +383 -0
- package/node/bin/aik.js +520 -0
- package/node/bin/install.js +409 -0
- package/node/enforcer/agent_enforcer_daemon.js +690 -0
- package/node/examples/.agent/constitution.yaml +38 -0
- package/node/examples/.agent/enforcer.yaml +33 -0
- package/node/examples/.agent/habits/affirm_character_each_action.yaml +9 -0
- package/node/examples/.agent/habits/audit_not_silent.yaml +9 -0
- package/node/examples/.agent/habits/binding_map_one_core.yaml +9 -0
- package/node/examples/.agent/habits/character_hash_visible.yaml +9 -0
- package/node/examples/.agent/habits/check_duplication_before_debug.yaml +9 -0
- package/node/examples/.agent/habits/complete-thoroughly.yaml +16 -0
- package/node/examples/.agent/habits/consistent-info-across-files.yaml +18 -0
- package/node/examples/.agent/habits/document-for-next-agent.yaml +15 -0
- package/node/examples/.agent/habits/documented_rollback.yaml +9 -0
- package/node/examples/.agent/habits/drift_signal_detection.yaml +9 -0
- package/node/examples/.agent/habits/due-diligence.yaml +16 -0
- package/node/examples/.agent/habits/enterprise-grade-modular.yaml +16 -0
- package/node/examples/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
- package/node/examples/.agent/habits/forever_one_idea.yaml +9 -0
- package/node/examples/.agent/habits/graceful_degradation.yaml +9 -0
- package/node/examples/.agent/habits/idempotent_operations.yaml +9 -0
- package/node/examples/.agent/habits/interactive-no-stalls.yaml +16 -0
- package/node/examples/.agent/habits/layered_not_rewritten.yaml +9 -0
- package/node/examples/.agent/habits/lossless_consolidation.yaml +9 -0
- package/node/examples/.agent/habits/no-credential-leak.yaml +41 -0
- package/node/examples/.agent/habits/no-deception.yaml +14 -0
- package/node/examples/.agent/habits/no_hold_narration.yaml +9 -0
- package/node/examples/.agent/habits/one_concern_per_file.yaml +9 -0
- package/node/examples/.agent/habits/optimized-robust.yaml +16 -0
- package/node/examples/.agent/habits/registered_plugin_not_string.yaml +9 -0
- package/node/examples/.agent/habits/rename_as_layer_op.yaml +9 -0
- package/node/examples/.agent/habits/resolve-root-cause.yaml +15 -0
- package/node/examples/.agent/habits/rigorous-commits-no-push.yaml +18 -0
- package/node/examples/.agent/habits/safe-deletion-via-trash.yaml +20 -0
- package/node/examples/.agent/habits/safe_file_permissions.yaml +9 -0
- package/node/examples/.agent/habits/self-healing-portability.yaml +14 -0
- package/node/examples/.agent/habits/shippable-pride.yaml +15 -0
- package/node/examples/.agent/habits/single_source_of_truth.yaml +9 -0
- package/node/examples/.agent/habits/test_of_forever.yaml +9 -0
- package/node/examples/.agent/habits/timeout_and_retry.yaml +9 -0
- package/node/examples/.agent/habits/track_defects_openly.yaml +9 -0
- package/node/examples/.agent/habits/validate-against-real-source.yaml +14 -0
- package/node/examples/.agent/habits/verify-against-docs.yaml +15 -0
- package/node/examples/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
- package/node/examples/.agent/habits/versioning-discipline.yaml +18 -0
- package/node/package.json +53 -0
- package/node/src/enforcer/client.js +149 -0
- package/node/src/hooks/character.js +286 -0
- package/node/src/index.js +24 -0
- package/node/src/knowledge/indexer.js +486 -0
- package/node/src/knowledge/semantic.js +154 -0
- package/node/src/memory/index.js +355 -0
- package/node/tests/character.test.js +80 -0
- package/node/tests/habit-create.test.js +67 -0
- package/node/tests/habits-default.test.js +68 -0
- package/node/tests/install-chain.test.js +19 -0
- package/node/tests/install.test.js +104 -0
- package/package.json +65 -0
- package/python/agent_character_kit/__init__.py +388 -0
- package/python/agent_character_kit/__main__.py +333 -0
- package/python/agent_character_kit/_yaml.py +33 -0
- package/python/agent_character_kit/enforcer.py +379 -0
- package/python/agent_character_kit/memory.py +257 -0
- package/python/agent_character_kit/semantic.py +126 -0
- package/python/example_workspace/.agent/habits/affirm_character_each_action.yaml +9 -0
- package/python/example_workspace/.agent/habits/audit_not_silent.yaml +9 -0
- package/python/example_workspace/.agent/habits/binding_map_one_core.yaml +9 -0
- package/python/example_workspace/.agent/habits/character_hash_visible.yaml +9 -0
- package/python/example_workspace/.agent/habits/check_duplication_before_debug.yaml +9 -0
- package/python/example_workspace/.agent/habits/complete-thoroughly.yaml +16 -0
- package/python/example_workspace/.agent/habits/consistent-info-across-files.yaml +18 -0
- package/python/example_workspace/.agent/habits/document-for-next-agent.yaml +15 -0
- package/python/example_workspace/.agent/habits/documented_rollback.yaml +9 -0
- package/python/example_workspace/.agent/habits/drift_signal_detection.yaml +9 -0
- package/python/example_workspace/.agent/habits/due-diligence.yaml +16 -0
- package/python/example_workspace/.agent/habits/enterprise-grade-modular.yaml +16 -0
- package/python/example_workspace/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
- package/python/example_workspace/.agent/habits/forever_one_idea.yaml +9 -0
- package/python/example_workspace/.agent/habits/graceful_degradation.yaml +9 -0
- package/python/example_workspace/.agent/habits/idempotent_operations.yaml +9 -0
- package/python/example_workspace/.agent/habits/interactive-no-stalls.yaml +16 -0
- package/python/example_workspace/.agent/habits/layered_not_rewritten.yaml +9 -0
- package/python/example_workspace/.agent/habits/lossless_consolidation.yaml +9 -0
- package/python/example_workspace/.agent/habits/no-credential-leak.yaml +41 -0
- package/python/example_workspace/.agent/habits/no-deception.yaml +14 -0
- package/python/example_workspace/.agent/habits/no_hold_narration.yaml +9 -0
- package/python/example_workspace/.agent/habits/one_concern_per_file.yaml +9 -0
- package/python/example_workspace/.agent/habits/optimized-robust.yaml +16 -0
- package/python/example_workspace/.agent/habits/registered_plugin_not_string.yaml +9 -0
- package/python/example_workspace/.agent/habits/rename_as_layer_op.yaml +9 -0
- package/python/example_workspace/.agent/habits/resolve-root-cause.yaml +15 -0
- package/python/example_workspace/.agent/habits/rigorous-commits-no-push.yaml +18 -0
- package/python/example_workspace/.agent/habits/safe-deletion-via-trash.yaml +20 -0
- package/python/example_workspace/.agent/habits/safe_file_permissions.yaml +9 -0
- package/python/example_workspace/.agent/habits/self-healing-portability.yaml +14 -0
- package/python/example_workspace/.agent/habits/shippable-pride.yaml +15 -0
- package/python/example_workspace/.agent/habits/single_source_of_truth.yaml +9 -0
- package/python/example_workspace/.agent/habits/test_of_forever.yaml +9 -0
- package/python/example_workspace/.agent/habits/timeout_and_retry.yaml +9 -0
- package/python/example_workspace/.agent/habits/track_defects_openly.yaml +9 -0
- package/python/example_workspace/.agent/habits/validate-against-real-source.yaml +14 -0
- package/python/example_workspace/.agent/habits/verify-against-docs.yaml +15 -0
- package/python/example_workspace/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
- package/python/example_workspace/.agent/habits/versioning-discipline.yaml +18 -0
- package/python/hermes_plugin/README.md +63 -0
- package/python/hermes_plugin/__init__.py +367 -0
- package/python/hermes_plugin/config.yaml +20 -0
- package/python/hermes_plugin/plugin.yaml +7 -0
- package/python/hermes_plugin/test_plugin.py +166 -0
- package/python/pyproject.toml +19 -0
- 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)
|