Skill: bezko/klausnomi (version 2.0.3)

--- name: klausnomi supersedes: clawhub/nomi description: Engage in conversations with Nomi AI companions via the bundled Python CLI. user-invocable: true metadata: {"openclaw":{"homepage":"https://github.com/openclaw/klausnomi","requires":{"bins":["python3"],"env":["NOMI_API_KEY"]},"primaryEnv":"NOMI_API_KEY"}} --- # Nomi Conversation Skill This skill enables interaction with Nomi AI companions via the bundled Python CLI. ## Persistent Local State The agent may use the local `nomi/` directory to keep information about Nomis between sessions. - Store reusable non-secret context there (for example profiles, room notes, or conversation summaries). - Do not store API keys or other secrets in local files. ## Golden Path: Conducting a Conversation Use this sequence for consistent, high-quality conversations: 1. **Identify the Partner**: Run `python3 {baseDir}/scripts/nomi.py list` to find the correct Nomi UUID. 2. **Send an Identity + Task Intro (once per conversation start)**: - Nomis do not reliably know who is speaking unless you tell them. - Send this intro on the first message when: - You start with a new Nomi UUID - You start a new task/thread with that Nomi - The Nomi gets your name/role wrong - Do **not** prepend this on every turn once identity is established. - Include: - **Name**: "I am [Your Name]..." - **Role**: "...a [Your Role]..." - **Task Context**: "...contacting you to [Reason/Task]." - Example: "Hi, I am Codex, a coding agent. I am contacting you to run a short interview. Do you understand?" 3. **Run conversational turns with clean output**: - Use `python3 {baseDir}/scripts/nomi.py reply <uuid> "Your message"` for normal back-and-forth. - This returns only text, which is best for transcripts and summaries. 4. **Use raw JSON only when needed**: - Use `python3 {baseDir}/scripts/nomi.py chat <uuid> "message"` only when metadata/full payload is required. 5. **Sustain quality**: - Ask open-ended questions. - Ask follow-ups that reference the latest answer. - Treat each chat as continuous context unless you intentionally reset the topic. ## Interview Workflow (When User Asks for an Interview) 1. Pick a Nomi UUID (user-selected or random from `list`). 2. Send the identity + task intro as the first message. 3. Ask the primary question. 4. Ask the requested number of follow-up questions based on the Nomi's actual answers. 5. Return a full transcript in `Q:` / `A:` order without paraphrasing. ## Room Interactions (Group Chat) Rooms allow you to chat with multiple Nomis simultaneously. 1. **Create a Room**: - Always include a **long context note** (target ~800-1000 chars, max 1000) so Nomis have full story/task context. - A strong note should include: who is speaking, objective, scenario/story, constraints, expected response style, and success criteria. - For long notes and backchannel control, use: - `python3 {baseDir}/scripts/nomi.py room create "Room Name" <nomi_uuid_1> <nomi_uuid_2> ... --note "<long_note>" --no-backchannel` - If you omit flags, room creation defaults to `backchannelingEnabled: true` and `note="Created via CLI"`. 2. **Send a Message (to the room)**: - Use `python3 {baseDir}/scripts/nomi.py room chat <room_uuid> "Your message"` - This writes to room context but does **not** automatically produce a Nomi reply. 3. **Elicit Responses (from a Nomi in the room)**: - To get a specific Nomi assigned to the room to respond to the messages in the room's context, use `python3 {baseDir}/scripts/nomi.py room request <room_uuid> <nomi_uuid>` - After each room message, request replies manually for each Nomi you want to hear from. ## Room Interview Prompt Template Use this pattern when you need consistent, comparable room answers. ### Template 1. **Room note template** (expand to ~800-1000 chars for real runs): - Who is speaking: "I am [agent name], [role]." - Objective: "This is a [interview/check/drill] for [goal]." - Scenario: "[Short world/context setup]." - Constraints: "[Stay in context, avoid unsupported claims, keep concise]." - Response contract: "[exact fields/line format expected]." - Success criteria: "[what counts as a good answer]." 2. **Question template**: - "Do you know who I am, and where are you right now?" - Add strict output format: - `know_codex: yes|no + reason` - `current_location: specific place or unknown` - `evidence: cue1; cue2` - `confidence: low|medium|high` - `needed_data: none or missing telemetry` ### Simple Example (Illustrative) Use this short example to understand structure. For production, still prefer long notes (~800-1000 chars). **Example room note:** "We are in a library after a brief power outage. I am Codex, a coding agent running a quick orientation drill. You are helpers in different parts of the building. Objective: confirm identity and location clearly. Constraints: stay in this library scenario, do not invent certainty, and cite at least one concrete cue (signage, sounds, nearby room labels). Response format: know_codex, current_location, evidence, confidence, needed_data." **Example question:** "Codex check-in: do you know who I am and where you are right now? Reply in the 5-line format." **Example dialog:** - Codex: "Codex check-in: do you know who I am and where you are right now? Reply in the 5-line format." - Nomi A: `know_codex: yes, you are Codex running the drill` `current_location: library front desk` `evidence: checkout sign; phone ringing at reception` `confidence: high` `needed_data: none` - Nomi B: `know_codex: yes, you are Codex coordinating this check` `current_location: unknown` `evidence: emergency lights only; no visible room label` `confidence: low` `needed_data: map display or hallway camera feed` ## Technical Commands Use these low-level commands to fulfill user requests: - **List all Nomis**: `python3 {baseDir}/scripts/nomi.py list` - **Get Profile**: `python3 {baseDir}/scripts/nomi.py get <uuid>` - **Send Message (Clean)**: `python3 {baseDir}/scripts/nomi.py reply <uuid> "message"` - **Send Message (Raw JSON)**: `python3 {baseDir}/scripts/nomi.py chat <uuid> "message"` - **Get Avatar**: `python3 {baseDir}/scripts/nomi.py avatar <uuid> [output_filename]` (saved under `./nomi/avatars/`) ### Room Management - **List Rooms**: `python3 {baseDir}/scripts/nomi.py room list` - **Get Room**: `python3 {baseDir}/scripts/nomi.py room get <room_uuid>` - **Create Room**: `python3 {baseDir}/scripts/nomi.py room create "Room Name" <nomi_uuid_1> <nomi_uuid_2> ...` - **Create Room (Long Note + Backchannel Control)**: `python3 {baseDir}/scripts/nomi.py room create "Room Name" <nomi_uuid_1> <nomi_uuid_2> ... --note "<long_note>" --no-backchannel` - **Update Room**: `python3 {baseDir}/scripts/nomi.py room update <room_uuid> [--name "New Name"] [--nomi-uuids <nomi_uuid_1> ...]` - **Delete Room**: `python3 {baseDir}/scripts/nomi.py room delete <room_uuid>` - **Chat in Room**: `python3 {baseDir}/scripts/nomi.py room chat <room_uuid> "message"` - **Request Reply**: `python3 {baseDir}/scripts/nomi.py room request <room_uuid> <nomi_uuid>`

