filekeeper-cli 0.1.0__tar.gz

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.
Files changed (36) hide show
  1. filekeeper_cli-0.1.0/LICENSE +21 -0
  2. filekeeper_cli-0.1.0/PKG-INFO +498 -0
  3. filekeeper_cli-0.1.0/README.md +475 -0
  4. filekeeper_cli-0.1.0/pyproject.toml +114 -0
  5. filekeeper_cli-0.1.0/pyproject.toml.orig +85 -0
  6. filekeeper_cli-0.1.0/src/filekeeper/__init__.py +35 -0
  7. filekeeper_cli-0.1.0/src/filekeeper/cli/__init__.py +5 -0
  8. filekeeper_cli-0.1.0/src/filekeeper/cli/app.py +866 -0
  9. filekeeper_cli-0.1.0/src/filekeeper/cli/exit_codes.py +84 -0
  10. filekeeper_cli-0.1.0/src/filekeeper/cli/output.py +244 -0
  11. filekeeper_cli-0.1.0/src/filekeeper/core/__init__.py +51 -0
  12. filekeeper_cli-0.1.0/src/filekeeper/core/config.py +133 -0
  13. filekeeper_cli-0.1.0/src/filekeeper/core/database.py +226 -0
  14. filekeeper_cli-0.1.0/src/filekeeper/core/diffing.py +127 -0
  15. filekeeper_cli-0.1.0/src/filekeeper/core/errors.py +278 -0
  16. filekeeper_cli-0.1.0/src/filekeeper/core/history/__init__.py +20 -0
  17. filekeeper_cli-0.1.0/src/filekeeper/core/history/blobs.py +92 -0
  18. filekeeper_cli-0.1.0/src/filekeeper/core/history/journal.py +430 -0
  19. filekeeper_cli-0.1.0/src/filekeeper/core/history/repository.py +152 -0
  20. filekeeper_cli-0.1.0/src/filekeeper/core/history/undo.py +358 -0
  21. filekeeper_cli-0.1.0/src/filekeeper/core/models.py +293 -0
  22. filekeeper_cli-0.1.0/src/filekeeper/core/operations/__init__.py +9 -0
  23. filekeeper_cli-0.1.0/src/filekeeper/core/operations/directory.py +475 -0
  24. filekeeper_cli-0.1.0/src/filekeeper/core/operations/edit.py +1239 -0
  25. filekeeper_cli-0.1.0/src/filekeeper/core/operations/read.py +370 -0
  26. filekeeper_cli-0.1.0/src/filekeeper/core/operations/search.py +198 -0
  27. filekeeper_cli-0.1.0/src/filekeeper/core/operations/write.py +763 -0
  28. filekeeper_cli-0.1.0/src/filekeeper/core/paths.py +236 -0
  29. filekeeper_cli-0.1.0/src/filekeeper/core/safety/__init__.py +5 -0
  30. filekeeper_cli-0.1.0/src/filekeeper/core/safety/atomic.py +129 -0
  31. filekeeper_cli-0.1.0/src/filekeeper/core/safety/hashing.py +73 -0
  32. filekeeper_cli-0.1.0/src/filekeeper/core/safety/sensitive.py +65 -0
  33. filekeeper_cli-0.1.0/src/filekeeper/core/safety/textdata.py +142 -0
  34. filekeeper_cli-0.1.0/src/filekeeper/core/safety/vcs.py +51 -0
  35. filekeeper_cli-0.1.0/src/filekeeper/core/service.py +706 -0
  36. filekeeper_cli-0.1.0/src/filekeeper/core/workspace.py +300 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sangingeum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,498 @@
