Metadata-Version: 2.4
Name: filez4eva
Version: 5.1.0
Summary: Shift scans, photos, and other files into structured folders for permanent safekeeping
License: MIT
License-File: LICENSE
Author: Steampunk Wizard
Author-email: filez4eva@steamwiz.io
Requires-Python: >=3.14,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Requires-Dist: kwark (>=3.0.1,<4.0.0)
Requires-Dist: pyyaml (>=6.0,<7.0)
Requires-Dist: wizlib (>=4.0.8,<5.0.0)
Project-URL: Homepage, https://steamwiz.io
Project-URL: Repository, https://github.com/SteamWiz/Filez4Eva
Description-Content-Type: text/markdown

# Filez4Eva

⚠️ DISCLAIMER: This is a hobby/personal project. Not a commercial product. Not for production use.

## Shift scans, photos, and other files into structured folders for permanent safekeeping

Filez4Eva is a command-line tool that helps you organize files by naming them correctly and placing them in the right directory structure. It's especially useful for managing account documents, scanned files, and other personal records.

## Installation

It's best to install using `pipx`:

```bash
pipx install filez4eva
```

If you don't have pipx installed, visit [the pipx site](https://pypa.github.io/pipx/) for installation instructions.

## Usage

### Stowing Individual Files

To organize a single file (like a downloaded PDF):

```bash
filez4eva stow-file ~/Desktop/123456789SomeFileIDownloaded.pdf
```