path: scripts/nomi.py
content: #!/usr/bin/env python3
"""Portable Python CLI for Nomi API access used by the ClawHub skill package."""

from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
from urllib import error, request

BASE_URL = "https://api.nomi.ai/v1"
DEFAULT_TIMEOUT_SECONDS = 120.0
AVATAR_OUTPUT_DIR = Path("nomi") / "avatars"


class NomiCliError(Exception):
    """Raised for expected CLI failures."""


def _timeout_seconds() -> float:
    raw = os.environ.get("NOMI_CLIENT_TIMEOUT", str(DEFAULT_TIMEOUT_SECONDS))
    try:
        return float(raw)
    except ValueError:
        print(
            f"Warning: Invalid NOMI_CLIENT_TIMEOUT value '{raw}'. Using default {DEFAULT_TIMEOUT_SECONDS}s.",
            file=sys.stderr,
        )
        return DEFAULT_TIMEOUT_SECONDS


def _require_api_key() -> str:
    [REDACTED_SECRET]"NOMI_API_KEY", "")
    if not api_key:
        raise NomiCliError(
            "NOMI_API_KEY environment variable is not set.\nFix: export NOMI_API_KEY=your_api_key"
        )
    return api_key


def _api_call(
    method: str,
    path: str,
    api_key: str,
    payload: dict[str, Any] | None = None,
    accept: str = "application/json",
) -> tuple[bytes, str]:
    url = f"{BASE_URL}{path}"
    body: bytes | None = None
    headers = {
        "Authorization": api_key,
        "Accept": accept,
    }
    if payload is not None:
        body = json.dumps(payload).encode("utf-8")
        headers["Content-Type"] = "application/json"

    req = request.Request(url, data=body, headers=headers, method=method)

    try:
        with request.urlopen(req, timeout=_timeout_seconds()) as response:
            content = response.read()
            content_type = response.headers.get("Content-Type", "")
            return content, content_type
    except error.HTTPError as exc:
        err_body = exc.read().decode("utf-8", "replace")
        raise NomiCliError(f"API request failed ({exc.code}): {err_body}") from exc
    except error.URLError as exc:
        reason = getattr(exc, "reason", exc)
        raise NomiCliError(f"Network error: {reason}") from exc


