# ported

**Pack what your AI assistant knows about you into one file you own, and carry it to the next one.**

Switching assistants means starting over: your name, how you like answers written, what you are working on, the
things you asked it never to do. `ported` turns what you already have (a memory list, a markdown notes file or a chat
transcript) into one sealed **port file**, and turns that file into a block you can paste into whichever assistant
you use next.

Before anything moves, it **finds and scrubs secrets**: e-mail addresses, phone numbers, card numbers (Luhn-checked),
IBANs (checksum-checked), API keys, private keys, passwords and wallet recovery phrases.

Zero dependencies. One file. Node 18+ and the browser.

```bash
npm test                                   # 12 tests
node bin/ported.js pack examples/chat.txt -o ana.port.json
node bin/ported.js render ana.port.json    # paste this into your next assistant
node examples/move.js                      # pack two sources, merge them, settle a conflict
```

## What it reads

| Input | How it is read |
|---|---|
| A memory list (one item per line, bullets or not) | every line is a memory, sorted into a kind |
| Markdown notes | lines are memories; headings like "About me", "Style" or "Projects" set the kind |
| A chat transcript (`User:` / `Assistant:` lines) | only your own turns; keeps lasting statements, skips questions and things said in passing ("I'm so tired today") |
| A JSON transcript (`[{ role, content }]` or `{ messages: [...] }`) | same as above; content arrays are flattened |
| A port file | read back in as it is |

Every memory gets one of five kinds, which decide the order and what is kept first when space runs out:
**instruction** ("always use metric units"), **identity** ("I live in Lisbon"), **preference** ("I prefer short
answers"), **project** ("I am learning Japanese") and **fact** (anything else).

## The port file

This is `examples/chat.txt` packed, with two of its nine memories shown:

```json
{
  "format": "ported/1",
  "tool": "ported 0.1.0",
  "created": "2026-10-09T12:00:00.000Z",
  "name": "Ana",
  "instructions": "",
  "memories": [
    { "id": "47c30486d7", "kind": "instruction", "text": "Please always use metric units.", "from": "turn 3" },
    { "id": "871e547307", "kind": "identity", "text": "My work email is [email removed], keep it handy.", "from": "turn 7" }
  ],
  "counts": { "instruction": 2, "identity": 4, "preference": 2, "project": 1, "fact": 0 },
  "chars": 294,
  "seal": "sha256:0e27fcc6be4a…"
}
```

The seal is a SHA-256 of the file's canonical JSON. `verify()` tells you whether anything was changed after packing.

## Use

```js
const P = require("ported");                 // or <script src="ported.js"> → window.Ported

const { port, report } = P.pack({ text: chatText, name: "Ana", secrets: "mask", budget: 4000 });
report.secrets;      // which lines held secrets, and what kind
report.conflicts;    // "I live in Lisbon" vs "I live in Porto"
report.overBudget;   // what did not fit
P.render(port, "prompt");   // or "markdown", "list", "json"
P.verify(port);             // true until someone edits it
```

| Function | What it does |
|---|---|
| `parse(text)` | detects the format and returns `{ format, items: [{ text, kind, from }] }` |
| `scan(text)` / `redact(text)` | find secrets / replace them with `[email removed]` and the like |
| `pack(opts)` | `{ text or items, name, instructions, secrets: "mask" \| "drop" \| "keep", budget, now }` → `{ port, report }` |
| `merge(a, b, opts)` | folds duplicates, flags conflicts; `conflicts: "prefer-a"` or `"prefer-b"` settles them |
| `fit(items, chars)` | keeps instructions first, then identity, preferences, projects, facts |
| `dedupe(items)` / `conflicts(items)` / `similar(a, b)` | the pieces `pack` and `merge` use |
| `render(port, as)` | `prompt` (ready to paste), `markdown`, `list` or `json` |
| `seal(port)` / `verify(port)` / `sha256(text)` | integrity |

### Secrets

`secrets: "mask"` (the default) keeps the memory and removes the secret. `"drop"` leaves the whole line behind.
`"keep"` keeps it and marks the memory `sensitive: true`, for when you really mean to move it. Card numbers and
IBANs only count when their checksums pass, so an order number does not get flagged as a card.

### Conflicts

Two memories that say different things about the same slot (name, home, work, language, timezone, a favourite, a
preference "X over Y") are reported as a conflict instead of being folded together as duplicates:
"I prefer tea over coffee" and "I prefer coffee over tea" share every word, and are not the same memory.

## CLI

```
ported pack <file> [--name N] [--secrets mask|drop|keep] [--budget N] [-o out.port.json]
ported scan <file>                          exit code 2 when secrets are found
ported render <port.json> [--as prompt|markdown|list|json]
ported merge <a.port.json> <b.port.json> [--prefer a|b] [-o out.port.json]
ported verify <port.json>
```

## Honest limits

- Reading transcripts is a heuristic. It keeps lasting first-person statements and will miss some, or keep one it
  should not. Look at the report before you move anything.
- It does not log in anywhere or talk to any assistant. You export your data from one, and paste the rendered block
  (or the port file) into the next.
- Secret detection covers common formats, not every format in the world. Read what you paste.
- The seal shows tampering. It does not hide anything: a port file is plain JSON.

MIT licence.