Filez4Eva will interactively prompt for:
- Cabinet, only if several are configured (with tab-completion of cabinet
  names; see [Cabinets](#cabinets))
- Date in YYYYMMDD format
- Account name (with tab-completion from existing accounts)
- Part name (with tab-completion from existing files for that account)

Values can also be piped in as a YAML mapping on stdin, which is handy for
other tools that produce a record for the file:

```bash
printf 'date: 20240213\naccount: acct\npart: statement\n' \
  | filez4eva stow-file ~/Desktop/statement.pdf
```

- Recognised keys are `cabinet`, `date`, `account` and `part`; any other keys
  are ignored.
- Command-line flags override values from stdin.
- Anything still missing is prompted for as usual.
- `date` may be `20240213`, `'20240213'` or `2024-02-13`. If it isn't a valid
  date, `stow-file` exits with an error.
- If stdin is empty, isn't valid YAML, or isn't a mapping, it is ignored.
- Values are read as plain text, so `0123` stays `0123` and `yes` stays `yes`.
- `~` and `null` are treated as missing, so that value is prompted for.
- Stdin is only read by `stow-file` itself. `scan-dir` ignores it.

However the values are supplied (flags, stdin or prompts), `account` must be a
single directory name (not empty, `.` or `..`, and without `/` or `\`), and
`part` may contain only letters, digits and hyphens. An invalid value from a
flag or stdin makes `stow-file` exit with an error. An invalid value typed at
a prompt is rejected and the prompt asks again.

### Processing Multiple Files

To process all files in a directory:

```bash
filez4eva scan-dir ~/Desktop
```

This interactive process lets you:
- Skip a file (x)
- Preview a file (p)
- Stow a file (s)
- Delete a file (d)
- Quit processing (q)

Stowing a file works like [`process`](#processing-a-file-with-ai): the file
is transcribed (reusing an up-to-date transcript) and classified, then the
summary and doctype are shown and the `Date:`, `Account:` and `Part:` prompts
are pre-filled with the proposed values. Press Enter to accept a value, or
type a new one. With several cabinets, the cabinet is asked for once, before
anything is sent to the model.

- If transcription or classification fails (for example, no API key or an
  unsupported file type), the error is shown and the prompts start empty, as
  they would without AI.
- Empty input at any prompt returns to the action choice for the same file,
  without moving anything.

Transcripts written by `transcribe` are not offered as files of their own:
`X.md` is skipped when `X` is in the same directory (for example
`scan.pdf.md` next to `scan.pdf`). A `.md` file with no such companion is
offered as usual. Deleting a file also deletes its transcript. Stowing a file
moves its transcript with it (see [stow-file](#command-line-options)).

### Transcribing Files

To transcribe a file to Markdown using an AI model:

```bash
filez4eva transcribe ~/Desktop/scan.pdf
```

This writes `scan.pdf.md` next to the file (the full file name plus `.md`)
and prints its path on stdout. The transcript looks like:

```markdown
---
source: scan.pdf
sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b
model: claude-opus-4-6
transcribed: '2026-10-10T17:42:05+00:00'
---
<!-- Generated by an AI process from a binary file. ... -->

...Markdown transcription...
```

- Supported file types are PDF, PNG, JPEG, GIF, WebP, plain text, Markdown
  and CSV.
- If the transcript already exists and its `sha256` matches the file, nothing
  is sent to the model and the existing path is printed (status `Unchanged`
  on stderr). If the file has changed, or the transcript has no readable
  front matter, it is transcribed again and overwritten.
- `--force` (or `-f`) transcribes again even if the transcript is up to date.
- If transcription fails (e.g. an unsupported file type or an API error),
  `transcribe` exits with an error and any existing transcript is left as it
  was.

See [Transcription settings](#transcription-settings) for the model and API
key.

### Classifying Files

To have an AI model propose where a file should be filed:

```bash
filez4eva classify ~/Desktop/scan.pdf
```

`classify` transcribes the file first if needed (exactly as `transcribe`
does, reusing an up-to-date transcript), then asks the model to classify the
transcript and prints a YAML record on stdout:

```yaml
file: /Users/me/Desktop/scan.pdf
transcript: /Users/me/Desktop/scan.pdf.md
cabinet: accounts
date: '20261002'
account: amazon
part: receipt
doctype: receipt
summary: Amazon.ca order receipt for a USB hub
```

- `date` is the document's own date, as YYYYMMDD.
- `doctype` is one of the configured [doctypes](#classification-settings),
  or `other`.
- `summary` is a single line.
- The model is told the cabinet's `description`, the accounts already in the
  cabinet with the parts already used for each (the same ones offered by
  tab completion, so only with the default pattern), and asked to reuse them
  when they fit. It is also given the doctype names and descriptions.
- The cabinet is chosen as for `stow-file`: `--cabinet NAME`, the only
  cabinet, or a prompt.
- If transcription or classification fails, or the model's answer isn't a
  valid date, account, part or doctype, `classify` exits with an error and
  prints nothing on stdout.

Nothing is moved. To file the document as proposed, pipe the record into
`stow-file`, which reads `cabinet`, `date`, `account` and `part` and ignores
the other keys:

```bash
filez4eva classify ~/Desktop/scan.pdf | filez4eva stow-file ~/Desktop/scan.pdf
```

### Processing a File with AI

To transcribe, classify, confirm and stow a file in one go:

```bash
filez4eva process ~/Desktop/scan.pdf
```

`process`:
1. transcribes the file if needed (as `transcribe` does, reusing an
   up-to-date transcript)
2. classifies it (as `classify` does)
3. shows the summary and doctype, then prompts for `Cabinet:` (only if
   several cabinets are configured and it wasn't just chosen), `Date:`,
   `Account:` and `Part:`, each pre-filled with the proposed value. Press
   Enter to accept it, or type a new value (backspace clears the pre-filled
   value first). Tab completion works as for `stow-file`.
4. stows the file and its transcript, exactly as `stow-file` does, and prints
   the destination path on stdout

- Confirmation is always interactive.
- Empty input at any prompt cancels without moving anything. An invalid value
  is rejected and the prompt asks again.
- The cabinet is chosen as for `classify` (`--cabinet NAME`, the only
  cabinet, or a prompt) before anything is sent to the model, and the whole
  configuration is checked first.
- `--dry-run` transcribes, classifies and prompts as usual, then prints where
  the file (and its transcript) would go, without moving anything.

### Command Line Options

Global options:
- `--config PATH`: Specify path to config file
- `--debug`: Enable debug output

stow-file command:
- `--cabinet, -c NAME`: Specify the cabinet (destination) to stow into
- `--date, -d DATE`: Specify date in YYYYMMDD format
- `--account, -a ACCOUNT`: Specify account name
- `--part, -p PART`: Specify part name
- `file`: Path to the file to stow

After a successful move, `stow-file` prints the absolute destination path on
stdout (the `Done` status goes to stderr), so scripts can capture it, e.g.
`dest=$(filez4eva stow-file -d 20240213 -a acct -p part file.pdf)`.

If the file has a transcript (`FILE.md`, written by `transcribe`), `stow-file`
moves it too, named after the stowed file. By default it goes adjacent to the
stowed file: `.../20261002-receipt.pdf` gets `.../20261002-receipt.pdf.md`.
It can instead go into a parallel tree; see
[Transcript location](#transcript-location). If a transcript already exists at
its destination, `stow-file` fails before moving anything.

scan-dir command:
- `dir`: Optional path to directory to scan (defaults to configured source)

transcribe command:
- `--force, -f`: Transcribe again even if the transcript is up to date
- `file`: Path to the file to transcribe

classify command:
- `--cabinet, -c NAME`: Specify the cabinet to classify for
- `file`: Path to the file to classify

process command:
- `--cabinet, -c NAME`: Specify the cabinet to classify for and stow into
- `--dry-run`: Show where the file would go without moving anything
- `file`: Path to the file to process

## Configuration

Create a `filez4eva.yml` file with:

```yaml
filez4eva:
  source: '~/Desktop'          # Default source directory
  target: '~/Dropbox/accounts' # Target directory for stowed files
```

Files will be organized in this pattern:
```
~/Dropbox/accounts/<year>/<account>/<date>-<part>.<extension>
```

### Destination pattern

The layout under `target` can be changed with the optional `pattern` key:

```yaml
filez4eva:
  target: '~/Dropbox/accounts'
  pattern: '{account}/{year}/{part}-{date}{ext}'
```

The default is `'{year}/{account}/{date}-{part}{ext}'`, which gives the layout
shown above. Quote the pattern in YAML, since it starts with `{`.

Placeholders:
- `{year}`: four-digit year, e.g. `2024`
- `{date}`: date as YYYYMMDD, e.g. `20240213`
- `{account}`: account name
- `{part}`: part name
- `{ext}`: the source file's extension, including the dot (e.g. `.pdf`), or
  nothing if the file has no extension

Use `/` to create subdirectories; missing directories are created. Any other
text is used literally. A pattern is a configuration error, and `stow-file`
exits before moving anything or prompting, if it:
- uses a placeholder not listed above, or a positional one like `{}` or `{0}`
- uses format specs, conversions, attributes or indexes (e.g. `{year:>4}`,
  `{account!r}`, `{year.x}`, `{account[0]}`)
- has unbalanced braces
- is absolute, starts with `~`, or contains `..`

Tab completion of accounts and parts reads existing files in the default
layout, so it only works with the default pattern. With any other pattern the
prompts still accept typed values but offer no completion.

### Cabinets

To file into more than one destination, configure named cabinets:

```yaml
filez4eva:
  cabinets:
    accounts:
      target: ~/Dropbox/accounts
      pattern: '{year}/{account}/{date}-{part}{ext}'
      description: Personal account records
    memories:
      target: ~/Dropbox/memories
      pattern: '{account}/{year}/{part}-{date}{ext}'
```

Each cabinet has:
- `target` (required): the directory files are stowed under
- `pattern` (optional): the layout under the target, as described above.
  Defaults to the top-level `pattern` if set, otherwise
  `'{year}/{account}/{date}-{part}{ext}'`
- `description` (optional): a note on what the cabinet holds
- `transcripts` (optional): where transcripts of files stowed in this cabinet
  go, with `location` and `root` keys. Each key defaults to the top-level
  `transcripts` value; see [Transcript location](#transcript-location)

Choose the cabinet with `--cabinet NAME` (or `-c NAME`), or with a `cabinet`
key on stdin; the flag wins if both are given. If neither is given and there
is more than one cabinet, `stow-file` prompts for one, with tab-completion of
the names. With only one cabinet it is used without asking. An unknown name
from the flag or stdin is an error; an unknown name typed at the prompt is
rejected and the prompt asks again. `scan-dir` asks for a cabinet for each
file it stows (once per file, before classifying it).

Tab completion of accounts and parts reads the chosen cabinet's target.

If there is no `cabinets` section (or it is empty), the top-level `target` and
`pattern` form a single cabinet named `default`, so existing configurations
work unchanged. If `cabinets` is present, the top-level `target` is ignored.

The whole configuration is checked before any prompt: a cabinet with no
`target`, or with an invalid pattern or transcripts setting, is an error even
if it isn't the one being used.

### Transcript location

When `stow-file` stows a file that has a transcript, the transcript goes
next to the stowed file by default (`adjacent`). To keep transcripts in a
parallel tree instead:

```yaml
filez4eva:
  transcripts:
    location: parallel           # or 'adjacent' (the default)
    root: ~/Dropbox/transcripts
```

With `location: parallel`, the transcript goes to
`<root>/<cabinet>/<path under the cabinet target>.md`. For example, stowing
into the `accounts` cabinet at `2026/amazon/20261002-receipt.pdf` puts the
transcript at `~/Dropbox/transcripts/accounts/2026/amazon/20261002-receipt.pdf.md`.
Missing directories are created, and the root may be on another filesystem.
Without a `cabinets` section the single cabinet is named `default`, so
transcripts go under `<root>/default/`.

A cabinet can override either key with its own `transcripts` block; a key it
doesn't set falls back to the top-level value:

```yaml
filez4eva:
  transcripts:
    location: parallel
    root: ~/Dropbox/transcripts
  cabinets:
    accounts:
      target: ~/Dropbox/accounts         # transcripts in the parallel tree
    memories:
      target: ~/Dropbox/memories
      transcripts:
        location: adjacent               # transcripts next to the files
```

A `location` other than `adjacent` or `parallel`, `parallel` without a
`root`, or a `transcripts` value (top-level or in a cabinet) that isn't a
mapping is a configuration error, and `stow-file` exits before prompting or
moving anything. An error in a top-level setting names the top level rather
than a cabinet. An empty `transcripts:` block, or an empty `location`, counts
as unset, but other values such as `no` or `0` are rejected as invalid
locations.

This only affects `stow-file`. `transcribe` and `scan-dir` always write the
transcript of an incoming file next to it, as `FILE.md`.

### Transcription settings

`transcribe` uses these optional keys:

```yaml
filez4eva:
  transcribe:
    model: claude-opus-4-6              # Anthropic model ID
  anthropic:
    key: $(op read op://vault/anthropic/credential)
```

- `transcribe: model:` defaults to `claude-opus-4-6`.
- `anthropic: key:` is the Anthropic API key. Like other values it may use
  `$(...)` to run a command, such as a password manager lookup. It can also
  be set with the `FILEZ4EVA_ANTHROPIC_KEY` environment variable, which takes
  precedence over the config file. If neither is set, the Anthropic SDK's own
  `ANTHROPIC_API_KEY` environment variable is used.

Filez4Eva only reads its own configuration; Kwark's configuration is never
used.

### Classification settings

`classify` uses these optional keys, as well as the `transcribe` model and
the `anthropic` key above:

```yaml
filez4eva:
  classify:
    model: claude-haiku-4-5     # Anthropic model ID (the default)
  doctypes:
    receipt:
      description: Proof of purchase or payment
    bill:
      description: A request for payment, usually with a due date
```

- `classify: model:` defaults to `claude-haiku-4-5`. Transcription still
  uses `transcribe: model:`.
- `doctypes` maps each document type to an optional `description`, which is
  shown to the model. Names must be single words (letters, digits, `-` and
  `_`). `other` is always available; configure it only to change its
  description. With no `doctypes`, every document's doctype is `other`.
- A `doctypes` value that isn't a mapping, a name that isn't a single word,
  or a description that isn't text is a configuration error, and `classify`
  exits before calling the model.

