Compare commits

..

No commits in common. "main" and "fix/update-tools-skip-bird" have entirely different histories.

30 changed files with 482 additions and 928 deletions

View File

@ -16,35 +16,21 @@ These tools are essential for a capable openclaw instance - screen capture, came
- **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 |
| [**discrawl**](https://github.com/steipete/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/steipete/gogcli) | Google CLI for Gmail, Calendar, Drive, and Contacts |
| [**goplaces**](https://github.com/steipete/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 |
| [**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 |
| [**CodexBar**](https://github.com/steipete/CodexBar) | macOS menu bar app for Codex, Claude, and other provider usage |
| [**oracle**](https://github.com/steipete/oracle) | Bundle prompts + files for AI queries |
## Usage (as openclaw plugins)
@ -53,10 +39,8 @@ 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/discrawl"; }
{ source = "github:openclaw/nix-steipete-tools?dir=tools/peekaboo"; }
{ source = "github:openclaw/nix-steipete-tools?dir=tools/summarize"; }
{ source = "github:openclaw/nix-steipete-tools?dir=tools/wacrawl"; }
];
```
@ -74,53 +58,13 @@ inputs.nix-steipete-tools.url = "github:openclaw/nix-steipete-tools";
# Then use:
inputs.nix-steipete-tools.packages.aarch64-darwin.camsnap
inputs.nix-steipete-tools.packages.aarch64-darwin.discrawl
inputs.nix-steipete-tools.packages.aarch64-darwin.peekaboo
inputs.nix-steipete-tools.packages.aarch64-darwin.wacrawl
inputs.nix-steipete-tools.packages.aarch64-darwin.codexbar-app
# etc.
# Linux examples:
inputs.nix-steipete-tools.packages.x86_64-linux.camsnap
inputs.nix-steipete-tools.packages.x86_64-linux.discrawl
inputs.nix-steipete-tools.packages.aarch64-linux.gogcli
inputs.nix-steipete-tools.packages.x86_64-linux.summarize
inputs.nix-steipete-tools.packages.x86_64-linux.wacrawl
```
### Home Manager modules
Some packages also ship Home Manager modules so they can be enabled directly
without hand-writing install and launchd wiring. For CodexBar:
```nix
{
imports = [ inputs.nix-steipete-tools.homeManagerModules.codexbar ];
programs.codexbar.enable = true;
}
```
That adds the CodexBar app package to `home.packages`. Home Manager can then
expose the app bundle through its Darwin app targets. If you want Home Manager
to own startup too, enable the launchd agent explicitly:
```nix
{
programs.codexbar = {
enable = true;
launchd.enable = true;
launchd.keepAlive = false;
};
}
```
### macOS app bundles
If you only want the raw app package, use the package output directly:
```nix
inputs.nix-steipete-tools.packages.aarch64-darwin.codexbar-app
```
## Skills syncing
@ -141,7 +85,7 @@ Tools track upstream GitHub releases directly (not Homebrew).
go run ./cmd/update-tools
```
Fetches latest release versions/URLs/hashes and updates the Nix expressions.
Fetches latest release versions/URLs/hashes and updates the Nix expressions. Oracle uses pnpm and auto-derives its hash via build mismatch.
## CI
@ -153,12 +97,6 @@ Fetches latest release versions/URLs/hashes and updates the Nix expressions.
Automated PRs keep everything fresh without manual intervention.
## Temporarily disabled
`bird` is not exported right now because the upstream GitHub release assets for
v0.8.0 are gone. The npm package still exists, but this flake does not currently
build npm dependencies for it.
## License
Tools are packaged as-is from upstream. See individual tool repos for their licenses.

View File

@ -60,14 +60,14 @@ 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")

View File

@ -140,6 +140,85 @@ 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 {
@ -147,26 +226,6 @@ func main() {
}
tools := []Tool{
{
Name: "discrawl",
Repo: "steipete/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",
@ -177,17 +236,6 @@ func main() {
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "gogcli.nix"),
},
{
Name: "goplaces",
Repo: "steipete/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",
@ -208,11 +256,20 @@ func main() {
},
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",
Assets: []AssetSpec{
{System: "aarch64-darwin", Regex: regexp.MustCompile(`peekaboo-macos-(?:arm64|universal)\.tar\.gz`)},
{System: "aarch64-darwin", Regex: regexp.MustCompile(`peekaboo-macos-universal\.tar\.gz`)},
},
NixFile: filepath.Join(repoRoot, "nix", "pkgs", "peekaboo.nix"),
},
@ -256,4 +313,9 @@ 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

@ -12,17 +12,16 @@
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" ];
codexbar-app = [ "aarch64-darwin" ];
oracle = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
};
in {
packages = forAllSystems (system:
@ -37,12 +36,6 @@
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 {};
})
@ -55,6 +48,9 @@
// (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 {};
})
@ -67,16 +63,14 @@
// (lib.optionalAttrs (supports "imsg") {
imsg = pkgs.callPackage ./nix/pkgs/imsg.nix {};
})
// (lib.optionalAttrs (supports "codexbar-app") {
codexbar-app = pkgs.callPackage ./nix/pkgs/codexbar-app.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;
};
})
);
homeManagerModules = {
codexbar = import ./nix/modules/home-manager/codexbar.nix { inherit self; };
default = self.homeManagerModules.codexbar;
};
checks = forAllSystems (system: self.packages.${system});
};
}

View File

@ -61,6 +61,15 @@ 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("")
}

View File

@ -1,88 +0,0 @@
{ self }:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkEnableOption mkIf mkOption types;
cfg = config.programs.codexbar;
system = pkgs.stdenv.hostPlatform.system;
defaultPackage =
if self.packages ? ${system} && self.packages.${system} ? codexbar-app then
self.packages.${system}.codexbar-app
else
null;
in
{
options.programs.codexbar = {
enable = mkEnableOption "CodexBar macOS menu bar app";
package = mkOption {
type = types.nullOr types.package;
default = defaultPackage;
defaultText = lib.literalExpression "inputs.nix-steipete-tools.packages.\${pkgs.system}.codexbar-app";
description = "CodexBar app package.";
};
launchd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Configure a launchd agent to manage the CodexBar process.
CodexBar can also manage launch-at-login from inside the app. Enable this
option when you want Home Manager and launchd to own startup instead.
'';
};
keepAlive = mkOption {
type = types.bool;
default = false;
description = "Whether launchd should restart CodexBar after it exits.";
};
environmentVariables = mkOption {
type = types.attrsOf types.str;
default = {
PATH = "${config.home.profileDirectory}/bin:/usr/bin:/bin:/usr/sbin:/sbin";
};
defaultText = lib.literalExpression ''
{
PATH = "\${config.home.profileDirectory}/bin:/usr/bin:/bin:/usr/sbin:/sbin";
}
'';
description = "Environment variables passed to the CodexBar launchd agent.";
};
};
};
config = mkIf cfg.enable {
assertions = [
(lib.hm.assertions.assertPlatform "programs.codexbar" pkgs lib.platforms.darwin)
{
assertion = cfg.package != null;
message = "programs.codexbar.package must be set on this platform.";
}
{
assertion = !cfg.launchd.enable || config.launchd.enable;
message = "programs.codexbar.launchd.enable requires launchd.enable.";
}
];
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
launchd.agents.codexbar = mkIf cfg.launchd.enable {
enable = true;
config = {
ProgramArguments = [ "${cfg.package}/Applications/CodexBar.app/Contents/MacOS/CodexBar" ];
ProcessType = "Interactive";
RunAtLoad = true;
KeepAlive = cfg.launchd.keepAlive;
EnvironmentVariables = cfg.launchd.environmentVariables;
};
};
};
}

View File

@ -1,37 +0,0 @@
{
lib,
stdenvNoCC,
fetchzip,
}:
stdenvNoCC.mkDerivation {
pname = "codexbar-app";
version = "0.23";
src = fetchzip {
url = "https://github.com/steipete/CodexBar/releases/download/v0.23/CodexBar-0.23.zip";
hash = "sha256-4LtZZzoP9tofNhy8jVkaopE+aivn0PRBEeJBGpDnbE8=";
stripRoot = false;
};
dontUnpack = true;
installPhase = ''
runHook preInstall
mkdir -p "$out/Applications"
app_path="$(find "$src" -maxdepth 2 -name 'CodexBar.app' -print -quit)"
if [ -z "$app_path" ]; then
echo "CodexBar.app not found in $src" >&2
exit 1
fi
cp -R "$app_path" "$out/Applications/CodexBar.app"
runHook postInstall
'';
meta = with lib; {
description = "CodexBar macOS menu bar app bundle";
homepage = "https://github.com/steipete/CodexBar";
license = licenses.mit;
platforms = [ "aarch64-darwin" ];
};
}

View File

@ -1,53 +0,0 @@
{ lib, stdenv, fetchurl }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/discrawl/releases/download/v0.6.5/discrawl_0.6.5_darwin_arm64.tar.gz";
hash = "sha256-nxt8Pq0ldbf4QefkBYDMIEzp9dRZUBuK7GBygI+4HDc=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/discrawl/releases/download/v0.6.5/discrawl_0.6.5_linux_amd64.tar.gz";
hash = "sha256-0lLlZYWZgvebBNWfZjicdB+o9tJ43N4n3CxAQ9mLfHM=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/discrawl/releases/download/v0.6.5/discrawl_0.6.5_linux_arm64.tar.gz";
hash = "sha256-k+ls+9dKvBLaMbThCoNgn6vQxGhviDeXaH6JmbeBrLM=";
};
};
in
stdenv.mkDerivation {
pname = "discrawl";
version = "0.6.5";
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/steipete/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.14.0/gogcli_0.14.0_darwin_arm64.tar.gz";
hash = "sha256-qJ+H7dc+oPn7E57kvEIwlAc990nkpiGSqlFCDy9kZjo=";
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_darwin_arm64.tar.gz";
hash = "sha256-sNPDBSmr40X1Epzm7ZRzJfcDHU7p70kLHl12/5c4/J8=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/gogcli/releases/download/v0.14.0/gogcli_0.14.0_linux_amd64.tar.gz";
hash = "sha256-sq2qUDYnqlbZGGzxBHp5CqFfjdGFIkgN1P8UBgyd0hs=";
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_linux_amd64.tar.gz";
hash = "sha256-f8DVB7LQprzprE6IyGu3jZo2ckRXEu9FiomMM7KqIV0=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/gogcli/releases/download/v0.14.0/gogcli_0.14.0_linux_arm64.tar.gz";
hash = "sha256-KOq4AyYyjUvL6tMq4WtOZu2WYTdtJR1g44uFmJt8oHs=";
url = "https://github.com/steipete/gogcli/releases/download/v0.10.0/gogcli_0.10.0_linux_arm64.tar.gz";
hash = "sha256-Q48xgr47bF3Om3kyKm6tnH9nOW9g0Em7i0o5AEhPZRU=";
};
};
in
stdenv.mkDerivation {
pname = "gogcli";
version = "0.14.0";
version = "0.10.0";
src = fetchurl sources.${stdenv.hostPlatform.system};

View File

@ -1,60 +1,31 @@
{ lib, stdenv, fetchurl }:
{ lib, buildGoModule, fetchFromGitHub }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/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/steipete/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/steipete/goplaces/releases/download/v0.4.0/goplaces_0.4.0_linux_amd64.tar.gz";
hash = "sha256-cUHDAZ4Ep50K2ljLn8LyJeWPJlcr/sytP95UsNmSv3w=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/goplaces/releases/download/v0.4.0/goplaces_0.4.0_linux_arm64.tar.gz";
hash = "sha256-YjeLRDLwuQbUOYZYnpNkuYDvsm54PB1jmqTHas2T5VY=";
};
buildGoModule rec {
pname = "goplaces";
version = "0.2.2-dev";
src = fetchFromGitHub {
owner = "joshp123";
repo = "goplaces";
rev = "a71fe3de986a78607d923f397113d7eb1babc111";
hash = "sha256-Lufp9+fwcoluNZR9iDYoIJ1yu3sM/8Q/EOkAlq5VLdk=";
};
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 (New)";
homepage = "https://github.com/steipete/goplaces";
description = "Modern Go client + CLI for the Google Places API";
homepage = "https://github.com/joshp123/goplaces";
license = licenses.mit;
platforms = builtins.attrNames sources;
platforms = platforms.darwin ++ platforms.linux;
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.5.0/imsg-macos.zip";
hash = "sha256-rirJZ+8eQf0Ea3RXD4bHrWljSpGEb+wtbMdrsEtHPAo=";
url = "https://github.com/steipete/imsg/releases/download/v0.4.0/imsg-macos.zip";
hash = "sha256-0OXjM+6IGS1ZW/7Z7s5g417K0DABRZZtWtJ0WMM+QHs=";
};
};
in
stdenv.mkDerivation {
pname = "imsg";
version = "0.5.0";
version = "0.4.0";
src = fetchurl sources.${stdenv.hostPlatform.system};

128
nix/pkgs/oracle.nix Normal file
View File

@ -0,0 +1,128 @@
{ 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-beta4/peekaboo-macos-arm64.tar.gz";
hash = "sha256-74eXVHpRAmcs0mzK3GLh/3So78AEMZzXBvx1Zg7uOkc=";
url = "https://github.com/steipete/Peekaboo/releases/download/v3.0.0-beta3/peekaboo-macos-universal.tar.gz";
hash = "sha256-d+rfb9XFTqxktIRNXMiHiQttb0XUmvYbBcbinqLL0kU=";
};
};
in
stdenv.mkDerivation {
pname = "peekaboo";
version = "3.0.0-beta4";
version = "3.0.0-beta3";
src = fetchurl sources.${stdenv.hostPlatform.system};

View File

@ -1,4 +1,4 @@
{ lib, stdenv, fetchurl }:
{ lib, stdenv, fetchurl, watchman }:
let
sources = {
@ -30,6 +30,8 @@ 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";

View File

@ -15,17 +15,17 @@
let
pname = "summarize";
version = "0.14.1";
version = "0.11.1";
binSources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/summarize/releases/download/v0.14.1/summarize-macos-arm64-v0.14.1.tar.gz";
hash = "sha256-X3aBGiAjoOSIINxHyGOzg/b88fpCZQcv0cM2L0KqS8c=";
url = "https://github.com/steipete/summarize/releases/download/v0.11.1/summarize-macos-arm64-v0.11.1.tar.gz";
hash = "sha256-RJNeCxWfbMCOrD6ABR4wSALdtl1sImS4Wkjz1TdPmTE=";
};
};
src = fetchurl {
url = "https://github.com/steipete/summarize/archive/refs/tags/v${version}.tar.gz";
hash = "sha256-UAf7IXerk5VM3DyLyMD259Dh55ZjcZPK/3T2kPj9RO4=";
hash = "sha256-2s3XAy9evHRZQthTWRhIjkv7Z40K5PiUFHEIGo6XxBs=";
};
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-s1I3TTX3uw/iJpbWvoNE5kg9XQVjw0mnA5pRTVs7ypY=";
hash = "sha256-vqTXJ64DpanUwdUlSS3Fa+zy70qIGgsNtKzYF82J1RU=";
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 = "${lib.getDev nodejs}";
npm_config_nodedir = "${nodejs.dev}";
npm_config_build_from_source = "1";
PNPM_CONFIG_IGNORE_SCRIPTS = "1";
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
@ -120,7 +120,6 @@ 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"

View File

@ -1,53 +0,0 @@
{ lib, stdenv, fetchurl }:
let
sources = {
"aarch64-darwin" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.0/wacrawl_0.2.0_darwin_arm64.tar.gz";
hash = "sha256-Pk3G8ZBsZOSU9PNgtuJPzQC2XDe71yUSkPHXxqGL+vQ=";
};
"x86_64-linux" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.0/wacrawl_0.2.0_linux_amd64.tar.gz";
hash = "sha256-xZBLpgzLG+3NClMwanZ2V79Qqw36acpT2MI5z3gfHoo=";
};
"aarch64-linux" = {
url = "https://github.com/steipete/wacrawl/releases/download/v0.2.0/wacrawl_0.2.0_linux_arm64.tar.gz";
hash = "sha256-oiPgR7yWd5aYTYqlIZST89ELSdetW6nvTaE8rl78ZTc=";
};
};
in
stdenv.mkDerivation {
pname = "wacrawl";
version = "0.2.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/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,38 +0,0 @@
{
description = "openclaw plugin: discrawl";
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 {};
discrawl = packagesForSystem.discrawl or null;
in
if discrawl == null then null else {
name = "discrawl";
skills = [ ./skills/discrawl ];
packages = [ discrawl ];
needs = {
stateDirs = [ ".discrawl" ];
requiredEnv = [ ];
};
};
in {
packages = lib.genAttrs systems (system:
let
discrawl = (root.packages.${system} or {}).discrawl or null;
in
if discrawl == null then {}
else { discrawl = discrawl; }
);
openclawPlugin = pluginFor;
};
}

View File

@ -1,74 +0,0 @@
---
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/steipete/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.

View File

@ -1,65 +0,0 @@
{
"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": {
"lastModified": 1772109602,
"narHash": "sha256-Lzs6tzCkOOtDaZIRSGvS6ykJOtu0hZrdI95kxhkSIuw=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "eddb00d4c1037a983913fccf32880aece050cd32",
"type": "github"
},
"original": {
"narHash": "sha256-Lzs6tzCkOOtDaZIRSGvS6ykJOtu0hZrdI95kxhkSIuw=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "eddb00d4c1037a983913fccf32880aece050cd32",
"type": "github"
}
}
},
"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=eddb00d4c1037a983913fccf32880aece050cd32&narHash=sha256-Lzs6tzCkOOtDaZIRSGvS6ykJOtu0hZrdI95kxhkSIuw=";
root.url = "github:openclaw/nix-steipete-tools?rev=dbf0a31a57407d9140e32357ea8d0215bd9feed9&narHash=sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=";
};
outputs = { self, nixpkgs, root }:

View File

@ -1,65 +0,0 @@
{
"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": {
"lastModified": 1770237944,
"narHash": "sha256-nfCSSyNU97XpKVPgo6mODBwrVeTOuMCl3i18QuGjpN0=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "6352c8247b3b889d7f17bce1f09d6c58fd34932c",
"type": "github"
},
"original": {
"narHash": "sha256-nfCSSyNU97XpKVPgo6mODBwrVeTOuMCl3i18QuGjpN0=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "6352c8247b3b889d7f17bce1f09d6c58fd34932c",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,6 +1,6 @@
---
name: imsg
description: iMessage/SMS CLI for listing chats, history, and sending messages via Messages.app.
description: iMessage/SMS CLI for listing chats, history, watch, and sending.
homepage: https://imsg.to
metadata:
{
@ -23,100 +23,52 @@ metadata:
}
---
# imsg
# imsg Actions
Use `imsg` to read and send iMessage/SMS via macOS Messages.app.
## Overview
## When to Use
Use `imsg` to read and send Messages.app iMessage/SMS on macOS.
✅ **USE this skill when:**
Requirements: Messages.app signed in, Full Disk Access for your terminal, and Automation permission to control Messages.app for sending.
- User explicitly asks to send iMessage or SMS
- Reading iMessage conversation history
- Checking recent Messages.app chats
- Sending to phone numbers or Apple IDs
## Inputs to collect
## When NOT to Use
- Recipient handle (phone/email) for `send`
- `chatId` for history/watch (from `imsg chats --limit 10 --json`)
- `text` and optional `file` path for sends
❌ **DON'T use this skill when:**
## Actions
- 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
### List chats
```bash
imsg chats --limit 10 --json
```
### View History
### Fetch chat 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 for New Messages
### Watch a chat
```bash
imsg watch --chat-id 1 --attachments
```
### Send Messages
### Send a message
```bash
# 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
imsg send --to "+14155551212" --text "hi" --file /path/pic.jpg
```
## Service Options
## Notes
- `--service imessage` — Force iMessage (requires recipient has iMessage)
- `--service sms` — Force SMS (green bubble)
- `--service auto` — Let Messages.app decide (default)
- `--service imessage|sms|auto` controls delivery.
- Confirm recipient + message before sending.
## Safety Rules
## Ideas to try
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"
```
- Use `imsg chats --limit 10 --json` to discover chat ids.
- Watch a high-signal chat to stream incoming messages.

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

@ -0,0 +1,38 @@
{
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

@ -0,0 +1,125 @@
---
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

@ -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 the user asks for a "voice" reply (e.g., "crazy scientist voice", "explain in voice"), generate audio and send it:
When Peter asks for a "voice" reply (e.g., "crazy scientist voice", "explain in voice"), generate audio and send it:
```bash
# Generate audio file

View File

@ -26,40 +26,21 @@ 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)

View File

@ -1,65 +0,0 @@
{
"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": {
"lastModified": 1769797395,
"narHash": "sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "dbf0a31a57407d9140e32357ea8d0215bd9feed9",
"type": "github"
},
"original": {
"narHash": "sha256-QkPl/Rgk9DXgaVNhjvHHHjy5e81j+MzcVOouZRdUTLA=",
"owner": "openclaw",
"repo": "nix-steipete-tools",
"rev": "dbf0a31a57407d9140e32357ea8d0215bd9feed9",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

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

View File

@ -1,38 +0,0 @@
{
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

@ -1,69 +0,0 @@
---
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`.