Compare commits

..

45 Commits

Author SHA1 Message Date
openclaw-ci
76225ee7e3 update tool releases 2026-05-08 16:16:44 +00:00
openclaw-ci
0dea6487c4 update tool releases 2026-05-08 14:12:49 +00:00
openclaw-ci
dde43ea049 update tool releases 2026-05-08 05:12:01 +00:00
openclaw-ci
747ce88d6d update tool releases 2026-05-08 04:05:44 +00:00
openclaw-ci
8f288290e9 update tool releases 2026-05-06 22:26:05 +00:00
joshp123
4c1cee3c7e Fix QMD Darwin package on Garnix
Add xcbuild to the Darwin QMD build inputs so node-gyp can satisfy its xcodebuild version probe inside the sandbox instead of relying on host CLT state.

Tests: nix build .#checks.aarch64-darwin.qmd-smoke --no-link --accept-flake-config; nix shell nixpkgs#go --command go test ./...; git diff --check
2026-05-06 11:34:01 +02:00
openclaw-ci
4d1adfbdd7 update tool releases 2026-05-06 09:12:11 +00:00
joshp123
a0e7ac5ef1 package qmd for OpenClaw tools
What:
- add a source-built qmd package and plugin metadata
- add a no-model-download qmd smoke check
- teach update-tools to notice qmd releases and the new sonoscli asset names

Why:
- nix-openclaw needs QMD as an internal runtime battery on Darwin and Linux
- the maintainer automation should not require a separate manual qmd bump path

Tests:
- nix build .#qmd .#checks.aarch64-darwin.qmd-smoke --accept-flake-config --no-link
- nix shell nixpkgs#go --command go test ./...
2026-05-06 09:31:57 +02:00
joshp123
1732fb7a5c docs: define tool packaging boundaries 2026-05-06 08:52:42 +02:00
joshp123
08955054f4 use relative root for tool plugin flakes
Point per-tool plugin flakes at the repository root with a relative input so bundled plugin evaluation uses the same source tree instead of stale historical nix-openclaw-tools commits.

Tests: go test ./...; nix flake show --all-systems; nix build 'git+file:///Users/josh/code/nix-openclaw-tools?dir=tools/goplaces#packages.aarch64-darwin.goplaces' --no-link
2026-05-05 12:13:40 +02:00
joshp123
675f4ba420 rename OpenClaw tools flake
Move the flake identity from nix-steipete-tools to nix-openclaw-tools, update transferred upstream repos to openclaw, and drop stale CodexBar/bird packaging.

Tests: go test ./...; nix flake show --all-systems; nix build .#gogcli .#goplaces .#summarize .#camsnap .#sonoscli --no-link
2026-05-05 12:04:06 +02:00
clawdbot-ci
76bccdda2d update tool releases 2026-05-05 09:23:07 +00:00
clawdbot-ci
3bc73a214a update tool releases 2026-05-05 06:16:42 +00:00
clawdbot-ci
388231a52e update tool releases 2026-05-05 05:25:08 +00:00
clawdbot-ci
38650966bf update tool releases 2026-05-05 01:10:35 +00:00
clawdbot-ci
c94e18bc15 update tool releases 2026-05-04 03:05:05 +00:00
clawdbot-ci
620dc9666c update tool releases 2026-05-03 15:44:43 +00:00
Peter Steinberger
433c7b06af
fix: match peekaboo arm64 release asset 2026-05-03 16:40:35 +01:00
clawdbot-ci
de56f637f2 update tool releases 2026-04-27 07:17:00 +00:00
Dave Dennis
3103831ea1
feat: add CodexBar app package and module
Thanks @0xdsqr for the contribution.

Co-authored-by: 0xdsqr <me@dsqr.dev>
2026-04-27 05:57:45 +01:00
clawdbot-ci
1d8fe51b1e update tool releases 2026-04-26 06:02:42 +00:00
clawdbot-ci
81cbbedb53 update tool releases 2026-04-26 02:49:32 +00:00
Dave Dennis
26a27195ca
feat: add crawling tool plugins
Adds discrawl and wacrawl packages, plugin flakes, skills, updater wiring, and README entries.
2026-04-26 03:16:51 +01:00
Dan Sanduleac
3584e2b6d2
chore: add summarize subflake lock
Adds the committed lock file from PR #8.
2026-04-26 03:04:42 +01:00
Peter Steinberger
2afbbd55bf
fix: disable broken bird package export 2026-04-26 03:03:55 +01:00
Peter Steinberger
457023efd4
fix: resolve package build blockers 2026-04-26 03:02:50 +01:00
clawdbot-ci
3e8e39bd85 sync skills from clawdbot 2026-04-23 21:47:04 +00:00
clawdbot-ci
9647ce7fcb update tool releases 2026-04-22 18:22:21 +00:00
clawdbot-ci
65a58f8367 update tool releases 2026-04-20 22:27:49 +00:00
clawdbot-ci
d17f197c47 update tool releases 2026-04-08 00:02:19 +00:00
clawdbot-ci
5f677a283d sync skills from clawdbot 2026-03-23 04:19:03 +00:00
clawdbot-ci
cd4c429ff3 sync skills from clawdbot 2026-03-15 07:59:40 +00:00
clawdbot-ci
526067c585 update tool releases 2026-03-12 02:36:51 +00:00
clawdbot-ci
9392ba9ac6 sync skills from clawdbot 2026-03-11 14:26:05 +00:00
clawdbot-ci
561592b0b1 update tool releases 2026-03-09 06:43:03 +00:00
clawdbot-ci
2b97c49e03 update tool releases 2026-02-26 12:46:07 +00:00
joshp123
50194a9b8e fix(gogcli-plugin): repin tool flake root to openclaw rev with 0.11.1
Why
- tools/gogcli was pinned to an older root rev, so plugin output still resolved
  gogcli 0.9.0 even after package bump.

What
- update tools/gogcli root input to openclaw/nix-steipete-tools rev eddb00d...
- refresh tools/gogcli/flake.lock root input to same rev

Tests
- cd tools/gogcli && nix build .#gogcli
- cd tools/gogcli && ./result/bin/gog version  # 0.11.1
2026-02-26 13:42:18 +01:00
joshp123
eddb00d4c1 chore(gogcli): pin to joshp123 fork v0.11.1
Why
- Need immediate delivery of refresh-token rotation persistence fix before upstream release.
- Keep existing binary-tar packaging flow (no package-model changes).

What
- bump gogcli package version 0.11.0 -> 0.11.1
- switch release artifact URLs to joshp123/gogcli v0.11.1
- update SRI hashes for darwin arm64, linux amd64, linux arm64 assets

Tests
- nix build .#gogcli
- ./result/bin/gog version
2026-02-26 13:40:02 +01:00
clawdbot-ci
95ebfa73f4 sync skills from clawdbot 2026-02-21 02:00:17 +00:00
joshp123
c110209720 🤖 chore: add lock files for goplaces/gogcli plugin flakes
What:
- add tools/gogcli/flake.lock
- add tools/goplaces/flake.lock

Why:
- avoid ad-hoc lockfile mutation warnings when these plugin flakes are evaluated as inputs
- keep plugin flake inputs deterministic for downstream consumers

Tests:
- nix flake lock (tools/gogcli)
- nix flake lock (tools/goplaces)
2026-02-18 09:25:35 -08:00
joshp123
7cd25c98bb 🤖 refactor: remove oracle from nix-steipete-tools
What:
- delete oracle package and plugin exports
- remove oracle from tool update and skill sync pipelines
- remove oracle mentions from README

Why:
- oracle is no longer part of the bundled toolchain
- keep package/update surface aligned with active bundled plugins only

Tests:
- go test ./... (pass)
- nix flake check --no-build (pass)
2026-02-17 19:43:35 -08:00
clawdbot-ci
d5fc8e2b07 sync skills from clawdbot 2026-02-18 02:22:05 +00:00
clawdbot-ci
b668498d7d update tool releases 2026-02-16 06:57:14 +00:00
clawdbot-ci
90516869c1 update tool releases 2026-02-15 04:04:37 +00:00
joshp123
c718196c26 feat: package goplaces from steipete releases + auto-update 2026-02-14 19:16:00 -08:00
50 changed files with 1296 additions and 817 deletions

View File

@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0
- name: Sync skills from clawdbot
- name: Sync skills from OpenClaw
run: go run ./cmd/sync-skills
- name: Commit & push if changed
@ -26,8 +26,8 @@ jobs:
echo "No changes"
exit 0
fi
git config user.name "clawdbot-ci"
git config user.email "ci@clawdbot"
git config user.name "openclaw-ci"
git config user.email "ci@openclaw.local"
git add -A
git commit -m "sync skills from clawdbot"
git commit -m "sync skills from openclaw"
git push

View File

@ -31,8 +31,8 @@ jobs:
echo "No changes"
exit 0
fi
git config user.name "clawdbot-ci"
git config user.email "ci@clawdbot"
git config user.name "openclaw-ci"
git config user.email "ci@openclaw.local"
git add -A
git commit -m "update tool releases"
git push

31
AGENTS.md Normal file
View File

@ -0,0 +1,31 @@
# AGENTS.md — nix-openclaw-tools
This repo packages OpenClaw-adjacent CLI tools and plugin metadata. It is part of the public OpenClaw Nix packaging boundary, not Josh's private machine config.
## Boundary Model
- OpenClaw owns product/runtime behavior.
- nix-openclaw owns the batteries-included Nix module surface, runtime injection, launchd/systemd wiring, and package-contract checks.
- nix-openclaw-tools owns reproducible packages for tool CLIs and the plugin metadata/skills that describe them.
- nixos-config and other downstream machine repos should only choose hosts, secrets, accounts, and which plugins are enabled. If a downstream repo has to build a tool, copy a tool package, or hand-wire a tool into an agent PATH, fix this repo or nix-openclaw instead.
## Packaging Rules
- Packages must be Garnix-cacheable for supported systems. Do not rely on Homebrew, globally installed Node/Python, Xcode state outside Nix, or runtime `npm`/`npx`/`uvx`.
- Prefer upstream release assets when they exist. Build from source only when release assets do not exist or are not suitable.
- If a tool is a first-party OpenClaw battery, add/update package, plugin metadata, checks, and auto-update logic together.
- QMD belongs here when packaged for Nix OpenClaw. The package should provide the `qmd` CLI; nix-openclaw decides how to inject it into the OpenClaw runtime.
- Do not expose tools globally by policy from this repo. Export packages and plugin metadata; let nix-openclaw place them on the correct agent/runtime PATH.
## Automation
- `cmd/update-tools` is the release-asset updater. Keep it resilient: one upstream tool changing asset names should not silently rot the whole repo.
- QMD freshness should be checked by automation too. Josh should not have to remember a separate QMD bump path.
- `cmd/sync-skills` tracks OpenClaw skills from upstream main.
- Garnix should build package checks for every supported package/system after update commits.
## Workflow
- Trunk-based development on `main`.
- Keep commits small and logical.
- Run targeted package checks before committing; use CI/Garnix for expensive cross-platform proof.

View File

