@everme/codex 0.4.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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @everme/codex
2
+
3
+ Native EverMe lifecycle hook runner for Codex. It is invoked by the EverMe
4
+ Codex marketplace plugin and uses the stable `/api/v1/mem/*` BFF contract.
5
+
6
+ ## Lifecycle
7
+
8
+ - `SessionStart`: inject the EverMe profile snapshot.
9
+ - `UserPromptSubmit`: sanitize the prompt, search top 10 memories, and inject
10
+ recall without passive profile rows by default.
11
+ - `Stop`: stream the Codex rollout, save only the latest user turn, and flush
12
+ extraction every five turns.
13
+ - `PreCompact`: send a flush-only request.
14
+
15
+ All hook failures are fail-open and credentials are redacted from diagnostics.
16
+ `evercli plugin install codex` writes credentials to `~/.codex/everme.env`
17
+ with mode `0600`; the user reviews and trusts the commands in `/hooks`.
18
+
19
+ ## Configuration
20
+
21
+ The shared hook knobs are `EVERME_INJECT_TOPK`, `EVERME_INJECT_PROFILE`,
22
+ `EVERME_INJECT_MIN_SCORE`, `EVERME_FLUSH_EVERY_TURNS`,
23
+ `EVERME_FLUSH_MODE`, and `EVERME_STATE_DIR`.
24
+
25
+ ## License
26
+
27
+ Apache-2.0.
package/bin/hook.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { redactError, runHook } from "@everme/agent-sdk";
4
+ import { codexAdapter } from "../src/adapter.js";
5
+
6
+ main().catch((error) => {
7
+ const reason = redactError(error).replace(/\s+/g, " ").trim();
8
+ process.stderr.write(`EverMe Codex hook degraded: ${reason}\n`);
9
+ process.exitCode = 0;
10
+ });
11
+
12
+ async function main() {
13
+ const [, , command, event] = process.argv;
14
+ if (command !== "hook" || !event) return;
15
+ const input = await readStdinJSON();
16
+ const output = await runHook(event, input, codexAdapter);
17
+ if (output && Object.keys(output).length) {
18
+ process.stdout.write(JSON.stringify(output));
19
+ }
20
+ }
21
+
22
+ async function readStdinJSON() {
23
+ const chunks = [];
24
+ for await (const chunk of process.stdin) chunks.push(chunk);
25
+ if (!chunks.length) return {};
26
+ try {
27
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@everme/codex",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "description": "Native EverMe lifecycle hooks for Codex.",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "everme-codex": "./bin/hook.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "scripts": {
18
+ "test": "node --test tests/transcript.test.js tests/adapter.test.js tests/hook.test.js"
19
+ },
20
+ "keywords": [
21
+ "evermind",
22
+ "everme",
23
+ "codex",
24
+ "memory",
25
+ "hooks"
26
+ ],
27
+ "homepage": "https://everme.evermind.ai",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/EverMind-AI/EverMe.git",
31
+ "directory": "plugins/codex"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/EverMind-AI/EverMe/issues"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org"
39
+ },
40
+ "dependencies": {
41
+ "@everme/agent-sdk": "^0.4.0"
42
+ }
43
+ }
package/src/adapter.js ADDED
@@ -0,0 +1,41 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { readLastTurn } from "./transcript.js";
4
+
5
+ const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit"]);
6
+
7
+ export const codexAdapter = {
8
+ platform: "codex",
9
+
10
+ envFile() {
11
+ return process.env.EVERME_ENV_FILE_PATH || path.join(os.homedir(), ".codex", "everme.env");
12
+ },
13
+
14
+ normalizeInput(rawInput) {
15
+ return {
16
+ // No session_id → empty (writes are skipped downstream), like
17
+ // cursor/devin: a constant fallback would merge unrelated sessions
18
+ // into one conversation on the backend.
19
+ sessionId: rawInput?.session_id || "",
20
+ transcriptPath: rawInput?.transcript_path || "",
21
+ cwd: rawInput?.cwd || "",
22
+ prompt: rawInput?.prompt || "",
23
+ turnId: rawInput?.turn_id || "",
24
+ source: rawInput?.source || "",
25
+ };
26
+ },
27
+
28
+ readLastTurn(input) {
29
+ return readLastTurn(input?.transcriptPath);
30
+ },
31
+
32
+ formatOutput(event, { block = "" } = {}) {
33
+ if (!CONTEXT_EVENTS.has(event) || !block) return {};
34
+ return {
35
+ hookSpecificOutput: {
36
+ hookEventName: event,
37
+ additionalContext: block,
38
+ },
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,108 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ import { capRunes } from "@everme/agent-sdk";
4
+
5
+ export async function readLastTurn(transcriptPath) {
6
+ if (!transcriptPath) return [];
7
+ const lines = createInterface({
8
+ input: createReadStream(transcriptPath, { encoding: "utf8" }),
9
+ crlfDelay: Infinity,
10
+ });
11
+ let delta = [];
12
+ let foundUser = false;
13
+
14
+ for await (const line of lines) {
15
+ let event;
16
+ try {
17
+ event = JSON.parse(line);
18
+ } catch {
19
+ continue;
20
+ }
21
+ if (event?.type !== "response_item" || !event.payload) continue;
22
+ const message = mapPayload(event.payload, event.timestamp);
23
+ if (!message) continue;
24
+
25
+ if (message.role === "user") {
26
+ delta = [message];
27
+ foundUser = true;
28
+ } else if (foundUser) {
29
+ delta.push(message);
30
+ }
31
+ }
32
+ return foundUser ? delta : [];
33
+ }
34
+
35
+ function mapPayload(payload, timestampValue) {
36
+ const timestamp = normalizeTimestamp(timestampValue);
37
+ if (payload.type === "message") {
38
+ if (payload.role !== "user" && payload.role !== "assistant") return null;
39
+ const content = contentText(payload.content);
40
+ if (!content) return null;
41
+ return { role: payload.role, ...stamp(timestamp), content };
42
+ }
43
+ if (payload.type === "function_call") {
44
+ return {
45
+ role: "assistant",
46
+ ...stamp(timestamp),
47
+ toolCalls: [{
48
+ id: payload.call_id || `codex_tool_${timestamp ?? "untimed"}`,
49
+ type: "function",
50
+ name: payload.name || "unknown",
51
+ arguments: argumentText(payload.arguments),
52
+ }],
53
+ };
54
+ }
55
+ if (payload.type === "function_call_output" && payload.call_id) {
56
+ return {
57
+ role: "tool",
58
+ ...stamp(timestamp),
59
+ toolCallId: payload.call_id,
60
+ content: capText(payload.output || "tool result"),
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function stamp(timestamp) {
67
+ return timestamp === undefined ? {} : { timestamp };
68
+ }
69
+
70
+ function contentText(content) {
71
+ if (typeof content === "string") return capText(content);
72
+ if (!Array.isArray(content)) return "";
73
+ const parts = [];
74
+ for (const item of content) {
75
+ if (typeof item === "string") {
76
+ parts.push(item);
77
+ } else if (["input_text", "output_text", "text"].includes(item?.type) && typeof item.text === "string") {
78
+ parts.push(item.text);
79
+ }
80
+ }
81
+ return capText(parts.join("\n"));
82
+ }
83
+
84
+ function argumentText(value) {
85
+ if (typeof value === "string") return value;
86
+ try {
87
+ return JSON.stringify(value ?? {});
88
+ } catch {
89
+ return "{}";
90
+ }
91
+ }
92
+
93
+ function normalizeTimestamp(value) {
94
+ if (typeof value === "number" && Number.isFinite(value)) {
95
+ return value > 10_000_000_000 ? Math.trunc(value) : Math.trunc(value * 1000);
96
+ }
97
+ const parsed = Date.parse(value);
98
+ // undefined — never 0: 0 is a finite epoch (1970) that the SDK would ship
99
+ // as-is, while a missing timestamp makes the SDK stamp Date.now() instead,
100
+ // keeping the epoch-ms wire contract honest.
101
+ return Number.isFinite(parsed) ? parsed : undefined;
102
+ }
103
+
104
+ // SDK capRunes keeps head AND tail (0.7 head ratio) so the end of a long
105
+ // tool output — exit status, root-cause line — survives truncation.
106
+ function capText(value) {
107
+ return capRunes(String(value || "").trim());
108
+ }