oxtail 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.
@@ -0,0 +1,119 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ function clamp(n, lo, hi) {
3
+ return Math.max(lo, Math.min(hi, n));
4
+ }
5
+ function extractTextFromClaudeContent(content) {
6
+ if (typeof content === "string")
7
+ return content;
8
+ if (!Array.isArray(content))
9
+ return "";
10
+ const parts = [];
11
+ for (const block of content) {
12
+ if (!block || typeof block !== "object")
13
+ continue;
14
+ const b = block;
15
+ if (b.type === "text" && typeof b.text === "string") {
16
+ parts.push(b.text);
17
+ }
18
+ }
19
+ return parts.join("\n");
20
+ }
21
+ export function readClaudeTranscript(path, limit = 100) {
22
+ if (!existsSync(path)) {
23
+ return { messages: [], truncated: false, total_messages: 0 };
24
+ }
25
+ const raw = readFileSync(path, "utf8");
26
+ const messages = [];
27
+ for (const line of raw.split("\n")) {
28
+ if (!line)
29
+ continue;
30
+ let obj;
31
+ try {
32
+ obj = JSON.parse(line);
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ if (obj.type !== "user" && obj.type !== "assistant")
38
+ continue;
39
+ const role = obj.message?.role;
40
+ if (role !== "user" && role !== "assistant")
41
+ continue;
42
+ const text = extractTextFromClaudeContent(obj.message?.content);
43
+ if (!text)
44
+ continue;
45
+ messages.push({ role, text, timestamp: obj.timestamp ?? null });
46
+ }
47
+ const safeLimit = clamp(limit, 1, 1000);
48
+ const truncated = messages.length > safeLimit;
49
+ const tail = truncated ? messages.slice(-safeLimit) : messages;
50
+ return { messages: tail, truncated, total_messages: messages.length };
51
+ }
52
+ // Codex CLI injects two kinds of blocks into the first user message of a
53
+ // rollout that look identical to user input at the role/type level:
54
+ // 1. The AGENTS.md preamble, prefixed with the literal "# AGENTS.md
55
+ // instructions for " and wrapped in <INSTRUCTIONS>...</INSTRUCTIONS>.
56
+ // 2. An <environment_context>...</environment_context> block.
57
+ // Both prefixes are emitted by Codex itself, not typed by the user, so we
58
+ // drop them at the block level — preserving any other blocks in the same
59
+ // message in the unlikely case a real user authored mixed content.
60
+ export function isCodexInjectedBlock(text) {
61
+ const t = text.trimStart();
62
+ if (t.startsWith("# AGENTS.md instructions for "))
63
+ return true;
64
+ const trimmed = text.trim();
65
+ if (trimmed.startsWith("<environment_context>") && trimmed.endsWith("</environment_context>")) {
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ function extractTextFromCodexContent(content) {
71
+ if (!Array.isArray(content))
72
+ return "";
73
+ const parts = [];
74
+ for (const block of content) {
75
+ if (!block || typeof block !== "object")
76
+ continue;
77
+ const b = block;
78
+ if ((b.type === "input_text" || b.type === "output_text") && typeof b.text === "string") {
79
+ if (isCodexInjectedBlock(b.text))
80
+ continue;
81
+ parts.push(b.text);
82
+ }
83
+ }
84
+ return parts.join("\n");
85
+ }
86
+ export function readCodexTranscript(path, limit = 100) {
87
+ if (!existsSync(path)) {
88
+ return { messages: [], truncated: false, total_messages: 0 };
89
+ }
90
+ const raw = readFileSync(path, "utf8");
91
+ const messages = [];
92
+ for (const line of raw.split("\n")) {
93
+ if (!line)
94
+ continue;
95
+ let obj;
96
+ try {
97
+ obj = JSON.parse(line);
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ if (obj.type !== "response_item")
103
+ continue;
104
+ const p = obj.payload;
105
+ if (!p || p.type !== "message")
106
+ continue;
107
+ const role = p.role;
108
+ if (role !== "user" && role !== "assistant")
109
+ continue;
110
+ const text = extractTextFromCodexContent(p.content);
111
+ if (!text)
112
+ continue;
113
+ messages.push({ role, text, timestamp: obj.timestamp ?? null });
114
+ }
115
+ const safeLimit = clamp(limit, 1, 1000);
116
+ const truncated = messages.length > safeLimit;
117
+ const tail = truncated ? messages.slice(-safeLimit) : messages;
118
+ return { messages: tail, truncated, total_messages: messages.length };
119
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: oxtail-register
3
+ description: Register the current Codex agent session with the oxtail MCP peer registry. Use when the user asks to join oxtail, register with oxtail, run oxtail-register, fix oxtail client_session_id detection, or make this Codex session visible/readable to peer agents in the same project.
4
+ ---
5
+
6
+ # Oxtail Register
7
+
8
+ Register this Codex session with oxtail quickly and verify the result.
9
+
10
+ ## Communication Contract
11
+
12
+ This workflow is meant to be lightweight. For routine success, do not narrate
13
+ each internal step or paste diagnostic JSON. If a skill announcement is required,
14
+ keep it to one short sentence. The final response should be only:
15
+
16
+ ```text
17
+ Registered: <session_id>
18
+ Transcript: <transcript_path>
19
+ ```
20
+
21
+ If the session was already registered, use:
22
+
23
+ ```text
24
+ Already registered: <session_id>
25
+ Transcript: <transcript_path>
26
+ ```
27
+
28
+ Only explain the detection strategy, environment variables, or fallback behavior
29
+ when registration fails or the user explicitly asks.
30
+
31
+ ## Workflow
32
+
33
+ 1. Call `mcp__oxtail__get_my_session`.
34
+ 2. If `entry.client.session_id` is already non-null, report the compact "Already registered" result and stop.
35
+ 3. Run this shell command in the current project:
36
+
37
+ ```sh
38
+ printf '%s\n' "${CODEX_THREAD_ID:-$CODEX_COMPANION_SESSION_ID}"
39
+ ```
40
+
41
+ 4. If the command returns an empty string, do not scan transcripts unless the user asks. Report that Codex exposed neither `CODEX_THREAD_ID` nor `CODEX_COMPANION_SESSION_ID`.
42
+ 5. Call `mcp__oxtail__claim_session` with `{ "session_id": "<id from step 3>" }`. The response contains `session_id` and `transcript_path` directly — no extra verification call needed. Report the compact "Registered" result.
43
+
44
+ Prefer `CODEX_THREAD_ID`; `CODEX_COMPANION_SESSION_ID` is only a compatibility fallback.
@@ -0,0 +1,10 @@
1
+ interface:
2
+ display_name: "Oxtail Register"
3
+ short_description: "Register Codex with oxtail peers"
4
+ default_prompt: "Use $oxtail-register to register this Codex session with the oxtail peer registry."
5
+
6
+ dependencies:
7
+ tools:
8
+ - type: "mcp"
9
+ value: "oxtail"
10
+ description: "Local oxtail MCP server exposing session registration tools"
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "oxtail",
3
+ "version": "0.4.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Coordination layer for parallel AI coding agent sessions, exposed over MCP.",
7
+ "main": "dist/server.js",
8
+ "bin": {
9
+ "oxtail": "dist/server.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "integrations",
14
+ ".claude/commands/oxtail-join.md",
15
+ "AGENTS.md",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsx watch src/server.ts",
22
+ "start": "node dist/server.js",
23
+ "test": "node --import tsx --test 'src/**/*.test.ts'",
24
+ "prepack": "rm -rf dist && npm run build",
25
+ "prepublishOnly": "npm test"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/d4j3y2k/oxtail.git"
30
+ },
31
+ "homepage": "https://github.com/d4j3y2k/oxtail#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/d4j3y2k/oxtail/issues"
34
+ },
35
+ "author": "David Kim <d4j3y2k@gmail.com>",
36
+ "license": "MIT",
37
+ "keywords": [
38
+ "mcp",
39
+ "claude-code",
40
+ "codex",
41
+ "tmux",
42
+ "agents",
43
+ "model-context-protocol"
44
+ ],
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
+ "zod": "^4.4.3"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.10.0",
54
+ "tsx": "^4.21.0",
55
+ "typescript": "^5.7.0"
56
+ }
57
+ }