npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

opencode-fs-plugin

v1.5.1

Published

OpenCode 2 plugin: Linux-style filesystem, text, and HTTP(S) tools under the fs namespace.

Readme

opencode-fs-plugin

An OpenCode 2 plugin that adds a Linux-style filesystem, text, and HTTP(S) toolkit under the fs namespace. It is a maintained fork of kiu345/opencode-fs-plugin, rewritten with async I/O, permission integration, a symlink-aware sandbox, and unit, end-to-end and coverage tests.

Tools

| Native command | Tool | Main options | |---|---|---| | mkdir | fs.mkdir | parents, mode, verbose | | rmdir | fs.rmdir | parents, ignoreNonEmpty, verbose | | rm | fs.rm | recursive, force, dir, verbose | | cp | fs.cp | recursive, archive, force, noClobber, update, preserve, dereference, verbose | | mv | fs.mv | force, noClobber, update, verbose | | ls | fs.ls | all, almostAll, long, human, recursive, directory, inode, classify, size, numericIds, noGroup, noOwner, slash, quote, versionSort, extensionSort, fullTime, timeStyle, hide, ignore, groupDirectoriesFirst, sortBy, reverse, onePerLine | | stat | fs.stat | follow, format, terse, filesystem | | touch | fs.touch | accessOnly, modifyOnly, noCreate, date, timestamp, reference, noDereference | | truncate | fs.truncate | size, reference, noCreate, noDereference | | ln | fs.ln | symbolic, force, noDereference, relative, verbose | | readlink | fs.readlink | mode (raw, canonical, existing, missing), noNewline | | chmod | fs.chmod | octal/symbolic mode, reference, recursive, noDereference, verbose, changes, silent | | sha256sum/md5sum | fs.hash | algorithm, tag; hashes files or stdin | | du | fs.du | summarize, all, human, total, maxDepth, follow, oneFileSystem | | df | fs.df | human, inodes, printType, all | | find | fs.find | name, iname, pathPattern, lname, ilname, regex, iregex, type, size, mtime, mmin, maxDepth, minDepth, empty, perm, uid, gid, user, group, links, inum, samefile, fstype, newer, newerXY, printf, follow, print0, sort | | cat | fs.cat | number, numberNonBlank, squeezeBlank, showEnds, showTabs, showNonPrinting | | head | fs.head | lines, bytes, quiet, verbose | | tail | fs.tail | lines, bytes, fromStart, quiet, verbose, follow, timeoutMs, intervalMs, maxBytes | | tee | fs.tee | append, content | | wc | fs.wc | lines, words, bytes, chars, maxLineLength | | sort | fs.sort | numeric, generalNumeric, humanNumeric, version, reverse, unique, ignoreCase, dictionary, ignoreNonPrinting, ignoreLeadingBlanks, stable, check, checkQuiet, zeroTerminated, keys, field, separator | | uniq | fs.uniq | count, repeated, uniqueOnly, ignoreCase, skipFields, skipChars | | cut | fs.cut | fields, characters, bytes, delimiter, outputDelimiter, complement, onlyDelimited | | tr | fs.tr | set1, set2, delete, squeeze, complement; ranges and classes | | sed | fs.sed | script, expressions, inPlace, extendedRegexp, quiet; s/// subset | | grep | fs.grep | recursive, include, exclude, ignoreCase, invert, lineNumbers, withFilename, noFilename, filesWithMatches, filesWithoutMatch, count, onlyMatching, word, line, fixed, context, before, after, maxCount, noMessages | | tar | fs.tar | create/extract/list, gzip, directory, stripComponents, exclude, verbose, input, member | | zip | fs.zip | archive, paths, level, input, member | | unzip | fs.unzip | extract/list, directory, overwrite, noOverwrite | | gzip | fs.gzip | keep, force, stdout, level | | gunzip | fs.gunzip | keep, force, stdout, suffix | | mktemp | fs.mktemp | template, directory, tmpdir, suffix | | trash | fs.trash | XDG home trash | | diff | fs.diff | unified, brief, recursive, ignoreWhitespace, ignoreCase, exclude | | chown | fs.chown | user/uid, group/gid, recursive, noDereference | | ps | fs.ps | pids, user, command, sortBy, limit, noHeader, format | | pgrep | fs.pgrep | pattern, full, ignoreCase, user, newest, oldest, count, delimiter | | kill | fs.kill | pids, signal, dryRun; pid 0/1 and groups rejected | | file | fs.file | paths, mime; content magic detection | | base64 | fs.base64 | decode, wrap, ignoreGarbage | | od | fs.od | offset, length, width | | strings | fs.strings | minLength | | id | fs.id | only (user, group, groups) | | uname | fs.uname | kernel, nodename, release, version, machine | | uptime | fs.uptime | pretty, loadAverage | | date | fs.date | value, utc, format (iso, unix, rfc, date) | | findmnt | fs.findmnt | paths, fsType, noHeader | | mountpoint | fs.mountpoint | paths, quiet | | tac | fs.tac | reverse lines | | rev | fs.rev | reverse characters per line | | nl | fs.nl | all, start, width, separator | | paste | fs.paste | delimiters, serial | | comm | fs.comm | suppress1/2/3, outputDelimiter | | split | fs.split | lines/bytes, prefix, suffixLength, numeric, additionalSuffix | | shuf | fs.shuf | headCount, zeroTerminated | | fold | fs.fold | width, spaces | | sync | fs.sync | paths; file/directory fsync | | dd | fs.dd | input, output, blockSize, count, skip, seek, status | | cpio | fs.cpio | create/list/extract (newc), directory, members, input, member, verbose | | ar | fs.ar | create/list/extract, directory, members | | HTTP(S) | fs.http_request | url, method, headers, body, json, output, checksum, maxBytes, timeoutMs, failOnError, followRedirects, maxRedirects | | shell pipe | fs.pipe | stages, input, output, append |