@ -1,36 +1,50 @@
# nix-steipete-tools
# nix-openclaw-tools
> Core tools for openclaw. Batteries included. Always fresh.
> Reproducible OpenClaw tool packaging.
Nix packaging for [Peter Steinberger's](https://github.com/steipete) tools, with per-tool openclaw plugins. Part of the [nix-openclaw](https://github.com/openclaw/nix-openclaw) ecosystem.
Nix packaging for OpenClaw-adjacent tools, with per-tool OpenClaw plugin metadata. Part of the [nix-openclaw](https://github.com/openclaw/nix-openclaw) ecosystem.
Darwin/aarch64 plus Linux (x86_64/aarch64) for tools that ship Linux builds.
On Linux, `summarize` is built from source (Node 22 + pnpm) since upstream only ships a macOS Bun binary.
## Why this exists
These tools are essential for a capable openclaw instance - screen capture, camera access, TTS, messaging. Packaging them as Nix flakes with openclaw plugin metadata means:
OpenClaw should have a small default toolchain and explicit optional plugins. Packaging these tools as Nix flakes with OpenClaw plugin metadata means:
- **Reproducible**: Pinned versions, no Homebrew drift
- **Declarative**: Add a plugin, `home-manager switch`, done
- **Fresh**: CI keeps tools and skills at latest automatically
- **Integrated**: Skills teach your bot how to use each tool
## Pure Nix and Home Manager design
This repo is meant to be consumable directly from Nix flakes, Darwin, and Home
Manager configs. Packages should stay pinned and reproducible, and optional
Home Manager modules should expose normal `programs.<name>` options instead of
requiring downstream users to hand-write app bundle or launchd glue.
For macOS app bundles, prefer a Nix package plus Home Manager integration. Home
Manager can expose app bundles from `home.packages` through its Darwin app
targets, and modules can opt into `launchd.agents` when Home Manager should own
startup. Homebrew casks are still useful, but they belong in a nix-darwin
Homebrew configuration, not in these pure Nix package/module definitions.
## What's included
| Tool | What it does |
|------|--------------|
| [**summarize**](https://github.com/steipete/summarize) | Link → clean text → summary |
| [**gogcli**](https://github.com/steipete/gogcli) | Google CLI for Gmail, Calendar, Drive, and Contacts |
| [**goplaces**](https://github.com/steipete/goplaces) | Google Places API (New) CLI |
| [**discrawl**](https://github.com/openclaw/discrawl) | Mirror Discord into SQLite and search history locally |
| [**wacrawl**](https://github.com/steipete/wacrawl) | Read-only local archive and search for WhatsApp Desktop data |
| [**gogcli**](https://github.com/openclaw/gogcli) | Google CLI for Gmail, Calendar, Drive, and Contacts |
| [**goplaces**](https://github.com/openclaw/goplaces) | Google Places API (New) CLI |
| [**camsnap**](https://github.com/steipete/camsnap) | Capture snapshots/clips from RTSP/ONVIF cameras |
| [**sonoscli**](https://github.com/steipete/sonoscli) | Control Sonos speakers |
| [**bird**](https://github.com/steipete/bird) | Fast X CLI for tweeting, replying, and reading |
| [**peekaboo**](https://github.com/steipete/peekaboo) | Lightning-fast macOS screenshots & AI vision analysis |
| [**peekaboo**](https://github.com/openclaw/Peekaboo) | Lightning-fast macOS screenshots & AI vision analysis |
| [**poltergeist**](https://github.com/steipete/poltergeist) | Universal file watcher with auto-rebuild |
| [**sag**](https://github.com/steipete/sag) | Command-line ElevenLabs TTS with mac-style flags |
| [**imsg**](https://github.com/steipete/imsg) | iMessage/SMS CLI |
| [**oracle**](https://github.com/steipete/oracle) | Bundle prompts + files for AI queries |
| [**imsg**](https://github.com/openclaw/imsg) | iMessage/SMS CLI |
| [**qmd**](https://github.com/tobi/qmd) | On-device hybrid search for markdown knowledge bases |
## Usage (as openclaw plugins)
@ -38,9 +52,11 @@ Each tool is a subflake under `tools/<tool>/` exporting `openclawPlugin`. Point
```nix
programs.openclaw.plugins = [
{ source = "github:openclaw/nix-steipete-tools?dir=tools/camsnap"; }
{ source = "github:openclaw/nix-steipete-tools?dir=tools/peekaboo"; }
{ source = "github:openclaw/nix-steipete-tools?dir=tools/summarize"; }
{ source = "github:openclaw/nix-openclaw-tools?dir=tools/camsnap"; }
{ source = "github:openclaw/nix-openclaw-tools?dir=tools/discrawl"; }
{ source = "github:openclaw/nix-openclaw-tools?dir=tools/peekaboo"; }
{ source = "github:openclaw/nix-openclaw-tools?dir=tools/summarize"; }
{ source = "github:openclaw/nix-openclaw-tools?dir=tools/wacrawl"; }
];
```
@ -54,17 +70,21 @@ Each plugin bundles:
If you just want the binaries without the plugin wrapper:
```nix
inputs.nix-steipete-tools.url = "github:openclaw/nix-steipete-tools";
inputs.nix-openclaw-tools.url = "github:openclaw/nix-openclaw-tools";
# Then use:
inputs.nix-steipete-tools.packages.aarch64-darwin.camsnap
inputs.nix-steipete-tools.packages.aarch64-darwin.peekaboo
inputs.nix-openclaw-tools.packages.aarch64-darwin.camsnap
inputs.nix-openclaw-tools.packages.aarch64-darwin.discrawl
inputs.nix-openclaw-tools.packages.aarch64-darwin.peekaboo
inputs.nix-openclaw-tools.packages.aarch64-darwin.wacrawl
# etc.
# Linux examples:
inputs.nix-steipete-tools.packages.x86_64-linux.camsnap
inputs.nix-steipete-tools.packages.aarch64-linux.gogcli
inputs.nix-steipete-tools.packages.x86_64-linux.summarize
inputs.nix-openclaw-tools.packages.x86_64-linux.camsnap
inputs.nix-openclaw-tools.packages.x86_64-linux.discrawl
inputs.nix-openclaw-tools.packages.aarch64-linux.gogcli
inputs.nix-openclaw-tools.packages.x86_64-linux.summarize
inputs.nix-openclaw-tools.packages.x86_64-linux.wacrawl
```
## Skills syncing
@ -85,7 +105,11 @@ Tools track upstream GitHub releases directly (not Homebrew).
go run ./cmd/update-tools
```
Fetches latest release versions/URLs/hashes and updates the Nix expressions. Oracle uses pnpm and auto-derives its hash via build mismatch.
Fetches latest release versions/URLs/hashes and updates the Nix expressions.
QMD is source-packaged because upstream does not publish release assets. Keep it
fresh through the same maintainer automation path, but do not make its smoke
check pull embedding/reranking models; model prewarming belongs in nix-openclaw.
## CI
@ -95,7 +119,7 @@ Fetches latest release versions/URLs/hashes and updates the Nix expressions. Ora
| **update-tools** | Every 10 min | Checks for new tool releases |
| **Garnix** | On push | Builds all packages via `checks.*` (darwin + linux) |
Automated PRs keep everything fresh without manual intervention.
Automation commits directly when versions or skills change.
## License

View File

@ -52,7 +52,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
workdir, err := os.MkdirTemp("", "clawdbot-skills-")
workdir, err := os.MkdirTemp("", "openclaw-skills-")
if err != nil {
log.Fatal(err)
}
@ -60,18 +60,18 @@ func main() {
mappings := []Mapping{
{"summarize", "skills/summarize"},
{"discrawl", "skills/discrawl"},
{"wacrawl", "skills/wacrawl"},
{"gogcli", "skills/gog"},
{"camsnap", "skills/camsnap"},
{"sonoscli", "skills/sonoscli"},
{"bird", "skills/bird"},
{"peekaboo", "skills/peekaboo"},
{"sag", "skills/sag"},
{"imsg", "skills/imsg"},
{"oracle", "skills/oracle"},
}
log.Printf("[sync-skills] cloning clawdbot main")
if err := run("", "git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", "https://github.com/clawdbot/clawdbot.git", workdir); err != nil {
log.Printf("[sync-skills] cloning openclaw main")
if err := run("", "git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", "https://github.com/openclaw/openclaw.git", workdir); err != nil {
log.Fatal(err)
}
paths := []string{}

View File

@ -2,14 +2,16 @@ package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/clawdbot/nix-steipete-tools/internal"
"github.com/openclaw/nix-openclaw-tools/internal"
)
type Tool struct {
@ -68,6 +70,107 @@ func updateSourceBlock(path, system, url, hash string) error {
})
}
func readVersion(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
match := regexp.MustCompile(`version = "([^"]+)";`).FindStringSubmatch(string(data))
if len(match) < 2 {
return "", fmt.Errorf("version not found in %s", path)
}
return match[1], nil
}
func fetchText(url string) (string, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", err
}
if token := os.Getenv("GH_TOKEN"); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("fetch %s: %s: %s", url, resp.Status, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func qmdNodeModulesHash(upstreamFlake, system string) (string, error) {
re := regexp.MustCompile(fmt.Sprintf(`"?%s"?\s*=\s*"([^"]+)";`, regexp.QuoteMeta(system)))
match := re.FindStringSubmatch(upstreamFlake)
if len(match) < 2 {
return "", fmt.Errorf("qmd nodeModules hash for %s not found upstream", system)
}
hash := match[1]
if strings.Contains(hash, "AAAAAAAA") || strings.Contains(hash, "fake") {
return "", fmt.Errorf("qmd nodeModules hash for %s is not populated upstream", system)
}
return hash, nil
}
func updateQMD(repoRoot string) error {
log.Printf("[update-tools] qmd")
qmdFile := filepath.Join(repoRoot, "nix", "pkgs", "qmd.nix")
currentVersion, err := readVersion(qmdFile)
if err != nil {
return err
}
rel, err := internal.LatestRelease("tobi/qmd")
if err != nil {
return err
}
version := strings.TrimPrefix(rel.TagName, "v")
if currentVersion == version {
return nil
}
srcHash, err := internal.PrefetchGitHub("tobi", "qmd", "v"+version)
if err != nil {
return err
}
upstreamFlake, err := fetchText(fmt.Sprintf("https://raw.githubusercontent.com/tobi/qmd/v%s/flake.nix", version))
if err != nil {
return err
}
nodeHashes := map[string]string{}
for _, system := range []string{"aarch64-darwin", "x86_64-linux"} {
hash, err := qmdNodeModulesHash(upstreamFlake, system)
if err != nil {
return err
}
nodeHashes[system] = hash
}
if err := internal.ReplaceOnce(qmdFile, regexp.MustCompile(`version = "[^"]+";`), fmt.Sprintf(`version = "%s";`, version)); err != nil {
return err
}
srcRe := regexp.MustCompile(`(?s)src = fetchFromGitHub \{.*?hash = "sha256-[^"]+";`)
if err := internal.ReplaceOnceFunc(qmdFile, srcRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, fmt.Sprintf(`hash = "%s";`, srcHash))
}); err != nil {
return err
}
for system, hash := range nodeHashes {
re := regexp.MustCompile(fmt.Sprintf(`"%s" = "sha256-[^"]+";`, regexp.QuoteMeta(system)))
if err := internal.ReplaceOnce(qmdFile, re, fmt.Sprintf(`"%s" = "%s";`, system, hash)); err != nil {
return err
}
}
return nil
}
func updateSummarize(repoRoot string) error {
log.Printf("[update-tools] summarize")
summarizeFile := filepath.Join(repoRoot, "nix", "pkgs", "summarize.nix")
@ -140,85 +243,6 @@ func updateSummarize(repoRoot string) error {
return nil
}
func updateOracle(repoRoot string) error {
log.Printf("[update-tools] oracle")
oracleFile := filepath.Join(repoRoot, "nix", "pkgs", "oracle.nix")
orig, err := os.ReadFile(oracleFile)
if err != nil {
return err
}
rel, err := internal.LatestRelease("steipete/oracle")
if err != nil {
return err
}
version := strings.TrimPrefix(rel.TagName, "v")
var assetURL string
for _, a := range rel.Assets {
if matched, _ := regexp.MatchString(`oracle-[0-9.]+\.tgz`, a.Name); matched {
assetURL = a.BrowserDownloadURL
break
}
}
if assetURL == "" {
return fmt.Errorf("no asset matched for oracle")
}
assetHash, err := internal.PrefetchHash(assetURL)
if err != nil {
return err
}
lockHash, err := internal.PrefetchGitHub("steipete", "oracle", rel.TagName)
if err != nil {
return err
}
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`version = "[^"]+";`), fmt.Sprintf(`version = "%s";`, version)); err != nil {
return err
}
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`url = "[^"]+";`), fmt.Sprintf(`url = "%s";`, assetURL)); err != nil {
return err
}
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`hash = "sha256-[^"]+";`), fmt.Sprintf(`hash = "%s";`, assetHash)); err != nil {
return err
}
lockRe := regexp.MustCompile(`(?s)lockSrc = fetchFromGitHub \{[^}]*hash = "sha256-[^"]+";`)
if err := internal.ReplaceOnceFunc(oracleFile, lockRe, func(s string) string {
out := regexp.MustCompile(`rev = "[^"]+";`).ReplaceAllString(s, fmt.Sprintf(`rev = "%s";`, rel.TagName))
out = regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(out, fmt.Sprintf(`hash = "%s";`, lockHash))
return out
}); err != nil {
return err
}
pnpmRe := regexp.MustCompile(`(?s)pnpmDeps.*hash = "sha256-[^"]+";`)
if err := internal.ReplaceOnceFunc(oracleFile, pnpmRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, `hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";`)
}); err != nil {
return err
}
log.Printf("[update-tools] oracle: deriving pnpm hash")
logText, buildErr := internal.NixBuildOracle()
pnpmHash := internal.ExtractGotHash(logText)
if pnpmHash == "" {
// Restore original file so we don't leave a broken placeholder hash behind.
_ = os.WriteFile(oracleFile, orig, 0644)
// Surface some context in CI logs. This is usually a hash-mismatch line we failed to parse.
lines := strings.Split(logText, "\n")
start := 0
if len(lines) > 200 {
start = len(lines) - 200
}
log.Printf("[update-tools] oracle build output (last %d lines):\n%s", len(lines)-start, strings.Join(lines[start:], "\n"))
return fmt.Errorf("oracle pnpm hash not found (build err: %v)", buildErr)
}
if err := internal.ReplaceOnceFunc(oracleFile, pnpmRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, fmt.Sprintf(`hash = "%s";`, pnpmHash))
}); err != nil {
return err
}
return nil
}
func main() {
repoRoot, err := os.Getwd()
if err != nil {
@ -226,9 +250,29 @@ func main() {
}
tools := []Tool{
{
Name: "discrawl",
Repo: "openclaw/discrawl",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`discrawl_[0-9.]+_darwin_arm64\.tar\.gz`)},
{System: "x86_64-linux", Regex: regexp.MustCompile(`discrawl_[0-9.]+_linux_amd64\.tar\.gz`)},
{System: "aarch64-linux", Regex: regexp.MustCompile(`discrawl_[0-9.]+_linux_arm64\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "discrawl.nix"),
},
{
Name: "wacrawl",
Repo: "steipete/wacrawl",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`wacrawl_[0-9.]+_darwin_arm64\.tar\.gz`)},
{System: "x86_64-linux", Regex: regexp.MustCompile(`wacrawl_[0-9.]+_linux_amd64\.tar\.gz`)},
{System: "aarch64-linux", Regex: regexp.MustCompile(`wacrawl_[0-9.]+_linux_arm64\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "wacrawl.nix"),
},
{
Name: "gogcli",
Repo: "steipete/gogcli",
Repo: "openclaw/gogcli",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`gogcli_[0-9.]+_darwin_arm64\.tar\.gz`)},
{System: "x86_64-linux", Regex: regexp.MustCompile(`gogcli_[0-9.]+_linux_amd64\.tar\.gz`)},
@ -236,6 +280,17 @@ func main() {
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "gogcli.nix"),
},
{
Name: "goplaces",
Repo: "openclaw/goplaces",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`goplaces_[0-9.]+_darwin_arm64\.tar\.gz`)},
{System: "x86_64-darwin", Regex: regexp.MustCompile(`goplaces_[0-9.]+_darwin_amd64\.tar\.gz`)},
{System: "x86_64-linux", Regex: regexp.MustCompile(`goplaces_[0-9.]+_linux_amd64\.tar\.gz`)},
{System: "aarch64-linux", Regex: regexp.MustCompile(`goplaces_[0-9.]+_linux_arm64\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "goplaces.nix"),
},
{
Name: "camsnap",
Repo: "steipete/camsnap",
@ -250,26 +305,17 @@ func main() {
Name: "sonoscli",
Repo: "steipete/sonoscli",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`sonoscli-macos-arm64\.tar\.gz`)},
{System: "aarch64-darwin", Regex: regexp.MustCompile(`sonoscli_[0-9.]+_darwin_arm64\.tar\.gz`)},
{System: "x86_64-linux", Regex: regexp.MustCompile(`sonoscli_[0-9.]+_linux_amd64\.tar\.gz`)},
{System: "aarch64-linux", Regex: regexp.MustCompile(`sonoscli_[0-9.]+_linux_arm64\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "sonoscli.nix"),
},
{
Name: "bird",
Repo: "steipete/bird",
Optional: true, // repo got nuked; keep packaging pinned, but don't fail the updater
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`bird-macos-universal-v[0-9.]+\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "bird.nix"),
},
{
Name: "peekaboo",
Repo: "steipete/peekaboo",
Repo: "openclaw/Peekaboo",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`peekaboo-macos-universal\.tar\.gz`)},
{System: "aarch64-darwin", Regex: regexp.MustCompile(`peekaboo-macos-(?:arm64|universal)\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "peekaboo.nix"),
},
@ -292,7 +338,7 @@ func main() {
},
{
Name: "imsg",
Repo: "steipete/imsg",
Repo: "openclaw/imsg",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`imsg-macos\.zip`)},
},
@ -303,6 +349,9 @@ func main() {
if err := updateSummarize(repoRoot); err != nil {
log.Fatalf("update summarize failed: %v", err)
}
if err := updateQMD(repoRoot); err != nil {
log.Fatalf("update qmd failed: %v", err)
}
for _, tool := range tools {
if err := updateTool(tool); err != nil {
if tool.Optional {
@ -313,9 +362,4 @@ func main() {
}
}
if err := updateOracle(repoRoot); err != nil {
// Oracle releases occasionally ship with an out-of-date pnpm-lock.yaml.
// In that case, we keep the previously pinned version and still update other tools.
log.Printf("[update-tools] skipping oracle update: %v", err)
}
}

View File

@ -0,0 +1,33 @@
package main
import "testing"
func TestQMDNodeModulesHash(t *testing.T) {
upstream := `
nodeModulesHashes = {
x86_64-linux = "sha256-linux";
aarch64-darwin = "sha256-darwin";
};
`
got, err := qmdNodeModulesHash(upstream, "aarch64-darwin")
if err != nil {
t.Fatal(err)
}
if got != "sha256-darwin" {
t.Fatalf("got %q", got)
}
}
func TestQMDNodeModulesHashRejectsFake(t *testing.T) {
upstream := `
nodeModulesHashes = {
aarch64-darwin = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
`
_, err := qmdNodeModulesHash(upstream, "aarch64-darwin")
if err == nil {
t.Fatal("expected fake hash to be rejected")
}
}

View File

@ -1,5 +1,5 @@
{
description = "Nix packaging for steipete tools (clawdbot)";
description = "Nix packaging for OpenClaw tools";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
@ -12,16 +12,17 @@
forAllSystems = f: lib.genAttrs systems (system: f system);
packageSystems = {
summarize = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
discrawl = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
wacrawl = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
gogcli = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
goplaces = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
camsnap = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
sonoscli = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
bird = [ "aarch64-darwin" ];
peekaboo = [ "aarch64-darwin" ];
poltergeist = [ "aarch64-darwin" ];
sag = [ "aarch64-darwin" "x86_64-linux" ];
imsg = [ "aarch64-darwin" ];
oracle = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
qmd = [ "aarch64-darwin" "x86_64-linux" ];
};
in {
packages = forAllSystems (system:
@ -36,6 +37,12 @@
nodejs = if pkgs ? nodejs_22 then pkgs.nodejs_22 else pkgs.nodejs;
};
})
// (lib.optionalAttrs (supports "discrawl") {
discrawl = pkgs.callPackage ./nix/pkgs/discrawl.nix {};
})
// (lib.optionalAttrs (supports "wacrawl") {
wacrawl = pkgs.callPackage ./nix/pkgs/wacrawl.nix {};
})
// (lib.optionalAttrs (supports "gogcli") {
gogcli = pkgs.callPackage ./nix/pkgs/gogcli.nix {};
})
@ -48,9 +55,6 @@
// (lib.optionalAttrs (supports "sonoscli") {
sonoscli = pkgs.callPackage ./nix/pkgs/sonoscli.nix {};
})
// (lib.optionalAttrs (supports "bird") {
bird = pkgs.callPackage ./nix/pkgs/bird.nix {};
})
// (lib.optionalAttrs (supports "peekaboo") {
peekaboo = pkgs.callPackage ./nix/pkgs/peekaboo.nix {};
})
@ -63,14 +67,22 @@
// (lib.optionalAttrs (supports "imsg") {
imsg = pkgs.callPackage ./nix/pkgs/imsg.nix {};
})
// (lib.optionalAttrs (supports "oracle") {
oracle = pkgs.callPackage ./nix/pkgs/oracle.nix {
pkgs = pkgs;
pnpm = if pkgs ? pnpm_10 then pkgs.pnpm_10 else pkgs.pnpm;
};
// (lib.optionalAttrs (supports "qmd") {
qmd = pkgs.callPackage ./nix/pkgs/qmd.nix {};
})
);
checks = forAllSystems (system: self.packages.${system});
checks = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
packages = self.packages.${system};
in
packages
// (lib.optionalAttrs (packages ? qmd) {
qmd-smoke = pkgs.callPackage ./nix/checks/qmd-smoke.nix {
qmd = packages.qmd;
};
})
);
};
}

