Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions codex-marketplace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ saved. Your next messages will use its domain profile and knowledge folder.
The saved context keeps the investigation approach, system knowledge, findings,
and verified resolution—not the raw conversation.

This thread had nothing connected, so saving also connected it — the work it
just wrote up is the work it is still doing. A thread that already has a context
connected keeps it, even when you save under a new name; use `$neatcontext:use`
when you actually want to switch.
Nothing was connected, so saving also connected what it just wrote — the work it
wrote up is the work it is still doing. When a context is already connected it
stays connected, even if you save under a new name; use `$neatcontext:use` when
you actually want to switch.

After more work on the same subject, invoke `$neatcontext:save` again. With no
name it updates the context this thread is now connected to, previewing the
merged result and asking before applying it.
name it updates the connected context, previewing the merged result and asking
before applying it.

When a similar issue appears later, connect the saved context in a new Codex
thread with `$neatcontext:use`. NeatContext can also route you to the right
Expand Down Expand Up @@ -125,23 +125,26 @@ later.

### `$neatcontext:use [name or number]`

Connect a context to the current Codex thread.
Connect a context to Codex.

Invoke the skill without a name to see the available choices. Each Codex thread
keeps its own connected context.
Invoke the skill without a name to see the available choices. One context is
connected at a time, and every part of the plugin sees the same one — the
skills you invoke, the routing the model does for itself, and the grounding it
loads. Codex does not tell the plugin's MCP server which thread it is serving,
so the connection is shared by the Codex threads and windows on this machine
rather than kept per thread; switch it whenever the subject changes.

### `$neatcontext:disconnect`

Disconnect the context from the current Codex thread. Other threads keep their
own connections, and the context itself is not deleted.
Disconnect the connected context. The context itself is not deleted.

### `$neatcontext:list`

List all contexts you can connect.

### `$neatcontext:status`

Show the context connected to the current thread and the current routing mode.
Show the connected context and the current routing mode.
It also reports problems such as missing context files or knowledge
folders.

Expand Down Expand Up @@ -206,15 +209,16 @@ is deleted with it.

### `$neatcontext:mode [auto|ask|manual]`

Choose how the current thread switches between contexts:
Choose how Codex switches between contexts:

- `auto` — switch on a clear match and tell you; ask when the choice is unclear;
this is the default
- `ask` — ask before every switch, clear match or not
- `manual` — switch only when you invoke `$neatcontext:use`

Invoke `$neatcontext:mode` without an argument to show the current mode. Add
`--global` to set the default for new threads:
Invoke `$neatcontext:mode` without an argument to show the current mode. Like
the connected context, the mode is shared by the Codex sessions on this
machine. `--global` sets the same default for every host that uses NeatContext:

```text
$neatcontext:mode auto --global
Expand Down
3 changes: 0 additions & 3 deletions codex-marketplace/plugins/neatcontext/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
"args": [
"./src/codex/mcp-bridge.mjs"
],
"env_vars": [
"CODEX_THREAD_ID"
],
"cwd": "."
}
}
Expand Down
44 changes: 20 additions & 24 deletions codex-marketplace/plugins/neatcontext/hooks/session-start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
// Profiles and knowledge are deliberately not injected here; get_context loads
// only the selected context after routing has chosen it.
//
// This hook is also the only moment anything in the plugin learns that `/new`
// happened. Codex starts a new thread inside the same process and does not
// restart the MCP server, so the bridge's `CODEX_THREAD_ID` still names the
// thread that just ended. The id delivered on stdin here is the current one,
// so it is recorded for the long-lived bridge to re-read — see
// src/core/host-session.mjs.
// The thread id Codex delivers on stdin is deliberately not used to scope
// anything. It would scope this hook and the skill-run CLI to a thread the MCP
// bridge cannot name, and the menu printed here would then describe a selection
// the bridge is not serving. See src/codex/session.mjs for what Codex does and
// does not expose.

import { readSelection } from "../src/core/local-state.mjs";
import { configureSessionId } from "../src/core/session.mjs";
import { pruneHostPointers, writeHostPointer } from "../src/core/host-session.mjs";
import "../src/codex/session.mjs";
import { pruneHostPointers } from "../src/core/host-session.mjs";
import {
menuEntries,
readRouting,
renderMenu,
resolveMode
} from "../src/core/routing.mjs";
import { sessionId } from "../src/core/session.mjs";
import { listAllContexts } from "../src/core/selection.mjs";

async function readInput() {
Expand All @@ -28,42 +28,38 @@ async function readInput() {
return raw.trim().length > 0 ? JSON.parse(raw) : {};
}

const input = await readInput();
const threadId =
typeof input.session_id === "string" && input.session_id.trim().length > 0
? input.session_id.trim()
: process.env.CODEX_THREAD_ID;
// Read and discard: Codex writes the hook payload to stdin and this hook has
// nothing left to take from it, but a reader that never drains it leaves the
// host writing into a pipe nobody empties.
await readInput().catch(() => ({}));

configureSessionId(() => threadId);

// Tell the long-lived bridge which thread this host process is on now. Silent
// on failure: recording this must never delay or break the start of a thread.
// Startup is also the natural moment to sweep pointers whose host is gone.
await writeHostPointer(threadId, { source: "session-start" }).catch(() => undefined);
// Earlier versions of this plugin left one pointer file per host process behind.
// Nothing writes them now; sweeping the ones whose process is gone is what
// clears them off machines that ran those versions.
await pruneHostPointers().catch(() => undefined);

const [{ contexts }, state, selection] = await Promise.all([
listAllContexts(),
readRouting(),
readSelection().catch(() => null)
]);
const mode = resolveMode(state, threadId);
const mode = resolveMode(state, sessionId());
const selected = selection?.available === false ? null : selection;
const menu = renderMenu(menuEntries(contexts, state), {
connectedId: selected?.contextId ?? null,
mode
});

const groundingGuidance = selected
? `The "${selected.contextName}" context is selected for this thread. For a request in its scope, call \`get_context\` only if its result is not already present since the latest context switch or compaction; otherwise reuse the existing result. Do not call \`get_context\` merely to check connection status.`
? `The "${selected.contextName}" context is connected. For a request in its scope, call \`get_context\` only if its result is not already present since the latest context switch or compaction; otherwise reuse the existing result. Do not call \`get_context\` merely to check connection status.`
: contexts.length > 0
? "No NeatContext context is selected for this thread. Do not call `get_context` to check connection status. Follow the routing menu, and load grounding only after `use_context` succeeds."
? "No NeatContext context is connected. Do not call `get_context` to check connection status. Follow the routing menu, and load grounding only after `use_context` succeeds."
: "No NeatContext contexts are currently available. Do not call `get_context`. Continue normal work without NeatContext grounding unless the user asks to create or import a context.";