1
+ Metadata-Version: 2.4
2
+ Name: filekeeper-cli
3
+ Version: 0.1.0
4
+ Summary: Safe, predictable filesystem operations for humans and AI agents, with atomic writes and workspace confinement.
5
+ Keywords: filesystem,atomic-write,cli,agents,mcp
6
+ Author: sangingeum
7
+ Author-email: sangingeum <sangingeum@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: System :: Filesystems
19
+ Requires-Python: >=3.11, <3.14
20
+ Project-URL: Homepage, https://github.com/sangingeum/filekeeper
21
+ Project-URL: Changelog, https://github.com/sangingeum/filekeeper/blob/main/CHANGELOG.md
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Filekeeper
25
+
26
+ Safe, predictable filesystem operations for humans, shell scripts and AI agents.
27
+ One Core library, a `keeper` command line, and an MCP server that all behave
28
+ identically because they call the same code.
29
+
30
+ Filekeeper confines every operation to a registered **workspace**, replaces whole
31
+ files atomically, refuses credential-shaped write targets, and reports structured
32
+ results with stable error codes.
33
+
34
+ ## Status
35
+
36
+ Version 0.1 is the **foundation, operations and reversibility** release. It
37
+ includes workspace registration and confinement, read/info/hash/list/search,
38
+ file and directory create/copy/move/delete, atomic whole-file replacement, text
39
+ detection and size limits, durable operation history with content-addressed
40
+ blobs, safe undo with conflict detection, staged literal text editing,
41
+ line-range editing, unified-diff application, the CLI, and the MCP server.
42
+
43
+ Not in this release, and deliberately so:
44
+
45
+ - redo (undo is the requirement; redo is convenience) — an undone operation is
46
+ recorded, but `redo` itself is not implemented
47
+ - explicit snapshots and restore (history blobs already hold the data)
48
+ - advisory locks with TTL (the tables exist; the API does not)
49
+ - batch execution, file watching, archives, history pruning UI, HTTP transport
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ uv tool install filekeeper-cli # or: pip install filekeeper-cli
55
+ keeper --help
56
+ ```
57
+
58
+ Requires Python 3.11 to 3.13. The published `filekeeper-cli` distribution has
59
+ **no third-party dependencies**; it installs the `filekeeper` import package and
60
+ the `keeper` console script. The MCP server is a separate distribution, so
61
+ installing the CLI never pulls in the MCP SDK:
62
+
63
+ ```bash
64
+ uv tool install filekeeper-mcp # Core + CLI + MCP server
65
+ filekeeper-mcp --root ~/project
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```bash
71
+ keeper init # create the home directory and database
72
+ keeper workspace create notes ~/notes # register a workspace
73
+ keeper workspace use notes # make it the default
74
+ keeper file create todo.md --content "# Todo\n"
75
+ keeper file read todo.md
76
+ keeper file write todo.md --content "# Todo\n- write tests\n"
77
+ keeper file list . --recursive --glob '*.md'
78
+ ```
79
+
80
+ Every command accepts `--json` for machine-readable output and `--workspace NAME`
81
+ or `--root PATH` to select a workspace. Selection precedence is explicit
82
+ `--workspace`, then explicit `--root`, then the configured default; Filekeeper
83
+ never guesses when several workspaces exist.
84
+
85
+ ## MCP server
86
+
87
+ `filekeeper-mcp` speaks the Model Context Protocol over STDIO. It is a thin
88
+ adapter over Core: no filesystem behaviour lives in it, so a guarded write or a
89
+ refusal is identical whichever adapter issued it.
90
+
91
+ ```bash
92
+ filekeeper-mcp --workspace notes
93
+ filekeeper-mcp --root ~/work/tree --home ~/.local/share/filekeeper
94
+ ```
95
+
96
+ `--workspace` and `--root` are mutually exclusive; `--home` overrides
97
+ `FILEKEEPER_HOME`; `--log-level` controls stderr only, because stdout carries
98
+ protocol messages. `--actor` and `--task-id` are optional audit metadata recorded
99
+ on every mutation. There is no network listener in this release.
100
+
101
+ The allowed roots come from start-up configuration only. No tool argument can
102
+ widen them, register a workspace or point the server at another tree.
103
+
104
+ ### Tools
105
+
106
+ Read-only: `workspace_list`, `workspace_info`, `file_read`, `file_read_many`,
107
+ `directory_list`, `file_info`, `file_search`, `content_search`, `history_list`,
108
+ `history_get`, `history_diff`.
109
+
110
+ Mutating: `file_create`, `file_write`, `file_copy`, `file_move`, `file_delete`,
111
+ `directory_create`, `directory_delete`, `file_replace_text`,
112
+ `file_replace_lines`, `file_insert_lines`, `file_delete_lines`,
113
+ `file_apply_patch`, `undo_operation`, `undo_last`.
114
+
115
+ The same conventions apply as in the CLI: workspace-relative paths, 1-based
116
+ inclusive line numbers, `expected_hash` / `expected_content` guards, `dry_run` on
117
+ every mutation, and `operation_id` / `undo_available` on every mutation. Each
118
+ tool's description states its own semantics, and each carries protocol
119
+ annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
120
+ `openWorldHint`) that describe what it actually does.
121
+
122
+ Requests that could exhaust memory are refused with `LIMIT_EXCEEDED`, not
123
+ truncated: 32 paths per `file_read_many`, a 1 MiB patch, and at most 200 search
124
+ results, 1000 directory entries or 200 history entries per request. Errors are
125
+ structured data in the same envelope the CLI uses: `ok` plus `error`, carrying the
126
+ stable Core code, `retryable`, and `details` (including the caller's own path),
127
+ with no stack trace and no host path.
128
+
129
+ A tool that mutates returns the Core `operation_id` and `undo_available` exactly
130
+ as the CLI does, so `history_list`, `history_diff` and `undo_operation` work the
131
+ same way from an agent host. The optional audit field is `task_id`; it is
132
+ supplied by server configuration rather than trusted from a tool argument.
133
+
134
+ ## Where state lives
135
+
136
+ **Project-local by default.** A workspace's own state — the operation journal and
137
+ content-addressed blobs — lives in `<workspace>/.filekeeper/`, created on the
138
+ first mutation that needs it. It travels with the checkout, so a fresh clone does
139
+ not silently lose undo history, and deleting a project takes its blobs with it.
140
+
141
+ Workspace *registrations* live in the Filekeeper home directory
142
+ (`~/.local/share/filekeeper` on Linux, or `FILEKEEPER_HOME`), because a registry
143
+ of many roots cannot live inside one of them.
144
+
145
+ Set `state_location = "global"` in `config.toml` to keep one shared store under the
146
+ home directory instead. The trade-off: history survives deleting the checkout, but
147
+ per-project attribution and pruning become harder, and undo history no longer
148
+ travels with the tree.
149
+
150
+ If the workspace is a git repository, Filekeeper adds `.filekeeper/` to
151
+ `.gitignore` when it creates the state directory. Outside a repository it writes
152
+ nothing.
153
+
154
+ ## Durability guarantees, in plain words
155
+
156
+ - **One file, replaced at a time, is atomic.** A whole-file write goes to a
157
+ temporary file in the same directory, is flushed and fsynced, then renamed over
158
+ the destination. If the process dies or the disk fills before the rename, the
159
+ file still holds its complete previous content. Never a truncated file, never a
160
+ half-written mix.
161
+ - **Multiple files are recoverable, not atomic.** Copy, move and recursive
162
+ directory deletion affect more than one path as a sequence of individually safe
163
+ steps. A crash between steps leaves some paths changed and others not. History
164
+ makes the whole operation recoverable: a crash leaves a *pending* record, the
165
+ next run resolves it honestly, and `keeper history undo` can put the previous
166
+ bytes back. It is still not all-or-nothing, and you should not treat a batch of
167
+ changes as a transaction.
168
+ - **The order is fixed.** Before anything is touched, the previous and new
169
+ contents are written and fsynced as immutable blobs, then a pending operation
170
+ record is committed with SQLite in full-synchronous mode. Only then is the file
171
+ replaced, verified, and the operation marked committed.
172
+ - **History has a size cap.** A file above `max_history_blob_bytes` (4 MiB by
173
+ default) is still recorded, but its bytes are not stored: the result says
174
+ history was skipped for it and `undo_available` is false. One large edit cannot
175
+ silently fill the store.
176
+
177
+ ## Line numbers
178
+
179
+ Line numbers are **1-based and inclusive** everywhere: Core, CLI, JSON output, and
180
+ tests. `--from-line 10 --to-line 20` returns lines 10 through 20, both included.
181
+ Ranges beyond the end of the file are rejected rather than silently clamped.
182
+
183
+ ## Editing
184
+
185
+ Every edit goes through the same journal sequence as a whole-file write: blobs,
186
+ pending record, atomic replacement, commit. Each application therefore returns a
187
+ real `operation_id` and is undoable with `keeper history undo`.
188
+
189
+ ### Literal text replacement
190
+
191
+ `keeper file replace-text PATH --old TEXT --new TEXT (--first | --all)`
192
+
193
+ This is the primary edit. Matching is staged, and the result always names the
194
+ stage that matched:
195
+
196
+ 1. `exact` — the literal text appears exactly as given;
197
+ 2. `normalised` — indentation and trailing whitespace are tolerated, and the
198
+ replacement takes the indentation of the line it replaces. A stage-2 match is
199
+ never applied silently;
200
+ 3. `none` — no match: the command fails with `NO_MATCH` and the nearest line
201
+ numbers and their text.
202
+
203
+ The policy is explicit: `--first` or `--all`. `match_count` reports how many
204
+ matches were found. `--expected-match-count N` refuses a different count, and
205
+ `--expected-content TEXT` guards the exact text of a single target region.
206
+ `--expected-hash` guards the whole file. `--dry-run` prints a unified diff and
207
+ writes nothing. Replacement is always literal: the search and replacement text
208
+ are never interpreted as a regular expression or a regex template.
209
+
210
+ ### Line ranges
211
+
212
+ ```
213
+ keeper file replace-lines PATH --start N --end M --replacement TEXT
214
+ keeper file insert-lines PATH --position before|after|end [--line N] --content TEXT
215
+ keeper file delete-lines PATH --start N --end M
216
+ ```
217
+
218
+ Line numbers are 1-based and inclusive. Out-of-range references are rejected,
219
+ never clamped. End-of-file insertion is the explicit `--position end`; it never
220
+ overloads a line number. The file's newline convention and byte-order mark are
221
+ preserved, and the same hash/content guards and dry-run apply.
222
+
223
+ ### Unified diffs
224
+
225
+ `keeper file apply-patch [PATCH]` reads a patch file, or patch text from standard
226
+ input. The whole patch is parsed and every hunk is checked against its file
227
+ before anything is written. A failed or ambiguous hunk rejects the patch; no
228
+ partial patch is applied silently. `--dry-run` validates and reports every file
229
+ and hunk without touching the tree. The result carries per-file and per-hunk
230
+ status, and `--json` carries the same data.
231
+
232
+ A multi-file patch is recorded as **one logical operation**, and undo restores
233
+ every file. Application itself is **recoverable, not atomic across files**: if a
234
+ later file fails after earlier ones were written, the written files are rolled
235
+ back to their pre-patch bytes and the failure names what was restored. A crash is
236
+ resolved from the pending journal record, never from an all-or-nothing commit.
237
+
238
+ ## Commands
239
+
240
+ ```
241
+ keeper init
242
+ keeper workspace create NAME ROOT
243
+ keeper workspace list | show NAME | use NAME | remove NAME
244
+ keeper file read PATH [--from-line N] [--to-line M] [--max-bytes N] [--encoding E]
245
+ keeper file read-many PATH... [--from-line N] [--to-line M] [--max-bytes-per-file N]
246
+ keeper file info PATH
247
+ keeper file hash PATH
248
+ keeper file exists PATH
249
+ keeper file list [PATH] [--recursive] [--glob PATTERN] [--max-entries N]
250
+ keeper file search PATH GLOB [--max-results N] # find paths
251
+ keeper file grep PATH QUERY [--regex] [--glob PATTERN] [--ignore-case]
252
+ keeper file create PATH (--content TEXT | --input FILE | stdin) [--allow-existing] [--dry-run]
253
+ keeper file write PATH (--content TEXT | --input FILE | stdin) [--expected-hash HASH] [--dry-run]
254
+ keeper file copy SOURCE DESTINATION [--expected-destination-hash HASH] [--dry-run]
255
+ keeper file move SOURCE DESTINATION [--overwrite] [--dry-run]
256
+ keeper file delete PATH [--expected-hash HASH] [--dry-run]
257
+ keeper file replace-text PATH --old TEXT --new TEXT (--first | --all) [guards] [--dry-run]
258
+ keeper file replace-lines PATH --start N --end M --replacement TEXT [guards] [--dry-run]
259
+ keeper file insert-lines PATH --position before|after|end [--line N] --content TEXT [guards] [--dry-run]
260
+ keeper file delete-lines PATH --start N --end M [guards] [--dry-run]
261
+ keeper file apply-patch [PATCH] [--dry-run]
262
+ keeper dir create PATH [--parents] [--dry-run]
263
+ keeper dir move SOURCE DESTINATION [--dry-run]
264
+ keeper dir delete PATH [--recursive --confirm-recursive] [--dry-run]
265
+ keeper history list [--path PATH] [--actor NAME] [--task-id ID] [--limit N]
266
+ keeper history show OPERATION_ID
267
+ keeper history diff OPERATION_ID
268
+ keeper history undo OPERATION_ID
269
+ keeper history undo-last [--actor NAME]
270
+ ```
271
+
272
+ Global options: `--workspace NAME`, `--root PATH`, `--json`, `--quiet`,
273
+ `--verbose`, `--home PATH`, `--actor NAME`, `--task-id ID`.
274
+
275
+ `--actor` and `--task-id` are recorded with a mutation and act as filters for
276
+ `history list`; they are optional and carry no host-specific meaning.
277
+
278
+ ## History and undo
279
+
280
+ Every mutation is recorded as an operation with its affected paths, before and
281
+ after content hashes, file modes, existence on both sides, a timestamp, and the
282
+ optional `actor` / `task_id`. Blob content is content-addressed, so identical
283
+ content is stored once.
284
+
285
+ ```bash
286
+ keeper history list # newest first, this workspace only
287
+ keeper history show op_1a2b3c4d # operation, status and per-path hashes
288
+ keeper history diff op_1a2b3c4d # unified diff, never the whole file body
289
+ keeper history undo op_1a2b3c4d # restore the state before that operation
290
+ keeper history undo-last # the most recent undoable operation here
291
+ ```
292
+
293
+ Undo is safe, not best-effort. Before changing anything it compares every
294
+ affected path with the state the operation recorded:
295
+
296
+ - if the path still matches the recorded **after** state, undo restores the
297
+ recorded **before** bytes;
298
+ - if it already matches the **before** state (a partially applied multi-file
299
+ operation), undo leaves it alone;
300
+ - if it matches neither, someone changed it afterwards: the undo is refused with
301
+ `UNDO_CONFLICT` (exit code 12) and **nothing is modified**.
302
+
303
+ Undoing a create removes the file only while it still matches what was created.
304
+ Undoing a delete restores the file only while the destination is still absent.
305
+ Undoing a move checks both ends. A directory operation validates every path in
306
+ the tree first. `undo-last` only ever looks at the selected workspace.
307
+
308
+ The undo is itself recorded as an operation, so `history list` remains a
309
+ truthful ledger. **Redo is deferred** — it is not half-implemented; undoing an
310
+ undo is the supported way to go forward again.
311
+
312
+ `keeper file read` writes file content to standard output unchanged (line endings
313
+ included). Metadata goes to stderr, or into the JSON object with `--json`.
314
+ `--input FILE` reads replacement content from a file, avoiding shell argument
315
+ limits; with neither flag, content is read from standard input when it is not a
316
+ terminal.
317
+
318
+ ## JSON output
319
+
320
+ Every command returns one object. Success carries `"ok": true` and the result
321
+ fields:
322
+
323
+ ```json
324
+ {
325
+ "ok": true,
326
+ "workspace": "notes",
327
+ "path": "todo.md",
328
+ "changed": true,
329
+ "before_hash": "sha256:...",
330
+ "after_hash": "sha256:...",
331
+ "summary": "Wrote todo.md.",
332
+ "operation_id": "op_1a2b3c4d5e6f7a8b",
333
+ "undo_available": true,
334
+ "dry_run": false,
335
+ "warnings": [],
336
+ "items": [{"path": "todo.md", "change": "modified", "before_hash": "sha256:...", "after_hash": "sha256:..."}]
337
+ }
338
+ ```
339
+
340
+ Failure carries a structured error and no traceback:
341
+
342
+ ```json
343
+ {
344
+ "ok": false,
345
+ "error": {
346
+ "code": "CONTENT_CONFLICT",
347
+ "message": "file content changed since it was read; refusing to overwrite",
348
+ "retryable": false,
349
+ "details": {
350
+ "path": "todo.md",
351
+ "expected_hash": "sha256:...",
352
+ "actual_hash": "sha256:...",
353
+ "recovery": "re-read the target, then retry against its current hash"
354
+ }
355
+ }
356
+ }
357
+ ```
358
+
359
+ **Response shape.** Every document — success or failure, CLI or MCP — is one
360
+ object whose first field is `ok`. A success document is flat: the Core result
361
+ fields sit directly beside `ok`. A failure document has exactly two top-level
362
+ fields, `ok` and `error`, and `error` has exactly `code`, `message`, `retryable`
363
+ and `details`; nothing is duplicated at the top level. The caller's own path is
364
+ in `error.details.path`, and `details.recovery` states how to proceed where a
365
+ retry is meaningful. `retryable` describes the *identical* request: a stale hash
366
+ is `false`, because the same call will fail the same way until the caller
367
+ re-reads the file and writes against its new hash.
368
+
369
+ `operation_id` is `null` only for a dry run (nothing was recorded).
370
+ `undo_available` is `false` when an affected file was above the history size cap,
371
+ so its previous bytes were deliberately not stored; `history_skipped` then names
372
+ the paths.
373
+
374
+ Editing results add `match_stage` and `match_count` (text replacement),
375
+ `preview_diff` (dry run), and, for patches, `patch_files` with per-hunk status.
376
+ A failure that was rolled back reports the restored paths in the error details.
377
+
378
+ ## Exit codes
379
+
380
+ | Code | Meaning |
381
+ | ---- | ------- |
382
+ | 0 | success |
383
+ | 1 | unexpected error |
384
+ | 2 | invalid arguments or validation error |
385
+ | 3 | path, workspace or recorded operation not found |
386
+ | 4 | permission or workspace-boundary violation |
387
+ | 5 | concurrency conflict: stale expected hash, no matching text, or a match count other than the expected one |
388
+ | 6 | absolute path resolves outside the workspace |
389
+ | 7 | patch could not be applied; the pre-patch state is intact |
390
+ | 8 | a failed patch could not be fully rolled back |
391
+ | 9 | storage or database error |
392
+ | 10 | write refused: the target looks like credential material |
393
+ | 11 | symlink target resolves outside the workspace |
394
+ | 12 | undo refused: a path changed after the operation |
395
+ | 13 | history needed for restore is missing, corrupt or was skipped |
396
+
397
+ `keeper file exists` exits 3 when the path does not exist, which makes it usable as
398
+ a shell predicate.
399
+
400
+ ## Error codes
401
+
402
+ `INVALID_ARGUMENT`, `PATH_NOT_FOUND`, `NOT_A_FILE`, `NOT_A_DIRECTORY`,
403
+ `PATH_OUTSIDE_WORKSPACE`, `ABSOLUTE_PATH_OUTSIDE_WORKSPACE`,
404
+ `SYMLINK_ESCAPE_REFUSED`, `PATH_IS_STATE_PATH`, `SYMLINK_WRITE_REFUSED`,
405
+ `SPECIAL_FILE_REFUSED`, `SENSITIVE_PATH_REFUSED`, `ALREADY_EXISTS`,
406
+ `CONTENT_CONFLICT`, `NO_MATCH`, `MATCH_COUNT_MISMATCH`, `PATCH_FAILED`,
407
+ `PARTIAL_RECOVERY`, `BINARY_CONTENT_REFUSED`, `LIMIT_EXCEEDED`,
408
+ `INVALID_PATTERN`, `WORKSPACE_NOT_FOUND`, `WORKSPACE_EXISTS`,
409
+ `NO_WORKSPACE_SELECTED`, `STORAGE_ERROR`, `MIGRATION_ERROR`,
410
+ `OPERATION_NOT_FOUND`, `UNDO_CONFLICT`, `HISTORY_UNAVAILABLE`,
411
+ `NOT_IMPLEMENTED_YET`, `INTERNAL_ERROR`.
412
+
413
+ Codes are stable; messages are not a contract.
414
+
415
+ ## Safety rules
416
+
417
+ - **Confinement.** Relative paths resolve inside the workspace root. Absolute paths
418
+ are accepted only when they land inside it after canonical resolution. Paths
419
+ containing `..` are rejected with `PATH_OUTSIDE_WORKSPACE` (exit 4). An
420
+ absolute path that resolves outside is refused with
421
+ `ABSOLUTE_PATH_OUTSIDE_WORKSPACE` (exit 6), and a symlink whose target leaves
422
+ the workspace with `SYMLINK_ESCAPE_REFUSED` (exit 11). A symlink is never a
423
+ write target (the link itself would be replaced, not its target). Device
424
+ files, sockets and FIFOs are refused.
425
+ - **Credential-shaped targets are refused.** Writes to `.env`, `.env.*`, `*.pem`,
426
+ `*.key`, `id_rsa` and its siblings, `*.p12`, `*.pfx`, `authorized_keys`,
427
+ `.netrc`, and anything under `.ssh/`, `.gnupg/` or `.aws/` fail with
428
+ `SENSITIVE_PATH_REFUSED` (exit code 10). Override deliberately with
429
+ `--i-accept-secret-write-risk` (Core: `allow_sensitive_write=True`). The flag is
430
+ long because the mistake is expensive.
431
+ - **Version-control-ignored targets warn.** Writing a path that git ignores still
432
+ succeeds, but the result carries a warning that the change will not be committed.
433
+ - **No shell.** Filekeeper never interpolates paths into a shell command.
434
+ - **Own state is off limits.** Paths inside `.filekeeper/` are refused, so the tool
435
+ can never record its own state files in its own history.
436
+
437
+ ## Text, binary and size limits
438
+
439
+ A file is treated as binary when its first 8 KiB contains a NUL byte. Text
440
+ operations refuse binary content and undecodable bytes instead of returning
441
+ mojibake; `--encoding` names an encoding explicitly. A UTF-8 byte-order mark is
442
+ preserved across rewrites, and line endings are **never** normalised — the detected
443
+ convention is reported, not applied. Line-range edits and patch hunks write
444
+ caller content using the file's existing convention, so editing a CRLF file does
445
+ not silently convert it to LF.
446
+
447
+ Reads and writes are bounded (5 MiB and 20 MiB by default), as are search results,
448
+ directory entries and recursion depth. Bounded reads report `truncated`,
449
+ `truncation_reason` and `next_start_line` instead of quietly returning a prefix.
450
+ Adjust the limits in `config.toml` under `[filekeeper]` in the home directory.
451
+
452
+ ## Python API
453
+
454
+ ```python
455
+ from filekeeper import Filekeeper
456
+
457
+ app = Filekeeper() # or Filekeeper(home="/path/to/home")
458
+ app.init()
459
+ workspace = app.create_workspace("notes", "/home/me/notes")
460
+ service = app.workspace(name="notes") # or root="/path" for a transient workspace
461
+
462
+ read = service.read_file("todo.md", start_line=1, end_line=20)
463
+ service.write_file("todo.md", "# Todo\n", expected_hash=read.content_hash)
464
+ ```
465
+
466
+ Mutating methods return `MutationResult` with `operation_id`, `before_hash`,
467
+ `after_hash`, `undo_available`, `summary`, `warnings` and per-path `items`.
468
+ Editing methods add `match_stage`, `match_count`, `preview_diff` and
469
+ `patch_files`.
470
+
471
+ The same history is available from Python:
472
+
473
+ ```python
474
+ record = service.get_operation(operation_id)
475
+ recent = service.list_history(path="todo.md", actor="editor", limit=10)
476
+ diff = service.diff_operation(operation_id)
477
+ service.undo(operation_id, actor="editor")
478
+ service.undo_last(actor="editor")
479
+ ```
480
+
481
+ Core imports only the Python standard library. `packages/filekeeper-mcp/` is a
482
+ separate distribution so that installing the CLI never pulls in the MCP SDK; it
483
+ ships the STDIO server described above.
484
+
485
+ ## Development
486
+
487
+ ```bash
488
+ uv sync
489
+ uv run pytest
490
+ uv run ruff check .
491
+ uv run mypy
492
+ ```
493
+
494
+ `docs/decisions.md` records the design choices this specification left open.
495
+
496
+ ## License
497
+
498
+ MIT. See `LICENSE`.