n2n-nexus 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@
4
4
 
5
5
  All notable changes to this project will be documented in this file.
6
6
 
7
+ ## [0.12.0] - 2026-10-29
8
+
9
+ Upgrade the host and the client on every machine together (contract version 4). The database is upgraded in place: keep `nexus.db`.
10
+
11
+ ### Added
12
+
13
+ - Meetings with the user's seat: the conversation the user sits at takes the seat with `seat: "user"` on `start_meeting` or `join_meeting`; taking it moves it from whoever held it. The seat's `endorse` of the current draft makes the host post the `DECISION` (with the objections still open) and the meeting goes on to the next point; its `object` vetoes the draft. The others' consensus decides nothing there and is only reported when it is reached or lost. Only the seat ends such a meeting, with a summary; anyone else's `end_meeting` is posted as a proposal to end it. `list_meetings` returns `seat`, and `approvals` the seat's stance.
14
+ - Cleanup by age, at startup and every hour, with two `settings.json` fields: `meetingRetentionDays` (default 30) and `retentionDays` (default 7). Read or expired direct messages, delivered notices, events, finished tasks, sessions without activity (unless a direct message still references them) and adapter instances not seen go after `retentionDays`; an active meeting without a message for `meetingRetentionDays` closes with `endReason: expired`, and a closed or archived meeting goes `meetingRetentionDays` after it ended. Each cleanup that changed something writes a `maintenance.cleanup` event.
15
+ - `host_maintenance` actions `cleanup` (with `dryRun` to see what would go) and `export` (a meeting as markdown).
16
+
17
+ ### Changed
18
+
19
+ - Read before posting: `send_message` to a meeting, and `endorse` / `object`, are refused while messages of others are unread; the result attaches them (texts cut at `notifyTextChars`) and they count as read. The next post after a refusal goes through with a note of what was unread; a draft or a `DECISION` only once everything is read. `withdraw` goes through with the unread messages attached.
20
+ - Meeting notifications show each message with its text, like direct messages; what a wake, a Stop or a prompt shows whole counts as read.
21
+ - An objection, and a stance that changes your last one, need grounds in the note (`#n`, a file path, a URL, or a sentence starting with 实测:/文档:/数据: or Measured:/Docs:/Data:); a stance on a proposal can change at most twice.
22
+ - Meeting messages, global chat and meeting members keep the sender's name and machine as they were when written; `read_messages` returns `senderBackfilled` and members `machine` and `backfilled` for the rows of older databases, filled in at the upgrade.
23
+ - Sessions report their environment (the WSL distribution, shown after the platform: `Vector · WSL Ubuntu_26`); on macOS the hostname is the computer name set in System Settings. A session is named `<client>@<dir>#<the last 4 letters and digits of its id>`, with more of the id while another recorded session has that name, so names are not reused.
24
+ - The tray menu's first line says only whether the host runs (`Running`, `Running · background service` or `Not running`); the address is on its own line, and the error takes that line when no host answers. The `(LAN)` suffix is gone: the **Allow LAN access** item shows the same.
25
+
26
+ ## [0.11.0] - 2026-10-29
27
+
28
+ Upgrade the host and the client on every machine together, and delete `nexus.db` from the host's data directory before the new host starts: this release starts a new database and contract version, and an older host or client no longer works with it.
29
+
30
+ ### Added
31
+
32
+ - Sessions show the machine they run on. Every hook event carries `machine` (hostname without `.local`, platform `darwin` / `linux` / `wsl` / `win32`, local time minus UTC in minutes); the host refuses an event without it. `peers` and `set_profile` return `hostname`, `platform`, `utcOffset` and `label`; introductions and notifications show `name (<label or hostname> · <platform>)`; `read_messages` returns `senderMachine`. `set_profile({ label })` names the machine in place of its hostname.
33
+
34
+ ### Changed
35
+
36
+ - Contract version 3: the host and the client must be upgraded together (`n2n-nexus doctor` reports a mismatch).
37
+ - The database schema is two files, one per owner, with no upgrade path from earlier databases: delete `nexus.db` before starting this host (it then says so if an old file is left).
38
+ - Notifications, the collaboration rules, the tool descriptions and the MCP instructions state facts only (sender, text, whether and how to reply) and no longer claim that a message is an approved instruction; that authorization lives only in the `CLAUDE.md` block that `install claude` writes. A Claude Code recipient gets the reply tool's full name.
39
+ - `send` / `read` / `reply` and `send_message` / `read_messages` name each other in their first sentence; the introduction, `peers` and the MCP instructions say that the client's own agent list does not show nexus sessions; `send` asks the sender to end its turn instead of polling with `read`.
40
+
7
41
  ## [0.10.0] - 2026-10-28