const guidance = [
"NeatContext is installed for this Codex thread.",
"NeatContext is installed for this Codex session.",
groundingGuidance,
"Connect or switch contexts inside this thread with `use_context` or the explicit `$neatcontext:use` skill. Disconnect the current context with `$neatcontext:disconnect`. There is no Desktop connection right now.",
"Connect or switch contexts from here with `use_context` or the explicit `$neatcontext:use` skill. Disconnect the current context with `$neatcontext:disconnect`. There is no Desktop connection right now.",
menu,
"Use `$neatcontext:save` to preserve durable work from the visible conversation. Never parse Codex transcript files for that workflow."
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: disconnect
description: Disconnect the NeatContext Context from the current Codex thread. Use when the user explicitly invokes this skill or asks to disconnect, detach, clear, or stop using the connected context.
description: Disconnect the connected NeatContext Context from Codex. Use when the user explicitly invokes this skill or asks to disconnect, detach, clear, or stop using the connected context.
---

# Disconnect context
Expand All @@ -12,5 +12,4 @@ file. Run:
node "<plugin-root>/src/codex/neatcontext-cli.mjs" disconnect
```

Relay the output verbatim. Do not run a redundant status check. The command
affects only the current Codex thread.
Relay the output verbatim. Do not run a redundant status check.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
interface:
display_name: "Disconnect Context"
short_description: "Disconnect this Codex thread from its context"
default_prompt: "Use $neatcontext:disconnect to disconnect the context from this thread."
short_description: "Disconnect the connected context"
default_prompt: "Use $neatcontext:disconnect to disconnect the connected context."

policy:
allow_implicit_invocation: false
2 changes: 1 addition & 1 deletion codex-marketplace/plugins/neatcontext/skills/list/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: list
description: List the local NeatContext Contexts available to the current Codex thread. Use when the user asks what contexts exist, what can be connected, or explicitly invokes this skill.
description: List the local NeatContext Contexts available to Codex. Use when the user asks what contexts exist, what can be connected, or explicitly invokes this skill.
---

# List contexts
Expand Down
2 changes: 1 addition & 1 deletion codex-marketplace/plugins/neatcontext/skills/mode/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: mode
description: Show or set NeatContext routing to auto, ask, or manual for the current Codex thread, with an optional global default for new threads. Use only when the user explicitly invokes this skill or clearly asks to change routing behavior.
description: Show or set NeatContext routing to auto, ask, or manual for Codex, with an optional default shared with every other NeatContext host. Use only when the user explicitly invokes this skill or clearly asks to change routing behavior.
---

# Routing mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: status
description: Report the NeatContext context and routing mode active in the current Codex thread, including missing-file or stale-routing warnings. Use when the user asks which context is connected or explicitly invokes this skill.
description: Report the NeatContext context and routing mode active in Codex, including missing-file or stale-routing warnings. Use when the user asks which context is connected or explicitly invokes this skill.
---

# Context status
Expand Down
2 changes: 1 addition & 1 deletion codex-marketplace/plugins/neatcontext/skills/use/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: use
description: Connect or switch this Codex thread to a local NeatContext Context by name or list number. Use only when the user explicitly invokes this skill, names a context to connect, or agrees to a routing suggestion.
description: Connect or switch Codex to a local NeatContext Context by name or list number. Use only when the user explicitly invokes this skill, names a context to connect, or agrees to a routing suggestion.
---

# Use context
Expand Down
47 changes: 17 additions & 30 deletions codex-marketplace/plugins/neatcontext/src/codex/mcp-bridge.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// NeatContext plugin MCP server for Codex.
//
// Codex starts this process with an environment that carries no thread id and
// no way to derive one, so the selection it reads is the one every other part
// of the plugin reads — see src/codex/session.mjs. Nothing here tries to
// discover which thread it is serving: a bridge that scoped itself to a thread
// would answer from a file the skills cannot see.
//
// Behaviors kept from the Claude Code bridge:
// * this process outlives the thread it was spawned in — /new starts a new
// one without restarting it — so the host session is re-resolved before
// every message rather than read once from the environment. Without that,
// the bridge goes on serving the pre-/new thread's context while the
// SessionStart hook and the skills write the new one's.
// * initialize advertises tools.listChanged, and we poll the selected
// context so the host refreshes its tool list when the user runs
// $neatcontext:use (or the session routes itself).
Expand All @@ -15,7 +16,7 @@
// get_context instead of silently vanishing.

import readline from "node:readline";
import { publishSessionId, refreshSessionId } from "./session.mjs";
import "./session.mjs";
import { readSelection } from "../core/local-state.mjs";
import {
CONTEXT_MISSING_MESSAGE,
Expand Down Expand Up @@ -47,8 +48,8 @@ const GET_CONTEXT_TOOL = {
title: "Get Context",
description:
"Load the domain profile and local knowledge pointers for the NeatContext Context " +
"already selected for this thread. Do not call merely to discover whether a Context " +
"is selected.",
"already connected in this Codex session. Do not call merely to discover whether a " +
"Context is connected.",
inputSchema: {
type: "object",
properties: {
Expand Down Expand Up @@ -80,7 +81,7 @@ const GET_CONTEXT_TOOL = {
// are both locked. `$neatcontext:save` is the one that always opens: it builds
// the first context out of the conversation already happening. So it leads when
// there is nothing to connect.
const NOTHING_CONNECTED_HEAD = "No NeatContext Context is selected for this thread.";
const NOTHING_CONNECTED_HEAD = "No NeatContext Context is connected.";

// The manual-mode version, and the fallback whenever no menu follows. Routing
// is off here, so a command the user types is genuinely the only way forward.
Expand Down Expand Up @@ -189,7 +190,7 @@ const ROUTING_TOOLS = new Map([
// get_context instead, which is re-read on every call and refreshed live by
// tools/list_changed. These instructions do one job: get get_context called at
// the right moments.
const CONTEXT_INSTRUCTIONS = `This Codex thread has a selected NeatContext Context: one domain profile and local knowledge stored on this machine.
const CONTEXT_INSTRUCTIONS = `This Codex session has a connected NeatContext Context: one domain profile and local knowledge stored on this machine.

For a request in that Context's scope, call get_context only when its current result is not already present since the latest context switch or compaction; otherwise reuse the existing result. Never call get_context merely to check connection status. Read the profile in full when grounding is loaded.

Expand All @@ -202,7 +203,7 @@ Cite the exact file path of anything you rely on. When the profile and the knowl
// session or from another window on the same workspace. So this must never
// state "nothing is connected" as a settled fact; it defers the current state
// to get_context, which is the only thing that stays true.
const NO_CONTEXT_INSTRUCTIONS = `No NeatContext Context was selected when this thread started. A Context can be selected later with use_context.
const NO_CONTEXT_INSTRUCTIONS = `No NeatContext Context was connected when this session started. A Context can be connected later with use_context.

Do not call get_context merely to check connection status. Continue normal work without NeatContext grounding until use_context succeeds or the user explicitly asks to refresh NeatContext state.

Expand Down Expand Up @@ -514,17 +515,10 @@ let lastVersion = undefined;
// What the host's tool list depends on. Switching between contexts has to
// change this; so does the routing mode, because leaving manual has to make the
// routing tools appear without waiting for a restart.
// Re-resolve which thread this process is serving, and publish the answer so a
// skill-run command can tell whether its write is the one this bridge will read.
async function syncSession() {
await refreshSessionId();
await publishSessionId();
}

async function currentVersion() {
// The session is part of it: `/new` changes what this process is grounded in
// without changing anything the selection or the mode can report, and the
// host has to be told to drop the previous thread's extension tools.
// The session is part of it for hosts that scope by one. Codex does not give
// this process a session to scope by, so here it is a constant and the
// selection below is what moves.
const session = sessionId() ?? "none";
const mode = resolveMode(await readRouting(), sessionId());
const context = await activeContext();
Expand All @@ -539,11 +533,6 @@ async function currentVersion() {

async function handleMessage(message) {
const isNotification = message.id === undefined || message.id === null;
// Before anything reads a selection or a routing mode: which thread this
// host is on may have changed since the last message, and every one of those
// is per session.
await syncSession();

// Routing tools decide which context serves the session next, so they are
// answered before that choice is read.
if (message.method === "tools/call" && ROUTING_TOOLS.has(message.params?.name)) {
Expand Down Expand Up @@ -655,10 +644,8 @@ function startVersionWatch() {
watching = true;
setInterval(async () => {
if (!started) return;
// The host does not send a message when the user runs `/new`, so this tick
// is where a thread change is noticed if nothing else asks first — and
// where the published answer stays fresh enough to be checked against.
await syncSession();
// The host does not send a message when a skill connects a context, so this
// tick is where that is noticed if nothing else asks first.
const version = await currentVersion();
if (version !== null && version !== lastVersion) {
lastVersion = version;
Expand Down
Loading