2
go.mod
View File

@ -1,3 +1,3 @@
module github.com/clawdbot/nix-steipete-tools
module github.com/openclaw/nix-openclaw-tools
go 1.22

View File

@ -61,15 +61,6 @@ func PrefetchGitHub(owner, repo, rev string) (string, error) {
return res.Hash, nil
}
func NixBuildOracle() (string, error) {
cmd := exec.Command("nix", "build", ".#oracle")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
return out.String(), err
}
func NixBuildSummarize() (string, error) {
return NixBuildSummarizeSystem("")
}

25
nix/checks/qmd-smoke.nix Normal file
View File

@ -0,0 +1,25 @@
{
runCommand,
qmd,
}:
runCommand "qmd-smoke" { nativeBuildInputs = [ qmd ]; } ''
set -eu
export HOME="$TMPDIR/home"
export XDG_CONFIG_HOME="$TMPDIR/config"
export XDG_CACHE_HOME="$TMPDIR/cache"
export XDG_DATA_HOME="$TMPDIR/data"
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME" "$TMPDIR/notes"
printf '%s\n\n%s\n' '# Smoke' 'qmd packaging smoke' > "$TMPDIR/notes/smoke.md"
qmd --help >/dev/null
qmd collection list >/dev/null
qmd collection add "$TMPDIR/notes" --name smoke
qmd update
qmd search packaging --json | grep -q packaging
qmd status >/dev/null
touch "$out"
''

