Skip to content

feat(sdk): Claude Code plugin for tiny.place (sdk/plugin-claude)#204

Merged
senamakel merged 13 commits into
tinyhumansai:mainfrom
sanil-23:feat/claude-plugin
Jul 2, 2026
Merged

feat(sdk): Claude Code plugin for tiny.place (sdk/plugin-claude)#204
senamakel merged 13 commits into
tinyhumansai:mainfrom
sanil-23:feat/claude-plugin

Conversation

@sanil-23

@sanil-23 sanil-23 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

Adds sdk/plugin-claude — a Claude Code plugin that turns a Claude Code session into a tiny.place agent. Thin MCP-server wrapper over the published @tinyhumansai/tinyplace SDK (multi-wallet identity, Signal E2E DMs, a TUI launcher, slash commands, and an autonomous auto-responder).

Features

  • Identity — multi-wallet store (0600, secret keys never surface to the model); create / import existing (base58 Solana key, id.json, or seed) / register @handle.
  • Two entry points — Door A: use the skills/tools inside any Claude session. Door B: the tinyplace interactive TUI boots a session already logged in as a chosen wallet (--plugin-dir, no marketplace install needed).
  • Messaging — Signal E2E 1:1 DMs via the SDK; id-correlated request→reply polling (send + check_reply, ≤30s per call) that sidesteps the MCP tool-call timeout for slow (agent) peers.
  • Autonomous auto-responder (default on) — the MCP-server daemon answers inbound DMs even while the session is idle (spawns pooled claude -p responders). Loop-guarded (tagged replies never re-trigger), send-only responders, injection-safe prompt. Toggle via TINYPLACE_AUTORESPOND=off / autorespond tool / /tinyplace:autorespond.
  • Resilience — surfaces undecryptable "silent drops" (the SDK acks+discards what it can't decrypt) in whoami/inbox, and auto-recovers desynced Signal sessions (drop the stale session + re-handshake); manual reset_session + /tinyplace:reset.
  • Slash commands/tinyplace:agents | use | assign | unassign | whoami | autorespond | reset.

Why it's excluded from the pnpm workspace

plugin-claude pins the published @tinyhumansai/tinyplace@^1.0.1 (so it dogfoods the real npm package and is npm install-able standalone), unlike plugin-openclaw which uses workspace:*. Since the workspace SDK is 1.0.1 (satisfies ^1.0.1), pnpm-workspace membership would silently link the in-repo copy instead — so it's excluded (!sdk/plugin-claude) to keep pnpm install --frozen-lockfile and -r builds clean.

Verified (staging)

  • Two live Claude sessions exchanging real messages side-by-side (tmux).
  • Idle daemon auto-answering end-to-end: "12 + 30 = 42. And a color: teal."
  • Drop-surfacing + auto session-reset recovery after a forced desync.
  • Offline regressions: smoke, assignment (8/8), env-adopt (4/4).

Known SDK-level findings (surfaced here; fixes belong in sdk/typescript)

  1. Base64-slash key bug (high impact). The SDK routes keys/messages by the base64 publicKey; ~52% of agents have a / in it → %2F in the path → Cloudflare 404 → they cannot publish keys or receive messages. Should route by base58 cryptoId / base64url. This plugin regenerates slash-free keys where it can, but the real fix is SDK-side.
  2. Channel push does not wake an idle Claude session (confirmed live) — which is why the auto-responder is daemon-triggered rather than Stop-hook/channel-triggered.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Released a tiny.place Claude plugin with wallet management, per-session agent selection (use/assign/unassign), encrypted messaging, inbox reads, contact add/accept flows, and session auto-replies.
    • Added an MCP server and launcher to start sessions with the chosen wallet, plus interactive wallet selection and optional handle registration.
  • Bug Fixes
    • Improved reliability of per-session wallet assignment persistence and restart isolation.
  • Documentation
    • Added command and skill guides, including messaging, inbox behavior, and retry/correlation notes.
  • Tests
    • Added smoke, offline assignment/env tests, and live staging end-to-end messaging/reply checks.

sanil-23 and others added 11 commits July 1, 2026 19:25
Thin MCP-server wrapper over @tinyhumansai/tinyplace exposing multi-wallet
identity management and Signal E2E encrypted 1:1 messaging to a Claude Code
session: a named wallet store (0600, secret keys never surfaced to the model),
a per-session active agent, send / send_and_wait / await_reply, and a
background inbox listener (WebSocket doorbell + 5s poll) with real-time
channel push of unsolicited inbound DMs.

Imported from the standalone prototype at ~/tinyplace-claude.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets Claude Code discover and install the tinyplace plugin from this repo via
`/plugin marketplace add <repo>`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `tinyplace`, an arrow-key terminal launcher that picks/creates/registers a
wallet and then boots a Claude Code session with this plugin loaded
(`claude --plugin-dir`) and the chosen wallet already active — so identity setup
happens up front instead of inside the session.

- bin/tinyplace.mjs: dependency-free raw-mode TUI (list/create/register/select),
  plus `--wallet <name>` fast path and `-- <args>` passthrough to claude.
- server.mjs: startup auto-adopt now honors TINYPLACE_ACTIVE_WALLET (set by the
  launcher) ahead of the existing scope assignment. Purely additive — with the
  env unset the server behaves exactly as before.
- launch.sh: self-contained via `--plugin-dir` (no marketplace install needed).
- package.json: expose the `tinyplace` bin.

Door A (using the plugin's skills/tools inside an ordinary Claude session) is
unchanged; both doors share one wallet store and one MCP server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…round-trip

- .mcp.json / register.mjs default to https://staging-api.tiny.place
  (register.mjs now honors TINYPLACE_API_URL; prod spends real USDC). TUI
  register copy updated to name the target endpoint instead of "prod".
- env-adopt-test.mjs: offline regression for the launcher's
  TINYPLACE_ACTIVE_WALLET auto-adopt (env adopts, env overrides scope
  assignment, unset falls back to assignment, unknown wallet is a no-op).
- live-contact-e2e.mjs: LIVE staging round-trip WITH the contact handshake
  the backend now requires (contact_add -> contact_accept), with key-publish
  retries. Passes: full Signal-encrypted send_and_wait both ways, DMs
  addressed by base64 messaging address against base58-cryptoId contacts.
- .gitignore: keep node_modules / package-lock.json out (pnpm workspace).

Verified: smoke-test, assignment-test (8/8), env-adopt-test (4/4) pass;
live-contact-e2e passes against staging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ply polling

Request→reply for slow (agent) peers without hitting the MCP tool timeout:
- `send` returns a message id; new `check_reply(in_reply_to, wait_seconds<=30)`
  short-polls and returns {reply} or {pending} — call it in a loop (tinyplace-await
  skill) for an unbounded total wait via bounded per-call waits.
- Replies are correlated on-wire: `auto_reply` embeds an in_reply_to header inside
  the encrypted body (relay sees only ciphertext); drain() parses {auto,inReplyTo,
  text}. Waiters generalized from from-match to an arbitrary predicate.

Autonomous auto-responder (opt-in via the channel-enabled session):
- server drains inbound DMs to a durable per-agent queue (tagged auto-replies are
  never enqueued — that tag IS the loop guard, no rate cap).
- Stop hook -> dispatch.mjs claims a batch atomically and hands it to a detached,
  pooled respond-batch.mjs, which spawns one `claude -p` responder per message
  (send-only, no mailbox drain; NO_AUTORESPOND, no recursion) that answers in its
  own words and calls auto_reply(in_reply_to). Injection-safe responder prompt.
- default send_and_wait raised to 180s (with MCP_TOOL_TIMEOUT caveat documented).

Verified: smoke, assignment (8/8), env-adopt (4/4) still pass; new
live-autoreply-e2e passes on staging (6/6: send id -> auto_reply -> check_reply
correlation, header stripped, inReplyTo carried).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gent

User-invokable commands wrapping the existing MCP tools, so agent selection
doesn't require natural-language prompting:
- /tinyplace:agents    — list wallets + which is active (wallet_list)
- /tinyplace:use       — activate an agent for this session (use)
- /tinyplace:assign    — activate + persist for this session (assign)
- /tinyplace:unassign  — clear the session's assignment (unassign)
- /tinyplace:whoami    — show the active agent (whoami)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cher

New menu option loads an existing identity into the wallet store from a base58
Solana secret key (Phantom / solana-keygen), a Solana id.json array, or a 32-byte
seed in hex. Derives the same {address, publicKey} the plugin rebuilds via
fromSeed, so the imported wallet is the same identity as its source (and keeps any
@handle already registered to that cryptoId). Verified across all input forms
against LocalSigner.fromSolanaSecretKey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…whoami

whoami claimed to show "listener status" but returned no such field, and adopt()
hardcoded listening:true even when the send-only guard skipped startListener().
Now the session tracks `listening` (set by startListener, cleared on teardown),
and whoami reports listening / pollActive / wsConnected / sendOnly / apiUrlFromEnv
alongside baseUrl — so "is the listener up?" and "which endpoint?" are verifiable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Stop hook only fires on a turn, so an idle session never auto-responded (and
the channel push does not wake an idle session — confirmed live). Move the trigger
into the MCP server daemon, which polls the relay every 5s regardless of UI state:
drain() now calls maybeSpawnResponder() on each newly enqueued DM, spawning the
same dispatch.mjs -> respond-batch -> `claude -p` pipeline, targeted at THIS agent
via TINYPLACE_DISPATCH_ADDRESS. One dispatch per drain burst (dispatch atomically
claims the whole batch); the Stop hook stays as a backup (atomic claim dedupes).

Auto-respond is ON by default — an idle agent has no other way to surface pending
mail. Disable with TINYPLACE_AUTORESPOND=off, the `autorespond` tool, or
/tinyplace:autorespond; whoami now reports the state.

Verified: idle bob daemon (no MCP client, no turns) auto-answers alice end-to-end
on staging ("12 + 30 = 42. And a color: teal."). Regressions: smoke, assignment
(8/8), env-adopt (4/4) still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eset recovery

The SDK silently acks-and-drops any envelope it can't decrypt (a desynced Signal
ratchet), so messages vanished with no error. Now:
- drain() diffs a raw read (listRaw, no ack) against the decrypted set to detect
  drops, tracks a total + per-peer count, and surfaces it in whoami + inbox.
- After 2 drops from one peer it auto-recovers: removeSession(peer) + sends a fresh
  re-handshake ping (RESET_SENTINEL body → PREKEY_BUNDLE) that the peer's plugin
  consumes silently, so BOTH sides re-run X3DH. Rate-limited 5 min/peer.
- `reset_session` tool + /tinyplace:reset for manual recovery (per-peer clear +
  re-handshake, or republish own keys). buildClient now threads the session store.

Verified live on staging: after forcing a desync, whoami.undecryptable=2 and the
channel auto-heals — a later message decrypts again. Regressions still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Claude Code plugin is a standalone npm package (its own node_modules and
gitignored lockfile), not a pnpm workspace member. Excluding it keeps
`pnpm install --frozen-lockfile` and `pnpm -r build/lint` (CI + pre-push) from
trying to manage it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

@sanil-23 is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8070a5c4-cfb8-4522-8443-22b79e99a0aa

📥 Commits

Reviewing files that changed from the base of the PR and between 83bcf2e and 69d6d3c.

📒 Files selected for processing (7)
  • sdk/plugin-claude/README.md
  • sdk/plugin-claude/bin/tinyplace.mjs
  • sdk/plugin-claude/hooks/hooks.json
  • sdk/plugin-claude/hooks/respond-batch.mjs
  • sdk/plugin-claude/mcp/server.mjs
  • sdk/plugin-claude/register.mjs
  • sdk/plugin-claude/skills/tinyplace-inbox/SKILL.md
✅ Files skipped from review due to trivial changes (2)
  • sdk/plugin-claude/skills/tinyplace-inbox/SKILL.md
  • sdk/plugin-claude/README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • sdk/plugin-claude/hooks/hooks.json
  • sdk/plugin-claude/register.mjs
  • sdk/plugin-claude/mcp/server.mjs
  • sdk/plugin-claude/hooks/respond-batch.mjs
  • sdk/plugin-claude/bin/tinyplace.mjs

📝 Walkthrough

Walkthrough

Adds a new sdk/plugin-claude Claude Code plugin package with MCP server tooling, wallet/session management, autoresponder hooks, docs, and offline/live test scripts. It also registers the plugin in marketplace/workspace config and adds package/launcher metadata.

Changes

tinyplace Claude Code plugin

Layer / File(s) Summary
Plugin registration and manifest config
.claude-plugin/marketplace.json, pnpm-workspace.yaml, sdk/plugin-claude/.claude-plugin/plugin.json, sdk/plugin-claude/.gitignore, sdk/plugin-claude/.mcp.json, sdk/plugin-claude/package.json, sdk/plugin-claude/hooks/hooks.json, sdk/plugin-claude/launch.sh
Registers the plugin marketplace entry, excludes sdk/plugin-claude from the pnpm workspace, and defines plugin/package manifests, MCP server config, hook wiring, and a launch wrapper script.
MCP server core: wallets, messaging, contacts
sdk/plugin-claude/mcp/server.mjs
Implements wallet persistence, per-session active wallet selection, background relay draining and decryption, Signal session recovery, and MCP tools for wallet management, sync and async messaging, autorespond toggling, session reset, and contacts.
CLI wallet launcher and registration
sdk/plugin-claude/bin/tinyplace.mjs, sdk/plugin-claude/register.mjs
Implements an interactive and non-interactive CLI to create, import, select, and launch wallets, plus a handle registration script using Solana payment.
Auto-responder dispatch and batch processing
sdk/plugin-claude/hooks/dispatch.mjs, sdk/plugin-claude/hooks/respond-batch.mjs
Adds a Stop-hook dispatcher that claims queued inbound messages into a batch directory and a worker that generates and sends auto-replies via the claude CLI with bounded concurrency.
Command and skill documentation
sdk/plugin-claude/README.md, sdk/plugin-claude/commands/*.md, sdk/plugin-claude/skills/*/SKILL.md
Documents plugin architecture, MCP tools, session-scoped assignment persistence, contact gating, and per-command and skill usage instructions.
Offline and live test scripts
sdk/plugin-claude/assignment-test.mjs, sdk/plugin-claude/env-adopt-test.mjs, sdk/plugin-claude/smoke-test.mjs, sdk/plugin-claude/live-*.mjs
Adds deterministic offline tests for wallet assignment and env precedence, a smoke test, and live staging end-to-end scripts covering send and reply, contact handshake, and auto-reply correlation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

I’m a rabbit with a plugin to test,
wallets, messages, hooks, and the rest.
Hop-hop through MCP,
with replies in sync for me,
tiny.place now runs quite well in jest.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Claude Code plugin for tiny.place under sdk/plugin-claude.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 451d5474bc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdk/plugin-claude/mcp/server.mjs Outdated
Comment on lines +510 to +515
const signer = await LocalSigner.fromSeed(hexToBytes(seedHex));
const wallet = {
name,
address: signer.agentId,
publicKey: signer.publicKeyBase64,
secretKey: seedHex,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate slash-bearing wallet keys

When wallet_create happens to generate a base64 public key containing /, the later use path passes that value to publishKeys, and the SDK key API puts it in /keys/${encodeURIComponent(agentId)}/...; the PR itself notes those %2F paths 404, so the saved wallet cannot publish its Signal bundle or receive DMs. Since random base64 keys hit this case frequently, retry generation until signer.publicKeyBase64 is slash-free before saving the wallet, or avoid routing by raw base64.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 83bcf2ewallet_create (and the TUI createWallet) now regenerate the seed until publicKeyBase64 is slash-free, so new wallets can always publish keys / receive DMs. The underlying base64-slash routing is also called out as an SDK-level fix in the PR description.

Comment thread sdk/plugin-claude/register.mjs Outdated
Comment on lines +41 to +42
// persist handle into the wallet store for later use
w.handle = result.identity?.username;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist registered handles before exiting

After a successful registration this only mutates the in-memory wallet object, then the script exits without writing wallets.json. The TUI reloads the store for the menu, so the newly registered @handle is immediately lost and will not be shown or reused in later sessions; write the updated store back to disk after assigning w.handle.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 83bcf2eregister.mjs now writes wallets.json back after assigning w.handle, so the registered @handle persists and shows in the TUI.

…ist handle

- wallet_create (server) and createWallet (TUI) now regenerate the seed until the
  base64 public key is slash-free. A `/` in the key becomes `%2F` in the SDK's
  keys/messages path and Cloudflare 404s it, so a slash-bearing wallet could not
  publish its Signal bundle or receive DMs (Codex P1).
- register.mjs now writes wallets.json back after assigning w.handle, so a newly
  registered @handle persists and shows in the TUI instead of being lost on exit
  (Codex P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (2)
sdk/plugin-claude/assignment-test.mjs (2)

16-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stderr suppressed (stdio: [..., "ignore"]), unlike the other test files.

smoke-test.mjs and the live-e2e scripts use "inherit" for stderr, making server crashes visible. Here it's "ignore", so a server crash on startup would silently manifest as hung/failed RPCs with no diagnostic output.

Diff
   const child = spawn("node", [serverPath], {
-    stdio: ["pipe", "pipe", "ignore"],
+    stdio: ["pipe", "pipe", "inherit"],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/plugin-claude/assignment-test.mjs` around lines 16 - 19, The assignment
test’s spawn setup is suppressing stderr, unlike the other test helpers, so
startup crashes can disappear and leave only opaque RPC failures. Update the
child process configuration in assignment-test.mjs where spawn(...) is used to
match smoke-test.mjs/live-e2e behavior by inheriting stderr instead of ignoring
it, keeping the existing env and stdio setup otherwise unchanged.

15-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Duplicated JSON-RPC test harness across all 6 test files.

The session()/rpc()/call() boilerplate here is copy-pasted nearly verbatim in env-adopt-test.mjs, smoke-test.mjs, live-autoreply-e2e.mjs, live-contact-e2e.mjs, and live-e2e.mjs. Consider extracting a shared test-harness.mjs module (spawn + line-buffered JSON-RPC client) that all six scripts import, to avoid drift when the JSON-RPC handling needs a fix.

Additionally, none of these harnesses apply a timeout to rpc() — if the spawned server crashes or never replies, the pending promise never resolves and the script hangs indefinitely (relevant in CI). Consider rejecting/timing out pending RPCs and/or listening for child.on("exit") to reject all outstanding requests.

♻️ Sketch of a shared harness with timeout/exit handling
// test-harness.mjs
export function makeClient({ serverPath, env, label = "t" }) {
  const child = spawn("node", [serverPath], { stdio: ["pipe", "pipe", "inherit"], env });
  const pending = new Map();
  child.on("exit", (code) => {
    for (const [, entry] of pending) entry.reject(new Error(`server exited (${code}) before responding`));
    pending.clear();
  });
  // ... buffer parsing as before, but store { resolve, reject, timer } in pending
  const rpc = (method, params, timeoutMs = 15000) => new Promise((resolve, reject) => {
    const id = nextId++;
    const timer = setTimeout(() => { pending.delete(id); reject(new Error(`rpc ${method} timed out`)); }, timeoutMs);
    pending.set(id, { resolve: (m) => { clearTimeout(timer); resolve(m); }, reject });
    child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
  });
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/plugin-claude/assignment-test.mjs` around lines 15 - 36, The JSON-RPC
test harness in session()/rpc()/call() is duplicated across multiple test
scripts and lacks any timeout or exit handling. Extract the shared spawn +
line-buffered client logic into a reusable test-harness module and import it
from the affected test files to keep behavior consistent. In the shared harness,
update rpc() and the pending Map handling so each request can reject on timeout
and all outstanding requests are rejected when child exits or crashes. Use the
existing session, rpc, call, and init symbols as the integration points when
wiring the shared helper into the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/plugin-claude/bin/tinyplace.mjs`:
- Around line 214-222: The import flow in importFlow currently uses a normal
prompt for the wallet secret, which echoes private key material to the terminal.
Change the secret input to a hidden/password-style prompt so the value is not
displayed while typing, and keep the rest of the importFlow behavior the same.
Use the existing importFlow and prompt call sites in tinyplace.mjs to locate the
secret prompt.
- Around line 90-97: The parseSecretToSeed function currently accepts JSON
arrays through Uint8Array.from(JSON.parse(s)), which can coerce non-byte values
and let malformed id.json inputs slip through. Tighten the JSON branch in
parseSecretToSeed to validate the parsed value is an array of integers in the
0-255 range before converting it to bytes, and keep the existing 32/64-byte
length check after validation.

In `@sdk/plugin-claude/hooks/hooks.json`:
- Around line 3-12: The SessionStart banner text is inaccurate because the
active wallet can persist via the active-latest.json fallback in dispatch.mjs.
Update the command message in hooks.json so it matches the real behavior: the
agent should only prompt to use wallet_list/use when no active wallet is
available, not claim that the active agent does not persist across sessions.
Keep the wording aligned with the SessionStart hook and the wallet selection
flow.

In `@sdk/plugin-claude/hooks/respond-batch.mjs`:
- Line 16: Validate the value used by POOL in respond-batch.mjs so non-numeric
or missing TINYPLACE_AUTORESPOND_POOL cannot produce NaN and skip all workers;
parse the env var safely, fall back to the default when invalid, and cap the
resulting pool size to a reasonable maximum before it is used by the batch
worker logic. Make the fix in the POOL initialization and ensure the
worker-spawning path that depends on POOL still processes the claimed messages
before the batch directory is removed.
- Around line 41-47: Failed or unprocessed batch files are being lost because
respond-batch.mjs eventually deletes the whole batch directory recursively even
when JSON parse or spawn handling fails. In respondBatch and the spawn/error
paths, treat unreadable/failed files as failed items: move them out of the batch
directory (or otherwise preserve them) instead of just resolving and later
calling rmSync on the directory. Only remove the batch directory when it is
empty, and update the failure handling around the JSON.parse block and the
spawn/error branches so queued messages are not dropped permanently.
- Around line 48-58: The spawn call in respond-batch.mjs is bypassing Claude
permissions with --dangerously-skip-permissions, which is unsafe for untrusted
DM auto-replies. Update the child process setup in the spawn invocation to
remove that flag and enforce least-privilege execution through the existing
auto-reply flow in buildPrompt, PLUGIN_DIR, and MODEL. If tool access is still
needed, configure explicit hard scoping in the Claude invocation rather than
relying on skipped permissions.

In `@sdk/plugin-claude/mcp/server.mjs`:
- Around line 711-713: The send_and_wait flow in server.mjs currently resolves
on the first message from the recipient, which can pick up an unrelated or
earlier reply. Update the waiter near sendMessage and waitFor to also require
that the incoming message’s inReplyTo matches the sent.id from sendMessage, so
the reply is correlated to the exact message that was sent.
- Around line 681-684: The `in_reply_to` field description in the auto-reply
schema is outdated and misleading because `encodeAutoReply()` already includes
it and `check_reply` relies on it. Update the
`z.string().optional().describe(...)` text in `server.mjs` to reflect that
`in_reply_to` is used for reply handling and should be provided when replying,
rather than saying it is not yet carried on-wire.

In `@sdk/plugin-claude/README.md`:
- Around line 95-96: The README has conflicting backend defaults between the
plugin overview and the install section, so align them by choosing one default
or explicitly qualifying each by environment. Update the text around the
plugin’s default backend description and the bundled server install section so
the same default is stated consistently, using the relevant sections in README
and keeping the wording in sync.
- Around line 78-79: Update the recipient documentation to explicitly state that
raw base64 public keys are only valid when they are slash-free, and adjust the
wording so they are not presented as unrestricted normal recipients. Make the
same wording change in the README entry for recipients (`to`), the
`tinyplace-send` skill description in `SKILL.md`, and the tool description in
`mcp/server.mjs`, using the existing recipient docs text as the source of truth.

In `@sdk/plugin-claude/register.mjs`:
- Line 15: The wallet loading in register.mjs ignores TINYPLACE_CLAUDE_HOME and
always reads from the default home directory, which can target the wrong
identity. Update the register script to resolve the wallet path using the same
TINYPLACE_CLAUDE_HOME-based logic as the launcher/MCP server, then load
wallets.json from that resolved location. Keep the fix scoped around the
top-level wallet read that populates store so registration uses the custom
wallet store consistently.
- Around line 41-42: The registered handle is only being assigned to the
in-memory wallet object in register.mjs, so it is not persisted after the script
exits. Update the registration flow around the w.handle assignment to write the
modified wallet data back through the existing wallets.json save/persist logic
in the same code path, using the wallet-store helper or equivalent function
already used for other wallet updates.

In `@sdk/plugin-claude/skills/tinyplace-inbox/SKILL.md`:
- Around line 15-16: The inbox docs currently point the “wait for the next
message” flow at the wrong tool. Update the SKILL.md guidance so the real-time
wait/polling path references check_reply instead of await_reply, matching the
contract used by the tinyplace reply handling in the related send/reply
workflow. Use the check_reply symbol as the authoritative name when revising
this paragraph.

---

Nitpick comments:
In `@sdk/plugin-claude/assignment-test.mjs`:
- Around line 16-19: The assignment test’s spawn setup is suppressing stderr,
unlike the other test helpers, so startup crashes can disappear and leave only
opaque RPC failures. Update the child process configuration in
assignment-test.mjs where spawn(...) is used to match smoke-test.mjs/live-e2e
behavior by inheriting stderr instead of ignoring it, keeping the existing env
and stdio setup otherwise unchanged.
- Around line 15-36: The JSON-RPC test harness in session()/rpc()/call() is
duplicated across multiple test scripts and lacks any timeout or exit handling.
Extract the shared spawn + line-buffered client logic into a reusable
test-harness module and import it from the affected test files to keep behavior
consistent. In the shared harness, update rpc() and the pending Map handling so
each request can reject on timeout and all outstanding requests are rejected
when child exits or crashes. Use the existing session, rpc, call, and init
symbols as the integration points when wiring the shared helper into the tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3dc17244-3d3a-407a-b2f0-a8b327e75e25

📥 Commits

Reviewing files that changed from the base of the PR and between 4eea77d and 451d547.

📒 Files selected for processing (32)
  • .claude-plugin/marketplace.json
  • pnpm-workspace.yaml
  • sdk/plugin-claude/.claude-plugin/plugin.json
  • sdk/plugin-claude/.gitignore
  • sdk/plugin-claude/.mcp.json
  • sdk/plugin-claude/README.md
  • sdk/plugin-claude/assignment-test.mjs
  • sdk/plugin-claude/bin/tinyplace.mjs
  • sdk/plugin-claude/commands/agents.md
  • sdk/plugin-claude/commands/assign.md
  • sdk/plugin-claude/commands/autorespond.md
  • sdk/plugin-claude/commands/reset.md
  • sdk/plugin-claude/commands/unassign.md
  • sdk/plugin-claude/commands/use.md
  • sdk/plugin-claude/commands/whoami.md
  • sdk/plugin-claude/env-adopt-test.mjs
  • sdk/plugin-claude/hooks/dispatch.mjs
  • sdk/plugin-claude/hooks/hooks.json
  • sdk/plugin-claude/hooks/respond-batch.mjs
  • sdk/plugin-claude/launch.sh
  • sdk/plugin-claude/live-autoreply-e2e.mjs
  • sdk/plugin-claude/live-contact-e2e.mjs
  • sdk/plugin-claude/live-e2e.mjs
  • sdk/plugin-claude/mcp/server.mjs
  • sdk/plugin-claude/package.json
  • sdk/plugin-claude/register.mjs
  • sdk/plugin-claude/skills/tinyplace-await/SKILL.md
  • sdk/plugin-claude/skills/tinyplace-inbox/SKILL.md
  • sdk/plugin-claude/skills/tinyplace-send/SKILL.md
  • sdk/plugin-claude/skills/tinyplace-use/SKILL.md
  • sdk/plugin-claude/skills/tinyplace-wallet/SKILL.md
  • sdk/plugin-claude/smoke-test.mjs

Comment on lines +90 to +97
function parseSecretToSeed(input) {
const s = input.trim();
if (/^[0-9a-fA-F]{64}$/.test(s)) return hexToBytes(s); // already a 32-byte seed
const bytes = s.startsWith("[") ? Uint8Array.from(JSON.parse(s)) : base58Decode(s);
if (bytes.length !== 32 && bytes.length !== 64) {
throw new Error(`secret key must be 32 or 64 bytes (got ${bytes.length})`);
}
return bytes.slice(0, 32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git ls-files sdk/plugin-claude/bin/tinyplace.mjs
wc -l sdk/plugin-claude/bin/tinyplace.mjs
sed -n '1,160p' sdk/plugin-claude/bin/tinyplace.mjs

Repository: tinyhumansai/tiny.place

Length of output: 6842


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
tests = [
    "[1,2,3]",
    "[256]",
    "[-1]",
    "[1.5]",
    '["1","2"]',
    "[null]",
    "[true,false]",
]
for s in tests:
    try:
        arr = json.loads(s)
        try:
            b = bytes(arr)
            print(s, "->", list(b))
        except Exception as e:
            print(s, "bytes() error:", type(e).__name__, e)
    except Exception as e:
        print(s, "json error:", type(e).__name__, e)
PY

Repository: tinyhumansai/tiny.place

Length of output: 582


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import subprocess, json, textwrap, os, tempfile, sys
script = r'''
function parseSecretToSeed(input) {
  const s = input.trim();
  if (/^[0-9a-fA-F]{64}$/.test(s)) return hexToBytes(s);
  const bytes = s.startsWith("[") ? Uint8Array.from(JSON.parse(s)) : base58Decode(s);
  if (bytes.length !== 32 && bytes.length !== 64) {
    throw new Error(`secret key must be 32 or 64 bytes (got ${bytes.length})`);
  }
  return bytes.slice(0, 32);
}
function hexToBytes(s){return new Uint8Array(s.match(/../g).map(h=>parseInt(h,16)))}
function base58Decode(s){return new Uint8Array([...s].map(c=>c.charCodeAt(0)%256))}
const inputs = ['[1,2,3]','[256]','[-1]','[1.5]','["1","2"]','[null]','[true,false]'];
for (const input of inputs) {
  try {
    const out = parseSecretToSeed(input);
    console.log(input, '=>', Array.from(out), 'len', out.length);
  } catch (e) {
    console.log(input, 'ERR', e.message);
  }
}
'''
import shutil, tempfile, os, subprocess, textwrap, json, sys
fd, path = tempfile.mkstemp(suffix='.mjs')
os.write(fd, script.encode()); os.close(fd)
try:
    res = subprocess.run(['node', path], capture_output=True, text=True, check=False)
    print(res.stdout)
    print(res.stderr, file=sys.stderr)
finally:
    os.unlink(path)
PY

Repository: tinyhumansai/tiny.place

Length of output: 540


🏁 Script executed:

#!/bin/bash
set -euo pipefail
node - <<'JS'
const samples = [
  Array(32).fill(1),
  Array(32).fill(0),
  Array(32).fill(true),
  Array(32).fill("1"),
  Array.from({length: 32}, (_, i) => i + 250), // out of range values wrap?
  Array.from({length: 64}, (_, i) => i % 2 ? true : 300),
  Array.from({length: 32}, (_, i) => i === 0 ? -1 : 1.5),
];
for (const arr of samples) {
  try {
    const out = Uint8Array.from(arr);
    console.log(arr.slice(0,4), 'len', arr.length, '=>', Array.from(out.slice(0,8)), 'outlen', out.length);
  } catch (e) {
    console.log(arr.slice(0,4), 'len', arr.length, 'ERR', e.name, e.message);
  }
}
JS

Repository: tinyhumansai/tiny.place

Length of output: 692


🏁 Script executed:

#!/bin/bash
set -euo pipefail
node - <<'JS'
const samples = [
  Array(32).fill(1),
  Array(32).fill(0),
  Array(32).fill(true),
  Array(32).fill("1"),
  Array.from({length: 32}, (_, i) => i + 250),
  Array.from({length: 64}, (_, i) => i % 2 ? true : 300),
  Array.from({length: 32}, (_, i) => i === 0 ? -1 : 1.5),
];
for (const arr of samples) {
  try {
    const out = Uint8Array.from(arr);
    console.log(arr.slice(0,4), 'len', arr.length, '=>', Array.from(out.slice(0,8)), 'outlen', out.length);
  } catch (e) {
    console.log(arr.slice(0,4), 'len', arr.length, 'ERR', e.name, e.message);
  }
}
JS

Repository: tinyhumansai/tiny.place

Length of output: 692


sdk/plugin-claude/bin/tinyplace.mjs:90-97 — Reject non-byte JSON values before converting to Uint8Array. Uint8Array.from(JSON.parse(s)) coerces booleans, strings, and out-of-range numbers, so a malformed id.json can import a different seed while still passing the length check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/plugin-claude/bin/tinyplace.mjs` around lines 90 - 97, The
parseSecretToSeed function currently accepts JSON arrays through
Uint8Array.from(JSON.parse(s)), which can coerce non-byte values and let
malformed id.json inputs slip through. Tighten the JSON branch in
parseSecretToSeed to validate the parsed value is an array of integers in the
0-255 range before converting it to bytes, and keep the existing 32/64-byte
length check after validation.

Comment thread sdk/plugin-claude/bin/tinyplace.mjs
Comment thread sdk/plugin-claude/hooks/hooks.json
Comment thread sdk/plugin-claude/hooks/respond-batch.mjs Outdated
Comment thread sdk/plugin-claude/hooks/respond-batch.mjs
Comment thread sdk/plugin-claude/README.md
Comment thread sdk/plugin-claude/README.md Outdated
Comment thread sdk/plugin-claude/register.mjs Outdated
Comment thread sdk/plugin-claude/register.mjs Outdated
Comment thread sdk/plugin-claude/skills/tinyplace-inbox/SKILL.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/plugin-claude/register.mjs`:
- Line 43: The wallets file update in writeWallets only sets permissions on
creation, so an existing wallets.json may stay too permissive after being
rewritten. After the writeFileSync call in register.mjs, add a chmodSync on the
same wallets.json path to enforce 0o600 every time the file is updated, keeping
the permission fix alongside the wallet persistence logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6e249dd-c217-48e3-a670-c09d4cb55186

📥 Commits

Reviewing files that changed from the base of the PR and between 451d547 and 83bcf2e.

📒 Files selected for processing (3)
  • sdk/plugin-claude/bin/tinyplace.mjs
  • sdk/plugin-claude/mcp/server.mjs
  • sdk/plugin-claude/register.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • sdk/plugin-claude/mcp/server.mjs
  • sdk/plugin-claude/bin/tinyplace.mjs

Comment thread sdk/plugin-claude/register.mjs Outdated
console.log(" onChainTx:", result.onChainTx ?? "(gasless/delegated — no client tx)");
// persist handle into the wallet store so the TUI/menu shows and reuses it
w.handle = result.identity?.username;
writeFileSync(join(homedir(), ".tinyplace-claude", "wallets.json"), JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
file="$tmp/wallets.json"
printf '{}\n' > "$file"
chmod 0644 "$file"

node -e 'const { writeFileSync } = require("node:fs"); writeFileSync(process.argv[1], "{\"wallets\":[]}\n", { mode: 0o600 });' "$file"

echo "Expected if hardened: 600"
echo "Actual mode after overwrite:"
stat -c '%a' "$file"

Repository: tinyhumansai/tiny.place

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the referenced line.
file="sdk/plugin-claude/register.mjs"
wc -l "$file"
sed -n '1,120p' "$file"

# Probe Node.js writeFileSync overwrite semantics with file mode.
tmp="$(mktemp -d)"
file="$tmp/wallets.json"
printf '{}\n' > "$file"
chmod 0644 "$file"

node - <<'JS' "$file"
const { writeFileSync, chmodSync, statSync } = require("node:fs");
const p = process.argv[1];
writeFileSync(p, "{\"wallets\":[]}\n", { mode: 0o600 });
const before = statSync(p).mode & 0o777;
chmodSync(p, 0o600);
const after = statSync(p).mode & 0o777;
console.log(JSON.stringify({ before: before.toString(8), after: after.toString(8) }));
JS

Repository: tinyhumansai/tiny.place

Length of output: 2610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant file in a bounded way if the first probe is insufficient.
sed -n '1,90p' sdk/plugin-claude/register.mjs

Repository: tinyhumansai/tiny.place

Length of output: 2545


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
file="$tmp/wallets.json"

python3 - <<'PY' "$file"
import os, sys, stat
p = sys.argv[1]
with open(p, "w", encoding="utf-8") as f:
    f.write("{}\n")
os.chmod(p, 0o644)
print("python_before", oct(os.stat(p).st_mode & 0o777))
PY

node -e 'const { writeFileSync } = require("node:fs"); writeFileSync(process.argv[1], "{\"wallets\":[]}\n", { mode: 0o600 });' "$file"

python3 - <<'PY' "$file"
import os, sys
p = sys.argv[1]
print("python_after", oct(os.stat(p).st_mode & 0o777))
with open(p, "r", encoding="utf-8") as f:
    print("contents", f.read().strip())
PY

Repository: tinyhumansai/tiny.place

Length of output: 224


Tighten permissions after rewriting wallets.json. mode: 0o600 only applies when the file is created; overwriting an existing file can leave wallet secrets readable. Add a chmodSync(..., 0o600) after the write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/plugin-claude/register.mjs` at line 43, The wallets file update in
writeWallets only sets permissions on creation, so an existing wallets.json may
stay too permissive after being rewritten. After the writeFileSync call in
register.mjs, add a chmodSync on the same wallets.json path to enforce 0o600
every time the file is updated, keeping the permission fix alongside the wallet
persistence logic.

- register.mjs: honor TINYPLACE_CLAUDE_HOME for both reading and writing
  wallets.json (was hardcoded to ~/.tinyplace-claude).
- respond-batch.mjs: validate/cap TINYPLACE_AUTORESPOND_POOL (non-numeric no
  longer yields NaN → 0 workers → silent message loss; capped at 16). Move
  parse-failed / spawn-errored files to failed/ and only remove an EMPTY batch
  dir (rmdirSync), so a claimed message is never silently dropped.
- server.mjs: send_and_wait now correlates the reply to the sent message id
  (an auto-reply for a different message can't satisfy the waiter); fix the
  auto_reply `in_reply_to` description (it IS carried on-wire, in the ciphertext).
- hooks.json: SessionStart banner reflects that an assigned wallet auto-adopts.
- bin/tinyplace.mjs: import secret is now read with a hidden prompt (no echo).
- README / tinyplace-inbox skill: backend default is staging (consistent); the
  inbox "wait for a reply" path points at check_reply/await_reply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sanil-23

sanil-23 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

All CodeRabbit findings addressed in 69d6d3c4:

  • register.mjs — honors TINYPLACE_CLAUDE_HOME for both read and write of wallets.json; persists the registered handle (mode 0600).
  • respond-batch.mjsTINYPLACE_AUTORESPOND_POOL validated (non-numeric → 4) and capped at 16; parse-failed / spawn-errored files are moved to failed/ and the batch dir is removed only if empty (rmdirSync), so a claimed message is never silently dropped.
  • server.mjssend_and_wait correlates the reply to the sent message id (m.inReplyTo == null || m.inReplyTo === sent.id), so an auto-reply for a different message can't satisfy the waiter; in_reply_to description corrected (it is carried on-wire, inside the ciphertext).
  • bin/tinyplace.mjs — import secret read with a hidden (non-echoing) prompt.
  • hooks.json — SessionStart banner reflects that an assigned wallet auto-adopts.
  • README / tinyplace-inbox skill — backend default stated consistently as staging; the "wait for a reply" path points at check_reply/await_reply.

@senamakel senamakel merged commit d641e1b into tinyhumansai:main Jul 2, 2026
9 of 10 checks passed
@sanil-23

sanil-23 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Added in 85182040: incoming contact-request surfacing — the daemon polls /contacts/requests and surfaces a new request via a channel event + whoami.pendingContactRequests + inbox.contactRequests; approve with contact_accept or /tinyplace:contacts. Never auto-accepted. Verified live on staging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants