@muvon/octofs 0.9.0

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 (3) hide show
  1. package/README.md +574 -0
  2. package/cli.js +84 -0
  3. package/package.json +21 -0
package/README.md ADDED
@@ -0,0 +1,574 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ™ Octofs
4
+
5
+ **Give your AI assistant filesystem superpowers**
6
+
7
+ [![CI](https://github.com/muvon/octofs/actions/workflows/ci.yml/badge.svg)](https://github.com/muvon/octofs/actions/workflows/ci.yml)
8
+ [![Rust](https://img.shields.io/badge/Rust-1.95+-orange.svg?logo=rust)](https://www.rust-lang.org)
9
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
10
+ [![MCP](https://img.shields.io/badge/MCP-2026--07--28-green.svg)](https://modelcontextprotocol.io)
11
+ [![Version](https://img.shields.io/crates/v/octofs.svg)](https://crates.io/crates/octofs)
12
+
13
+ *Standalone Rust binary that exposes filesystem tools over the Model Context Protocol. Built on `rmcp` 3.0, `tokio`, and `axum`.*
14
+
15
+ [Installation](#installation) ยท [Quick Start](#quick-start) ยท [Features](#features) ยท [Tools Reference](#mcp-tools-reference) ยท [Architecture](#architecture)
16
+
17
+ MCP Registry: [`io.github.Muvon/octofs`](https://github.com/muvon/octofs)
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Why Octofs?
24
+
25
+ Your AI coding assistant (Cursor, Claude, Windsurf, etc.) is smart โ€” but it's **blind to your filesystem**. Octofs bridges that gap, giving your AI:
26
+
27
+ - **Eyes** โ€” Read files, search content, explore directories
28
+ - **Hands** โ€” Create, edit, batch-modify files atomically
29
+ - **Context** โ€” Execute commands, manage working directories
30
+ - **Reach** โ€” Transparent SSH/SFTP: every file tool accepts `ssh://` URLs
31
+
32
+ ```
33
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
34
+ โ”‚ You: "Refactor all error handling to use anyhow::Context" โ”‚
35
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
36
+ โ”‚ AI without Octofs: โ”‚
37
+ โ”‚ โ€ข "I can't see your project structure" โ”‚
38
+ โ”‚ โ€ข "Please paste the relevant files" โ”‚
39
+ โ”‚ โ€ข *Wastes 10 minutes on back-and-forth* โ”‚
40
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
41
+ โ”‚ AI with Octofs: โ”‚
42
+ โ”‚ โ€ข Reads your codebase directly โ”‚
43
+ โ”‚ โ€ข Finds all error handling patterns โ”‚
44
+ โ”‚ โ€ข Suggests atomic batch edits โ”‚
45
+ โ”‚ โ€ข Applies changes with your approval โ”‚
46
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
47
+ ```
48
+
49
+ ## What Makes It Different
50
+
51
+ | Feature | Octofs | Typical Alternatives |
52
+ |---------|--------|---------------------|
53
+ | **Implementation** | Compiled Rust binary, no runtime | Python/Node script |
54
+ | **Content Search** | Built-in search with context lines | String matching only |
55
+ | **Batch Operations** | Atomic multi-edit on single file | One-at-a-time |
56
+ | **Line Modes** | Hash-based (stable across edits) or number-based | Number-only |
57
+ | **Transport** | STDIO + Streamable HTTP | STDIO only |
58
+ | **Shell Integration** | Foreground + background process support | Limited or none |
59
+ | **Remote Files** | Transparent SSH/SFTP on every file tool | None |
60
+ | **Safety** | Gitignore-aware, stale-write detection, path validation | Full filesystem access |
61
+
62
+ ---
63
+
64
+ ## Installation
65
+
66
+ ### Cargo (crates.io)
67
+
68
+ ```bash
69
+ cargo install octofs
70
+ ```
71
+
72
+ ### Homebrew
73
+
74
+ ```bash
75
+ brew install muvon/tap/octofs
76
+ ```
77
+
78
+ ### Pre-built Binaries
79
+
80
+ Download from [GitHub Releases](https://github.com/muvon/octofs/releases) for your platform:
81
+
82
+ | Platform | Target |
83
+ |----------|--------|
84
+ | Linux (x86_64) | `x86_64-unknown-linux-musl` |
85
+ | Linux (ARM64) | `aarch64-unknown-linux-musl` |
86
+ | Windows (x86_64) | `x86_64-pc-windows-msvc` |
87
+ | Windows (ARM64) | `aarch64-pc-windows-msvc` |
88
+ | macOS (Intel) | `x86_64-apple-darwin` |
89
+ | macOS (Apple Silicon) | `aarch64-apple-darwin` |
90
+
91
+ ### MCP Registry
92
+
93
+ One-click install via the [MCP Registry](https://github.com/muvon/octofs) entry `io.github.Muvon/octofs`.
94
+
95
+ ### From Source
96
+
97
+ Requires Rust 1.95+.
98
+
99
+ ```bash
100
+ git clone https://github.com/muvon/octofs
101
+ cd octofs
102
+ cargo build --release
103
+ # Binary at ./target/release/octofs
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ### 1. Configure Your AI Assistant
111
+
112
+ The CLI uses a `mcp` subcommand โ€” your config must pass `["mcp"]` as args.
113
+
114
+ **Cursor** (`~/.cursor/mcp.json`):
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "octofs": {
119
+ "command": "octofs",
120
+ "args": ["mcp"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "octofs": {
131
+ "command": "octofs",
132
+ "args": ["mcp"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ **Windsurf** (`~/.windsurf/mcp.json`):
139
+ ```json
140
+ {
141
+ "mcpServers": {
142
+ "octofs": {
143
+ "command": "octofs",
144
+ "args": ["mcp"]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ > If Octofs isn't on your `PATH`, use the full path to the binary (e.g. `/usr/local/bin/octofs` or `./target/release/octofs`).
151
+
152
+ ### 2. Restart Your AI Assistant
153
+
154
+ The MCP server starts automatically when your AI assistant connects.
155
+
156
+ ### 3. Try It
157
+
158
+ Ask your AI assistant to:
159
+ - "Show me the project structure"
160
+ - "Read the main.rs file"
161
+ - "Search for all uses of `unwrap()` in the codebase"
162
+ - "Create a new file called `test.rs`"
163
+
164
+ ---
165
+
166
+ ## Features
167
+
168
+ ### ๐Ÿ“ Filesystem Operations
169
+
170
+ - **View Files & Directories** โ€” Read a single file (call `view` in parallel for several), list directories with glob patterns, search content
171
+ - **Smart Truncation** โ€” Large files are truncated intelligently to avoid overwhelming context windows
172
+ - **Gitignore-Aware** โ€” Respects `.gitignore` patterns during directory traversal
173
+ - **Line Ranges** โ€” Read specific line ranges with negative indexing (`-1` = last line)
174
+ - **Remote Files (SSH/SFTP)** โ€” Every file tool accepts `ssh://user@host:port/path` URLs (see [Remote Filesystem](#remote-filesystem-sshsftp))
175
+
176
+ ### โœ๏ธ Text Editing
177
+
178
+ - **Create Files** โ€” Create new files with automatic parent directory creation
179
+ - **String Replace** โ€” Replace exact string matches with fuzzy fallback for whitespace
180
+ - **Delete** โ€” Remove a file (recoverable via undo)
181
+ - **Undo** โ€” Revert last edit (up to 10 undo levels per file, in-memory)
182
+ - **Batch Edit** โ€” Perform multiple insert/replace operations atomically on a single file
183
+ - **Stale-Write Protection** โ€” Edits fail fast if the file changed on disk since it was last viewed (external-edit detection, like an IDE's "file changed on disk" guard)
184
+
185
+ ### ๐Ÿ” Code Intelligence
186
+
187
+ - **Content Search** โ€” Search for strings within files with context lines
188
+ - **Line Extraction** โ€” Copy specific line ranges from one file to another
189
+
190
+ ### ๐Ÿ–ฅ๏ธ Shell & System
191
+
192
+ - **Command Execution** โ€” Run shell commands with output capture
193
+ - **Background Processes** โ€” Run long commands in background, get PID for later management
194
+ - **Working Directory** โ€” Set/get/reset working directory context for operations
195
+
196
+ ---
197
+
198
+ ## Configuration
199
+
200
+ ### Line Identifiers
201
+
202
+ Every line is addressed by a composite id `N:hh` โ€” its 1-indexed position plus a
203
+ 2-character hex hash (FNV-1a) of its content. `view` renders lines as `N:hh|content`:
204
+
205
+ ```
206
+ 1:a3|fn main() {
207
+ 2:f1| println!("Hello");
208
+ 3:0e|}
209
+ ```
210
+
211
+ Edit tools take these ids back as targets and verify the hash against the file
212
+ before applying anything. A stale id (the file changed since it was viewed) fails
213
+ with the current content around the target and where the expected content moved โ€”
214
+ so the model retargets from the error instead of re-reading the file. Edit results
215
+ are diffs with fresh ids, so edits chain without re-viewing.
216
+
217
+ This is the single line-id format โ€” there is no mode switch. Plain line numbers
218
+ are still accepted where a position alone is safe: `view` ranges (negative counts
219
+ from the end) and the insert anchors `0` (file start) / `-1` (append).
220
+
221
+ ### Shell Misuse Enforcement
222
+
223
+ Octofs detects shell misuse โ€” commands like `cat`, `grep`, `find`, or `sed` that
224
+ should use the dedicated MCP tools instead โ€” and rejects them with an error
225
+ explaining which tool to use. The call fails; nothing executes. This is
226
+ intentional and not configurable: the dedicated tools give the model line ids,
227
+ gitignore-awareness, and remote-host support that raw shell output cannot.
228
+ Pipelines (`cargo build 2>&1 | grep error`) remain allowed โ€” only standalone
229
+ invocations of those programs are blocked.
230
+
231
+ Other guidance (out-of-bounds ranges, fuzzy-match notices) is appended to
232
+ successful responses as โš ๏ธ hints without failing the call.
233
+
234
+ ### Transport Modes
235
+
236
+ #### STDIO (default)
237
+
238
+ Standard input/output transport. Works with all MCP clients.
239
+
240
+ ```bash
241
+ octofs mcp
242
+ ```
243
+
244
+ #### HTTP
245
+
246
+ Streamable HTTP transport for remote access or multi-client scenarios.
247
+
248
+ ```bash
249
+ octofs mcp --bind 0.0.0.0:12345
250
+ ```
251
+
252
+ Connect clients to `http://localhost:12345/mcp`.
253
+
254
+ ### Working Directory
255
+
256
+ By default, Octofs operates in the current directory. Specify a different root:
257
+
258
+ ```json
259
+ {
260
+ "mcpServers": {
261
+ "octofs": {
262
+ "command": "octofs",
263
+ "args": ["mcp", "--path", "/path/to/your/project"]
264
+ }
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### Remote Filesystem (SSH/SFTP)
270
+
271
+ All path parameters โ€” and `--path` itself โ€” accept `ssh://` or `sftp://` URLs:
272
+
273
+ ```bash
274
+ # Remote session root: relative paths resolve on the remote host
275
+ octofs mcp --path ssh://deploy@example.com/var/www/app --ssh-key ~/.ssh/id_ed25519
276
+ ```
277
+
278
+ ```
279
+ view path="ssh://deploy@example.com/etc/nginx/nginx.conf"
280
+ ```
281
+
282
+ - **Authentication** โ€” fully automatic, like OpenSSH, honoring `~/.ssh/config`: the agent your config names for the host (`IdentityAgent`, e.g. 1Password) or `$SSH_AUTH_SOCK`, then key files โ€” `--ssh-key` if given, the host's `IdentityFile` entries, then the defaults in `~/.ssh` (`id_ed25519`, `id_ecdsa`). If plain `ssh host` works on your machine, octofs works too โ€” nothing to configure. Passphrase-protected key files are not supported directly; use an agent instead.
283
+ - **RSA keys are not supported** โ€” the Rust `rsa` crate has an unfixed timing side-channel (Marvin attack, [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071)), so octofs is built without RSA entirely. Use an ed25519 key instead (`ssh-keygen -t ed25519`); ecdsa also works. RSA-only setups fail with a clear error naming the key.
284
+ - **Host keys** โ€” verified against `~/.ssh/known_hosts` with the OpenSSH `accept-new` policy: unknown hosts are recorded on first use, a changed key fails closed.
285
+ - **`--ssh-timeout SECS`** โ€” connection timeout (default 30). Connections are pooled per host, kept alive with transport keepalives, and reconnected automatically if they drop.
286
+ - **`shell` stays local** โ€” commands always run on the machine where Octofs runs; only file tools (`view`, `text_editor`, `batch_edit`, `extract_lines`, `workdir`) reach remote hosts.
287
+
288
+ ---
289
+
290
+ ## MCP Tools Reference
291
+
292
+ ### `view` โ€” Read files, list directories, search content
293
+
294
+ **File reading:** (`path` is a single path; `start`/`end` are line numbers or line ids)
295
+ ```json
296
+ {"path": "src/main.rs"} // whole file
297
+ {"path": "src/main.rs", "start": 10, "end": 20} // lines 10โ€“20
298
+ {"path": "src/main.rs", "start": 42, "end": 42} // single line
299
+ {"path": "src/main.rs", "start": 80} // line 80 โ†’ end of file
300
+ {"path": "src/main.rs", "start": -20} // last 20 lines
301
+ {"path": "src/main.rs", "start": "12:a3", "end": "20:f1"} // line ids from prior output
302
+ ```
303
+
304
+ Output renders every line as `N:hh|content` โ€” the `N:hh` prefix is the line id
305
+ that `batch_edit` and `extract_lines` take as targets.
306
+
307
+ To read several files, make multiple `view` calls โ€” they run in parallel.
308
+
309
+ **Directory listing:**
310
+ ```json
311
+ {"path": "src/"}
312
+ {"path": "src/", "pattern": "*.rs"}
313
+ {"path": "src/", "max_depth": 2, "include_hidden": true}
314
+ ```
315
+
316
+ `pattern` is a glob, not a content search: without `/` it matches the whole filename at any depth; with `/` it matches the workdir-relative path. It supports `*`, `?`, character classes such as `[abc]`, and `|` alternatives such as `*.rs|*.toml`.
317
+
318
+ **Content search:** (literal by default; set `regex: true` for a Rust regex, `(?i)` = case-insensitive)
319
+ ```json
320
+ {"path": "src", "content": "fn main"}
321
+ {"path": "src", "content": "unwrap()", "context": 3}
322
+ {"path": "src", "content": "(?i)error", "regex": true}
323
+ ```
324
+
325
+ Directory listings annotate each file as `path<TAB>NL<TAB>~Nt` (line count + estimated tokens) so you can budget reads before opening files; binary files show `path<TAB>(binary)`.
326
+
327
+ ---
328
+
329
+ ### `text_editor` โ€” Create, edit, replace text
330
+
331
+ **Create file:**
332
+ ```json
333
+ {"command": "create", "path": "src/new.rs", "content": "pub fn new() {}"}
334
+ ```
335
+
336
+ **Replace string:** (`old_text` must match exactly once)
337
+ ```json
338
+ {
339
+ "command": "str_replace",
340
+ "path": "src/main.rs",
341
+ "old_text": "fn old()",
342
+ "new_text": "fn new()"
343
+ }
344
+ ```
345
+
346
+ **Replace ALL occurrences** (rename-style edits):
347
+ ```json
348
+ {
349
+ "command": "str_replace",
350
+ "path": "src/main.rs",
351
+ "old_text": "old_name",
352
+ "new_text": "new_name",
353
+ "replace_all": true
354
+ }
355
+ ```
356
+
357
+ Matching is progressive: exact โ†’ escaped-literal recovery (double-escaped `\n`/`\t`
358
+ interpreted when the result matches uniquely) โ†’ whitespace-normalized fuzzy with
359
+ indentation adjustment โ†’ rich diagnostics with the closest candidates and their
360
+ line ids. CRLF files are matched in LF space and keep their line endings on write.
361
+
362
+ **Delete file:** (recoverable with `undo_edit`)
363
+ ```json
364
+ {"command": "delete", "path": "src/old.rs"}
365
+ ```
366
+
367
+ **Undo last edit:**
368
+ ```json
369
+ {"command": "undo_edit", "path": "src/main.rs"}
370
+ ```
371
+
372
+ ---
373
+
374
+ ### `batch_edit` โ€” Atomic multi-operation edits
375
+
376
+ Perform multiple insert/replace operations on a single file atomically.
377
+
378
+ Each operation has a `start`. For `replace` it's the first line of the range as a
379
+ line id copied from `view` output (add `end` for a range; omit it for a single
380
+ line). For `insert` it's the anchor to insert after โ€” a line id, or the integers
381
+ `0` (file start) / `-1` (after last line). Every id is verified against the file
382
+ before anything applies; a stale id fails with the current content so the model
383
+ can retarget without a re-view. The result is a diff with fresh ids.
384
+
385
+ **Insert at beginning:**
386
+ ```json
387
+ {
388
+ "path": "src/main.rs",
389
+ "operations": [
390
+ {"operation": "insert", "start": 0, "content": "// Header\n"}
391
+ ]
392
+ }
393
+ ```
394
+
395
+ **Replace lines:**
396
+ ```json
397
+ {
398
+ "path": "src/main.rs",
399
+ "operations": [
400
+ {"operation": "replace", "start": "10:4b", "end": "15:c2", "content": "new code here"}
401
+ ]
402
+ }
403
+ ```
404
+
405
+ ---
406
+
407
+ ### `extract_lines` โ€” Copy lines between files
408
+
409
+ ```json
410
+ {
411
+ "from_path": "src/utils.rs",
412
+ "from_start": 10,
413
+ "from_end": 25,
414
+ "append_path": "src/new.rs",
415
+ "append_line": -1
416
+ }
417
+ ```
418
+
419
+ `from_end` is optional (omit to copy a single line). `from_start`, `from_end`, and
420
+ `append_line` each accept a line number or a line id (`"12:a3"`, verified against
421
+ the file). `append_line` positions the copy in the target: `0` = beginning,
422
+ `-1` = end, `N` = after line N.
423
+
424
+ ---
425
+
426
+ ### `shell` โ€” Execute commands
427
+
428
+ **Foreground:**
429
+ ```json
430
+ {"command": "cargo test"}
431
+ {"command": "cd foo && cargo build"}
432
+ ```
433
+
434
+ **Background:**
435
+ ```json
436
+ {"command": "python -m http.server 8000", "background": true}
437
+ // Returns PID; the response includes the platform-specific kill command
438
+ ```
439
+
440
+ > On Windows, shutdown cleanup terminates only direct child processes (no Unix
441
+ > process-group semantics); use `taskkill /PID <pid> /T` for process trees.
442
+
443
+ ---
444
+
445
+ ### `workdir` โ€” Manage working directory
446
+
447
+ **Get current:**
448
+ ```json
449
+ {}
450
+ ```
451
+
452
+ **Set new:**
453
+ ```json
454
+ {"path": "/path/to/project"}
455
+ ```
456
+
457
+ **Reset to session root:**
458
+ ```json
459
+ {"reset": true}
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Architecture
465
+
466
+ ```
467
+ octofs/
468
+ โ”œโ”€โ”€ src/
469
+ โ”‚ โ”œโ”€โ”€ main.rs # Entry point, STDIO/HTTP server setup, signal handling
470
+ โ”‚ โ”œโ”€โ”€ cli.rs # CLI argument parsing (clap): octofs mcp [OPTIONS]
471
+ โ”‚ โ”œโ”€โ”€ mcp/
472
+ โ”‚ โ”‚ โ”œโ”€โ”€ mod.rs # McpToolCall, SessionRoot
473
+ โ”‚ โ”‚ โ”œโ”€โ”€ server.rs # OctofsServer (rmcp tool impl), Params structs, SessionWorkdir
474
+ โ”‚ โ”‚ โ”œโ”€โ”€ request_ctx.rs # Per-request hint queue + stale-file stamps
475
+ โ”‚ โ”‚ โ””โ”€โ”€ fs/ # Filesystem tool implementations
476
+ โ”‚ โ”‚ โ”œโ”€โ”€ mod.rs # Re-exports
477
+ โ”‚ โ”‚ โ”œโ”€โ”€ core.rs # view, text_editor, batch_edit, extract_lines
478
+ โ”‚ โ”‚ โ”œโ”€โ”€ file_ops.rs # view_file_spec, create_file_spec
479
+ โ”‚ โ”‚ โ”œโ”€โ”€ text_editing.rs # str_replace, batch_edit, undo, per-file locking
480
+ โ”‚ โ”‚ โ”œโ”€โ”€ directory.rs # Directory listing + content search
481
+ โ”‚ โ”‚ โ”œโ”€โ”€ search.rs # search_content
482
+ โ”‚ โ”‚ โ”œโ”€โ”€ shell.rs # Command execution, background, process cleanup
483
+ โ”‚ โ”‚ โ”œโ”€โ”€ workdir.rs # Working directory management
484
+ โ”‚ โ”‚ โ”œโ”€โ”€ remote.rs # SSH/SFTP path abstraction, SshHandler, SFTP pool
485
+ โ”‚ โ”‚ โ””โ”€โ”€ fs_tests.rs # Integration tests (cfg(test))
486
+ โ”‚ โ””โ”€โ”€ utils/
487
+ โ”‚ โ”œโ”€โ”€ mod.rs # Module re-exports
488
+ โ”‚ โ”œโ”€โ”€ line_hash.rs # Composite line ids (N:hh), FNV-1a hashes, endpoint parsing
489
+ โ”‚ โ””โ”€โ”€ truncation.rs # Token estimation, smart truncation
490
+ ```
491
+
492
+ **Key design decisions:**
493
+
494
+ - **rmcp SDK** โ€” Official Rust MCP SDK (`rmcp` 3.0) for protocol handling
495
+ - **Tokio** โ€” Async runtime; all file I/O uses `tokio::fs`, never blocking `std::fs`
496
+ - **File locking** โ€” Per-file `tokio::sync::Mutex` prevents concurrent write conflicts
497
+ - **Undo history** โ€” Up to 10 snapshots per file, in-memory (lost on restart)
498
+ - **Path resolution** โ€” Relative paths resolve against the session workdir; no canonicalization (files may not exist yet)
499
+ - **Stale-write detection** โ€” every edit target's `N:hh` id is verified against the file at apply time; a stale id fails with the current content instead of editing the wrong line
500
+
501
+ ---
502
+
503
+ ## Development
504
+
505
+ ```bash
506
+ # Build
507
+ cargo build --release
508
+
509
+ # Run tests
510
+ cargo test
511
+
512
+ # Lint (zero warnings policy)
513
+ cargo clippy
514
+
515
+ # Format
516
+ cargo fmt
517
+
518
+ # Run locally
519
+ cargo run -- mcp
520
+ ```
521
+
522
+ ### Running Tests
523
+
524
+ ```bash
525
+ # All tests
526
+ cargo test
527
+
528
+ # Specific test
529
+ cargo test test_view_file
530
+
531
+ # With output
532
+ cargo test -- --nocapture
533
+ ```
534
+
535
+ ---
536
+
537
+ ## Contributing
538
+
539
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
540
+
541
+ **Quick checklist:**
542
+ 1. Run `cargo fmt` before committing
543
+ 2. Ensure `cargo clippy` passes with zero warnings
544
+ 3. Add tests for new functionality
545
+ 4. Update documentation as needed
546
+
547
+ ---
548
+
549
+ ## Security
550
+
551
+ See [SECURITY.md](SECURITY.md) for security policy and reporting vulnerabilities.
552
+
553
+ ---
554
+
555
+ ## License
556
+
557
+ Apache-2.0 โ€” See [LICENSE](LICENSE)
558
+
559
+ ---
560
+
561
+ ## Acknowledgments
562
+
563
+ - [rmcp](https://crates.io/crates/rmcp) โ€” Official Rust MCP SDK
564
+ - [Model Context Protocol](https://modelcontextprotocol.io) โ€” The protocol specification
565
+
566
+ ---
567
+
568
+ <div align="center">
569
+
570
+ **Built with ๐Ÿฆ€ by [Muvon](https://muvon.io)**
571
+
572
+ *Star us on GitHub if Octofs helps you ship faster! โญ*
573
+
574
+ </div>
package/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // Generic launcher shipped inside every @muvon/* npm package. Downloads the
3
+ // matching binary from the GitHub release on first run, caches it under
4
+ // ~/.cache/muvon, then execs it with the caller's args.
5
+ //
6
+ // No build-time substitution: name/version come from the package.json next to
7
+ // this file, and the GitHub repo is derived from the scoped package name.
8
+ 'use strict'
9
+
10
+ const fs = require('fs')
11
+ const os = require('os')
12
+ const path = require('path')
13
+ const { spawnSync } = require('child_process')
14
+ const { execFileSync } = require('child_process')
15
+ const { Readable } = require('stream')
16
+ const { pipeline } = require('stream/promises')
17
+
18
+ const { name, version } = require('./package.json')
19
+
20
+ const TARGETS = {
21
+ 'darwin-arm64': 'aarch64-apple-darwin',
22
+ 'darwin-x64': 'x86_64-apple-darwin',
23
+ 'linux-arm64': 'aarch64-unknown-linux-musl',
24
+ 'linux-x64': 'x86_64-unknown-linux-musl',
25
+ 'win32-arm64': 'aarch64-pc-windows-msvc',
26
+ 'win32-x64': 'x86_64-pc-windows-msvc',
27
+ }
28
+
29
+ const bin = name.split('/')[1]
30
+ const repo = `muvon/${bin}`
31
+ const isWindows = process.platform === 'win32'
32
+ const dir = path.join(os.homedir(), '.cache', 'muvon', bin, version)
33
+ const exe = path.join(dir, isWindows ? `${bin}.exe` : bin)
34
+
35
+ async function install() {
36
+ const target = TARGETS[`${process.platform}-${process.arch}`]
37
+ if (!target) {
38
+ throw new Error(`unsupported platform ${process.platform}-${process.arch}`)
39
+ }
40
+
41
+ const ext = isWindows ? 'zip' : 'tar.gz'
42
+ const url = `https://github.com/${repo}/releases/download/${version}/${bin}-${version}-${target}.${ext}`
43
+ const res = await fetch(url)
44
+ if (!res.ok) {
45
+ throw new Error(`download failed (${res.status}): ${url}`)
46
+ }
47
+
48
+ // Unpack into a pid-scoped directory and rename into place: concurrent runs
49
+ // (MCP clients spawn several servers at once) must never see a partial binary.
50
+ const tmp = `${dir}.${process.pid}`
51
+ fs.rmSync(tmp, { recursive: true, force: true })
52
+ fs.mkdirSync(tmp, { recursive: true })
53
+ const archive = path.join(tmp, `archive.${ext}`)
54
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(archive))
55
+ // bsdtar reads zip too, and ships with Windows 10 1803+.
56
+ execFileSync('tar', ['-xf', archive, '-C', tmp], { stdio: 'inherit' })
57
+ fs.rmSync(archive)
58
+ fs.chmodSync(path.join(tmp, path.basename(exe)), 0o755)
59
+
60
+ fs.mkdirSync(path.dirname(dir), { recursive: true })
61
+ try {
62
+ fs.renameSync(tmp, dir)
63
+ } catch (err) {
64
+ // Another process won the race; its copy is equivalent.
65
+ if (!fs.existsSync(exe)) throw err
66
+ fs.rmSync(tmp, { recursive: true, force: true })
67
+ }
68
+ }
69
+
70
+ async function main() {
71
+ if (!fs.existsSync(exe)) {
72
+ // stderr only โ€” stdout is the MCP stdio transport.
73
+ process.stderr.write(`${bin}: downloading ${version}...\n`)
74
+ await install()
75
+ }
76
+ const run = spawnSync(exe, process.argv.slice(2), { stdio: 'inherit' })
77
+ if (run.error) throw run.error
78
+ process.exit(run.status === null ? 1 : run.status)
79
+ }
80
+
81
+ main().catch((err) => {
82
+ process.stderr.write(`${bin}: ${err.message}\n`)
83
+ process.exit(1)
84
+ })
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@muvon/octofs",
3
+ "version": "0.9.0",
4
+ "description": "Standalone MCP filesystem tools server \u2014 view, edit, shell, workdir.",
5
+ "mcpName": "io.github.Muvon/octofs",
6
+ "bin": {
7
+ "octofs": "cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js"
11
+ ],
12
+ "license": "Apache-2.0",
13
+ "homepage": "https://github.com/muvon/octofs",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/muvon/octofs.git"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ }
21
+ }