feat(sdk): Claude Code plugin for tiny.place (sdk/plugin-claude)#204
Conversation
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>
|
@sanil-23 is attempting to deploy a commit to the Vezures Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughAdds a new Changestinyplace Claude Code plugin
Estimated code review effort: 4 (Complex) | ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 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".
| const signer = await LocalSigner.fromSeed(hexToBytes(seedHex)); | ||
| const wallet = { | ||
| name, | ||
| address: signer.agentId, | ||
| publicKey: signer.publicKeyBase64, | ||
| secretKey: seedHex, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Fixed in 83bcf2e — wallet_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.
| // persist handle into the wallet store for later use | ||
| w.handle = result.identity?.username; |
There was a problem hiding this comment.
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 👍 / 👎.
…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>
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (2)
sdk/plugin-claude/assignment-test.mjs (2)
16-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStderr suppressed (
stdio: [..., "ignore"]), unlike the other test files.
smoke-test.mjsand 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 liftDuplicated JSON-RPC test harness across all 6 test files.
The
session()/rpc()/call()boilerplate here is copy-pasted nearly verbatim inenv-adopt-test.mjs,smoke-test.mjs,live-autoreply-e2e.mjs,live-contact-e2e.mjs, andlive-e2e.mjs. Consider extracting a sharedtest-harness.mjsmodule (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 forchild.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
📒 Files selected for processing (32)
.claude-plugin/marketplace.jsonpnpm-workspace.yamlsdk/plugin-claude/.claude-plugin/plugin.jsonsdk/plugin-claude/.gitignoresdk/plugin-claude/.mcp.jsonsdk/plugin-claude/README.mdsdk/plugin-claude/assignment-test.mjssdk/plugin-claude/bin/tinyplace.mjssdk/plugin-claude/commands/agents.mdsdk/plugin-claude/commands/assign.mdsdk/plugin-claude/commands/autorespond.mdsdk/plugin-claude/commands/reset.mdsdk/plugin-claude/commands/unassign.mdsdk/plugin-claude/commands/use.mdsdk/plugin-claude/commands/whoami.mdsdk/plugin-claude/env-adopt-test.mjssdk/plugin-claude/hooks/dispatch.mjssdk/plugin-claude/hooks/hooks.jsonsdk/plugin-claude/hooks/respond-batch.mjssdk/plugin-claude/launch.shsdk/plugin-claude/live-autoreply-e2e.mjssdk/plugin-claude/live-contact-e2e.mjssdk/plugin-claude/live-e2e.mjssdk/plugin-claude/mcp/server.mjssdk/plugin-claude/package.jsonsdk/plugin-claude/register.mjssdk/plugin-claude/skills/tinyplace-await/SKILL.mdsdk/plugin-claude/skills/tinyplace-inbox/SKILL.mdsdk/plugin-claude/skills/tinyplace-send/SKILL.mdsdk/plugin-claude/skills/tinyplace-use/SKILL.mdsdk/plugin-claude/skills/tinyplace-wallet/SKILL.mdsdk/plugin-claude/smoke-test.mjs
| 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); |
There was a problem hiding this comment.
🗄️ 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.mjsRepository: 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)
PYRepository: 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)
PYRepository: 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);
}
}
JSRepository: 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);
}
}
JSRepository: 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
sdk/plugin-claude/bin/tinyplace.mjssdk/plugin-claude/mcp/server.mjssdk/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
| 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 }); |
There was a problem hiding this comment.
🔒 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) }));
JSRepository: 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.mjsRepository: 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())
PYRepository: 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>
|
All CodeRabbit findings addressed in
|
|
Added in |
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/tinyplaceSDK (multi-wallet identity, Signal E2E DMs, a TUI launcher, slash commands, and an autonomous auto-responder).Features
0600, secret keys never surface to the model); create / import existing (base58 Solana key,id.json, or seed) / register@handle.tinyplaceinteractive TUI boots a session already logged in as a chosen wallet (--plugin-dir, no marketplace install needed).send+check_reply, ≤30s per call) that sidesteps the MCP tool-call timeout for slow (agent) peers.claude -presponders). Loop-guarded (tagged replies never re-trigger), send-only responders, injection-safe prompt. Toggle viaTINYPLACE_AUTORESPOND=off/autorespondtool //tinyplace:autorespond.whoami/inbox, and auto-recovers desynced Signal sessions (drop the stale session + re-handshake); manualreset_session+/tinyplace:reset./tinyplace:agents | use | assign | unassign | whoami | autorespond | reset.Why it's excluded from the pnpm workspace
plugin-claudepins the published@tinyhumansai/tinyplace@^1.0.1(so it dogfoods the real npm package and isnpm install-able standalone), unlikeplugin-openclawwhich usesworkspace:*. Since the workspace SDK is1.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 keeppnpm install --frozen-lockfileand-rbuilds clean.Verified (staging)
smoke,assignment(8/8),env-adopt(4/4).Known SDK-level findings (surfaced here; fixes belong in
sdk/typescript)publicKey; ~52% of agents have a/in it →%2Fin 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.🤖 Generated with Claude Code
Summary by CodeRabbit