@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 +27 -0
- package/bin/hook.js +31 -0
- package/package.json +43 -0
- package/src/adapter.js +41 -0
- package/src/transcript.js +108 -0
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
|
+
}
|