integral-mind 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hooks.js ADDED
@@ -0,0 +1,261 @@
1
+ import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { resolveConfig } from "./config.js";
6
+ import { redactSecrets } from "./redact.js";
7
+ import { ImClient } from "./client.js";
8
+ /**
9
+ * Claude Code session-end hook: `im hooks install claude` wires a
10
+ * SessionEnd entry into ~/.claude/settings.json; at session end Claude
11
+ * Code invokes `... hooks claude-session-end` with a JSON payload on
12
+ * stdin ({ session_id, transcript_path, cwd, reason }), and this module
13
+ * reshapes the transcript into the session envelope, redacts it, and
14
+ * pushes it.
15
+ *
16
+ * Contract: the hook process ALWAYS exits 0. A failing push must never
17
+ * break or block the Claude session — failures are appended to
18
+ * ~/.local/state/integral-mind/hooks.log instead.
19
+ */
20
+ const HOOK_MARKER = "hooks claude-session-end";
21
+ export function claudeSettingsPath(env = process.env) {
22
+ return env.INTEGRAL_MIND_CLAUDE_SETTINGS ?? join(homedir(), ".claude", "settings.json");
23
+ }
24
+ export function hookLogPath(env = process.env) {
25
+ return (env.INTEGRAL_MIND_HOOK_LOG ?? join(homedir(), ".local", "state", "integral-mind", "hooks.log"));
26
+ }
27
+ /** The command Claude Code will run: absolute node + absolute cli.js. */
28
+ export function hookCommand() {
29
+ const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
30
+ return `${JSON.stringify(process.execPath)} ${JSON.stringify(cliPath)} ${HOOK_MARKER}`;
31
+ }
32
+ /**
33
+ * A missing file is an empty config; anything else that prevents reading
34
+ * or parsing ABORTS the operation — installing over unreadable settings
35
+ * would silently erase whatever Claude Code had configured.
36
+ */
37
+ async function readSettings(path) {
38
+ let raw;
39
+ try {
40
+ raw = await readFile(path, "utf8");
41
+ }
42
+ catch (err) {
43
+ if (err.code === "ENOENT")
44
+ return {};
45
+ throw new Error(`Cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ throw new Error(`${path} is not valid JSON — fix or remove it before installing the hook (refusing to overwrite it)`);
53
+ }
54
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
55
+ throw new Error(`${path} does not contain a JSON object — refusing to overwrite it`);
56
+ }
57
+ return parsed;
58
+ }
59
+ /** Atomic replace: write a sibling temp file, then rename over the target. */
60
+ async function writeSettings(path, serialized) {
61
+ await mkdir(dirname(path), { recursive: true });
62
+ const tmp = `${path}.integral-mind-${process.pid}.tmp`;
63
+ await writeFile(tmp, serialized);
64
+ await rename(tmp, path);
65
+ }
66
+ /**
67
+ * Read-modify-write that never silently discards a concurrent update.
68
+ * Cooperative locking is impossible here — Claude Code writes
69
+ * settings.json without honoring any lock we could take — so instead of
70
+ * pretending to prevent the race, we detect losing it: after the atomic
71
+ * rename, re-read the file; if it no longer matches what we wrote,
72
+ * another writer won mid-cycle, and the loop re-applies our modification
73
+ * on top of THEIR fresh state. Either our write is the visible state or
74
+ * it is re-merged — nothing is dropped without a retry.
75
+ * Returns false when `modify` decides there is nothing to change.
76
+ */
77
+ async function updateSettings(path, modify) {
78
+ for (let attempt = 0; attempt < 5; attempt++) {
79
+ const settings = await readSettings(path);
80
+ const next = modify(settings);
81
+ if (next === null)
82
+ return false;
83
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
84
+ await writeSettings(path, serialized);
85
+ const after = await readFile(path, "utf8").catch(() => null);
86
+ if (after === serialized)
87
+ return true;
88
+ // A concurrent writer overlapped; loop to re-apply on their state.
89
+ }
90
+ throw new Error(`${path} keeps changing while updating it — is a session ending? Retry shortly.`);
91
+ }
92
+ /**
93
+ * Ours = the command *ends with* our subcommand AND references this CLI
94
+ * (cli.js, the npm package name, or a bare `im`). A foreign command that
95
+ * merely mentions the marker somewhere in its arguments is not claimed.
96
+ */
97
+ function isOurs(entry) {
98
+ return (entry.hooks ?? []).some((h) => {
99
+ const command = h.command?.trim() ?? "";
100
+ if (!command.endsWith(HOOK_MARKER))
101
+ return false;
102
+ if (command === hookCommand())
103
+ return true; // exact current install
104
+ // Package identity, not just "some cli.js": the path must contain this
105
+ // package's name (npm: node_modules/integral-mind/...; repo checkout:
106
+ // apps/im-cli/...), or be the bare bin / npx invocation.
107
+ return (/(^|[\/"'\s])(integral-mind|im-cli)[\/"']/.test(command) ||
108
+ command === `im ${HOOK_MARKER}` ||
109
+ command === `npx integral-mind ${HOOK_MARKER}`);
110
+ });
111
+ }
112
+ /** Idempotent: a second install is a no-op. Returns what happened. */
113
+ export async function installClaudeHook(env = process.env) {
114
+ const wrote = await updateSettings(claudeSettingsPath(env), (settings) => {
115
+ const sessionEnd = settings.hooks?.SessionEnd ?? [];
116
+ if (sessionEnd.some(isOurs))
117
+ return null;
118
+ return {
119
+ ...settings,
120
+ hooks: {
121
+ ...settings.hooks,
122
+ SessionEnd: [...sessionEnd, { hooks: [{ type: "command", command: hookCommand() }] }],
123
+ },
124
+ };
125
+ });
126
+ return wrote ? "installed" : "already-installed";
127
+ }
128
+ /** Removes only our entries; other hooks survive untouched. */
129
+ export async function uninstallClaudeHook(env = process.env) {
130
+ const wrote = await updateSettings(claudeSettingsPath(env), (settings) => {
131
+ const sessionEnd = settings.hooks?.SessionEnd ?? [];
132
+ const kept = sessionEnd.filter((entry) => !isOurs(entry));
133
+ if (kept.length === sessionEnd.length)
134
+ return null;
135
+ const hooks = { ...settings.hooks };
136
+ if (kept.length > 0)
137
+ hooks.SessionEnd = kept;
138
+ else
139
+ delete hooks.SessionEnd;
140
+ const next = { ...settings };
141
+ if (Object.keys(hooks).length > 0)
142
+ next.hooks = hooks;
143
+ else
144
+ delete next.hooks;
145
+ return next;
146
+ });
147
+ return wrote ? "uninstalled" : "not-installed";
148
+ }
149
+ /**
150
+ * Lenient Claude Code transcript (.jsonl) reader: keeps user/assistant
151
+ * text, renders tool activity as short placeholders, skips everything it
152
+ * doesn't recognize. Transcript format drift must degrade, not throw.
153
+ */
154
+ export function parseTranscript(jsonl) {
155
+ const messages = [];
156
+ for (const line of jsonl.split("\n")) {
157
+ if (!line.trim())
158
+ continue;
159
+ let obj;
160
+ try {
161
+ obj = JSON.parse(line);
162
+ }
163
+ catch {
164
+ continue;
165
+ }
166
+ const type = obj.type;
167
+ if (type !== "user" && type !== "assistant")
168
+ continue;
169
+ const message = obj.message;
170
+ if (!message)
171
+ continue;
172
+ const role = message.role === "assistant" ? "assistant" : "user";
173
+ const content = renderContent(message.content);
174
+ if (!content.trim())
175
+ continue;
176
+ const ts = typeof obj.timestamp === "string" ? obj.timestamp : undefined;
177
+ messages.push({ role, content, ...(ts ? { ts } : {}) });
178
+ }
179
+ return messages;
180
+ }
181
+ function renderContent(content) {
182
+ if (typeof content === "string")
183
+ return content;
184
+ if (!Array.isArray(content))
185
+ return "";
186
+ const parts = [];
187
+ for (const block of content) {
188
+ if (typeof block !== "object" || block === null)
189
+ continue;
190
+ const b = block;
191
+ if (b.type === "text" && typeof b.text === "string")
192
+ parts.push(b.text);
193
+ else if (b.type === "tool_use" && typeof b.name === "string")
194
+ parts.push(`[tool_use: ${b.name}]`);
195
+ else if (b.type === "tool_result")
196
+ parts.push("[tool_result]");
197
+ }
198
+ return parts.join("\n");
199
+ }
200
+ export function defaultSessionEndDeps(env = process.env) {
201
+ return {
202
+ pushSession: async (envelope) => {
203
+ const resolved = await resolveConfig(env);
204
+ if ("error" in resolved)
205
+ throw new Error(resolved.error);
206
+ return new ImClient(resolved).pushSession(envelope);
207
+ },
208
+ log: async (line) => {
209
+ const path = hookLogPath(env);
210
+ await mkdir(dirname(path), { recursive: true });
211
+ await appendFile(path, `${new Date().toISOString()} ${line}\n`);
212
+ },
213
+ readTranscript: (path) => readFile(path, "utf8"),
214
+ };
215
+ }
216
+ /**
217
+ * The hook entrypoint. Never throws; always resolves 0 so Claude Code is
218
+ * never blocked by a failing push.
219
+ */
220
+ export async function runClaudeSessionEnd(stdinJson, deps) {
221
+ try {
222
+ const payload = JSON.parse(stdinJson);
223
+ if (!payload.transcript_path) {
224
+ await deps.log("skip: no transcript_path in hook payload");
225
+ return 0;
226
+ }
227
+ const messages = parseTranscript(await deps.readTranscript(payload.transcript_path));
228
+ if (messages.length < 2) {
229
+ await deps.log(`skip: transcript has ${messages.length} usable message(s)`);
230
+ return 0;
231
+ }
232
+ const project = payload.cwd ? basename(payload.cwd) : "unknown-project";
233
+ const envelope = {
234
+ agent: "claude-code",
235
+ title: `Claude Code — ${project}${payload.session_id ? ` (${payload.session_id.slice(0, 8)})` : ""}`,
236
+ ...(messages[0]?.ts ? { started_at: messages[0].ts } : {}),
237
+ messages,
238
+ };
239
+ let redactedCount = 0;
240
+ for (const message of messages) {
241
+ const { text, findings } = redactSecrets(message.content);
242
+ message.content = text;
243
+ redactedCount += findings.reduce((sum, f) => sum + f.count, 0);
244
+ }
245
+ // The title embeds the cwd basename — hook metadata gets the same
246
+ // redaction contract as message content.
247
+ const titleScrub = redactSecrets(String(envelope.title));
248
+ envelope.title = titleScrub.text;
249
+ redactedCount += titleScrub.findings.reduce((sum, f) => sum + f.count, 0);
250
+ const result = await deps.pushSession(envelope);
251
+ await deps.log(`pushed session ${String(result.source_id)} (${messages.length} messages, ${redactedCount} redaction(s), deduplicated=${String(result.deduplicated)})`);
252
+ }
253
+ catch (err) {
254
+ // Swallow everything: the log line is the only side effect of failure.
255
+ await deps
256
+ .log(`push failed: ${err instanceof Error ? err.message : String(err)}`)
257
+ .catch(() => { });
258
+ }
259
+ return 0;
260
+ }
261
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
package/dist/redact.js ADDED
@@ -0,0 +1,77 @@
1
+ // Order matters: structured/specific shapes run before the generic
2
+ // high-entropy catch-all so findings carry the most precise type.
3
+ export const REDACTION_RULES = [
4
+ {
5
+ type: "pem-block",
6
+ pattern: /-----BEGIN [A-Z0-9 ]{0,48}PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]{0,48}PRIVATE KEY-----/g,
7
+ },
8
+ { type: "anthropic-api-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g },
9
+ { type: "openai-api-key", pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g },
10
+ { type: "integral-mind-token", pattern: /\bimk_[A-Za-z0-9_-]{16,}\b/g },
11
+ { type: "aws-access-key", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
12
+ { type: "github-token", pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g },
13
+ { type: "github-fine-grained-token", pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
14
+ { type: "google-api-key", pattern: /\bAIza[A-Za-z0-9_-]{30,}\b/g },
15
+ { type: "slack-token", pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g },
16
+ { type: "stripe-key", pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
17
+ { type: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
18
+ {
19
+ // Square brackets are excluded so an earlier rule's [REDACTED:…]
20
+ // placeholder (which contains a colon) can never re-match as user:pass.
21
+ type: "connection-string",
22
+ pattern: /\b[a-z][a-z0-9+.-]{1,24}:\/\/[^\s:@/[\]]{1,64}:[^\s@[\]]{1,256}@[^\s"'`]+/gi,
23
+ },
24
+ {
25
+ type: "assignment-secret",
26
+ pattern: /\b((?:api[_-]?key|secret|token|passwd|password|credential)[a-z0-9_-]*)(\s*[:=]\s*)["']?(?!\[REDACTED)[^\s"']{8,}["']?/gi,
27
+ },
28
+ ];
29
+ const HIGH_ENTROPY_CANDIDATE = /\b[A-Za-z0-9+/=_-]{32,}\b/g;
30
+ const ENTROPY_THRESHOLD = 4.2;
31
+ export function shannonEntropy(value) {
32
+ const counts = new Map();
33
+ for (const ch of value)
34
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
35
+ let entropy = 0;
36
+ for (const count of counts.values()) {
37
+ const p = count / value.length;
38
+ entropy -= p * Math.log2(p);
39
+ }
40
+ return entropy;
41
+ }
42
+ export function redactSecrets(text) {
43
+ const counts = new Map();
44
+ let out = text;
45
+ for (const rule of REDACTION_RULES) {
46
+ out = out.replace(rule.pattern, (match, ...rest) => {
47
+ counts.set(rule.type, (counts.get(rule.type) ?? 0) + 1);
48
+ if (rule.type === "assignment-secret") {
49
+ // Keep the key name; replace only the value.
50
+ const [name, sep] = rest;
51
+ return `${name}${sep}[REDACTED:${rule.type}]`;
52
+ }
53
+ return `[REDACTED:${rule.type}]`;
54
+ });
55
+ }
56
+ out = out.replace(HIGH_ENTROPY_CANDIDATE, (match) => {
57
+ if (match.startsWith("[REDACTED"))
58
+ return match;
59
+ // Long hex (git SHAs) and uuid-ish strings are common in transcripts;
60
+ // only redact when entropy says "random secret", not "identifier".
61
+ if (shannonEntropy(match) < ENTROPY_THRESHOLD)
62
+ return match;
63
+ counts.set("high-entropy-string", (counts.get("high-entropy-string") ?? 0) + 1);
64
+ return "[REDACTED:high-entropy-string]";
65
+ });
66
+ const findings = [...counts.entries()]
67
+ .map(([type, count]) => ({ type, count }))
68
+ .sort((a, b) => a.type.localeCompare(b.type));
69
+ return { text: out, findings };
70
+ }
71
+ export function summarizeFindings(findings) {
72
+ if (findings.length === 0)
73
+ return "redaction: nothing to scrub";
74
+ const parts = findings.map((f) => `${f.type}×${f.count}`);
75
+ return `redaction: scrubbed ${parts.join(", ")}`;
76
+ }
77
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"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"}