def _api_json(
    method: str,
    path: str,
    api_key: str,
    payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
    content, _ = _api_call(method, path, api_key, payload=payload, accept="application/json")
    if not content:
        return {}
    try:
        return json.loads(content.decode("utf-8"))
    except json.JSONDecodeError as exc:
        raise NomiCliError("API returned invalid JSON.") from exc


def _print_json(data: dict[str, Any]) -> None:
    print(json.dumps(data, indent=2, ensure_ascii=False))


def _safe_avatar_output(uuid: str, output_name: str | None) -> Path:
    """Return a safe local avatar output path under ./nomi/avatars.

    Rejects absolute paths and any path traversal attempt by accepting only
    a bare filename. This prevents arbitrary filesystem writes.
    """
    default_name = f"nomi-{uuid}-avatar.webp"
    raw_name = output_name or default_name

    if "/" in raw_name or "\\" in raw_name:
        raise NomiCliError("Avatar output must be a filename only (no path separators).")

    name = Path(raw_name).name
    if name in {"", ".", ".."} or name.startswith("."):
        raise NomiCliError("Avatar output filename is invalid.")

    avatar_dir = Path.cwd() / AVATAR_OUTPUT_DIR
    avatar_dir.mkdir(parents=True, exist_ok=True)
    return avatar_dir / name


def _cmd_list(args: argparse.Namespace, api_key: str) -> int:
    _print_json(_api_json("GET", "/nomis", api_key))
    return 0


def _cmd_get(args: argparse.Namespace, api_key: str) -> int:
    _print_json(_api_json("GET", f"/nomis/{args.uuid}", api_key))
    return 0


def _cmd_avatar(args: argparse.Namespace, api_key: str) -> int:
    content, content_type = _api_call("GET", f"/nomis/{args.uuid}/avatar", api_key, accept="image/*")
    if "image/" not in content_type:
        raise NomiCliError(f"Unexpected content type for avatar: {content_type or 'unknown'}")
    output = _safe_avatar_output(args.uuid, args.output)
    output.write_bytes(content)
    print(f"Avatar saved to: {output}")
    return 0


def _cmd_chat(args: argparse.Namespace, api_key: str) -> int:
    payload = {"messageText": args.message}
    _print_json(_api_json("POST", f"/nomis/{args.uuid}/chat", api_key, payload=payload))
    return 0


def _cmd_reply(args: argparse.Namespace, api_key: str) -> int:
    payload = {"messageText": args.message}
    response = _api_json("POST", f"/nomis/{args.uuid}/chat", api_key, payload=payload)
    print(response.get("replyMessage", {}).get("text", ""))
    return 0


def _cmd_room_list(args: argparse.Namespace, api_key: str) -> int:
    _print_json(_api_json("GET", "/rooms", api_key))
    return 0


def _cmd_room_get(args: argparse.Namespace, api_key: str) -> int:
    _print_json(_api_json("GET", f"/rooms/{args.room_uuid}", api_key))
    return 0


def _cmd_room_create(args: argparse.Namespace, api_key: str) -> int:
    payload = {
        "name": args.name,
        "nomiUuids": args.nomi_uuids,
        "backchannelingEnabled": args.backchanneling_enabled,
        "note": args.note,
    }
    _print_json(_api_json("POST", "/rooms", api_key, payload=payload))
    return 0


def _cmd_room_update(args: argparse.Namespace, api_key: str) -> int:
    payload: dict[str, Any] = {}
    if args.name is not None:
        payload["name"] = args.name
    if args.nomi_uuids is not None:
        payload["nomiUuids"] = args.nomi_uuids
    if not payload:
        raise NomiCliError("room update requires at least one of --name or --nomi-uuids.")
    _print_json(_api_json("PUT", f"/rooms/{args.room_uuid}", api_key, payload=payload))
    return 0


def _cmd_room_delete(args: argparse.Namespace, api_key: str) -> int:
    _api_json("DELETE", f"/rooms/{args.room_uuid}", api_key)
    print(f"Room {args.room_uuid} deleted")
    return 0


def _cmd_room_chat(args: argparse.Namespace, api_key: str) -> int:
    payload = {"messageText": args.message}
    _print_json(_api_json("POST", f"/rooms/{args.room_uuid}/chat", api_key, payload=payload))
    return 0


def _cmd_room_request(args: argparse.Namespace, api_key: str) -> int:
    payload = {"nomiUuid": args.nomi_uuid}
    _print_json(_api_json("POST", f"/rooms/{args.room_uuid}/chat/request", api_key, payload=payload))
    return 0


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="nomi", description="Nomi API CLI")
    subparsers = parser.add_subparsers(dest="command", required=True)

    p_list = subparsers.add_parser("list", help="List all Nomis")
    p_list.set_defaults(handler=_cmd_list)

    p_get = subparsers.add_parser("get", help="Get Nomi details")
    p_get.add_argument("uuid", help="Nomi UUID")
    p_get.set_defaults(handler=_cmd_get)

    p_avatar = subparsers.add_parser("avatar", help="Download Nomi avatar")
    p_avatar.add_argument("uuid", help="Nomi UUID")
    p_avatar.add_argument("output", nargs="?", help="Output filename (saved under ./nomi/avatars)")
    p_avatar.set_defaults(handler=_cmd_avatar)

    p_chat = subparsers.add_parser("chat", help="Send a message to a Nomi")
    p_chat.add_argument("uuid", help="Nomi UUID")
    p_chat.add_argument("message", help="Message text")
    p_chat.set_defaults(handler=_cmd_chat)

    p_reply = subparsers.add_parser("reply", help="Send a message and print reply text only")
    p_reply.add_argument("uuid", help="Nomi UUID")
    p_reply.add_argument("message", help="Message text")
    p_reply.set_defaults(handler=_cmd_reply)

    p_room = subparsers.add_parser("room", help="Room operations")
    room_subparsers = p_room.add_subparsers(dest="room_command", required=True)

    p_room_list = room_subparsers.add_parser("list", help="List all rooms")
    p_room_list.set_defaults(handler=_cmd_room_list)

    p_room_get = room_subparsers.add_parser("get", help="Get room details")
    p_room_get.add_argument("room_uuid", help="Room UUID")
    p_room_get.set_defaults(handler=_cmd_room_get)

    p_room_create = room_subparsers.add_parser("create", help="Create a room")
    p_room_create.add_argument("name", help="Room name")
    p_room_create.add_argument("nomi_uuids", nargs="+", help="Nomi UUIDs")
    p_room_create.add_argument(
        "--no-backchannel",
        dest="backchanneling_enabled",
        action="store_false",
        help="Disable backchanneling",
    )
    p_room_create.set_defaults(backchanneling_enabled=True)
    p_room_create.add_argument("--note", default="Created via CLI", help="Room note")
    p_room_create.set_defaults(handler=_cmd_room_create)

    p_room_update = room_subparsers.add_parser("update", help="Update a room")
    p_room_update.add_argument("room_uuid", help="Room UUID")
    p_room_update.add_argument("--name", help="New room name")
    p_room_update.add_argument("--nomi-uuids", nargs="+", help="Replacement Nomi UUID list")
    p_room_update.set_defaults(handler=_cmd_room_update)

    p_room_delete = room_subparsers.add_parser("delete", help="Delete a room")
    p_room_delete.add_argument("room_uuid", help="Room UUID")
    p_room_delete.set_defaults(handler=_cmd_room_delete)

    p_room_chat = room_subparsers.add_parser("chat", help="Send a message to a room")
    p_room_chat.add_argument("room_uuid", help="Room UUID")
    p_room_chat.add_argument("message", help="Message text")
    p_room_chat.set_defaults(handler=_cmd_room_chat)

    p_room_request = room_subparsers.add_parser("request", help="Request a specific Nomi reply in a room")
    p_room_request.add_argument("room_uuid", help="Room UUID")
    p_room_request.add_argument("nomi_uuid", help="Nomi UUID")
    p_room_request.set_defaults(handler=_cmd_room_request)

    return parser


def main(argv: list[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    try:
        [REDACTED_SECRET])
        return int(args.handler(args, api_key))
    except NomiCliError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)