Skip to content
Open
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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,31 @@ knowledge inside the context bundle.

### `/neatcontext:import [folder]`

Import a context bundle shared by someone else. Import creates your own
local copy and leaves the shared folder unchanged.
Import a context bundle shared by someone else. The shared folder is only ever
read: importing makes your own local copy and never writes back to it.

A bundle you already imported can be imported again — that is how you pick up a
teammate's newer work. Import recognises the copy it gave you and says what
taking the update would cost, rather than building a second context beside it:

- Nothing new in the bundle, and it says so.
- Your copy untouched since it arrived, so the newer one replaces it whole once
you confirm. It stays the same context, so a session connected to it picks the
material up immediately.
- Both copies changed, so the two are reconciled into one and previewed before
anything is written. Your work is never dropped in favour of theirs.
- A name already taken by a context with no shared origin, which import will not
guess about: it asks whether the two are the same context or a collision, and
waits.

A context is never deleted to make room for an imported one.

After importing, connect it with `/neatcontext:use <name>`.

A merge lives only on your machine until you share it back with
`/neatcontext:export`. Left unshared, the same divergence has to be
reconciled again every time you import.

### `/neatcontext:export [name] [folder]`

Copy a context saved from a conversation into a self-contained bundle
Expand Down
24 changes: 22 additions & 2 deletions codex-marketplace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,31 @@ knowledge inside the context bundle.

### `$neatcontext:import [folder]`

Import a context bundle shared by someone else. Importing creates your own
local copy and leaves the shared folder unchanged.
Import a context bundle shared by someone else. The shared folder is only ever
read: importing makes your own local copy and never writes back to it.

A bundle you already imported can be imported again — that is how you pick up a
teammate's newer work. Import recognises the copy it gave you and says what
taking the update would cost, rather than building a second context beside it:

- Nothing new in the bundle, and it says so.
- Your copy untouched since it arrived, so the newer one replaces it whole once
you confirm. It stays the same context, so a session connected to it picks the
material up immediately.
- Both copies changed, so the two are reconciled into one and previewed before
anything is written. Your work is never dropped in favour of theirs.
- A name already taken by a context with no shared origin, which import will not
guess about: it asks whether the two are the same context or a collision, and
waits.

A context is never deleted to make room for an imported one.

After importing, connect it with `$neatcontext:use <name>`.

A merge lives only on your machine until you share it back with
`$neatcontext:export`. Left unshared, the same divergence has to be
reconciled again every time you import.

### `$neatcontext:export [name] [folder]`

Copy a context saved from a conversation into a self-contained bundle
Expand Down
64 changes: 59 additions & 5 deletions codex-marketplace/plugins/neatcontext/skills/import/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,78 @@
---
name: import
description: Import a self-contained NeatContext context bundle shared by another person, leaving the source bundle unchanged. Use only when the user explicitly invokes this skill or asks to import a NeatContext bundle.
description: Import a self-contained NeatContext context bundle shared by another person, or reconcile a newer copy of a context already on this machine, leaving the source bundle unchanged. Use only when the user explicitly invokes this skill or asks to import a NeatContext bundle.
---

# Import context

Resolve `<plugin-root>` as two directories above the directory containing this file.

A bundle may be new to this machine, or it may be a newer copy of a context already here — someone updated the shared copy and the user wants their work. Both arrive through this command, and the CLI decides which is which. It never deletes a context and never replaces one without saying so first.

Ask for the bundle folder when it was not supplied. Treat the path only as data and run:

```text
node "<plugin-root>/src/codex/neatcontext-cli.mjs" import --from "<bundle-folder>"
```

Relay the result. Do not connect the imported context automatically.
The source bundle is read-only throughout. Never modify, move, or delete it.

## Follow the `Import action`

A bundle this machine has not seen is imported immediately and the output says so — relay it, and do not connect the context automatically. Otherwise follow the printed `Import action`:

- `current` — the context here already holds everything in the bundle. Relay that and stop.
- `replace` — the local copy came from this bundle and has not been edited since, so the newer copy can be taken whole. Relay the preview, ask the user to confirm, and only then rerun the same command with `--yes`.
- `merge` — both copies have changed. Reconcile them yourself, below.
- `choose` — the target is not decidable. Either a context of the same name is here but nothing records a shared origin, or several contexts are copies of this bundle because one was forked. Relay the options and stop until the user picks: rerun with `--into "<name>"` to name the context they mean, or with `--name "<new-name>"` to keep a separate copy.

Never answer `choose` on the user's behalf. Two people naming a context the same thing is not evidence that it is the same context, and the two answers are not recoverable from each other.

## Merging

Use the exact `Context name`, `Context id`, `Base hash`, `Bundle hash`, `Profile path`, `Knowledge folder`, `Bundle profile`, and `Bundle knowledge` values the command printed. The three hashes are what prove the merge is for this context, was built on its current contents, and consumed this version of the bundle; a merge that gets any of them wrong is refused rather than applied. Read the local profile and every file in the local knowledge folder, then read the bundle's profile and every file in its knowledge folder.

