n2n-nexus 0.11.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,25 @@
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
+
7
26
  ## [0.11.0] - 2026-10-29
8
27
 
9
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.
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
 
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Where this conversation runs, which the hook command adds to the input of
3
3
  * every event it forwards as `machine`, so that other sessions can tell the machines apart
4
- * (several sessions share a name and a working directory across machines).
4
+ * (sessions on different machines or WSL distributions often share a working directory).
5
5
  * Node built-ins only: the hook command imports this on client events.
6
6
  */
7
+ import { execFileSync } from "node:child_process";
7
8
  import fs from "node:fs";
8
9
  import os from "node:os";
9
10
  /** darwin, linux, win32 as Node reports them; wsl for a Linux that runs under Windows. */
@@ -17,11 +18,31 @@ export function platformOf(platform = process.platform, readProcVersion = () =>
17
18
  return "linux";
18
19
  }
19
20
  }
20
- /** The host name without the `.local` suffix that macOS adds. */
21
- export function hostnameOf(hostname = os.hostname()) {
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
+ }
22
38
  return hostname.replace(/\.local$/i, "");
23
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
+ }
24
44
  export function machineInfo(now = new Date()) {
45
+ const platform = platformOf();
25
46
  // 0 - x, not -x: at UTC -x is -0, which JSON writes as 0.
26
- return { hostname: hostnameOf(), platform: platformOf(), utcOffset: 0 - now.getTimezoneOffset() };
47
+ return { hostname: hostnameOf(), platform, environment: environmentOf(platform), utcOffset: 0 - now.getTimezoneOffset() };
27
48
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": 3,
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": 3,
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": 3,
2
+ "contractVersion": 4,
3
3
  "clients": [
4
4
  "claude",
5
5
  "codex"
@@ -78,12 +78,26 @@
78
78
  "replyTool": "The reply tool is %s.",
79
79
  "noReplyMark": "(no reply needed)",
80
80
  "earlierUnread": "%d earlier direct message(s) still unread; read them with read.",
81
- "meetingNews": "Meeting \"%s\" (%s): %d new message(s) (#%d–#%d) from %s.",
82
- "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.]",
83
88
  "meetingEnded": "Meeting \"%s\" (%s) has ended%s. No reply needed.",
84
89
  "stanceNotice": "%s %s draft #%d of meeting \"%s\" (%s)%s. No reply needed.",
85
90
  "approversChanged": "Required approvers of draft #%d changed (version %d): %s.",
86
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",
87
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.",
88
102
  "stanceWaiting": "Meeting \"%s\" (%s): draft #%d still waits for the stance of %s. No reply needed.",
89
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."
@@ -196,6 +210,10 @@
196
210
  "win32"
197
211
  ]
198
212
  },
213
+ "environment": {
214
+ "type": "string",
215
+ "description": "The WSL distribution, \"\" otherwise"
216
+ },
199
217
  "utcOffset": {
200
218
  "type": "integer",
201
219
  "minimum": -1440,
@@ -205,6 +223,7 @@
205
223
  "required": [
206
224
  "hostname",
207
225
  "platform",
226
+ "environment",
208
227
  "utcOffset"
209
228
  ]
210
229
  }