Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,19 +708,27 @@ mininaru pair naru.example.com:9090 \
```

Successful pairing writes the server address to `client.json`, so ordinary
commands use it automatically. `--server` overrides that default:
commands use it automatically. Setup also records `mode: client`, which routes
chat, prompts, agent and skill reads, and session commands to that server.
`--server` overrides that default:

```sh
mininaru
mininaru -p 'summarise the current session' --session
mininaru session list
mininaru session usage <id>
mininaru agent list
mininaru skill list
mininaru skill show <name>
mininaru --server naru.example.com:9090
```

The TUI streams answer and reasoning deltas, tool progress, cancellation, and
dangerous-tool approval over one bidirectional RPC. Sessions, history, tool
logs, compaction, and token usage stay in the server's SQLite database.
dangerous-tool approval over one bidirectional RPC. The model and session stay
on the server, while tools are advertised and executed by the client. Builtin
tools therefore operate on the client machine and MCP tools come from the
client's `mcp.json`. Tool results and logs return to the server-owned session.
One-shot `-p` still refuses dangerous tools because it has no approval UI.

Manage paired devices on the server host:

Expand Down Expand Up @@ -842,6 +850,10 @@ Run `/pair code:<code>` in Discord within 10 minutes. The paired Discord user
becomes an admin. Admins can add regular users with `/user add`; users are
scoped to that configured bot.

File, search, and shell tools approved through Discord are always rooted at the
server process user's home directory. They do not inherit the directory from
which `mininaru serve` happened to start.

The bot answers when an authorized user mentions it in a server channel, and
answers every authorized message in a DM without requiring a mention. A mention
from someone who is not paired is ignored without a reply, so an unauthorized
Expand Down
41 changes: 41 additions & 0 deletions api/mininaru/v1/mininaru.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ service PairingService {

service MininaruService {
rpc ListAgents(ListAgentsRequest) returns (ListAgentsResponse);
rpc ListSkills(ListSkillsRequest) returns (ListSkillsResponse);
rpc GetSkill(GetSkillRequest) returns (Skill);
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse);
rpc CreateSession(CreateSessionRequest) returns (Session);
rpc GetSession(GetSessionRequest) returns (SessionDetail);
Expand Down Expand Up @@ -114,6 +116,23 @@ message ListAgentsResponse {
string default_agent_id = 2;
}

message Skill {
string name = 1;
string description = 2;
string scope = 3;
string body = 4;
}

message ListSkillsRequest {}

message ListSkillsResponse {
repeated Skill skills = 1;
}

message GetSkillRequest {
string name = 1;
}

message ListSessionsRequest {
string agent = 1;
}
Expand Down Expand Up @@ -166,6 +185,20 @@ message ChatStart {
string session_id = 1;
string content = 2;
string thinking = 3;
repeated ToolDefinition tools = 4;
}

message ToolDefinition {
string name = 1;
string description = 2;
string parameters_json = 3;
string permission = 4;
}

message ToolResult {
string request_id = 1;
string result = 2;
string error = 3;
}

message ApprovalDecision {
Expand All @@ -185,6 +218,7 @@ message ChatClientEvent {
ChatStart start = 1;
ApprovalDecision approval = 2;
Empty cancel = 3;
ToolResult tool_result = 4;
}
}

Expand Down Expand Up @@ -212,6 +246,12 @@ message ApprovalRequest {
string arguments = 3;
}

message ToolRequest {
string request_id = 1;
string tool_name = 2;
string arguments = 3;
}

message ChatCompleted {
Message message = 1;
Usage usage = 2;
Expand All @@ -231,5 +271,6 @@ message ChatServerEvent {
ApprovalRequest approval = 5;
ChatCompleted completed = 6;
ChatFailed failed = 7;
ToolRequest tool_request = 8;
}
}
17 changes: 16 additions & 1 deletion bot/discord/handlers/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package handlers

import (
"context"
"os"
"strings"
"sync"
"unicode"
Expand Down Expand Up @@ -245,6 +246,7 @@ func (d *Discord) answerFor(ctx context.Context, channelId, sourceChannelId, sou
var defs []modules.Def
var message *core.Message
var replyTo string
var home string

var err error

Expand Down Expand Up @@ -292,7 +294,20 @@ func (d *Discord) answerFor(ctx context.Context, channelId, sourceChannelId, sou
status.log("✗", "`"+label+"` — "+toolFailureReason(event.Error))
}
if role == core.DiscordRoleAdmin {
defs = modules.DefaultTools()
home, err = os.UserHomeDir()
if err != nil {
indicator.stop()
status.finish("❌", "Failed")
d.sendReplyTo(channelId, replyTo, conversationFailure("resolving the tool workspace", err))
return
}
defs, err = modules.DefaultToolsAt(home)
if err != nil {
indicator.stop()
status.finish("❌", "Failed")
d.sendReplyTo(channelId, replyTo, conversationFailure("opening the tool workspace", err))
return
}
message, err = target.ChatInput(ctx, session, content, parts, defs, onReasoning, onTool,
func(ctx context.Context, def modules.Def, arguments string) (bool, error) {
return d.approve(ctx, channelId, userId, def, arguments)
Expand Down
10 changes: 4 additions & 6 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,7 @@ func execute(cmd *cobra.Command, args []string) error {
}
}

if serverRef == "" {
serverRef = config.Client.Server.Address
}
if serverRef != "" {
return executeRemote(cmd.Context(), args, content)
}
serverRef = activeServerAddress()

if config.Client.Tools.Enabled {
err = withProgress(cmd.Context(), "connecting to mcp servers", func() error {
Expand All @@ -280,6 +275,9 @@ func execute(cmd *cobra.Command, args []string) error {
return err
}
}
if serverRef != "" {
return executeRemote(cmd.Context(), args, content)
}

agent, err = resolveAgent()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cli/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func pairWithServer(ctx context.Context, address, name, expected string) error {
}

config.Client.Server.Address = address
config.Client.Mode = config.ModeClient
err = config.ClientSave()
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions cli/preference.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ func agentListExecute(cmd *cobra.Command, args []string) error {
var cur *core.NaruAgent
var mark string

if activeServerAddress() != "" {
return remoteAgentListExecute(cmd.Context())
}

all = core.AgentAll()
if len(all) == 0 {
uiEmpty("no agents yet, add one with `mininaru agent add`")
Expand Down
Loading