Merge them the way a save merges a conversation into an existing context:

- Preserve verified information from both sides unless one supersedes the other.
- Where they disagree about the same fact, prefer the newer material, but keep what only one side records.
- Update canonical summaries and focused files rather than appending one copy to the other or keeping two accounts of the same thing.
- Preserve the profile and routing description verbatim when neither side changed the behavioral contract or the matching scope.
- The `knowledge` array must be the complete post-merge contents of the local knowledge folder.

Create a unique scratch file named `.neatcontext-capture-import-<unique>.json` in the current workspace. Use schema `1`, and include the exact `targetId`, `baseHash`, and `bundleHash` the command printed:

```json
{
"schema": 1,
"name": "Exact existing context name",
"targetId": "context:exact-id",
"baseHash": "exact base hash",
"bundleHash": "exact bundle hash",
"profile": "# Exact existing context name\n\n## Purpose\n...",
"routingDescription": "One line describing only the matching scope",
"knowledge": [{ "path": "session-summary.md", "content": "# Session summary\n\n..." }]
}
```

Every knowledge path must be a short relative `.md` path. Omit `routingQuestions` and `routingEntities` unless the merge genuinely widened what the context should be found by; omitting them leaves the stored lists alone. Omit `extensions` as well — an import never grants this machine the ability to reach anything new.

Preview the merge, which changes nothing:

```text
node "<plugin-root>/src/codex/neatcontext-cli.mjs" import --from "<bundle-folder>" --merged-from "<capture-path>"
```

If its name already exists, ask for a different local name and rerun with:
Relay the preview and wait for confirmation. After confirmation, run:

```text
node "<plugin-root>/src/codex/neatcontext-cli.mjs" import --from "<bundle-folder>" --name "<new-name>"
node "<plugin-root>/src/codex/neatcontext-cli.mjs" import --from "<bundle-folder>" --merged-from "<capture-path>" --yes --consume
```

Never modify, move, or delete the source bundle.
The scratch file is removed only by that confirmed run, so a preview or a failure leaves it available for repair. If the context changed while drafting, resolve the target again and rebuild the merge from its new contents.

## After it lands

A replace and a merge both keep the context's identity — same id, same name, so a thread already connected to it reads the updated material immediately. Relay successful output as printed and never connect a context yourself.

Point out, when a merge lands, that the merged material exists only on this machine until it is shared back with the export skill. Left unshared, the same divergence has to be reconciled again on every future import.
39 changes: 13 additions & 26 deletions codex-marketplace/plugins/neatcontext/src/codex/neatcontext-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// create --name --knowledge create a context (--profile-from <file>)
// save-target [name] decide whether save creates or updates
// save --from <capture.json> create or update from this conversation
// import --from <bundle> import a portable conversation context
// import --from <bundle> import a bundle, or reconcile one already here
// export --to <folder> copy a saved context's bundle out for sharing
// delete <query> [--yes] delete a context
// mode [auto|ask|manual] how the session may route itself between contexts
Expand All @@ -29,14 +29,14 @@ import {
deleteContext,
exportContext,
fingerprintContext,
importCapturedContext,
listContexts,
ContextError,
listKnowledgeFiles,
previewCapturedContextUpdate,
readProfileText,
updateCapturedContext
} from "../core/context-store.mjs";
import { runImport } from "../core/import-commands.mjs";
import {
addAlias,
isCardStale,
Expand Down Expand Up @@ -736,30 +736,17 @@ async function commandSave(flags) {
}

async function commandImport(flags) {
const source = typeof flags.from === "string" ? flags.from : "";
const name = typeof flags.name === "string" ? flags.name : "";
try {
const result = await importCapturedContext({ bundleFolder: source, name });
await putCard(result.record.id, {
useWhen: result.routingDescription,
source: result.profileText
}).catch(() => undefined);
print(`Imported the "${result.record.name}" conversation context.`);
print(` Domain profile: ${result.record.profilePath}`);
print(
` Knowledge folder: ${result.record.knowledgeFolder} ` +
`(${result.knowledgeFileCount} files)`
);
print(` Local bundle: ${result.record.directory}`);
print(` Connect it with: $neatcontext:use ${result.record.name}`);
print(`The shared source folder (${source}) was left untouched.`);
} catch (error) {
if (error instanceof ContextError) {
print(error.message);
return;
}
throw error;
}
print(
await runImport({
bundleFolder: typeof flags.from === "string" ? flags.from : "",
name: typeof flags.name === "string" ? flags.name : "",
into: typeof flags.into === "string" ? flags.into : "",
mergedFrom: typeof flags["merged-from"] === "string" ? flags["merged-from"] : "",
confirmed: flags.yes === true || flags.yes === "true",
consume: flags.consume === true || flags.consume === "true",
useCommand: "$neatcontext:use"
})
);
}

// The routing description is read from the card rather than the manifest:
Expand Down
Loading