Pipe

fs.pipe joins registered tools through byte buffers. Stages run in order; only pipeable tools can be used. output and append behave like a final tee.

await tools.fs.pipe({
  stages: [
    { tool: "cat", args: { paths: ["app.log"] } },
    { tool: "grep", args: { pattern: "ERROR", lineNumbers: true } },
    { tool: "sort", args: { unique: true } },
  ],
  output: "errors.txt",
})

New filter stages: tac, rev, nl, paste, shuf, fold, base64, od, and strings; dd and cpio are source/sink stages like tar.

fs.http_request is a pipe source, so network data can feed the same stages:

await tools.fs.pipe({
  stages: [
    { tool: "http_request", args: { url: "https://example.com/app.log" } },
    { tool: "grep", args: { pattern: "ERROR" } },
  ],
})

Pipe stages exchange whole buffers, so use maxBytes for large responses. For large downloads prefer fs.http_request({ output }), which streams to disk.

There is no shell, redirection operator, nested pipe, or find -exec.

Performance and concurrency

All I/O is asynchronous; the plugin never calls a synchronous fs API.

The concurrency limit defaults to os.availableParallelism(). Override it with the plugin option concurrency or the OPENCODE_FS_CONCURRENCY environment variable (the option wins). It bounds every parallel path, so a large tree cannot start unbounded work:

  • cp/mv tree copies
  • hash, gzip, and gunzip over multiple files
  • grep file reads
  • find, du, grep -r, and chmod -R traversal, including directory recursion and per-entry stats
  • diff -r file comparisons
  • ls long-format rendering

gzip/gunzip also use the libuv thread pool for compression; raise UV_THREADPOOL_SIZE if compression dominates.

CPU-bound JavaScript stages (sort, sed, diff text patching) stay single-threaded. Splitting them would need worker threads and is a deliberate gap; their input/output cost is dominated by the async I/O above.

Background jobs

Every fs.* tool accepts background: true. The call returns immediately with a job id and a status-file path:

const started = JSON.parse(
  await tools.fs.cp({
    sources: ["large-tree"],
    destination: "backup",
    recursive: true,
    background: true,
  }),
)

Wait for it with fs.job, or let the agent continue and check later:

const result = await tools.fs.job({
  id: started.jobId,
  wait: true,
  timeoutMs: 600000,
})

fs.job also supports status, cancel, and list:

await tools.fs.job({ id: started.jobId })              // current status
await tools.fs.job({ id: started.jobId, cancel: true }) // abort
await tools.fs.job({ list: true })                     // newest first

The status file lets the wait plugin observe a job without coupling the two plugins:

await tools.wait.for({
  condition: {
    type: "file_contains",
    path: started.statusPath,
    pattern: '"state":"completed"',
  },
  timeoutMs: 600000,
})

Jobs are process-local: they are lost on an OpenCode server restart, and their status files may remain under /tmp/opencode/fs-jobs/<server-pid>/. The manager allows 8 concurrent jobs and cleans up finished jobs after one hour. Results stay in memory and are returned by fs.job; the status file contains only the job state and metadata. Background permission prompts can appear after the starting call returns, and a denied permission fails the job.

Installing from npm

Add the registry package directly to the plugin list:

{
  "plugins": ["[email protected]"]
}

opencode plugin add [email protected] installs it and writes the config entry. A bare opencode-fs-plugin follows the latest tag.

OpenCode manages the package under its cache, so the path is not stable. If you also want the packaged skill, install it into a project instead:

npm install [email protected]
{
  "plugins": ["./node_modules/opencode-fs-plugin"],
  "skills": ["./node_modules/opencode-fs-plugin/skills"]
}

Installing from a release

Download the release tarball, then install it as a normal npm package:

VERSION=1.5.1
curl -L -o "opencode-fs-plugin-${VERSION}.tgz" \
  "https://github.com/masterjg/opencode-fs-plugin/releases/download/v${VERSION}/opencode-fs-plugin-${VERSION}.tgz"
npm install "./opencode-fs-plugin-${VERSION}.tgz"

Point OpenCode at the installed directory:

{
  "plugins": ["./node_modules/opencode-fs-plugin"]
}

Relative paths resolve from the config file that contains them, so use an absolute path in the global config. The tarball contains src/, skills/, SPEC.md and INSTALL.md; npm installs its runtime dependencies. Do not unpack it by hand.

OpenCode does not accept the release asset URL or a .tgz file directly in plugins; install it with npm first and point at the installed directory.

Installing from a Git tag

Every release is tagged v<version>. Add the tag directly to the plugin list; OpenCode installs it from Git:

{
  "plugins": ["github:masterjg/opencode-fs-plugin#v1.5.1"]
}

opencode plugin add 'github:masterjg/opencode-fs-plugin#v1.5.1' installs the same spec and writes the config entry. The equivalent full URL is git+https://github.com/masterjg/opencode-fs-plugin.git#v1.5.1. A tag is checked for updates; use a full commit hash to pin it.

Git plugins are installed under OpenCode's cache, so their package path is not stable. If you also want the packaged skill, install with npm instead:

npm install "github:masterjg/opencode-fs-plugin#v1.5.1"

Then use ./node_modules/opencode-fs-plugin in plugins and ./node_modules/opencode-fs-plugin/skills in skills.

Loading from a checkout

{
  "plugins": [
    "./repos/opencode-fs-plugin"
  ]
}

The plugin exports TypeScript source directly and targets Node 22+.

Skill discovery

The plugin ships skills/filesystem/SKILL.md. OpenCode does not search inside a plugin package automatically, so register the directory once:

{
  "skills": [
    "~/.config/opencode/repos/opencode-fs-plugin/skills"
  ]
}

If the plugin was installed from the release tarball, the npm registry, or with npm install from a Git tag into a project's node_modules, point at the packaged skill instead:

{
  "skills": [
    "./node_modules/opencode-fs-plugin/skills"
  ]
}

Relative skill paths resolve from the active working directory, not the config file; use an absolute path for a global install. Alternatively, symlink skills/filesystem into ~/.config/opencode/skills; OpenCode discovers that directory automatically. See INSTALL.md for all options, verification, and an agent-ready installation prompt.

The skill ID is filesystem; @filesystem loads it explicitly.

Permissions

Every tool asserts an action named after the tool (fs_cp, fs_rm, fs_pipe stages, and so on). Internal paths use project-relative resources; external targets first require external_directory on the absolute boundary. allow always persists the same patterns as the built-in tools.

fs.http_request is allowed by default, like fetch. Its resource is the request host (example.com:8443), so requests can be restricted per host:

{
  "permissions": [
    { "action": "fs_http_request", "resource": "api.github.com", "effect": "allow" },
    { "action": "fs_http_request", "resource": "*", "effect": "ask" }
  ]
}

Redirects re-assert the rule against each new host.

Agent rules can use wildcards:

{
  "permissions": [
    { "action": "fs_*", "resource": "*", "effect": "deny" }
  ]
}

To direct agents to this toolkit instead of the built-in shell, deny shell:

{
  "permissions": [
    { "action": "shell", "resource": "*", "effect": "deny" }
  ]
}

This removes the built-in shell tool; MCP servers and other tools that run commands are unaffected. Apply it globally or under agents.<id>.permissions when only some agents should be shell-free.

Safety

  • The sandbox root, its ancestors, /, $HOME and core system directories are hard stops for destructive operations.
  • Paths are resolved with symlink awareness. Traversing a link that escapes the project requires external_directory; removing or renaming the link itself is internal.
  • Archive extraction rejects absolute paths, .., and escaping symlinks.
  • http_request accepts only http: and https:, keeps TLS verification on, re-checks permissions on redirect hops, and writes downloads to a temporary file before renaming. Localhost and private addresses are not blocked by default.
  • rm is permanent. trash uses the XDG home trash and refuses cross-device moves instead of silently copying.
  • The sandbox is a guardrail, not a kernel security boundary. Symlink races remain possible.

Development

See AGENTS.md for the engineering rules: async-only I/O, core-count concurrency, and where every independent operation must be parallelized.

npm install
npm run typecheck
npm test
npm run coverage

npm run coverage enforces 100% statements, branches, functions and lines. A small number of OS-error branches are marked with /* v8 ignore next */ when they cannot be produced deterministically; all tool logic and normal error paths are exercised.

Releases

Every push to main runs .github/workflows/release.yml. It reads the version from package.json, runs typecheck and the 100% coverage gate, packs the npm tarball, uploads it as a workflow artifact, publishes it to npm with provenance when the version is new, and creates a GitHub release tagged v<version> when that release does not exist yet. Bump the version in package.json to publish a new release; an unchanged version is skipped with a notice.

The npm step uses trusted publishing (OIDC); no npm token is stored. The package's trusted publisher must authorize workflow release.yml in masterjg/opencode-fs-plugin. Keep Allow npm publish enabled on that connection: the workflow uses npm publish, not npm stage publish, so releases publish directly without per-version approval.

License

EPL-2.0. The original project and this fork remain under the same license.