@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,15 @@
1
+ # Habit: verify-against-docs
2
+ # Source question: "Did I verify my work against actual documentation when it was available?"
3
+ # Correct action: when real docs/source exist, verify against THEM, not against memory or README prose.
4
+ # Evidence: claims trace to the actual API/doc/source I read; where docs conflict with prose, docs win.
5
+ # Logic: docs are the contract the system actually honors; prose summarizing them drifts. You know it's
6
+ # right because it matches the authority, not a secondhand description.
7
+ name: "verify_against_docs"
8
+ prompt: "Did I verify against the actual docs when available?"
9
+ enforcement:
10
+ level: "reminder"
11
+ behavior:
12
+ kind: "standard"
13
+ assert: "When real docs/source exist, verify against THEM, not memory or README prose."
14
+ evidence: "Claims trace to the actual API/doc/source I read; where docs conflict with prose, docs win."
15
+ logic: "Docs are the contract the system honors; prose summarizing them drifts. Right = matches the authority, not a description."
@@ -0,0 +1,15 @@
1
+ # Habit: verify-functionality-not-syntax
2
+ # Correct action: verify functionality, not just syntax. A syntax pass is never 'done'.
3
+ # Evidence: ran the code / exercised the path; observed expected behavior. A green linter alone is
4
+ # not evidence of function.
5
+ # Logic: a thing that parses can still be wrong. Functional proof requires execution, not compilation.
6
+ # You know it works because you watched it work, not because it compiled.
7
+ name: "verify_functionality_not_syntax"
8
+ prompt: "Did I verify this 100% by testing functionality, not just a syntax pass?"
9
+ enforcement:
10
+ level: "reminder"
11
+ behavior:
12
+ kind: "standard"
13
+ assert: "Verify functionality, not just syntax. A syntax pass is never 'done'."
14
+ evidence: "Ran the code / exercised the path; observed expected behavior. A green linter alone is not evidence of function."
15
+ logic: "A thing that parses can still be wrong. Functional proof requires execution, not compilation."
@@ -0,0 +1,18 @@
1
+ # Habit: versioning-discipline
2
+ # Source question: "Is the version accurate, present, and self-identifying?"
3
+ # Correct action: stamp a VERSION file and an AIK_VERSION (or equivalent) constant so the
4
+ # artifact identifies its own version; bump it on every meaningful change;
5
+ # never ship a dir that can't state what it is.
6
+ # Evidence: a VERSION file exists at repo root, the runtime constant matches it, and the
7
+ # two are updated together whenever behavior changes.
8
+ # Logic: an artifact that can't name its own version can't be debugged, diffed, or trusted.
9
+ # Self-identifying = the dir tells you what it is without external context.
10
+ name: "versioning_discipline"
11
+ prompt: "Is the version stamped, matching, and bumped on change?"
12
+ enforcement:
13
+ level: "reminder"
14
+ behavior:
15
+ kind: "standard"
16
+ assert: "Stamp VERSION file + runtime version constant; they match; bump both on any meaningful change."
17
+ evidence: "VERSION file at repo root; runtime constant equals it; both updated together when behavior changes."
18
+ logic: "An artifact that can't name its own version can't be debugged or trusted. Self-identifying is non-negotiable."
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@character-kit/agent-character-kit",
3
+ "version": "1.0.0",
4
+ "description": "Universal agent character enforcement, knowledge indexing, and memory with YAML frontmatter. True plugin: install once, reference as @character-kit in any harness config.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "ack": "node/bin/ack.js",
8
+ "character-kit": "node/bin/ack.js"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "test": "node --test tests/*.test.js",
13
+ "start": "node bin/aik.js",
14
+ "install": "node node/bin/install.js",
15
+ "ack-install": "node node/bin/install.js"
16
+ },
17
+ "keywords": [
18
+ "agent",
19
+ "character",
20
+ "agent-character",
21
+ "enforcement",
22
+ "knowledge",
23
+ "memory",
24
+ "semantic-search",
25
+ "hooks",
26
+ "yaml",
27
+ "frontmatter",
28
+ "claude",
29
+ "cursor",
30
+ "gemini",
31
+ "hermes",
32
+ "opencode"
33
+ ],
34
+ "author": "drdeeks",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "gray-matter": "^4.0.3",
38
+ "js-yaml": "^4.1.0",
39
+ "glob": "^10.3.0",
40
+ "commander": "^11.1.0"
41
+ },
42
+ "optionalDependencies": {
43
+ "@xenova/transformers": "^2.17.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "overrides": {
49
+ "onnxruntime-web": "^1.27.0",
50
+ "onnx-proto": "^8.0.1",
51
+ "protobufjs": "^7.2.4"
52
+ }
53
+ }
@@ -0,0 +1,149 @@
1
+ import net from "net";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ // Socket default MUST match the daemon (agent_enforcer_daemon.js): it lives
6
+ // under AGENT_WORKSPACE/.agent/enforcer.sock so the kit is portable (no /run).
7
+ // Override with ENFORCER_SOCKET if your deployment differs.
8
+ const DEFAULT_SOCKET = process.env.AGENT_WORKSPACE
9
+ ? path.join(process.env.AGENT_WORKSPACE, ".agent", "enforcer.sock")
10
+ : path.join(process.env.HOME || "/root", ".agent-character-kit", "workspace", ".agent", "enforcer.sock");
11
+
12
+ const SOCKET_PATH = process.env.ENFORCER_SOCKET || DEFAULT_SOCKET;
13
+
14
+ /**
15
+ * Enforcer Client — RPC to the character enforcer daemon.
16
+ *
17
+ * The enforcer daemon runs as a separate systemd service.
18
+ * The agent CANNOT modify, patch, or kill it.
19
+ * All tool calls go through here for validation.
20
+ */
21
+ export class EnforcerClient {
22
+ constructor(socketPath) {
23
+ this.socketPath = socketPath || SOCKET_PATH;
24
+ }
25
+
26
+ /**
27
+ * Send RPC request to enforcer daemon.
28
+ * @param {string} method - RPC method name
29
+ * @param {object} params - Method parameters
30
+ * @returns {Promise<object>} - Response from enforcer
31
+ */
32
+ async call(method, params = {}) {
33
+ return new Promise((resolve, reject) => {
34
+ if (!fs.existsSync(this.socketPath)) {
35
+ resolve({ error: "enforcer socket not found", denied: false });
36
+ return;
37
+ }
38
+
39
+ let retried = false;
40
+ const doCall = () => {
41
+ const socket = net.createConnection(this.socketPath);
42
+ const request = JSON.stringify({ method, params }) + "\n";
43
+ let data = "";
44
+
45
+ const timeout = setTimeout(() => {
46
+ socket.destroy();
47
+ resolve({ error: "enforcer timeout", denied: false });
48
+ }, 5000);
49
+
50
+ socket.on("connect", () => {
51
+ socket.write(request);
52
+ });
53
+
54
+ socket.on("data", (chunk) => {
55
+ data += chunk.toString();
56
+ if (data.includes("\n")) {
57
+ clearTimeout(timeout);
58
+ socket.destroy();
59
+ try {
60
+ const parsed = JSON.parse(data.trim());
61
+ // Retry once on "invalid request" (first-request buffering issue)
62
+ if (parsed.error === "invalid request" && !retried) {
63
+ retried = true;
64
+ console.error("[EnforcerClient] Retrying after invalid request...");
65
+ setTimeout(doCall, 10);
66
+ return;
67
+ }
68
+ resolve(parsed);
69
+ } catch (e) {
70
+ resolve({ error: "invalid response", denied: false });
71
+ }
72
+ }
73
+ });
74
+
75
+ socket.on("error", (err) => {
76
+ clearTimeout(timeout);
77
+ resolve({ error: err.message, denied: false });
78
+ });
79
+
80
+ socket.on("timeout", () => {
81
+ clearTimeout(timeout);
82
+ socket.destroy();
83
+ resolve({ error: "socket timeout", denied: false });
84
+ });
85
+ };
86
+ doCall();
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Validate a tool call through the enforcer.
92
+ * @param {string} tool - Tool name
93
+ * @param {object} params - Tool parameters
94
+ * @param {string} characterHash - Agent character hash
95
+ * @returns {Promise<{allowed: boolean, reason?: string, reflection?: string}>}
96
+ */
97
+ async validateTool(tool, params, characterHash = "unknown") {
98
+ // The daemon's socket contract is FLAT: { method:"execute_tool",
99
+ // params:{ tool, command, character_hash } }. executeTool(tool, params)
100
+ // reads params.command via _extractCommand. Do NOT nest params inside
101
+ // params — that was the bug that made every call pass (command resolved
102
+ // to the tool name instead of the real command).
103
+ const command =
104
+ (params && (params.command || params.cmd || params.code)) || "";
105
+ const response = await this.call("execute_tool", {
106
+ tool,
107
+ command,
108
+ character_hash: characterHash,
109
+ });
110
+
111
+ // Enforcer unreachable → fail closed by default (see processToolCall).
112
+ if (response.error) {
113
+ return {
114
+ allowed: false,
115
+ error: true,
116
+ reason: `Enforcer unavailable: ${response.error}`,
117
+ reflection:
118
+ "The enforcer could not be reached. character cannot be verified, " +
119
+ "so the action is blocked. A guard that fails open is no guard.",
120
+ };
121
+ }
122
+
123
+ if (response.denied) {
124
+ return {
125
+ allowed: false,
126
+ reason: response.reason || "Denied by enforcer",
127
+ reflection: response.reflection || "",
128
+ };
129
+ }
130
+
131
+ return { allowed: true };
132
+ }
133
+
134
+ /**
135
+ * Send heartbeat to enforcer.
136
+ */
137
+ async heartbeat(status = "ok") {
138
+ return this.call("heartbeat", { status });
139
+ }
140
+
141
+ /**
142
+ * Validate workspace integrity.
143
+ */
144
+ async validateWorkspace() {
145
+ return this.call("validate_workspace");
146
+ }
147
+ }
148
+
149
+ export default new EnforcerClient();
@@ -0,0 +1,286 @@
1
+ import { EnforcerClient } from "../enforcer/client.js";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const AUDIT_DIR = path.join(
6
+ process.env.HOME || "/root",
7
+ "var", "log", "agent-enforcer"
8
+ );
9
+ const AUDIT_LOG = path.join(AUDIT_DIR, "tool-audit.jsonl");
10
+
11
+ /**
12
+ * Character Hook — Core enforcement for all tool calls.
13
+ *
14
+ * This is the gatekeeper. Every tool call from any framework
15
+ * goes through here before execution.
16
+ *
17
+ * Flow:
18
+ * 1. Receive tool call (framework-specific format)
19
+ * 2. Normalize to unified format
20
+ * 3. Validate through enforcer daemon
21
+ * 4. Return allow/deny in framework-native format
22
+ * 5. Log to audit trail
23
+ */
24
+
25
+ // ─── Framework Detection ────────────────────────────────────────────────────
26
+
27
+ function detectFramework(payload) {
28
+ if (payload.hook_event_name) {
29
+ const evt = payload.hook_event_name;
30
+ if (["PreToolUse", "PostToolUse", "SessionStart", "Stop"].includes(evt)) return "claude";
31
+ if (["BeforeTool", "AfterTool", "BeforeToolSelection"].includes(evt)) return "gemini";
32
+ if (["preToolUse", "postToolUse", "beforeShellExecution"].includes(evt)) return "cursor";
33
+ }
34
+ if (payload.tool_name && payload.args) return "hermes";
35
+ if (payload.tool && payload.args) return "opencode";
36
+ return "generic";
37
+ }
38
+
39
+ // ─── Normalization ──────────────────────────────────────────────────────────
40
+
41
+ function normalizeInput(payload, framework) {
42
+ switch (framework) {
43
+ case "claude":
44
+ return {
45
+ tool: payload.tool_name || "unknown",
46
+ params: payload.tool_input || {},
47
+ event: (payload.hook_event_name || "PreToolUse").toLowerCase(),
48
+ sessionId: payload.session_id,
49
+ cwd: payload.cwd,
50
+ };
51
+ case "cursor":
52
+ return {
53
+ tool: payload.tool_name || payload.tool || "unknown",
54
+ params: payload.tool_input || payload.args || {},
55
+ event: (payload.hook_event_name || "preToolUse").toLowerCase(),
56
+ sessionId: payload.session_id,
57
+ cwd: payload.cwd,
58
+ };
59
+ case "gemini":
60
+ return {
61
+ tool: payload.tool_name || "unknown",
62
+ params: payload.tool_input || {},
63
+ event: (payload.hook_event_name || "BeforeTool").toLowerCase(),
64
+ sessionId: payload.session_id,
65
+ cwd: payload.cwd,
66
+ };
67
+ case "hermes":
68
+ return {
69
+ tool: payload.tool_name || "unknown",
70
+ params: payload.args || {},
71
+ event: "pre_tool_call",
72
+ sessionId: payload.task_id,
73
+ };
74
+ case "opencode":
75
+ return {
76
+ tool: payload.tool || payload.tool_name || "unknown",
77
+ params: payload.args || payload.tool_input || {},
78
+ event: payload.event || "tool.execute.before",
79
+ sessionId: payload.session_id,
80
+ };
81
+ default:
82
+ return {
83
+ tool: payload.tool || payload.tool_name || "unknown",
84
+ params: payload.params || payload.args || payload.tool_input || {},
85
+ event: payload.event || "pre_tool_use",
86
+ sessionId: payload.session_id,
87
+ };
88
+ }
89
+ }
90
+
91
+ // ─── Output Formatting ──────────────────────────────────────────────────────
92
+
93
+ function formatOutput(result, framework, original) {
94
+ const { allowed, reason } = result;
95
+
96
+ switch (framework) {
97
+ case "claude":
98
+ return {
99
+ hookSpecificOutput: {
100
+ hookEventName: original.hook_event_name || "PreToolUse",
101
+ permissionDecision: allowed ? "allow" : "deny",
102
+ ...(allowed ? {} : { permissionDecisionReason: reason }),
103
+ },
104
+ };
105
+
106
+ case "cursor":
107
+ return {
108
+ permission: allowed ? "allow" : "deny",
109
+ ...(allowed ? {} : { reason }),
110
+ };
111
+
112
+ case "gemini":
113
+ return {
114
+ decision: allowed ? "allow" : "deny",
115
+ ...(allowed ? {} : { reason }),
116
+ };
117
+
118
+ case "hermes":
119
+ if (allowed) return {};
120
+ return { action: "block", message: reason };
121
+
122
+ case "opencode":
123
+ return {
124
+ block: !allowed,
125
+ ...(allowed ? {} : { reason }),
126
+ };
127
+
128
+ default:
129
+ return {
130
+ allow: allowed,
131
+ ...(allowed ? {} : { reason, reflection: result.reflection }),
132
+ };
133
+ }
134
+ }
135
+
136
+ // ─── Audit Trail ────────────────────────────────────────────────────────────
137
+
138
+ function auditLog(event, tool, params, result) {
139
+ try {
140
+ if (!fs.existsSync(AUDIT_DIR)) {
141
+ fs.mkdirSync(AUDIT_DIR, { recursive: true });
142
+ }
143
+ const entry = {
144
+ ts: new Date().toISOString(),
145
+ event,
146
+ tool,
147
+ params,
148
+ result,
149
+ };
150
+ fs.appendFileSync(AUDIT_LOG, JSON.stringify(entry) + "\n");
151
+ } catch {
152
+ // Silent fail — audit logging should never block enforcement
153
+ }
154
+ }
155
+
156
+ // ─── Main Hook ──────────────────────────────────────────────────────────────
157
+
158
+ const ENFORCER = new EnforcerClient();
159
+
160
+ /**
161
+ * Process a tool call through the character hook.
162
+ *
163
+ * @param {object} payload - Raw tool call from framework
164
+ * @param {object} options - { framework: "auto"|"claude"|..., enforcer: EnforcerClient }
165
+ * @returns {Promise<{output: object, exitCode: number}>}
166
+ */
167
+ export async function processToolCall(payload, options = {}) {
168
+ const framework = options.framework === "auto"
169
+ ? detectFramework(payload)
170
+ : (options.framework || "generic");
171
+
172
+ const enforcer = options.enforcer || ENFORCER;
173
+ const normalized = normalizeInput(payload, framework);
174
+
175
+ // Skip enforcer internal calls
176
+ if (["validate_workspace", "heartbeat", "execute_tool"].includes(normalized.tool)) {
177
+ return { output: formatOutput({ allowed: true }, framework, payload), exitCode: 0 };
178
+ }
179
+
180
+ // Fail-closed by default: if the enforcer can't be reached, block.
181
+ // Opt out only in development with ACK_FAIL_OPEN=1 (never in production).
182
+ const failClosed = !process.env.ACK_FAIL_OPEN;
183
+
184
+ const isPre = [
185
+ "pre_tool_use", "pretooluse", "beforetool",
186
+ "pre_tool_call", "tool.execute.before"
187
+ ].includes(normalized.event);
188
+
189
+ let result;
190
+ if (isPre) {
191
+ result = await enforcer.validateTool(
192
+ normalized.tool,
193
+ normalized.params,
194
+ normalized.sessionId || "unknown"
195
+ );
196
+
197
+ // Enforcer unreachable → enforce the closed posture.
198
+ if (result.error && !failClosed) {
199
+ result = { allowed: true };
200
+ }
201
+
202
+ auditLog("pre_tool_use", normalized.tool, normalized.params, result);
203
+ } else {
204
+ auditLog("post_tool_use", normalized.tool, normalized.params, payload.result);
205
+ result = { allowed: true };
206
+ }
207
+
208
+ const output = formatOutput(result, framework, payload);
209
+ const exitCode = !result.allowed && ["claude", "cursor", "gemini"].includes(framework) ? 2 : 0;
210
+
211
+ return { output, exitCode };
212
+ }
213
+
214
+ /**
215
+ * Generate framework-specific hook configuration.
216
+ */
217
+ export function generateConfig(framework, hookCommand) {
218
+ const cmd = hookCommand || "npx aik hook";
219
+
220
+ switch (framework) {
221
+ case "claude":
222
+ return {
223
+ hooks: {
224
+ PreToolUse: [{
225
+ matcher: "*",
226
+ hooks: [{ type: "command", command: `${cmd} --framework claude` }],
227
+ }],
228
+ },
229
+ };
230
+
231
+ case "cursor":
232
+ return {
233
+ version: 1,
234
+ hooks: {
235
+ preToolUse: [{ command: `${cmd} --framework cursor`, matcher: "*" }],
236
+ },
237
+ };
238
+
239
+ case "gemini":
240
+ return {
241
+ hooks: {
242
+ BeforeTool: [{
243
+ matcher: ".*",
244
+ hooks: [{
245
+ name: "character-enforcer",
246
+ type: "command",
247
+ command: `${cmd} --framework gemini`,
248
+ }],
249
+ }],
250
+ },
251
+ };
252
+
253
+ case "hermes":
254
+ return `# Add to your Hermes plugin:
255
+ const { processToolCall } = require("agent-character-kit");
256
+
257
+ ctx.register_hook("pre_tool_call", async (toolName, args, taskId) => {
258
+ const result = await processToolCall(
259
+ { tool_name: toolName, args, task_id: taskId },
260
+ { framework: "hermes" }
261
+ );
262
+ return result.output;
263
+ });
264
+ `;
265
+
266
+ case "opencode":
267
+ return `// Add to your OpenCode plugin:
268
+ import { processToolCall } from "agent-character-kit";
269
+
270
+ export default async ({ tool, args }) => {
271
+ const result = await processToolCall(
272
+ { tool, args },
273
+ { framework: "opencode" }
274
+ );
275
+ return result.output;
276
+ };
277
+ `;
278
+
279
+ default:
280
+ return {
281
+ hooks: {
282
+ pre_tool_use: [{ command: `${cmd} --framework generic` }],
283
+ },
284
+ };
285
+ }
286
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Agent Character Kit
3
+ *
4
+ * Core: Character hook enforcement via enforcer daemon
5
+ * Secondary: Knowledge indexing + memory with YAML frontmatter
6
+ *
7
+ * Every document gets proper YAML frontmatter for efficient searching.
8
+ * Every tool call gets validated through the enforcer.
9
+ */
10
+
11
+ // ─── Core: Character Enforcement ─────────────────────────────────────────────
12
+
13
+ export { EnforcerClient } from "./enforcer/client.js";
14
+ export { processToolCall, generateConfig } from "./hooks/character.js";
15
+
16
+ // ─── Secondary: Knowledge & Memory ──────────────────────────────────────────
17
+
18
+ export { DocumentIndexer } from "./knowledge/indexer.js";
19
+ export { SemanticSearch } from "./knowledge/semantic.js";
20
+ export { Memory, DailyNotes, WeeklyDigest, LongTermMemory, KnowledgeGraph } from "./memory/index.js";
21
+
22
+ // ─── Version ────────────────────────────────────────────────────────────────
23
+
24
+ export const VERSION = "1.0.0";