View File

@ -1,39 +0,0 @@
{ lib, stdenv, fetchurl }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/bird/releases/download/v0.8.0/bird-macos-universal-v0.8.0.tar.gz";
hash = "sha256-PYm7QE6LDtTvMx8Nxi2HOFJjTKKoFK56Ssfv/BFDIM8=";
};
};
in
stdenv.mkDerivation {
pname = "bird";
version = "0.8.0";
src = fetchurl sources.${stdenv.hostPlatform.system};
dontConfigure = true;
dontBuild = true;
unpackPhase = ''
tar -xzf "$src"
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin"
cp bird "$out/bin/bird"
chmod 0755 "$out/bin/bird"
runHook postInstall
'';
meta = with lib; {
description = "Fast X CLI for tweeting, replying, and reading";
homepage = "https://github.com/steipete/bird";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "bird";
};
}

53
nix/pkgs/discrawl.nix Normal file
View File

@ -0,0 +1,53 @@
{ lib, stdenv, fetchurl }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/openclaw/discrawl/releases/download/v0.7.0/discrawl_0.7.0_darwin_arm64.tar.gz";
hash = "sha256-MdiCWs19GIVKfCtrQQHUrkfnEvcLpmjmwRYwELVfMlA=";
};
"x86_64-linux" = {
url = "https://github.com/openclaw/discrawl/releases/download/v0.7.0/discrawl_0.7.0_linux_amd64.tar.gz";
hash = "sha256-wLtFIDJ2gED+149rWlFaQ/Bdv+WnkH6+eoWXXsI8McE=";
};
"aarch64-linux" = {
url = "https://github.com/openclaw/discrawl/releases/download/v0.7.0/discrawl_0.7.0_linux_arm64.tar.gz";
hash = "sha256-yoBcWyV6vGqxTreTeE8qsZKZzkyli40PK/2LFj72GHk=";
};
};
in
stdenv.mkDerivation {
pname = "discrawl";
version = "0.7.0";
src = fetchurl sources.${stdenv.hostPlatform.system};
dontConfigure = true;
dontBuild = true;
unpackPhase = ''
tar -xzf "$src"
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin" "$out/share/doc/discrawl"
cp $(find . -type f -name discrawl | head -1) "$out/bin/discrawl"
chmod 0755 "$out/bin/discrawl"
if [ -f LICENSE ]; then
cp LICENSE "$out/share/doc/discrawl/"
fi
if [ -f README.md ]; then
cp README.md "$out/share/doc/discrawl/"
fi
runHook postInstall
'';
meta = with lib; {
description = "Mirror Discord into SQLite and search server history locally";
homepage = "https://github.com/openclaw/discrawl";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "discrawl";
};
}

View File

