mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
Merge remote-tracking branch 'origin/cli-agent-skill' into test/cli-612-613
* origin/cli-agent-skill: docs(cli-skill): update skill file to match CLI parity changes style: fix rustfmt formatting after rebase fix(nest): update skill content for CLI parity changes fix(nest): correct skill content inaccuracies + chmod intermediate dirs feat(nest): add sprout-cli agent skill to nest initialization
This commit is contained in:
@@ -24,6 +24,10 @@ const NEST_DIRS: &[&str] = &[
|
||||
/// Fully static — no runtime interpolation, no secrets, no user paths.
|
||||
const AGENTS_MD: &str = include_str!("nest_agents.md");
|
||||
|
||||
/// Default SKILL.md content for the sprout-cli Claude Code skill.
|
||||
/// Written to ~/.sprout/.claude/skills/sprout-cli/SKILL.md on first init.
|
||||
const SPROUT_CLI_SKILL_MD: &str = include_str!("nest_skill.md");
|
||||
|
||||
/// Returns the nest root path (`~/.sprout`), or `None` if the home
|
||||
/// directory cannot be resolved.
|
||||
pub fn nest_dir() -> Option<PathBuf> {
|
||||
@@ -43,10 +47,12 @@ pub fn ensure_nest() -> Result<(), String> {
|
||||
///
|
||||
/// - Creates the root directory and all subdirectories.
|
||||
/// - Writes `AGENTS.md` only if it doesn't already exist.
|
||||
/// - Sets 700 permissions on the root and all subdirectories (Unix).
|
||||
/// - Writes `.claude/skills/sprout-cli/SKILL.md` only if it doesn't already exist.
|
||||
/// - Sets 700 permissions on the root, all subdirectories, and the skill
|
||||
/// directory tree (Unix).
|
||||
///
|
||||
/// Idempotent: safe to call on every launch. Existing files are never
|
||||
/// overwritten — users can freely edit AGENTS.md and it will persist.
|
||||
/// overwritten — users can freely edit AGENTS.md or SKILL.md and they persist.
|
||||
///
|
||||
/// Rejects symlinks at the root path to prevent redirect attacks.
|
||||
///
|
||||
@@ -98,6 +104,27 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Write sprout-cli skill alongside AGENTS.md (same idempotent pattern).
|
||||
let skill_dir = root.join(".claude/skills/sprout-cli");
|
||||
fs::create_dir_all(&skill_dir).map_err(|e| format!("create {}: {e}", skill_dir.display()))?;
|
||||
|
||||
let skill_md = root.join(".claude/skills/sprout-cli/SKILL.md");
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&skill_md)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
use std::io::Write;
|
||||
file.write_all(SPROUT_CLI_SKILL_MD.as_bytes())
|
||||
.map_err(|e| format!("write {}: {e}", skill_md.display()))?;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(e) => {
|
||||
return Err(format!("create {}: {e}", skill_md.display()));
|
||||
}
|
||||
}
|
||||
|
||||
// Set owner-only permissions on root and all subdirectories.
|
||||
// Skip any path that is a symlink — chmod would affect the target.
|
||||
#[cfg(unix)]
|
||||
@@ -117,6 +144,23 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
|
||||
.map_err(|e| format!("set permissions on {}: {e}", path.display()))?;
|
||||
}
|
||||
}
|
||||
// Skill directory and its intermediate parents inside root get 700.
|
||||
// create_dir_all creates .claude/ and .claude/skills/ with umask
|
||||
// defaults — lock them down the same way we do NEST_DIRS.
|
||||
for dir in [
|
||||
root.join(".claude"),
|
||||
root.join(".claude/skills"),
|
||||
skill_dir.clone(),
|
||||
] {
|
||||
let is_symlink = dir
|
||||
.symlink_metadata()
|
||||
.map(|m| m.file_type().is_symlink())
|
||||
.unwrap_or(false);
|
||||
if !is_symlink {
|
||||
fs::set_permissions(&dir, perms.clone())
|
||||
.map_err(|e| format!("set permissions on {}: {e}", dir.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -260,6 +304,43 @@ mod tests {
|
||||
assert!(result.unwrap_err().contains("symlink"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_nest_creates_skill_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().join(".sprout");
|
||||
ensure_nest_at(&root).unwrap();
|
||||
let skill = root.join(".claude/skills/sprout-cli/SKILL.md");
|
||||
assert!(skill.exists(), "SKILL.md should exist");
|
||||
let content = fs::read_to_string(&skill).unwrap();
|
||||
assert_eq!(content, SPROUT_CLI_SKILL_MD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_nest_does_not_overwrite_skill_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().join(".sprout");
|
||||
ensure_nest_at(&root).unwrap();
|
||||
let skill = root.join(".claude/skills/sprout-cli/SKILL.md");
|
||||
fs::write(&skill, "custom skill content").unwrap();
|
||||
ensure_nest_at(&root).unwrap();
|
||||
assert_eq!(fs::read_to_string(&skill).unwrap(), "custom skill content");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_nest_skill_dir_has_700_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().join(".sprout");
|
||||
ensure_nest_at(&root).unwrap();
|
||||
// All three dirs in the skill path should be locked down.
|
||||
for dir in [".claude", ".claude/skills", ".claude/skills/sprout-cli"] {
|
||||
let path = root.join(dir);
|
||||
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o700, "{dir} should be 700");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_nest_skips_permissions_on_symlinked_child() {
|
||||
|
||||
623
desktop/src-tauri/src/managed_agents/nest_skill.md
Normal file
623
desktop/src-tauri/src/managed_agents/nest_skill.md
Normal file
@@ -0,0 +1,623 @@
|
||||
---
|
||||
name: sprout-cli
|
||||
description: >
|
||||
Use the Sprout CLI (`sprout` command) to interact with a Sprout relay: send
|
||||
and read messages, manage channels, set canvas documents, add reactions,
|
||||
open DMs, query user profiles, trigger workflows, search messages, post code
|
||||
diffs, manage repositories, publish social notes, upload files, and manage
|
||||
persistent agent memory. Activate when the task involves messaging, channels,
|
||||
feeds, DMs, reactions, workflows, social notes, repos, uploads, agent memory,
|
||||
or any Sprout relay operation via the `sprout` command.
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Sprout CLI Skill
|
||||
|
||||
## Environment
|
||||
|
||||
`SPROUT_PRIVATE_KEY` is pre-set by the harness. Never prompt for it, never read it, never echo it. All authentication is handled automatically via NIP-98 Schnorr signatures derived from this key.
|
||||
|
||||
`SPROUT_RELAY_URL` defaults to `http://localhost:3000`. Override only if explicitly instructed.
|
||||
|
||||
All output is JSON on stdout unless noted otherwise. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object. Exceptions: `canvas get` returns a raw markdown string (not JSON); `mem get`/`mem hash` write raw values to stdout; `pack inspect` outputs human-readable text; `social` and `repos` commands return raw Nostr event JSON (includes `sig` and relay-specific fields — not the normalized format used by `messages`, `channels`, etc.).
|
||||
|
||||
Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4), `conflict` (exit 5 — write conflict, relay rejected as superseded). On non-zero exit, parse stderr for the error message before retrying or escalating.
|
||||
|
||||
## Parameter Conventions
|
||||
|
||||
- `--channel` accepts UUID format (e.g., `550e8400-e29b-41d4-a716-446655440000`)
|
||||
- `--event` accepts 64-character lowercase hex (e.g., `a3f2...`). Do not pass Bech32-encoded `note1...` identifiers — convert first if needed.
|
||||
- `--pubkey` accepts 64-character lowercase hex. Do not pass `npub...` identifiers.
|
||||
- `--content -` reads content from stdin, enabling pipe-friendly workflows.
|
||||
- Content max 65,536 bytes. Larger content will be rejected with exit code 1.
|
||||
- Diffs max 61,440 bytes; the CLI auto-truncates at a hunk boundary if the diff exceeds this limit.
|
||||
|
||||
## Messaging
|
||||
|
||||
Send a message to a channel:
|
||||
|
||||
```bash
|
||||
sprout messages send --channel <UUID> --content "text"
|
||||
```
|
||||
|
||||
Send a threaded reply:
|
||||
|
||||
```bash
|
||||
sprout messages send --channel <UUID> --content "reply text" --reply-to <event-id>
|
||||
```
|
||||
|
||||
Send a forum post (kind 45001) or forum comment (kind 45003):
|
||||
|
||||
```bash
|
||||
sprout messages send --channel <UUID> --content "post title" --kind 45001
|
||||
sprout messages send --channel <UUID> --content "comment" --kind 45003 --reply-to <event-id>
|
||||
```
|
||||
|
||||
`--kind` routing: omitted or `9` sends a stream message; `45001` sends a forum post; `45003` sends a forum comment (requires `--reply-to`). Other kinds are rejected.
|
||||
|
||||
Attach files (uploads to Blossom, includes as imeta tags):
|
||||
|
||||
```bash
|
||||
sprout messages send --channel <UUID> --content "see attached" --file /path/to/image.png
|
||||
```
|
||||
|
||||
Explicitly mention users by pubkey (in addition to @name auto-resolution from message body):
|
||||
|
||||
```bash
|
||||
sprout messages send --channel <UUID> --content "cc @alice" --mention <hex-pubkey>
|
||||
```
|
||||
|
||||
`--file` and `--mention` are repeatable.
|
||||
|
||||
Read recent messages (default kinds: `[9, 40002, 40008, 45001, 45003]`):
|
||||
|
||||
```bash
|
||||
sprout messages get --channel <UUID> --limit 20
|
||||
sprout messages get --channel <UUID> --limit 50 --since 1716000000
|
||||
sprout messages get --channel <UUID> --limit 50 --before 1716050000
|
||||
sprout messages get --channel <UUID> --kinds "1,1984" # override default kinds
|
||||
```
|
||||
|
||||
Get a thread rooted at a specific event:
|
||||
|
||||
```bash
|
||||
sprout messages thread --channel <UUID> --event <event-id>
|
||||
sprout messages thread --channel <UUID> --event <event-id> --limit 100 --depth-limit 5
|
||||
```
|
||||
|
||||
Full-text search across all channels you can access:
|
||||
|
||||
```bash
|
||||
sprout messages search --query "architecture decision"
|
||||
sprout messages search --query "deploy" --limit 50
|
||||
```
|
||||
|
||||
Send a code diff with repository metadata (pipe the diff via stdin):
|
||||
|
||||
```bash
|
||||
git diff HEAD~1 | sprout messages send-diff --channel <UUID> --diff - --repo https://github.com/org/repo --commit abc123def456
|
||||
```
|
||||
|
||||
Edit a message you sent:
|
||||
|
||||
```bash
|
||||
sprout messages edit --event <event-id> --content "updated text"
|
||||
```
|
||||
|
||||
Delete a message:
|
||||
|
||||
```bash
|
||||
sprout messages delete --event <event-id>
|
||||
```
|
||||
|
||||
Vote on a forum post:
|
||||
|
||||
```bash
|
||||
sprout messages vote --event <event-id> --direction up
|
||||
sprout messages vote --event <event-id> --direction down
|
||||
```
|
||||
|
||||
## Channels
|
||||
|
||||
List all visible channels:
|
||||
|
||||
```bash
|
||||
sprout channels list
|
||||
```
|
||||
|
||||
Returns `[{channel_id, name, description, created_at}]`.
|
||||
|
||||
Filter channel lists:
|
||||
|
||||
```bash
|
||||
sprout channels list --member # only channels you've joined
|
||||
sprout channels list --visibility open # open or private
|
||||
```
|
||||
|
||||
Create a channel:
|
||||
|
||||
```bash
|
||||
sprout channels create --name "eng-backend" --type stream --visibility open
|
||||
sprout channels create --name "rfcs" --type forum --visibility open --description "Architecture RFCs"
|
||||
```
|
||||
|
||||
Returns `{event_id, channel_id, accepted, message}`. Use `channel_id` for subsequent operations.
|
||||
|
||||
Get channel details:
|
||||
|
||||
```bash
|
||||
sprout channels get --channel <UUID>
|
||||
```
|
||||
|
||||
Returns `{channel_id, name, description, created_at, pubkey}`, or `null` if not found.
|
||||
|
||||
Update channel metadata:
|
||||
|
||||
```bash
|
||||
sprout channels update --channel <UUID> --name "new-name" --description "new description"
|
||||
```
|
||||
|
||||
Join or leave:
|
||||
|
||||
```bash
|
||||
sprout channels join --channel <UUID>
|
||||
sprout channels leave --channel <UUID>
|
||||
```
|
||||
|
||||
Set topic or purpose:
|
||||
|
||||
```bash
|
||||
sprout channels topic --channel <UUID> --topic "Sprint 42 coordination"
|
||||
sprout channels purpose --channel <UUID> --purpose "Backend team daily sync"
|
||||
```
|
||||
|
||||
List members:
|
||||
|
||||
```bash
|
||||
sprout channels members --channel <UUID>
|
||||
```
|
||||
|
||||
Returns `[{pubkey, role}]`. Roles: `owner`, `admin`, `member`, `guest`, `bot`.
|
||||
|
||||
Manage members:
|
||||
|
||||
```bash
|
||||
sprout channels add-member --channel <UUID> --pubkey <hex> --role member
|
||||
sprout channels remove-member --channel <UUID> --pubkey <hex>
|
||||
```
|
||||
|
||||
`--role` is optional (defaults to `member`). Valid roles: `owner`, `admin`, `member`, `guest`, `bot`.
|
||||
|
||||
Admin operations (require `admin:channels` scope):
|
||||
|
||||
```bash
|
||||
sprout channels archive --channel <UUID>
|
||||
sprout channels unarchive --channel <UUID>
|
||||
sprout channels delete --channel <UUID>
|
||||
```
|
||||
|
||||
## Canvas
|
||||
|
||||
Get the canvas document for a channel (returns the markdown content string directly, or `null` if no canvas is set — this is NOT a JSON envelope):
|
||||
|
||||
```bash
|
||||
sprout canvas get --channel <UUID>
|
||||
```
|
||||
|
||||
Set the canvas (inline or via stdin):
|
||||
|
||||
```bash
|
||||
sprout canvas set --channel <UUID> --content "# Project Brief\n\nObjectives..."
|
||||
echo "# Doc" | sprout canvas set --channel <UUID> --content -
|
||||
```
|
||||
|
||||
## Reactions
|
||||
|
||||
Add a reaction to any event (message, note, etc.):
|
||||
|
||||
```bash
|
||||
sprout reactions add --event <hex-event-id> --emoji "👍"
|
||||
```
|
||||
|
||||
Remove a reaction:
|
||||
|
||||
```bash
|
||||
sprout reactions remove --event <hex-event-id> --emoji "👍"
|
||||
```
|
||||
|
||||
Get all reactions on an event:
|
||||
|
||||
```bash
|
||||
sprout reactions get --event <hex-event-id>
|
||||
```
|
||||
|
||||
Returns `{"reactions": [{emoji, count, pubkeys}]}` — reactions grouped by emoji with reactor pubkeys. Empty content on a reaction is normalized to `"+"`.
|
||||
|
||||
## DMs
|
||||
|
||||
List existing DM conversations:
|
||||
|
||||
```bash
|
||||
sprout dms list
|
||||
sprout dms list --limit 10
|
||||
```
|
||||
|
||||
Returns `[{dm_id, participants, created_at}]`.
|
||||
|
||||
Open a new DM (creates a group DM conversation):
|
||||
|
||||
```bash
|
||||
sprout dms open --pubkey <hex-pubkey>
|
||||
sprout dms open --pubkey <hex1> --pubkey <hex2> # group DM (up to 8)
|
||||
```
|
||||
|
||||
Returns `{event_id, dm_id, accepted, message}`. Use `dm_id` as the `--channel` value for subsequent `messages` commands.
|
||||
|
||||
Add a member to a DM group:
|
||||
|
||||
```bash
|
||||
sprout dms add-member --channel <UUID> --pubkey <hex-pubkey>
|
||||
```
|
||||
|
||||
## Users
|
||||
|
||||
Get your own profile:
|
||||
|
||||
```bash
|
||||
sprout users get
|
||||
```
|
||||
|
||||
Returns `[{display_name, about, picture, pubkey, ...}]` — always an array, even for a single profile lookup.
|
||||
|
||||
Get a specific user's profile:
|
||||
|
||||
```bash
|
||||
sprout users get --pubkey <hex-pubkey>
|
||||
```
|
||||
|
||||
Batch lookup (up to 200 pubkeys):
|
||||
|
||||
```bash
|
||||
sprout users get --pubkey <hex1> --pubkey <hex2> --pubkey <hex3>
|
||||
```
|
||||
|
||||
Search by display name:
|
||||
|
||||
```bash
|
||||
sprout users get --name "alice"
|
||||
```
|
||||
|
||||
Update your profile:
|
||||
|
||||
```bash
|
||||
sprout users set-profile --name "Alice" --avatar "https://example.com/avatar.png" --about "Backend engineer" --nip05 "alice@example.com"
|
||||
```
|
||||
|
||||
Get presence for one or more users:
|
||||
|
||||
```bash
|
||||
sprout users presence --pubkeys <hex1>,<hex2>
|
||||
```
|
||||
|
||||
Returns `[{pubkey, status, updated_at}]`. Status values: `online`, `away`, `offline`.
|
||||
|
||||
Set your own presence:
|
||||
|
||||
```bash
|
||||
sprout users set-presence --status online
|
||||
sprout users set-presence --status away
|
||||
sprout users set-presence --status offline
|
||||
```
|
||||
|
||||
Note: `set-presence` sends an ephemeral kind:20001 event that requires a WebSocket connection. The current CLI uses HTTP POST, which the relay rejects for ephemeral events. This command will fail until WebSocket support is added.
|
||||
|
||||
## Workflows
|
||||
|
||||
List workflows for a channel:
|
||||
|
||||
```bash
|
||||
sprout workflows list --channel <UUID>
|
||||
```
|
||||
|
||||
Returns `[{workflow_id, content, created_at, pubkey}]`.
|
||||
|
||||
Get a specific workflow definition:
|
||||
|
||||
```bash
|
||||
sprout workflows get --workflow <UUID>
|
||||
```
|
||||
|
||||
Returns `{workflow_id, content, created_at, pubkey}`, or `null` if not found.
|
||||
|
||||
Create a workflow (YAML definition inline or via stdin):
|
||||
|
||||
```bash
|
||||
sprout workflows create --channel <UUID> --yaml "name: review\nsteps: ..."
|
||||
cat workflow.yaml | sprout workflows create --channel <UUID> --yaml -
|
||||
```
|
||||
|
||||
Returns `{event_id, workflow_id, accepted, message}`.
|
||||
|
||||
Update a workflow (requires both `--channel` and `--workflow`):
|
||||
|
||||
```bash
|
||||
sprout workflows update --channel <UUID> --workflow <UUID> --yaml "name: updated\nsteps: ..."
|
||||
```
|
||||
|
||||
Delete a workflow:
|
||||
|
||||
```bash
|
||||
sprout workflows delete --workflow <UUID>
|
||||
```
|
||||
|
||||
Trigger a workflow:
|
||||
|
||||
```bash
|
||||
sprout workflows trigger --workflow <UUID>
|
||||
```
|
||||
|
||||
Approve or deny a pending workflow step:
|
||||
|
||||
```bash
|
||||
sprout workflows approve --token <UUID> # approve (default)
|
||||
sprout workflows approve --token <UUID> --approved false --note "needs revision"
|
||||
```
|
||||
|
||||
Get run history for a workflow:
|
||||
|
||||
```bash
|
||||
sprout workflows runs --workflow <UUID>
|
||||
sprout workflows runs --workflow <UUID> --limit 10
|
||||
```
|
||||
|
||||
Returns `[{event_id, kind, content, created_at, tags}]`. Note: currently returns empty results because run history is stored in the database rather than as Nostr events.
|
||||
|
||||
## Feed
|
||||
|
||||
Get your activity feed (mentions of your pubkey):
|
||||
|
||||
```bash
|
||||
sprout feed get --limit 20
|
||||
```
|
||||
|
||||
Returns events sorted newest-first (descending `created_at`). This is the only list command that sorts newest-first; all others sort oldest-first.
|
||||
|
||||
Poll for recent activity since a timestamp:
|
||||
|
||||
```bash
|
||||
sprout feed get --since 1716000000 --limit 50
|
||||
```
|
||||
|
||||
## Social
|
||||
|
||||
Nostr social protocol commands (NIP-01, NIP-02, NIP-51/NIP-65). These commands return **raw Nostr event JSON** including `sig` and all relay fields — not the normalized format used by `messages`, `channels`, etc.
|
||||
|
||||
Publish a text note (kind:1):
|
||||
|
||||
```bash
|
||||
sprout social publish --content "Hello world"
|
||||
sprout social publish --content "reply text" --reply-to <event-id>
|
||||
```
|
||||
|
||||
Set your contact list (kind:3):
|
||||
|
||||
```bash
|
||||
sprout social set-contacts --contacts '[{"pubkey":"<hex>","relay_url":"","petname":"alice"}]'
|
||||
```
|
||||
|
||||
Get a single event by ID:
|
||||
|
||||
```bash
|
||||
sprout social event --event <hex-event-id>
|
||||
```
|
||||
|
||||
Get a user's recent notes:
|
||||
|
||||
```bash
|
||||
sprout social notes --pubkey <hex> --limit 20
|
||||
sprout social notes --pubkey <hex> --limit 20 --before 1716050000
|
||||
```
|
||||
|
||||
Get a user's contact list:
|
||||
|
||||
```bash
|
||||
sprout social contacts --pubkey <hex>
|
||||
```
|
||||
|
||||
Publish a NIP-51/NIP-65 social list (supported kinds: 10000, 10001, 10002, 10003, 30000, 30003):
|
||||
|
||||
```bash
|
||||
sprout social set-list --kind 10002 --tags '[["r","wss://relay.example.com"]]'
|
||||
```
|
||||
|
||||
Read a user's social lists by kind:
|
||||
|
||||
```bash
|
||||
sprout social list --pubkey <hex> --kind 10002
|
||||
sprout social list --pubkey <hex> --kind 30000 --d-tag "friends"
|
||||
```
|
||||
|
||||
## Repos
|
||||
|
||||
Git repository announcements (NIP-34 kind:30617). Returns raw Nostr event JSON.
|
||||
|
||||
Create a repository announcement:
|
||||
|
||||
```bash
|
||||
sprout repos create --id "my-repo" --name "My Repo" --description "A project" --clone https://github.com/org/repo.git
|
||||
```
|
||||
|
||||
`--clone`, `--web`, and `--nostr-relay` flags are optional; `--clone` is repeatable.
|
||||
|
||||
Get a repository:
|
||||
|
||||
```bash
|
||||
sprout repos get --id "my-repo"
|
||||
sprout repos get --id "my-repo" --owner <hex-pubkey>
|
||||
```
|
||||
|
||||
List repositories:
|
||||
|
||||
```bash
|
||||
sprout repos list
|
||||
sprout repos list --owner <hex-pubkey> --limit 10
|
||||
```
|
||||
|
||||
## Upload
|
||||
|
||||
Upload a file to the relay's Blossom store:
|
||||
|
||||
```bash
|
||||
sprout upload file --file /path/to/image.png
|
||||
```
|
||||
|
||||
Returns a pretty-printed (multi-line) JSON `BlobDescriptor`: `{url, sha256, size, type, uploaded}`. Optional fields: `dim`, `blurhash`, `thumb`, `duration`.
|
||||
|
||||
## Mem (Agent Memory)
|
||||
|
||||
Persistent agent memory (NIP-AE engrams). Progress messages go to stderr; data goes to stdout. Exit code 5 on write conflicts.
|
||||
|
||||
List memory entries:
|
||||
|
||||
```bash
|
||||
sprout mem ls # tab-delimited: slug, created_at, event_id
|
||||
sprout mem ls --json # JSON array: [{slug, event_id, created_at}]
|
||||
```
|
||||
|
||||
Read a memory value (raw bytes to stdout, no trailing newline):
|
||||
|
||||
```bash
|
||||
sprout mem get <slug>
|
||||
```
|
||||
|
||||
Get the SHA-256 hash of a value (use as `--base-hash` for safe patching):
|
||||
|
||||
```bash
|
||||
sprout mem hash <slug>
|
||||
```
|
||||
|
||||
Set a memory value:
|
||||
|
||||
```bash
|
||||
sprout mem set <slug> "value"
|
||||
echo "value" | sprout mem set <slug> - # read from stdin
|
||||
sprout mem set <slug> - --allow-empty # allow empty stdin
|
||||
```
|
||||
|
||||
Patch a memory value with a unified diff (safer than `set` for concurrent access):
|
||||
|
||||
```bash
|
||||
sprout mem hash <slug> # get base hash first
|
||||
diff -u old.txt new.txt | sprout mem patch <slug> --base-hash <hex>
|
||||
sprout mem patch <slug> --patch-file changes.patch --base-hash <hex>
|
||||
sprout mem patch <slug> --no-base-hash # skip hash check (unsafe)
|
||||
sprout mem patch <slug> --base-hash <hex> --dry-run # preview without writing
|
||||
```
|
||||
|
||||
Delete (tombstone) a memory entry:
|
||||
|
||||
```bash
|
||||
sprout mem rm <slug>
|
||||
```
|
||||
|
||||
The `core` slug cannot be deleted.
|
||||
|
||||
## Pack (Local-Only)
|
||||
|
||||
Persona pack operations. No relay connection or `--private-key` needed.
|
||||
|
||||
```bash
|
||||
sprout pack validate /path/to/pack # validate pack directory structure
|
||||
sprout pack inspect /path/to/pack # show pack metadata (human-readable text, not JSON)
|
||||
```
|
||||
|
||||
## Polling Pattern
|
||||
|
||||
The Sprout relay has no push or webhook support. Poll with `--since` and sleep between iterations.
|
||||
|
||||
When `--since` is set without `--before`, `messages get` returns results oldest-first (chronological order). `feed get` always returns newest-first regardless of `--since`.
|
||||
|
||||
Recommended poll loop:
|
||||
|
||||
1. Run `sprout messages get --channel <UUID> --limit 50` — note the maximum `created_at` value from results.
|
||||
2. Sleep 10–30 seconds.
|
||||
3. Run `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
|
||||
4. Repeat from step 2, advancing `--since` each iteration.
|
||||
|
||||
Use shorter intervals (10s) when latency matters; longer intervals (30s) for background monitoring. Avoid intervals under 5 seconds to prevent relay rate limiting.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Most write commands return `{event_id, accepted, message}`. Exceptions noted below.
|
||||
|
||||
**Read commands:**
|
||||
|
||||
| Command | Required Flags | Returns |
|
||||
|---------|---------------|---------|
|
||||
| `messages get` | `--channel` | `[{id, pubkey, kind, content, created_at, tags}]` |
|
||||
| `messages thread` | `--channel`, `--event` | `[{id, pubkey, kind, content, created_at, tags}]` |
|
||||
| `messages search` | `--query` | `[{id, pubkey, kind, content, created_at, tags}]` |
|
||||
| `channels list` | — | `[{channel_id, name, description, created_at}]` |
|
||||
| `channels get` | `--channel` | `{channel_id, name, description, created_at, pubkey}` or `null` |
|
||||
| `channels members` | `--channel` | `[{pubkey, role}]` |
|
||||
| `canvas get` | `--channel` | markdown string or `null` |
|
||||
| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
|
||||
| `dms list` | — | `[{dm_id, participants, created_at}]` |
|
||||
| `users get` | — | `[{display_name, about, picture, pubkey, ...}]` |
|
||||
| `users presence` | `--pubkeys` | `[{pubkey, status, updated_at}]` |
|
||||
| `workflows list` | `--channel` | `[{workflow_id, content, created_at, pubkey}]` |
|
||||
| `workflows get` | `--workflow` | `{workflow_id, content, created_at, pubkey}` or `null` |
|
||||
| `workflows runs` | `--workflow` | `[{event_id, kind, content, created_at, tags}]` |
|
||||
| `feed get` | — | event array, newest-first |
|
||||
|
||||
**Write commands** (all return `{event_id, accepted, message}` unless noted):
|
||||
|
||||
| Command | Required Flags | Notes |
|
||||
|---------|---------------|-------|
|
||||
| `messages send` | `--channel`, `--content` | |
|
||||
| `messages send-diff` | `--channel`, `--diff`, `--repo`, `--commit` | |
|
||||
| `messages edit` | `--event`, `--content` | |
|
||||
| `messages delete` | `--event` | |
|
||||
| `messages vote` | `--event`, `--direction` | |
|
||||
| `channels create` | `--name`, `--type`, `--visibility` | adds `channel_id` |
|
||||
| `channels update/join/leave/topic/purpose` | `--channel` (+ value flag) | |
|
||||
| `channels add-member/remove-member` | `--channel`, `--pubkey` | |
|
||||
| `channels archive/unarchive/delete` | `--channel` | |
|
||||
| `canvas set` | `--channel`, `--content` | |
|
||||
| `reactions add/remove` | `--event`, `--emoji` | |
|
||||
| `dms open` | `--pubkey` | adds `dm_id` |
|
||||
| `dms add-member` | `--channel`, `--pubkey` | |
|
||||
| `users set-profile` | one of `--name`/`--avatar`/`--about`/`--nip05` | |
|
||||
| `workflows create` | `--channel`, `--yaml` | adds `workflow_id` |
|
||||
| `workflows update` | `--channel`, `--workflow`, `--yaml` | |
|
||||
| `workflows delete/trigger` | `--workflow` | |
|
||||
| `workflows approve` | `--token` | |
|
||||
| `social publish` | `--content` | |
|
||||
| `social set-contacts` | `--contacts` | |
|
||||
|
||||
**Raw output commands** (return unprocessed Nostr event JSON including `sig`):
|
||||
|
||||
| Command | Required Flags |
|
||||
|---------|---------------|
|
||||
| `social event` | `--event` |
|
||||
| `social notes` | `--pubkey` |
|
||||
| `social contacts` | `--pubkey` |
|
||||
| `social set-list` | `--kind`, `--tags` |
|
||||
| `social list` | `--pubkey`, `--kind` |
|
||||
| `repos create` | `--id` |
|
||||
| `repos get` | `--id` |
|
||||
| `repos list` | — |
|
||||
|
||||
**Other output formats:**
|
||||
|
||||
| Command | Required Flags | Returns |
|
||||
|---------|---------------|---------|
|
||||
| `upload file` | `--file` | `{url, sha256, size, type, uploaded}` (pretty-printed) |
|
||||
| `mem ls` | — | tab-delimited lines or `--json` array |
|
||||
| `mem get` | `<slug>` | raw value (stdout, no newline) |
|
||||
| `mem hash` | `<slug>` | SHA-256 hex string |
|
||||
| `mem set` | `<slug>`, `<value>` | stderr progress only |
|
||||
| `mem patch` | `<slug>`, `--base-hash` or `--no-base-hash` | stderr progress only |
|
||||
| `mem rm` | `<slug>` | stderr progress only |
|
||||
| `pack validate` | `<path>` | "Valid." or errors |
|
||||
| `pack inspect` | `<path>` | human-readable text |
|
||||
Reference in New Issue
Block a user