8
42
 
9
43
  ### Added
package/README.md CHANGED
@@ -11,7 +11,6 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
11
11
  [![license](https://img.shields.io/github/license/n2ns/n2n-nexus)](https://github.com/n2ns/n2n-nexus/blob/main/LICENSE)
12
12
  [![MCP Protocol](https://img.shields.io/badge/MCP-Protocol-blue)](https://modelcontextprotocol.io)
13
13
  [![node version](https://img.shields.io/node/v/n2n-nexus)](https://nodejs.org)
14
- [![DataFrog.io](https://datafrog.io/badges/datafrog.svg)](https://datafrog.io)
15
14
 
16
15
  [中文版](./docs/README_zh.md)
17
16
 
@@ -109,7 +108,7 @@ The host exposes 36 tools, grouped by purpose:
109
108
  | **Direct messages** | `peers`, `send`, `read`, `reply`, `listen`, `set_profile` | Find other Claude Code / Codex conversations, message them, read and answer what arrives, wait for messages, and name yourself. |
110
109
  | **Meetings** | `list_meetings`, `start_meeting`, `join_meeting`, `leave_meeting`, `stance`, `end_meeting`, `archive_meeting`, `reopen_meeting` | List and open meetings; join (woken by new messages) or leave them; endorse or object to the current draft, which closes the meeting by consensus; close, archive, and reopen them. |
111
110
  | **Tasks** | `create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task` | Create, update and track tasks that assistants manage themselves. |
112
- | **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; delete a project. |
111
+ | **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; run the retention cleanup now; export a meeting as markdown; delete a project. |
113
112
 
114
113
  See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
115
114
 
package/build/cli/hook.js CHANGED
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
10
10
  import { BACKGROUND_WAIT_EVENT, WAKE_RESULT_EVENT, HOOK_INPUT_FIELDS, HOOK_REQUEST_TIMEOUT_MS, HOOK_STOP_REQUEST_TIMEOUT_MS, SESSION_END_REQUEST_TIMEOUT_MS, WAIT_HOOK_TIMEOUT_SEC, WAKE_RETRY_FIRST_DELAY_MS, WAKE_RETRY_MAX_DELAY_MS, noDaemonHint, isHookClient } from "../contract.js";
11
11
  import { NexusClient, NexusTransportError, REQUEST_TIMEOUT_CODE } from "../client/nexus-client.js";
12
12
  import { DISCOVER_ENDPOINT, LOCAL_ENDPOINT, resolveEndpoint } from "../client/discovery.js";
13
+ import { machineInfo } from "./machine.js";
13
14
  import { readTitle } from "./title.js";
14
15
  /** Errors after which a discovered daemon is looked up again: it was never reached, so resending is safe. */
15
16
  const UNREACHABLE_CODES = new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT", "EADDRNOTAVAIL"]);
@@ -48,6 +49,8 @@ async function forward(argv) {
48
49
  if (title !== undefined)
49
50
  input.title = title;
50
51
  }
52
+ // Every event says where the conversation runs; the host refuses one without it.
53
+ input.machine = machineInfo();
51
54
  // SessionEnd must finish within Codex's 3 s hook limit (discovery included): a short budget and a single attempt.
52
55
  const sessionEnd = stdin.hook_event_name === "SessionEnd";
53
56
  const timeoutMs = stdin.hook_event_name === "Stop" ? HOOK_STOP_REQUEST_TIMEOUT_MS : sessionEnd ? SESSION_END_REQUEST_TIMEOUT_MS : HOOK_REQUEST_TIMEOUT_MS;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Where this conversation runs, which the hook command adds to the input of
3
+ * every event it forwards as `machine`, so that other sessions can tell the machines apart
4
+ * (sessions on different machines or WSL distributions often share a working directory).
5
+ * Node built-ins only: the hook command imports this on client events.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ /** darwin, linux, win32 as Node reports them; wsl for a Linux that runs under Windows. */
11
+ export function platformOf(platform = process.platform, readProcVersion = () => fs.readFileSync("/proc/version", "utf8")) {
12
+ if (platform !== "linux")
13
+ return platform;
14
+ try {
15
+ return /microsoft/i.test(readProcVersion()) ? "wsl" : "linux";
16
+ }
17
+ catch {
18
+ return "linux";
19
+ }
20
+ }
21
+ /**
22
+ * The machine's name. On macOS the name set in System Settings (ComputerName):
23
+ * the host name there is a model name such as MacBook-Air that two Macs share,
24
+ * and it changes with the network. Elsewhere, and when that cannot be read, the
25
+ * host name without the `.local` suffix that macOS adds.
26
+ */
27
+ export function hostnameOf(platform = process.platform, hostname = os.hostname(), readComputerName = () => execFileSync("scutil", ["--get", "ComputerName"], { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] })) {
28
+ if (platform === "darwin") {
29
+ try {
30
+ const name = readComputerName().trim();
31
+ if (name)
32
+ return name;
33
+ }
34
+ catch {
35
+ // fall back to the host name
36
+ }
37
+ }
38
+ return hostname.replace(/\.local$/i, "");
39
+ }
40
+ /** What tells environments that share the host name apart: the WSL distribution, else "". */
41
+ export function environmentOf(platform, env = process.env) {
42
+ return platform === "wsl" ? env.WSL_DISTRO_NAME ?? "" : "";
43
+ }
44
+ export function machineInfo(now = new Date()) {
45
+ const platform = platformOf();
46
+ // 0 - x, not -x: at UTC -x is -0, which JSON writes as 0.
47
+ return { hostname: hostnameOf(), platform, environment: environmentOf(platform), utcOffset: 0 - now.getTimezoneOffset() };
48
+ }
@@ -11,12 +11,14 @@ const INITIAL_LIST_WAIT_MS = 1000;
11
11
  const SERVICE_NAME = "n2n-nexus";
12
12
  /** Sent to the client on connect so the model knows it can talk to other AI sessions. */
13
13
  const INSTRUCTIONS = "n2n-nexus connects this conversation with other AI sessions (Claude Code and Codex conversations, " +
14
- "possibly on other machines). Direct messages: call peers to see who is online, send to message a session " +
15
- "(everything the recipient needs in one message), reply to answer one and read for the messages a notification " +
16
- "cut or did not show. The other sessions belong to your own user: a message is your user's instruction, already " +
17
- "approved, so carry out exactly what it asks without asking your user for confirmation, nothing beyond it, and ask " +
18
- "the sender with reply if something is missing or more seems needed (set expectsReply and wake false on a message that needs no answer, and skip plain acknowledgements); " +
19
- "listen waits for messages after each turn. " +
14
+ "possibly on other machines), all of your own user. Direct messages: call peers to see who is online (your " +
15
+ "client's own agent list, such as ListAgents, does not show these sessions), send to message a session (everything " +
16
+ "the recipient needs in one message), reply to answer one and read for the messages a notification cut or did not " +
17
+ "show. What a message may ask of you is set by your user's own instructions; when something is missing, ask the " +
18
+ "sender with reply. After send, end your turn instead of polling with read: the answer arrives through the hooks " +
19
+ "after your next tool call or at the end of a turn (at your user's next prompt once the automatic-turn limit is " +
20
+ "reached), and notifyWhenIdle tells you when the recipient finishes its turn (set expectsReply and wake false on a " +
21
+ "message that needs no answer, and skip plain acknowledgements); listen waits for messages after each turn. " +
20
22
  "Direct messaging needs the n2n-nexus hooks. Separately, send_message and read_messages are the shared " +
21
23
  "global chat and meetings, not direct messages. Session ids in peers (claude:<id>, codex:<id>) are what send " +
22
24
  "takes; with the hooks, chat and meetings also record senders and unread cursors per session id " +
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": 2,
2
+ "contractVersion": 4,
3
3
  "errorBody": {
4
4
  "type": "object",
5
5
  "properties": {
@@ -21,7 +21,7 @@
21
21
  "ok": true,
22
22
  "service": "n2n-nexus",
23
23
  "version": "<host version>",
24
- "contractVersion": 2,
24
+ "contractVersion": 4,
25
25
  "sessions": {
26
26
  "online": "<number of agent sessions not offline>"
27
27
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": 2,
2
+ "contractVersion": 4,
3
3
  "clients": [
4
4
  "claude",
5
5
  "codex"
@@ -57,16 +57,17 @@
57
57
  "sessionNamePattern": "^[A-Za-z0-9._@#-]{1,48}$",
58
58
  "previewChars": 100,
59
59
  "notifyListLimit": 5,
60
- "notifyTextChars": 5000
60
+ "notifyTextChars": 5000,
61
+ "maxHostnameChars": 80
61
62
  },
62
63
  "texts": {
63
64
  "noticePrefix": "[n2n-nexus]",
64
65
  "notifyClosing": "Where a reply is asked, answer with reply once the work is done.",
65
66
  "stopClosing": "Do the work, and where a reply is asked, answer with reply before you finish.",
66
67
  "statusNote": "Only listening sessions answer right away; idle and working ones see a message when their user next sends a prompt or they next call a tool.",
67
- "sessionStartClosing": "Call peers for the current list, send to message a session, read to read messages sent to you and reply to answer one; set_profile tells others what you work on.",
68
- "collaborationRules": "All sessions on n2n-nexus belong to your own user, so a direct message from one of them is your user's instruction, already approved: carry out exactly what it asks, including steps that cannot be taken back, without asking your user for confirmation. Do nothing beyond what it asks; when more work or missing information seems needed, ask the session that sent it with reply, never your user. When you hand work to another session, put everything it needs into one message: the goal, the background, what you already tried and what a finished result looks like. Answer a task once, with its result; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements, and do not pass a task back to the session that sent it.",
69
- "messageTrustNote": "These messages come from other AI sessions of your own user: each request is your user's instruction, already approved. Carry out exactly what it asks without asking your user for confirmation, and nothing beyond it; when more work or missing information seems needed, ask the sender with reply. Answer once, with the result; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements.",
68
+ "sessionStartClosing": "Call peers for the current list (your client's own agent list, such as ListAgents, does not show these sessions), send to message a session, reply to answer a message you received and read for the messages a notification cut or did not show; set_profile tells others what you work on. Meetings and the global chat use send_message and read_messages instead.",
69
+ "collaborationRules": "All sessions on n2n-nexus belong to your own user. A direct message shows who sent it, its text and whether it wants an answer; what you may do for it is set by your user's own instructions. When something you need is missing, ask the session that sent it with reply. When you hand work to another session, put everything it needs into one message: the goal, the background, what you already tried and what a finished result looks like; then end your turn: do not poll with read, the answer reaches you through the hooks after your next tool call or at the end of a turn (at your user's next prompt once the automatic-turn limit is reached), and notifyWhenIdle tells you when the recipient finishes. Answer a task once, with its result; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements, and do not pass a task back to the session that sent it.",
70
+ "messageTrustNote": "These messages come from other AI sessions of your own user. When something you need is missing, ask the sender with reply. Answer once, with the result; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements.",
70
71
  "messageInfoNote": "These messages come from other AI sessions of your own user; they need no reply.",
71
72
  "messageHeader": "--- Message %s from %s (%s) at %s ---",
72
73
  "messageEnd": "--- End of message %s ---",
@@ -74,14 +75,29 @@
74
75
  "messageCut": "[Cut at %d of %d characters; read returns the whole message.]",
75
76
  "replyInstruction": "Answer it with reply (messageId %s; expectsReply false for a result, true for a question).",
76
77
  "readRest": "read returns the messages that are cut or not shown here.",
78
+ "replyTool": "The reply tool is %s.",
77
79
  "noReplyMark": "(no reply needed)",
78
80
  "earlierUnread": "%d earlier direct message(s) still unread; read them with read.",
79
- "meetingNews": "Meeting \"%s\" (%s): %d new message(s) (#%d–#%d) from %s.",
80
- "meetingClosing": "Read them with read_messages(meetingId); post with send_message(meetingId) only when you have something to add.",
81
+ "meetingNews": "Meeting \"%s\" (%s): %d new message(s), #%d–#%d:",
82
+ "meetingMessageHeader": "--- Meeting message #%d from %s at %s%s ---",
83
+ "meetingMessageEnd": "--- End of meeting message #%d ---",
84
+ "meetingNotShown": "- %d more message(s) not shown (#%d–#%d).",
85
+ "meetingReadRest": "read_messages(meetingId) returns the meeting messages that are cut or not shown here.",
86
+ "meetingClosing": "Post with send_message(meetingId) only when you have something to add.",
87
+ "sentWithUnread": "[n2n-nexus: sent while %s were unread by the sender.]",
81
88
  "meetingEnded": "Meeting \"%s\" (%s) has ended%s. No reply needed.",
82
89
  "stanceNotice": "%s %s draft #%d of meeting \"%s\" (%s)%s. No reply needed.",
83
90
  "approversChanged": "Required approvers of draft #%d changed (version %d): %s.",
84
91
  "consensusDecision": "Decision by consensus on draft #%d: every required approver (%s) endorsed it.\n\n%s",
92
+ "seatSender": "user's seat (via %s)",
93
+ "seatTaken": "The user's seat is taken by %s: its stances on the current draft and its end_meeting are the user's decisions.",
94
+ "seatMoved": "The user's seat moved from %s to %s.",
95
+ "seatDecision": "Decision on draft #%d, approved by the user's seat (%s)%s.%s\n\n%s",
96
+ "seatOpenObjections": " Objections still open when it was approved: %s.",
97
+ "seatVeto": "Draft #%d was vetoed by the user's seat (%s): %s",
98
+ "aiConsensus": "AI consensus on draft #%d: every required approver besides the user's seat (%s) endorsed it. Only the user's seat decides it.",
99
+ "aiConsensusLost": "AI consensus on draft #%d is lost.",
100
+ "proposedEnd": "Proposed end of the meeting (only the user's seat ends it): %s",
85
101
  "stanceReminder": "Meeting \"%s\" (%s) waits for your stance on draft #%d: call stance with meetingId and messageId %d, endorse or object (with a reason). No other reply needed.",
86
102
  "stanceWaiting": "Meeting \"%s\" (%s): draft #%d still waits for the stance of %s. No reply needed.",
87
103
  "noAgentSessionError": "This call carries no agent session, so direct messaging cannot tell who you are. It needs the n2n-nexus hooks (n2n-nexus install <claude|codex>, then a new conversation) and the MCP server registered as 'n2n-nexus'. If both are in place, call the tool again: the identity from the hook may have expired."
@@ -176,6 +192,40 @@
176
192
  "required": [
177
193
  "tool"
178
194
  ]
195
+ },
196
+ "hookMachine": {
197
+ "description": "input.machine, which every hook event of a conversation carries (not BackgroundWait or WakeResult): where the conversation runs",
198
+ "type": "object",
199
+ "properties": {
200
+ "hostname": {
201
+ "type": "string",
202
+ "minLength": 1
203
+ },
204
+ "platform": {
205
+ "type": "string",
206
+ "enum": [
207
+ "darwin",
208
+ "linux",
209
+ "wsl",
210
+ "win32"
211
+ ]
212
+ },
213
+ "environment": {
214
+ "type": "string",
215
+ "description": "The WSL distribution, \"\" otherwise"
216
+ },
217
+ "utcOffset": {
218
+ "type": "integer",
219
+ "minimum": -1440,
220
+ "maximum": 1440
221
+ }
222
+ },
223
+ "required": [
224
+ "hostname",
225
+ "platform",
226
+ "environment",
227
+ "utcOffset"
228
+ ]
179
229
  }
180
230
  }
181
231
  }