@ -3,22 +3,22 @@
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_darwin_arm64.tar.gz";
hash = "sha256-sNPDBSmr40X1Epzm7ZRzJfcDHU7p70kLHl12/5c4/J8=";
url = "https://github.com/openclaw/gogcli/releases/download/v0.15.0/gogcli_0.15.0_darwin_arm64.tar.gz";
hash = "sha256-WMc9nprZ+2m6Nd+MabD5eGXJ2XSmpRFfG7RuI/Ldl8k=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_linux_amd64.tar.gz";
hash = "sha256-f8DVB7LQprzprE6IyGu3jZo2ckRXEu9FiomMM7KqIV0=";
url = "https://github.com/openclaw/gogcli/releases/download/v0.15.0/gogcli_0.15.0_linux_amd64.tar.gz";
hash = "sha256-v6KpyAkr0ynbiaoQD4B0o/FCUePbcWuSdid5PkcUjJ0=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_linux_arm64.tar.gz";
hash = "sha256-Q48xgr47bF3Om3kyKm6tnH9nOW9g0Em7i0o5AEhPZRU=";
url = "https://github.com/openclaw/gogcli/releases/download/v0.15.0/gogcli_0.15.0_linux_arm64.tar.gz";
hash = "sha256-R3+DUVlX9asKDc9B/NSFSDF11PsY4O1hZXZsMQsqGzU=";
};
};
in
stdenv.mkDerivation {
pname = "gogcli";
version = "0.10.0";
version = "0.15.0";
src = fetchurl sources.${stdenv.hostPlatform.system};
@ -39,7 +39,7 @@ stdenv.mkDerivation {
meta = with lib; {
description = "Google CLI for Gmail, Calendar, Drive, and Contacts";
homepage = "https://github.com/steipete/gogcli";
homepage = "https://github.com/openclaw/gogcli";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "gog";

View File

@ -1,31 +1,60 @@
{ lib, buildGoModule, fetchFromGitHub }:
{ lib, stdenv, fetchurl }:
buildGoModule rec {
pname = "goplaces";
version = "0.2.2-dev";
src = fetchFromGitHub {
owner = "joshp123";
repo = "goplaces";
rev = "a71fe3de986a78607d923f397113d7eb1babc111";
hash = "sha256-Lufp9+fwcoluNZR9iDYoIJ1yu3sM/8Q/EOkAlq5VLdk=";
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/openclaw/goplaces/releases/download/v0.4.0/goplaces_0.4.0_darwin_arm64.tar.gz";
hash = "sha256-FJ8hVqQhp/Ppk17CrDJk4dmu2pvS2xBX5cAzafpaM8g=";
};
"x86_64-darwin" = {
url = "https://github.com/openclaw/goplaces/releases/download/v0.4.0/goplaces_0.4.0_darwin_amd64.tar.gz";
hash = "sha256-/vVK0hqh1q3tliSy4mRT9X3jRayYhWAfZLS+VsFd+NU=";
};
"x86_64-linux" = {
url = "https://github.com/openclaw/goplaces/releases/download/v0.4.0/goplaces_0.4.0_linux_amd64.tar.gz";
hash = "sha256-cUHDAZ4Ep50K2ljLn8LyJeWPJlcr/sytP95UsNmSv3w=";
};
"aarch64-linux" = {
url = "https://github.com/openclaw/goplaces/releases/download/v0.4.0/goplaces_0.4.0_linux_arm64.tar.gz";
hash = "sha256-YjeLRDLwuQbUOYZYnpNkuYDvsm54PB1jmqTHas2T5VY=";
};
};
subPackages = [ "cmd/goplaces" ];
ldflags = [
"-s"
"-w"
"-X github.com/steipete/goplaces/internal/cli.Version=${version}"
];
vendorHash = "sha256-OFTjLtKwYSy4tM+D12mqI28M73YJdG4DyqPkXS7ZKUg=";
meta = with lib; {
description = "Modern Go client + CLI for the Google Places API";
homepage = "https://github.com/joshp123/goplaces";
description = "Modern Go client + CLI for the Google Places API (New)";
homepage = "https://github.com/openclaw/goplaces";
license = licenses.mit;
platforms = platforms.darwin ++ platforms.linux;
platforms = builtins.attrNames sources;
mainProgram = "goplaces";
};
in
stdenv.mkDerivation {
pname = "goplaces";
version = "0.4.0";
src = fetchurl sources.${stdenv.hostPlatform.system};
dontConfigure = true;
dontBuild = true;
unpackPhase = ''
tar -xzf "$src"
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin" "$out/share/doc/goplaces"
cp $(find . -type f -name goplaces | head -1) "$out/bin/goplaces"
chmod 0755 "$out/bin/goplaces"
if [ -f LICENSE ]; then
cp LICENSE "$out/share/doc/goplaces/"
fi
if [ -f README.md ]; then
cp README.md "$out/share/doc/goplaces/"
fi
runHook postInstall
'';
inherit meta;
}

View File

@ -3,14 +3,14 @@
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/imsg/releases/download/v0.4.0/imsg-macos.zip";
hash = "sha256-0OXjM+6IGS1ZW/7Z7s5g417K0DABRZZtWtJ0WMM+QHs=";
url = "https://github.com/openclaw/imsg/releases/download/v0.8.0/imsg-macos.zip";
hash = "sha256-CO2pbs85AbN8FcKdai22BCElRiYJLcj9jf5sXUj1lE8=";
};
};
in
stdenv.mkDerivation {
pname = "imsg";
version = "0.4.0";
version = "0.8.0";
src = fetchurl sources.${stdenv.hostPlatform.system};
@ -35,7 +35,7 @@ stdenv.mkDerivation {
meta = with lib; {
description = "Send and read iMessage / SMS from the terminal";
homepage = "https://github.com/steipete/imsg";
homepage = "https://github.com/openclaw/imsg";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "imsg";

View File

@ -1,128 +0,0 @@
{ lib
, stdenv
, fetchurl
, fetchFromGitHub
, nodejs
, pnpm
, python3
, python3Packages
, pkg-config
, makeWrapper
, pkgs
, zstd
}:
let
pnpmFetchDepsPkg = pkgs.callPackage "${pkgs.path}/pkgs/build-support/node/fetch-pnpm-deps" {
inherit pnpm;
};
in
stdenv.mkDerivation (finalAttrs: {
pname = "oracle";
version = "0.8.5";
srcTarball = fetchurl {
url = "https://github.com/steipete/oracle/releases/download/v0.8.5/oracle-0.8.5.tgz";
hash = "sha256-MSb1+5wEHK38iq+yob7Tz7xov0Wh9zHcmXGs/l2KdMA=";
};
lockSrc = fetchFromGitHub {
owner = "steipete";
repo = "oracle";
rev = "v0.8.5";
hash = "sha256-q1l3IcVAj7Gb8Lp0JzQakLRg2AlVrJniMBHRwxKQVbM=";
};
srcPatched = stdenv.mkDerivation {
name = "oracle-src-patched";
src = finalAttrs.srcTarball;
nativeBuildInputs = [ python3 ];
dontConfigure = true;
dontBuild = true;
unpackPhase = ''
tar -xzf "$src"
'';
installPhase = ''
mkdir -p "$out"
if [ -d package ]; then
shopt -s dotglob
mv package/* "$out"/
else
cp -R . "$out"/
fi
cp -f "${finalAttrs.lockSrc}/pnpm-lock.yaml" "$out/pnpm-lock.yaml"
export OUT_DIR="$out"
python3 - <<'PY'
import json
import os
from pathlib import Path
path = Path(os.environ["OUT_DIR"]) / "package.json"
data = json.loads(path.read_text())
data.pop("packageManager", None)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
'';
};
src = finalAttrs.srcPatched;
pnpmDeps = (pnpmFetchDepsPkg.fetchPnpmDeps {
pname = finalAttrs.pname;
version = finalAttrs.version;
src = finalAttrs.srcPatched;
hash = "sha256-Tmwe55l4QMzXsO7F1kUSnGuZjkuMtDORNtqKnWZ/HrA=";
fetcherVersion = 3;
});
nativeBuildInputs = [
nodejs
pnpm
python3
python3Packages.setuptools
pkg-config
makeWrapper
zstd
];
env = {
PNPM_IGNORE_PACKAGE_MANAGER_CHECK = "1";
CI = "1";
HOME = "/tmp";
PNPM_HOME = "/tmp/pnpm-home";
PNPM_CONFIG_HOME = "/tmp/pnpm-config";
XDG_CACHE_HOME = "/tmp/pnpm-cache";
NPM_CONFIG_USERCONFIG = "/tmp/pnpm-config/.npmrc";
npm_config_nodedir = "${nodejs.dev}";
npm_config_build_from_source = "1";
PNPM_CONFIG_IGNORE_SCRIPTS = "1";
};
buildPhase = ''
runHook preBuild
mkdir -p "$HOME" "$PNPM_HOME" "$PNPM_CONFIG_HOME" "$XDG_CACHE_HOME"
export PNPM_STORE_PATH="$TMPDIR/pnpm-store"
mkdir -p "$PNPM_STORE_PATH"
tar --zstd -xf ${finalAttrs.pnpmDeps}/pnpm-store.tar.zst -C "$PNPM_STORE_PATH"
pnpm install --offline --no-frozen-lockfile --store-dir "$PNPM_STORE_PATH" --ignore-scripts
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/libexec" "$out/bin"
cp -r dist package.json vendor assets-oracle-icon.png node_modules "$out/libexec/"
chmod 0755 "$out/libexec/dist/bin/oracle-cli.js" "$out/libexec/dist/bin/oracle-mcp.js"
ln -s "$out/libexec/dist/bin/oracle-cli.js" "$out/bin/oracle"
ln -s "$out/libexec/dist/bin/oracle-mcp.js" "$out/bin/oracle-mcp"
runHook postInstall
'';
meta = with lib; {
description = "Bundle prompts + files for second-model review";
homepage = "https://github.com/steipete/oracle";
license = licenses.mit;
platforms = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
mainProgram = "oracle";
};
})

View File

@ -3,14 +3,14 @@
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/Peekaboo/releases/download/v3.0.0-beta3/peekaboo-macos-universal.tar.gz";
hash = "sha256-d+rfb9XFTqxktIRNXMiHiQttb0XUmvYbBcbinqLL0kU=";
url = "https://github.com/openclaw/Peekaboo/releases/download/v3.0.0-beta4/peekaboo-macos-arm64.tar.gz";
hash = "sha256-74eXVHpRAmcs0mzK3GLh/3So78AEMZzXBvx1Zg7uOkc=";
};
};
in
stdenv.mkDerivation {
pname = "peekaboo";
version = "3.0.0-beta3";
version = "3.0.0-beta4";
src = fetchurl sources.${stdenv.hostPlatform.system};
@ -31,7 +31,7 @@ stdenv.mkDerivation {
meta = with lib; {
description = "Lightning-fast macOS screenshots & AI vision analysis";
homepage = "https://github.com/steipete/peekaboo";
homepage = "https://github.com/openclaw/Peekaboo";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "peekaboo";

View File

@ -1,4 +1,4 @@
{ lib, stdenv, fetchurl, watchman }:
{ lib, stdenv, fetchurl }:
let
sources = {
@ -30,8 +30,6 @@ stdenv.mkDerivation {
runHook postInstall
'';
propagatedBuildInputs = [ watchman ];
meta = with lib; {
description = "Universal file watcher with auto-rebuild for any language or build system";
homepage = "https://github.com/steipete/poltergeist";

123
nix/pkgs/qmd.nix Normal file
View File

@ -0,0 +1,123 @@
{
lib,
stdenv,
stdenvNoCC,
fetchFromGitHub,
bun,
makeWrapper,
nodejs,
node-gyp,
python3,
sqlite,
darwin,
xcbuild,
}:
let
pname = "qmd";
version = "2.1.0";
src = fetchFromGitHub {
owner = "tobi";
repo = "qmd";
rev = "v${version}";
hash = "sha256-bqIVaNRTa8H5vrw3RwsD7QdtTa0xNvRuEVzlzE1hIBQ=";
};
nodeModulesHashes = {
"aarch64-darwin" = "sha256-qU+9KdR/nTocelyANS09I/4yaQ+7s1LvJNqB27IOK/c=";
"x86_64-linux" = "sha256-D0ezO4vqq4iswcAMU2DCql9ZAQvh3me6N9aDB5roq4w=";
};
system = stdenv.hostPlatform.system;
nodeModules = stdenvNoCC.mkDerivation {
pname = "qmd-node-modules";
inherit version src;
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
"GIT_PROXY_COMMAND"
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun ];
dontConfigure = true;
buildPhase = ''
runHook preBuild
export HOME="$(mktemp -d)"
bun install \
--backend copyfile \
--frozen-lockfile \
--ignore-scripts \
--no-progress \
--production
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p "$out"
cp -R node_modules "$out/"
runHook postInstall
'';
dontFixup = true;
outputHash = nodeModulesHashes.${system};
outputHashAlgo = "sha256";
outputHashMode = "recursive";
};
in
stdenv.mkDerivation {
inherit pname version src;
nativeBuildInputs = [
bun
makeWrapper
nodejs
node-gyp
python3
]
++ lib.optionals stdenv.hostPlatform.isDarwin [
darwin.cctools
xcbuild
];
buildInputs = [ sqlite ];
dontConfigure = true;
buildPhase = ''
runHook preBuild
export HOME="$(mktemp -d)"
cp -R ${nodeModules}/node_modules ./
chmod -R u+w node_modules
(cd node_modules/better-sqlite3 && node-gyp rebuild --release)
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin" "$out/lib/qmd"
cp -r node_modules src package.json "$out/lib/qmd/"
makeWrapper ${bun}/bin/bun "$out/bin/qmd" \
--add-flags "$out/lib/qmd/src/cli/qmd.ts" \
--set DYLD_LIBRARY_PATH "${sqlite.out}/lib" \
--set LD_LIBRARY_PATH "${sqlite.out}/lib"
runHook postInstall
'';
meta = with lib; {
description = "On-device hybrid search for markdown knowledge bases";
homepage = "https://github.com/tobi/qmd";
license = licenses.mit;
platforms = builtins.attrNames nodeModulesHashes;
mainProgram = "qmd";
};
}

View File

@ -3,18 +3,18 @@
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/sag/releases/download/v0.2.2/sag_0.2.2_darwin_universal.tar.gz";
hash = "sha256-BVS675EiF9nh85iPttdJLUbS9JEFpeuRdeP4YfOc0ok=";
url = "https://github.com/steipete/sag/releases/download/v0.3.0/sag_0.3.0_darwin_universal.tar.gz";
hash = "sha256-6JqfjA09qlBrdLp72NCbk0VNkZ/6zv5U0rXcYohAnxQ=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/sag/releases/download/v0.2.2/sag_0.2.2_linux_amd64.tar.gz";
hash = "sha256-/d/iVTZI/ZzTRGYQxVF22JeFDXn0qpXWC2BdOH/6vcg=";
url = "https://github.com/steipete/sag/releases/download/v0.3.0/sag_0.3.0_linux_amd64.tar.gz";
hash = "sha256-jUwjqeORPphMX3xhX36zy/W/X10PP0wP5Ryz5ggpKX4=";
};
};
in
stdenv.mkDerivation {
pname = "sag";
version = "0.2.2";
version = "0.3.0";
src = fetchurl sources.${stdenv.hostPlatform.system};

View File

@ -3,22 +3,22 @@
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/sonoscli/releases/download/v0.1.0/sonoscli-macos-arm64.tar.gz";
hash = "sha256-t5VUWXPrxgYXopiQEuO7k91Gx70oefyhbOZmF/XDwaw=";
url = "https://github.com/steipete/sonoscli/releases/download/v0.3.0/sonoscli_0.3.0_darwin_arm64.tar.gz";
hash = "sha256-xAVAldfh4R0xyGsEmNruBfICSFckv44z72fEshjTxJM=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/sonoscli/releases/download/v0.1.0/sonoscli_0.1.0_linux_amd64.tar.gz";
hash = "sha256-8g/sTD4P8Ctbpv5N0nZ1SpP+UH6CUuUwNEo4VjW01ZM=";
url = "https://github.com/steipete/sonoscli/releases/download/v0.3.0/sonoscli_0.3.0_linux_amd64.tar.gz";
hash = "sha256-CQjaSE/eVu6rdExYcIuOhXCQp7842A+qo7dhPw1CDms=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/sonoscli/releases/download/v0.1.0/sonoscli_0.1.0_linux_arm64.tar.gz";
hash = "sha256-EtBtsNcvD5OvryUjCQ5oy3H7w4etgfXs7PkdsefWdE0=";
url = "https://github.com/steipete/sonoscli/releases/download/v0.3.0/sonoscli_0.3.0_linux_arm64.tar.gz";
hash = "sha256-LpbCP/8DbknNeUmfCkm4gSBYPo723/e2r4KPT+1cpcc=";
};
};
in
stdenv.mkDerivation {
pname = "sonoscli";
version = "0.1.0";
version = "0.3.0";
src = fetchurl sources.${stdenv.hostPlatform.system};

View File

@ -15,17 +15,17 @@
let
pname = "summarize";
version = "0.11.1";
version = "0.14.1";
binSources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/summarize/releases/download/v0.11.1/summarize-macos-arm64-v0.11.1.tar.gz";
hash = "sha256-RJNeCxWfbMCOrD6ABR4wSALdtl1sImS4Wkjz1TdPmTE=";
url = "https://github.com/steipete/summarize/releases/download/v0.14.1/summarize-macos-arm64-v0.14.1.tar.gz";
hash = "sha256-X3aBGiAjoOSIINxHyGOzg/b88fpCZQcv0cM2L0KqS8c=";
};
};
src = fetchurl {
url = "https://github.com/steipete/summarize/archive/refs/tags/v${version}.tar.gz";
hash = "sha256-2s3XAy9evHRZQthTWRhIjkv7Z40K5PiUFHEIGo6XxBs=";
hash = "sha256-UAf7IXerk5VM3DyLyMD259Dh55ZjcZPK/3T2kPj9RO4=";
};
pnpmFetchDepsPkg = pkgs.callPackage "${pkgs.path}/pkgs/build-support/node/fetch-pnpm-deps" {
@ -36,7 +36,7 @@ let
pname = pname;
version = version;
src = src;
hash = "sha256-vqTXJ64DpanUwdUlSS3Fa+zy70qIGgsNtKzYF82J1RU=";
hash = "sha256-s1I3TTX3uw/iJpbWvoNE5kg9XQVjw0mnA5pRTVs7ypY=";
fetcherVersion = 3;
});
@ -72,7 +72,7 @@ if stdenv.isLinux then
PNPM_CONFIG_HOME = "/tmp/pnpm-config";
XDG_CACHE_HOME = "/tmp/pnpm-cache";
NPM_CONFIG_USERCONFIG = "/tmp/pnpm-config/.npmrc";
npm_config_nodedir = "${nodejs.dev}";
npm_config_nodedir = "${lib.getDev nodejs}";
npm_config_build_from_source = "1";
PNPM_CONFIG_IGNORE_SCRIPTS = "1";
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
@ -120,6 +120,7 @@ if stdenv.isLinux then
runHook preInstall
mkdir -p "$out/libexec" "$out/libexec/packages" "$out/libexec/apps" "$out/bin"
cp -r dist node_modules "$out/libexec/"
find "$out/libexec/node_modules" -name ".pnpm-workspace-state-v1.json" -delete
cp -r packages/core "$out/libexec/packages/"
cp -r apps/chrome-extension "$out/libexec/apps/"
chmod 0755 "$out/libexec/dist/cli.js"

53
nix/pkgs/wacrawl.nix Normal file
View File

@ -0,0 +1,53 @@
{ lib, stdenv, fetchurl }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.4/wacrawl_0.2.4_darwin_arm64.tar.gz";
hash = "sha256-vn6GN8inl06EwYCRW0xlDJMaj3iFP82Dbdt5HpH/NMo=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.4/wacrawl_0.2.4_linux_amd64.tar.gz";
hash = "sha256-kK4E24aJw4ItLK3lw3tsEbfcFe8OeR7/iOJlTInMRiA=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.4/wacrawl_0.2.4_linux_arm64.tar.gz";
hash = "sha256-k033oRaYjGdU9JVdQn3yfjmxWw/UtSPsjRTisVGCsfs=";
};
};
in
stdenv.mkDerivation {
pname = "wacrawl";
version = "0.2.4";
src = fetchurl sources.${stdenv.hostPlatform.system};
dontConfigure = true;
dontBuild = true;
unpackPhase = ''
tar -xzf "$src"
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/bin" "$out/share/doc/wacrawl"
cp $(find . -type f -name wacrawl | head -1) "$out/bin/wacrawl"
chmod 0755 "$out/bin/wacrawl"
if [ -f LICENSE ]; then
cp LICENSE "$out/share/doc/wacrawl/"
fi
if [ -f README.md ]; then
cp README.md "$out/share/doc/wacrawl/"
fi
runHook postInstall
'';
meta = with lib; {
description = "Read-only local archive and search for WhatsApp Desktop data";
homepage = "https://github.com/steipete/wacrawl";
license = licenses.mit;
platforms = builtins.attrNames sources;
mainProgram = "wacrawl";
};
}

View File

@ -1,224 +0,0 @@
---
name: bird
description: X/Twitter CLI for reading, searching, posting, and engagement via cookies.
homepage: https://bird.fast
metadata:
{
"openclaw":
{
"emoji": "🐦",
"requires": { "bins": ["bird"] },
"install":
[
{
"id": "brew",
"kind": "brew",
"formula": "steipete/tap/bird",
"bins": ["bird"],
"label": "Install bird (brew)",
"os": ["darwin"],
},
{
"id": "npm",
"kind": "node",
"package": "@steipete/bird",
"bins": ["bird"],
"label": "Install bird (npm)",
},
],
},
}
---
# bird 🐦
Fast X/Twitter CLI using GraphQL + cookie auth.
## Install
```bash
# npm/pnpm/bun
npm install -g @steipete/bird
# Homebrew (macOS, prebuilt binary)
brew install steipete/tap/bird
# One-shot (no install)
bunx @steipete/bird whoami
```
## Authentication
`bird` uses cookie-based auth.
Use `--auth-token` / `--ct0` to pass cookies directly, or `--cookie-source` for browser cookies.
Run `bird check` to see which source is active. For Arc/Brave, use `--chrome-profile-dir <path>`.
## Commands
### Account & Auth
```bash
bird whoami # Show logged-in account
bird check # Show credential sources
bird query-ids --fresh # Refresh GraphQL query ID cache
```
### Reading Tweets
```bash
bird read <url-or-id> # Read a single tweet
bird <url-or-id> # Shorthand for read
bird thread <url-or-id> # Full conversation thread
bird replies <url-or-id> # List replies to a tweet
```
### Timelines
```bash
bird home # Home timeline (For You)
bird home --following # Following timeline
bird user-tweets @handle -n 20 # User's profile timeline
bird mentions # Tweets mentioning you
bird mentions --user @handle # Mentions of another user
```
### Search
```bash
bird search "query" -n 10
bird search "from:steipete" --all --max-pages 3
```
### News & Trending
```bash
bird news -n 10 # AI-curated from Explore tabs
bird news --ai-only # Filter to AI-curated only
bird news --sports # Sports tab
bird news --with-tweets # Include related tweets
bird trending # Alias for news
```
### Lists
```bash
bird lists # Your lists
bird lists --member-of # Lists you're a member of
bird list-timeline <id> -n 20 # Tweets from a list
```
### Bookmarks & Likes
```bash
bird bookmarks -n 10
bird bookmarks --folder-id <id> # Specific folder
bird bookmarks --include-parent # Include parent tweet
bird bookmarks --author-chain # Author's self-reply chain
bird bookmarks --full-chain-only # Full reply chain
bird unbookmark <url-or-id>
bird likes -n 10
```
### Social Graph
```bash
bird following -n 20 # Users you follow
bird followers -n 20 # Users following you
bird following --user <id> # Another user's following
bird about @handle # Account origin/location info
```
### Engagement Actions
```bash
bird follow @handle # Follow a user
bird unfollow @handle # Unfollow a user
```
### Posting
```bash
bird tweet "hello world"
bird reply <url-or-id> "nice thread!"
bird tweet "check this out" --media image.png --alt "description"
```
**⚠️ Posting risks**: Posting is more likely to be rate limited; if blocked, use the browser tool instead.
## Media Uploads
```bash
bird tweet "hi" --media img.png --alt "description"
bird tweet "pics" --media a.jpg --media b.jpg # Up to 4 images
bird tweet "video" --media clip.mp4 # Or 1 video
```
## Pagination
Commands supporting pagination: `replies`, `thread`, `search`, `bookmarks`, `likes`, `list-timeline`, `following`, `followers`, `user-tweets`
```bash
bird bookmarks --all # Fetch all pages
bird bookmarks --max-pages 3 # Limit pages
bird bookmarks --cursor <cursor> # Resume from cursor
bird replies <id> --all --delay 1000 # Delay between pages (ms)
```
## Output Options
```bash
--json # JSON output
--json-full # JSON with raw API response
--plain # No emoji, no color (script-friendly)
--no-emoji # Disable emoji
--no-color # Disable ANSI colors (or set NO_COLOR=1)
--quote-depth n # Max quoted tweet depth in JSON (default: 1)
```
## Global Options
```bash
--auth-token <token> # Set auth_token cookie
--ct0 <token> # Set ct0 cookie
--cookie-source <source> # Cookie source for browser cookies (repeatable)
--chrome-profile <name> # Chrome profile name
--chrome-profile-dir <path> # Chrome/Chromium profile dir or cookie DB path
--firefox-profile <name> # Firefox profile
--timeout <ms> # Request timeout
--cookie-timeout <ms> # Cookie extraction timeout
```
## Config File
`~/.config/bird/config.json5` (global) or `./.birdrc.json5` (project):
```json5
{
cookieSource: ["chrome"],
chromeProfileDir: "/path/to/Arc/Profile",
timeoutMs: 20000,
quoteDepth: 1,
}
```
Environment variables: `BIRD_TIMEOUT_MS`, `BIRD_COOKIE_TIMEOUT_MS`, `BIRD_QUOTE_DEPTH`
## Troubleshooting
### Query IDs stale (404 errors)
```bash
bird query-ids --fresh
```
### Cookie extraction fails
- Check browser is logged into X
- Try different `--cookie-source`
- For Arc/Brave: use `--chrome-profile-dir`
---
**TL;DR**: Read/search/engage with CLI. Post carefully or use browser. 🐦

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -1,9 +1,9 @@
{
description = "openclaw plugin: bird";
description = "openclaw plugin: discrawl";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "path:../..";
};
outputs = { self, nixpkgs, root }:
@ -13,24 +13,24 @@
pluginFor = system:
let
packagesForSystem = root.packages.${system} or {};
bird = packagesForSystem.bird or null;
discrawl = packagesForSystem.discrawl or null;
in
if bird == null then null else {
name = "bird";
skills = [ ./skills/bird ];
packages = [ bird ];
if discrawl == null then null else {
name = "discrawl";
skills = [ ./skills/discrawl ];
packages = [ discrawl ];
needs = {
stateDirs = [];
requiredEnv = [];
stateDirs = [ ".discrawl" ];
requiredEnv = [ ];
};
};
in {
packages = lib.genAttrs systems (system:
let
bird = (root.packages.${system} or {}).bird or null;
discrawl = (root.packages.${system} or {}).discrawl or null;
in
if bird == null then {}
else { bird = bird; }
if discrawl == null then {}
else { discrawl = discrawl; }
);
openclawPlugin = pluginFor;

View File

@ -0,0 +1,74 @@
---
name: discrawl
description: Mirror Discord guild history into local SQLite and query it offline with search, messages, mentions, reports, and DM wiretap import.
homepage: https://github.com/openclaw/discrawl
metadata:
{
"openclaw":
{
"emoji": "🛰️",
"requires": { "bins": ["discrawl"] },
"install":
[
{
"id": "brew",
"kind": "brew",
"formula": "steipete/tap/discrawl",
"bins": ["discrawl"],
"label": "Install discrawl (brew)",
},
],
},
}
---
# discrawl
Use `discrawl` to mirror Discord guild data into local SQLite, then query it offline.
## When to Use
Use this skill when the user wants to:
- search Discord history locally without relying on Discord search
- archive a guild into SQLite for later queries
- inspect recent messages, mentions, channels, or members from a local archive
- import local Discord Desktop cache data for DM recovery/search
- publish or subscribe to a Git-backed Discord archive snapshot
## Requirements
- Discord bot token for guild sync, or an existing OpenClaw Discord config
- local Discord Desktop cache files only if using `wiretap`
- enough local disk for SQLite archive growth
## Setup
- Default config: `~/.discrawl/config.toml`
- Default database: `~/.discrawl/discrawl.db`
- Fastest setup when OpenClaw already has Discord configured:
- `discrawl init --from-openclaw ~/.openclaw/openclaw.json`
- Env-only setup:
- `export DISCORD_BOT_TOKEN="..."`
- `discrawl init`
## Common Commands
- Doctor: `discrawl doctor`
- Initial history: `discrawl sync --full`
- Incremental refresh: `discrawl sync`
- Live tail: `discrawl tail`
- Search: `discrawl search "panic nil pointer"`
- Recent channel messages: `discrawl messages --channel general --hours 24`
- Mentions: `discrawl mentions --user <user-id>`
- DM cache import: `discrawl wiretap`
- Local DM search: `discrawl dms --search "launch checklist"`
- Read-only SQL: `discrawl sql "select count(*) from messages"`
- Git-backed reader mode: `discrawl subscribe <private-repo-url>`
## Notes
- Bot-token sync reads only guilds/channels the bot can access.
- `wiretap` uses local Discord Desktop cache files only; it does not use a user token.
- Prefer `discrawl doctor` before a first sync.
- Use `sync --full` once for backfill, then plain `sync` for routine refreshes.

59
tools/gogcli/flake.lock generated Normal file
View File

@ -0,0 +1,59 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"root": "root_2"
}
},
"root_2": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"path": "../..",
"type": "path"
},
"original": {
"path": "../..",
"type": "path"
},
"parent": []
}
},
"root": "root",
"version": 7
}

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

59
tools/goplaces/flake.lock generated Normal file
View File

@ -0,0 +1,59 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"root": "root_2"
}
},
"root_2": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"path": "../..",
"type": "path"
},
"original": {
"path": "../..",
"type": "path"
},
"parent": []
}
},
"root": "root",
"version": 7
}

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=6352c8247b3b889d7f17bce1f09d6c58fd34932c&narHash=sha256-nfCSSyNU97XpKVPgo6mODBwrVeTOuMCl3i18QuGjpN0=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -1,8 +1,8 @@
---
name: goplaces
description: Query Google Places API (New) via the goplaces CLI for text search, place details, resolve, and reviews. Use for human-friendly place lookup or JSON output for scripts.
homepage: https://github.com/steipete/goplaces
metadata: {"clawdbot":{"emoji":"📍","requires":{"bins":["goplaces"],"env":["GOOGLE_PLACES_API_KEY"]},"primaryEnv":"GOOGLE_PLACES_API_KEY","install":[{"id":"brew","kind":"brew","formula":"steipete/tap/goplaces","bins":["goplaces"],"label":"Install goplaces (brew)"}]}}
homepage: https://github.com/openclaw/goplaces
metadata: {"openclaw":{"emoji":"📍","requires":{"bins":["goplaces"],"env":["GOOGLE_PLACES_API_KEY"]},"primaryEnv":"GOOGLE_PLACES_API_KEY","install":[{"id":"brew","kind":"brew","formula":"steipete/tap/goplaces","bins":["goplaces"],"label":"Install goplaces (brew)"}]}}
---
# goplaces

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -1,6 +1,6 @@
---
name: imsg
description: iMessage/SMS CLI for listing chats, history, watch, and sending.
description: iMessage/SMS CLI for listing chats, history, and sending messages via Messages.app.
homepage: https://imsg.to
metadata:
{
@ -23,52 +23,100 @@ metadata:
}
---
# imsg Actions
# imsg
## Overview
Use `imsg` to read and send iMessage/SMS via macOS Messages.app.
Use `imsg` to read and send Messages.app iMessage/SMS on macOS.
## When to Use
Requirements: Messages.app signed in, Full Disk Access for your terminal, and Automation permission to control Messages.app for sending.
✅ **USE this skill when:**
## Inputs to collect
- User explicitly asks to send iMessage or SMS
- Reading iMessage conversation history
- Checking recent Messages.app chats
- Sending to phone numbers or Apple IDs
- Recipient handle (phone/email) for `send`
- `chatId` for history/watch (from `imsg chats --limit 10 --json`)
- `text` and optional `file` path for sends
## When NOT to Use
## Actions
❌ **DON'T use this skill when:**
### List chats
- Telegram messages → use `message` tool with `channel:telegram`
- Signal messages → use Signal channel if configured
- WhatsApp messages → use WhatsApp channel if configured
- Discord messages → use `message` tool with `channel:discord`
- Slack messages → use `slack` skill
- Group chat management (adding/removing members) → not supported
- Bulk/mass messaging → always confirm with user first
- Replying in current conversation → just reply normally (OpenClaw routes automatically)
## Requirements
- macOS with Messages.app signed in
- Full Disk Access for terminal
- Automation permission for Messages.app (for sending)
## Common Commands
### List Chats
```bash
imsg chats --limit 10 --json
```
### Fetch chat history
### View History
```bash
# By chat ID
imsg history --chat-id 1 --limit 20 --json
# With attachments info
imsg history --chat-id 1 --limit 20 --attachments --json
```
### Watch a chat
### Watch for New Messages
```bash
imsg watch --chat-id 1 --attachments
```
### Send a message
### Send Messages
```bash
imsg send --to "+14155551212" --text "hi" --file /path/pic.jpg
# Text only
imsg send --to "+14155551212" --text "Hello!"
# With attachment
imsg send --to "+14155551212" --text "Check this out" --file /path/to/image.jpg
# Specify service
imsg send --to "+14155551212" --text "Hi" --service imessage
imsg send --to "+14155551212" --text "Hi" --service sms
```
## Notes
## Service Options
- `--service imessage|sms|auto` controls delivery.
- Confirm recipient + message before sending.
- `--service imessage` — Force iMessage (requires recipient has iMessage)
- `--service sms` — Force SMS (green bubble)
- `--service auto` — Let Messages.app decide (default)
## Ideas to try
## Safety Rules
- Use `imsg chats --limit 10 --json` to discover chat ids.
- Watch a high-signal chat to stream incoming messages.
1. **Always confirm recipient and message content** before sending
2. **Never send to unknown numbers** without explicit user approval
3. **Be careful with attachments** — confirm file path exists
4. **Rate limit yourself** — don't spam
## Example Workflow
User: "Text mom that I'll be late"
```bash
# 1. Find mom's chat
imsg chats --limit 20 --json | jq '.[] | select(.displayName | contains("Mom"))'
# 2. Confirm with user
# "Found Mom at +1555123456. Send 'I'll be late' via iMessage?"
# 3. Send after confirmation
imsg send --to "+1555123456" --text "I'll be late"
```

View File

@ -1,38 +0,0 @@
{
description = "openclaw plugin: oracle";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
};
outputs = { self, nixpkgs, root }:
let
lib = nixpkgs.lib;
systems = builtins.attrNames root.packages;
pluginFor = system:
let
packagesForSystem = root.packages.${system} or {};
oracle = packagesForSystem.oracle or null;
in
if oracle == null then null else {
name = "oracle";
skills = [ ./skills/oracle ];
packages = [ oracle ];
needs = {
stateDirs = [];
requiredEnv = [];
};
};
in {
packages = lib.genAttrs systems (system:
let
oracle = (root.packages.${system} or {}).oracle or null;
in
if oracle == null then {}
else { oracle = oracle; }
);
openclawPlugin = pluginFor;
};
}

View File

@ -1,125 +0,0 @@
---
name: oracle
description: Best practices for using the oracle CLI (prompt + file bundling, engines, sessions, and file attachment patterns).
homepage: https://askoracle.dev
metadata:
{
"openclaw":
{
"emoji": "🧿",
"requires": { "bins": ["oracle"] },
"install":
[
{
"id": "node",
"kind": "node",
"package": "@steipete/oracle",
"bins": ["oracle"],
"label": "Install oracle (node)",
},
],
},
}
---
# oracle — best use
Oracle bundles your prompt + selected files into one “one-shot” request so another model can answer with real repo context (API or browser automation). Treat output as advisory: verify against code + tests.
## Main use case (browser, GPT5.2 Pro)
Default workflow here: `--engine browser` with GPT5.2 Pro in ChatGPT. This is the common “long think” path: ~10 minutes to ~1 hour is normal; expect a stored session you can reattach to.
Recommended defaults:
- Engine: browser (`--engine browser`)
- Model: GPT5.2 Pro (`--model gpt-5.2-pro` or `--model "5.2 Pro"`)
## Golden path
1. Pick a tight file set (fewest files that still contain the truth).
2. Preview payload + token spend (`--dry-run` + `--files-report`).
3. Use browser mode for the usual GPT5.2 Pro workflow; use API only when you explicitly want it.
4. If the run detaches/timeouts: reattach to the stored session (dont re-run).
## Commands (preferred)
- Help:
- `oracle --help`
- If the binary isnt installed: `npx -y @steipete/oracle --help` (avoid `pnpx` here; sqlite bindings).
- Preview (no tokens):
- `oracle --dry-run summary -p "<task>" --file "src/**" --file "!**/*.test.*"`
- `oracle --dry-run full -p "<task>" --file "src/**"`
- Token sanity:
- `oracle --dry-run summary --files-report -p "<task>" --file "src/**"`
- Browser run (main path; long-running is normal):
- `oracle --engine browser --model gpt-5.2-pro -p "<task>" --file "src/**"`
- Manual paste fallback:
- `oracle --render --copy -p "<task>" --file "src/**"`
- Note: `--copy` is a hidden alias for `--copy-markdown`.
## Attaching files (`--file`)
`--file` accepts files, directories, and globs. You can pass it multiple times; entries can be comma-separated.
- Include:
- `--file "src/**"`
- `--file src/index.ts`
- `--file docs --file README.md`
- Exclude:
- `--file "src/**" --file "!src/**/*.test.ts" --file "!**/*.snap"`
- Defaults (implementation behavior):
- Default-ignored dirs: `node_modules`, `dist`, `coverage`, `.git`, `.turbo`, `.next`, `build`, `tmp` (skipped unless explicitly passed as literal dirs/files).
- Honors `.gitignore` when expanding globs.
- Does not follow symlinks.
- Dotfiles filtered unless opted in via pattern (e.g. `--file ".github/**"`).
- Files > 1 MB rejected.
## Engines (API vs browser)
- Auto-pick: `api` when `OPENAI_API_KEY` is set; otherwise `browser`.
- Browser supports GPT + Gemini only; use `--engine api` for Claude/Grok/Codex or multi-model runs.
- Browser attachments:
- `--browser-attachments auto|never|always` (auto pastes inline up to ~60k chars then uploads).
- Remote browser host:
- Host: `oracle serve --host 0.0.0.0 --port 9473 --token <secret>`
- Client: `oracle --engine browser --remote-host <host:port> --remote-token <secret> -p "<task>" --file "src/**"`
## Sessions + slugs
- Stored under `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).
- Runs may detach or take a long time (browser + GPT5.2 Pro often does). If the CLI times out: dont re-run; reattach.
- List: `oracle status --hours 72`
- Attach: `oracle session <id> --render`
- Use `--slug "<3-5 words>"` to keep session IDs readable.
- Duplicate prompt guard exists; use `--force` only when you truly want a fresh run.
## Prompt template (high signal)
Oracle starts with **zero** project knowledge. Assume the model cannot infer your stack, build tooling, conventions, or “obvious” paths. Include:
- Project briefing (stack + build/test commands + platform constraints).
- “Where things live” (key directories, entrypoints, config files, boundaries).
- Exact question + what you tried + the error text (verbatim).
- Constraints (“dont change X”, “must keep public API”, etc).
- Desired output (“return patch plan + tests”, “give 3 options with tradeoffs”).
## Safety
- Dont attach secrets by default (`.env`, key files, auth tokens). Redact aggressively; share only whats required.
## “Exhaustive prompt” restoration pattern
For long investigations, write a standalone prompt + file set so you can rerun days later:
- 630 sentence project briefing + the goal.
- Repro steps + exact errors + what you tried.
- Attach all context files needed (entrypoints, configs, key modules, docs).
Oracle runs are one-shot; the model doesnt remember prior runs. “Restoring context” means re-running with the same prompt + `--file …` set (or reattaching a still-running stored session).

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

48
tools/qmd/flake.nix Normal file
View File

@ -0,0 +1,48 @@
{
description = "openclaw plugin: qmd";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "../..";
};
outputs =
{
self,
nixpkgs,
root,
}:
let
lib = nixpkgs.lib;
systems = builtins.attrNames root.packages;
pluginFor =
system:
let
packagesForSystem = root.packages.${system} or { };
qmd = packagesForSystem.qmd or null;
in
if qmd == null then
null
else
{
name = "qmd";
skills = [ ./skills/qmd ];
packages = [ qmd ];
needs = {
stateDirs = [ ".local/share/qmd" ];
requiredEnv = [ ];
};
};
in
{
packages = lib.genAttrs systems (
system:
let
qmd = (root.packages.${system} or { }).qmd or null;
in
if qmd == null then { } else { qmd = qmd; }
);
openclawPlugin = pluginFor;
};
}

View File

@ -0,0 +1,143 @@
---
name: qmd
description: Search markdown knowledge bases, notes, and documentation using QMD. Use when users ask to search notes, find documents, or look up information.
license: MIT
compatibility: Requires qmd CLI or MCP server. In Nix OpenClaw, use the qmd package/plugin from nix-openclaw-tools.
metadata:
author: tobi
version: "2.0.0"
allowed-tools: Bash(qmd:*), mcp__qmd__*
---
# QMD - Quick Markdown Search
Local search engine for markdown content.
## Status
!`qmd status 2>/dev/null || echo "qmd CLI not found on this runtime PATH"`
## MCP: `query`
```json
{
"searches": [
{ "type": "lex", "query": "CAP theorem consistency" },
{ "type": "vec", "query": "tradeoff between consistency and availability" }
],
"collections": ["docs"],
"limit": 10
}
```
### Query Types
| Type | Method | Input |
|------|--------|-------|
| `lex` | BM25 | Keywords — exact terms, names, code |
| `vec` | Vector | Question — natural language |
| `hyde` | Vector | Answer — hypothetical result (50-100 words) |
### Writing Good Queries
**lex (keyword)**
- 2-5 terms, no filler words
- Exact phrase: `"connection pool"` (quoted)
- Exclude terms: `performance -sports` (minus prefix)
- Code identifiers work: `handleError async`
**vec (semantic)**
- Full natural language question
- Be specific: `"how does the rate limiter handle burst traffic"`
- Include context: `"in the payment service, how are refunds processed"`
**hyde (hypothetical document)**
- Write 50-100 words of what the *answer* looks like
- Use the vocabulary you expect in the result
**expand (auto-expand)**
- Use a single-line query (implicit) or `expand: question` on its own line
- Lets the local LLM generate lex/vec/hyde variations
- Do not mix `expand:` with other typed lines — it's either a standalone expand query or a full query document
### Intent (Disambiguation)
When a query term is ambiguous, add `intent` to steer results:
```json
{
"searches": [
{ "type": "lex", "query": "performance" }
],
"intent": "web page load times and Core Web Vitals"
}
```
Intent affects expansion, reranking, chunk selection, and snippet extraction. It does not search on its own — it's a steering signal that disambiguates queries like "performance" (web-perf vs team health vs fitness).
### Combining Types
| Goal | Approach |
|------|----------|
| Know exact terms | `lex` only |
| Don't know vocabulary | Use a single-line query (implicit `expand:`) or `vec` |
| Best recall | `lex` + `vec` |
| Complex topic | `lex` + `vec` + `hyde` |
| Ambiguous query | Add `intent` to any combination above |
First query gets 2x weight in fusion — put your best guess first.
### Lex Query Syntax
| Syntax | Meaning | Example |
|--------|---------|---------|
| `term` | Prefix match | `perf` matches "performance" |
| `"phrase"` | Exact phrase | `"rate limiter"` |
| `-term` | Exclude | `performance -sports` |
Note: `-term` only works in lex queries, not vec/hyde.
### Collection Filtering
```json
{ "collections": ["docs"] } // Single
{ "collections": ["docs", "notes"] } // Multiple (OR)
```
Omit to search all collections.
## Other MCP Tools
| Tool | Use |
|------|-----|
| `get` | Retrieve doc by path or `#docid` |
| `multi_get` | Retrieve multiple by glob/list |
| `status` | Collections and health |
## CLI
```bash
qmd query "question" # Auto-expand + rerank
qmd query $'lex: X\nvec: Y' # Structured
qmd query $'expand: question' # Explicit expand
qmd query --json --explain "q" # Show score traces (RRF + rerank blend)
qmd search "keywords" # BM25 only (no LLM)
qmd get "#abc123" # By docid
qmd multi-get "journals/2026-*.md" -l 40 # Batch pull snippets by glob
qmd multi-get notes/foo.md,notes/bar.md # Comma-separated list, preserves order
```
## HTTP API
```bash
curl -X POST http://localhost:8181/query \
-H "Content-Type: application/json" \
-d '{"searches": [{"type": "lex", "query": "test"}]}'
```
## Setup
```bash
qmd collection add ~/notes --name notes
qmd embed
```

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -6,7 +6,7 @@ metadata:
{
"openclaw":
{
"emoji": "🗣️",
"emoji": "🔊",
"requires": { "bins": ["sag"], "env": ["ELEVENLABS_API_KEY"] },
"primaryEnv": "ELEVENLABS_API_KEY",
"install":
@ -68,7 +68,7 @@ Confirm voice + speaker before long output.
## Chat voice responses
When Peter asks for a "voice" reply (e.g., "crazy scientist voice", "explain in voice"), generate audio and send it:
When the user asks for a "voice" reply (e.g., "crazy scientist voice", "explain in voice"), generate audio and send it:
```bash
# Generate audio file

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -26,21 +26,40 @@ metadata:
Use `sonos` to control Sonos speakers on the local network.
Quick start
## Quick start
- `sonos discover`
- `sonos status --name "Kitchen"`
- `sonos play|pause|stop --name "Kitchen"`
- `sonos volume set 15 --name "Kitchen"`
Common tasks
## Common tasks
- Grouping: `sonos group status|join|unjoin|party|solo`
- Favorites: `sonos favorites list|open`
- Queue: `sonos queue list|play|clear`
- Spotify search (via SMAPI): `sonos smapi search --service "Spotify" --category tracks "query"`
Notes
## Notes
- If SSDP fails, specify `--ip <speaker-ip>`.
- Spotify Web API search is optional and requires `SPOTIFY_CLIENT_ID/SECRET`.
- If there is an error, check the troubleshooting section and offer advice if there is a decent match.
## Troubleshooting
### `sonos discover` - `no route to host`
- On error `Error: write udp4 0.0.0.0:64326->239.255.255.250:1900: sendto: no route to host (Command exited with code 1)`
- Note that the port number after `0.0.0.0:` is ephemeral and will change and the netmask may not match exactly either
- The `sendto: no route to host` should stay consistent
- Advise the user that in `direct` (no Docker sandbox) mode on Mac OS that Settings -> Privacy & Security -> Local Network will need to be enabled for the top-level host parent process of the Gateway
- `node` if running via `launchd`
- `Terminal` if running the gateway directly in the terminal
- `Visual Studio Code` if running via terminal in VS Code
- An alternative option is to use `sandbox` (docker container) with network access allowed for that sandbox
### `sonos discover` - `bind: operation not permitted`
- On error `Error: listen udp4 0.0.0.0:0: bind: operation not permitted`
- Advise the user that they may be running via a Codex or other sandbox that does not permit network access (this can be replicated by running `sonos discover` within a Codex CLI session with sandbox enabled and not approving the escalation request)

59
tools/summarize/flake.lock generated Normal file
View File

@ -0,0 +1,59 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"root": "root_2"
}
},
"root_2": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"path": "../..",
"type": "path"
},
"original": {
"path": "../..",
"type": "path"
},
"parent": []
}
},
"root": "root",
"version": 7
}

View File

@ -3,7 +3,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
root.url = "../..";
};
outputs = { self, nixpkgs, root }:

View File

@ -1,6 +1,6 @@
---
name: summarize
description: Summarize or extract text/transcripts from URLs, podcasts, and local files (great fallback for “transcribe this YouTube/video”).
description: Summarize or transcribe URLs, YouTube/videos, podcasts, articles, transcripts, PDFs, and local files.
homepage: https://summarize.sh
metadata:
{

38
tools/wacrawl/flake.nix Normal file
View File

@ -0,0 +1,38 @@
{
description = "openclaw plugin: wacrawl";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?rev=16c7794d0a28b5a37904d55bcca36003b9109aaa&narHash=sha256-fFUnEYMla8b7UKjijLnMe%2BoVFOz6HjijGGNS1l7dYaQ%3D";
root.url = "path:../..";
};
outputs = { self, nixpkgs, root }:
let
lib = nixpkgs.lib;
systems = builtins.attrNames root.packages;
pluginFor = system:
let
packagesForSystem = root.packages.${system} or {};
wacrawl = packagesForSystem.wacrawl or null;
in
if wacrawl == null then null else {
name = "wacrawl";
skills = [ ./skills/wacrawl ];
packages = [ wacrawl ];
needs = {
stateDirs = [ ".wacrawl" ];
requiredEnv = [ ];
};
};
in {
packages = lib.genAttrs systems (system:
let
wacrawl = (root.packages.${system} or {}).wacrawl or null;
in
if wacrawl == null then {}
else { wacrawl = wacrawl; }
);
openclawPlugin = pluginFor;
};
}

View File

@ -0,0 +1,69 @@
---
name: wacrawl
description: Read-only local archive and search for WhatsApp Desktop chats, messages, and media metadata.
homepage: https://github.com/steipete/wacrawl
metadata:
{
"openclaw":
{
"emoji": "💬",
"requires": { "bins": ["wacrawl"] },
"install":
[
{
"id": "brew",
"kind": "brew",
"formula": "steipete/tap/wacrawl",
"bins": ["wacrawl"],
"label": "Install wacrawl (brew)",
},
],
},
}
---
# wacrawl
Use `wacrawl` to snapshot local WhatsApp Desktop data into a separate SQLite archive and search it offline.
## When to Use
Use this skill when the user wants to:
- inspect local WhatsApp Desktop history without opening the app
- archive chats into a local SQLite database for repeat queries
- search WhatsApp messages locally with filters
- list chats, recent messages, or archive status from a read-only import
## Requirements
- local WhatsApp Desktop data on the same machine
- enough local disk for `~/.wacrawl/wacrawl.db`
- understand that this is read-only inspection, not message sending
## Setup
- Default source: `~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared`
- Default archive DB: `~/.wacrawl/wacrawl.db`
- First sanity check:
- `wacrawl doctor`
- First import:
- `wacrawl import`
## Common Commands
- Doctor: `wacrawl doctor`
- Import fresh snapshot: `wacrawl import`
- Archive status: `wacrawl status`
- List chats: `wacrawl chats --limit 20`
- Recent messages: `wacrawl messages --limit 20`
- One chat: `wacrawl messages --chat 1234567890@s.whatsapp.net --limit 50`
- Search: `wacrawl search "release notes"`
- Filtered search: `wacrawl --json search "invoice" --from-them --after 2026-01-01`
## Notes
- `wacrawl` is read-only and does not send messages.
- It copies WhatsApp SQLite files into a temp snapshot before import.
- Use `--source` to override the WhatsApp Desktop container path.
- Use `--db` to archive somewhere other than `~/.wacrawl/wacrawl.db`.