neagen 0.1.6 → 0.1.7
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/README.md +6 -4
- package/lib/eve-product-tui.mjs +87 -36
- package/lib/turn-stream.mjs +102 -0
- package/lib/wake.mjs +35 -7
- package/neagen.mjs +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# neagen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Agentic networking**, in the terminal.
|
|
4
4
|
|
|
5
|
-
State a need, and your Neagen
|
|
6
|
-
|
|
7
|
-
every outbound. The CLI is a thin, secure client for your
|
|
5
|
+
State a need, and your Neagen represents you in the network: it finds the
|
|
6
|
+
right person, prepares the introduction, and shares only what you allow. You
|
|
7
|
+
review and approve every outbound. The CLI is a thin, secure client for your
|
|
8
|
+
hosted Neagen.
|
|
8
9
|
|
|
9
10
|
## Install & run
|
|
10
11
|
|
|
@@ -48,6 +49,7 @@ Your passkey stays the root of your identity.
|
|
|
48
49
|
| --- | --- | --- |
|
|
49
50
|
| `NEAGEN_HOST` | `https://neagen.xyz` | Server to connect to (local dev: `http://localhost:3000`) |
|
|
50
51
|
| `NEAGEN_PROFILE` | _(main)_ | Named credential slot, e.g. for multiple accounts |
|
|
52
|
+
| `NEAGEN_VERBOSE` | _(off)_ | Set to `1` to show full tool output and reasoning. Off by default, so you see a short line per tool and a brief thinking indicator. Type `/verbose` in the app for the current state. |
|
|
51
53
|
|
|
52
54
|
## Where your session is stored
|
|
53
55
|
|
package/lib/eve-product-tui.mjs
CHANGED
|
@@ -15,7 +15,16 @@ import {
|
|
|
15
15
|
} from "./api.mjs";
|
|
16
16
|
import { runDeviceGrantLogin } from "./device-login.mjs";
|
|
17
17
|
import { decorateApprovalRequests } from "./approval-format.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
droppedSessionFailed,
|
|
20
|
+
isBoundaryEvent,
|
|
21
|
+
neagenErrorToSessionFailed,
|
|
22
|
+
parseSseChunk,
|
|
23
|
+
readStreamChunks,
|
|
24
|
+
stalledSessionFailed,
|
|
25
|
+
TURN_STALL_MS,
|
|
26
|
+
unconfirmedBoundarySessionFailed,
|
|
27
|
+
} from "./turn-stream.mjs";
|
|
19
28
|
import { subscribeInboxWakeAuto } from "./wake.mjs";
|
|
20
29
|
|
|
21
30
|
const require = createRequire(import.meta.url);
|
|
@@ -81,24 +90,6 @@ function isNeagenCommitEvent(eventName) {
|
|
|
81
90
|
eventName === "neagen.gate_declined";
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
function neagenErrorToSessionFailed(data) {
|
|
85
|
-
return {
|
|
86
|
-
type: "session.failed",
|
|
87
|
-
data: {
|
|
88
|
-
error: {
|
|
89
|
-
code: data?.code ?? "turn_failed",
|
|
90
|
-
message: data?.message ?? "Turn failed.",
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isBoundaryEvent(event) {
|
|
97
|
-
return event?.type === "session.waiting" ||
|
|
98
|
-
event?.type === "session.completed" ||
|
|
99
|
-
event?.type === "session.failed";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
93
|
// Swap Eve's raw "Approve tool call: <name>" prompt for a reviewable card on
|
|
103
94
|
// the events that carry approval requests. Presentation only; see approval-format.mjs.
|
|
104
95
|
function decorateEventData(event, data) {
|
|
@@ -111,7 +102,9 @@ function decorateEventData(event, data) {
|
|
|
111
102
|
return data;
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
async function* readProductTurnEvents(response, onProductEvent) {
|
|
105
|
+
async function* readProductTurnEvents(response, onProductEvent, options = {}) {
|
|
106
|
+
const idleMs = options.idleMs ?? TURN_STALL_MS;
|
|
107
|
+
|
|
115
108
|
if (!response.ok) {
|
|
116
109
|
let message = `HTTP ${response.status}`;
|
|
117
110
|
const text = await response.text();
|
|
@@ -129,12 +122,22 @@ async function* readProductTurnEvents(response, onProductEvent) {
|
|
|
129
122
|
const decoder = new TextDecoder();
|
|
130
123
|
let buffer = "";
|
|
131
124
|
let heldBoundary;
|
|
125
|
+
// A boundary (session.waiting/completed/failed) has been yielded to the runner.
|
|
126
|
+
// A held boundary is only released by `done` or a commit event, so a delivered
|
|
127
|
+
// boundary means the turn committed server-side. Without one the TUI keyboard
|
|
128
|
+
// never re-enables, so the tail synthesizes a terminal event.
|
|
129
|
+
let boundaryDelivered = false;
|
|
130
|
+
// A commit event (finalized/waiting_saved/gate_declined) reached us: the turn
|
|
131
|
+
// committed server-side even if its boundary or `done` never did. The tail uses
|
|
132
|
+
// this to prefer a benign completion over a scary failure.
|
|
133
|
+
let commitSeen = false;
|
|
132
134
|
|
|
133
135
|
async function* handleEntry(entry) {
|
|
134
136
|
const { event, data } = entry;
|
|
135
137
|
|
|
136
138
|
if (event === "done") {
|
|
137
139
|
if (heldBoundary) {
|
|
140
|
+
boundaryDelivered = true;
|
|
138
141
|
yield heldBoundary;
|
|
139
142
|
heldBoundary = undefined;
|
|
140
143
|
}
|
|
@@ -142,14 +145,17 @@ async function* readProductTurnEvents(response, onProductEvent) {
|
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
if (event === "neagen.error") {
|
|
148
|
+
boundaryDelivered = true;
|
|
145
149
|
yield neagenErrorToSessionFailed(data);
|
|
146
150
|
heldBoundary = undefined;
|
|
147
151
|
return;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (isNeagenCommitEvent(event)) {
|
|
155
|
+
commitSeen = true;
|
|
151
156
|
onProductEvent(event, data);
|
|
152
157
|
if (heldBoundary) {
|
|
158
|
+
boundaryDelivered = true;
|
|
153
159
|
yield heldBoundary;
|
|
154
160
|
heldBoundary = undefined;
|
|
155
161
|
}
|
|
@@ -164,25 +170,46 @@ async function* readProductTurnEvents(response, onProductEvent) {
|
|
|
164
170
|
yield eveEvent;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
let streamError;
|
|
174
|
+
try {
|
|
175
|
+
for await (const value of readStreamChunks(reader, { idleMs })) {
|
|
176
|
+
buffer += decoder.decode(value, { stream: true });
|
|
177
|
+
const parsed = parseSseChunk(buffer);
|
|
178
|
+
buffer = parsed.trailing;
|
|
179
|
+
for (const entry of parsed.events) {
|
|
180
|
+
yield* handleEntry(entry);
|
|
181
|
+
}
|
|
175
182
|
}
|
|
176
|
-
}
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
if (buffer.trim()) {
|
|
185
|
+
const parsed = parseSseChunk(`${buffer}\n\n`);
|
|
186
|
+
for (const entry of parsed.events) {
|
|
187
|
+
yield* handleEntry(entry);
|
|
188
|
+
}
|
|
182
189
|
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
streamError = error;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
if (
|
|
194
|
+
if (boundaryDelivered) return;
|
|
195
|
+
|
|
196
|
+
// The stream ended abnormally: no boundary was released in the loop (no `done`,
|
|
197
|
+
// no commit). A boundary still held here was never confirmed, so releasing it
|
|
198
|
+
// would show success for a turn that may not have committed. Decide from what
|
|
199
|
+
// we saw, always synthesizing a terminal event so the runner re-enables input.
|
|
200
|
+
if (commitSeen) {
|
|
201
|
+
// Committed server-side even without a boundary/`done` back (e.g.
|
|
202
|
+
// gate_declined sends no session boundary). Prefer a benign completion over a
|
|
203
|
+
// scary failure, on a clean end and an unclean one alike.
|
|
204
|
+
yield { type: "session.completed", data: {} };
|
|
205
|
+
} else if (heldBoundary) {
|
|
206
|
+
yield unconfirmedBoundarySessionFailed();
|
|
207
|
+
} else if (streamError?.code === "turn_stalled") {
|
|
208
|
+
yield stalledSessionFailed();
|
|
209
|
+
} else {
|
|
210
|
+
yield droppedSessionFailed();
|
|
211
|
+
}
|
|
212
|
+
heldBoundary = undefined;
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
class ProductMessageResponse {
|
|
@@ -283,6 +310,14 @@ function modelOptions(models, activeId) {
|
|
|
283
310
|
function createModelCommandHandler({ host, token, productClient, title }) {
|
|
284
311
|
return {
|
|
285
312
|
async handle(command, context) {
|
|
313
|
+
if (command.name === "verbose") {
|
|
314
|
+
const on = process.env.NEAGEN_VERBOSE === "1";
|
|
315
|
+
return {
|
|
316
|
+
message: on
|
|
317
|
+
? "Verbose display is on: full tool output and reasoning are shown. Restart with NEAGEN_VERBOSE=0 neagen to hide them again."
|
|
318
|
+
: "Neagen shows a short line per tool and a brief thinking indicator, hiding raw tool output and reasoning. To see everything, restart with NEAGEN_VERBOSE=1 neagen.",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
286
321
|
if (command.name !== "model") {
|
|
287
322
|
return { message: `/${command.name} is not supported in the Neagen TUI.` };
|
|
288
323
|
}
|
|
@@ -406,13 +441,23 @@ export async function runProductTui({ profile } = {}) {
|
|
|
406
441
|
process.on("SIGINT", async () => { await stopWake(); process.exit(0); });
|
|
407
442
|
process.on("exit", () => void wake?.close());
|
|
408
443
|
|
|
444
|
+
// Non-technical Owners get a calm transcript by default: one short line per
|
|
445
|
+
// tool call (name + args, raw bash/file/JSON output hidden) and a brief
|
|
446
|
+
// thinking indicator instead of streamed chain-of-thought. NEAGEN_VERBOSE=1
|
|
447
|
+
// restores the full developer view. The runner captures these at construction
|
|
448
|
+
// and builds its renderer once, so this is a startup choice, not a live toggle
|
|
449
|
+
// (see the /verbose command note).
|
|
450
|
+
const verbose = process.env.NEAGEN_VERBOSE === "1";
|
|
451
|
+
const partDisplay = verbose ? "full" : "collapsed";
|
|
452
|
+
|
|
409
453
|
await new EveTUIRunner({
|
|
410
454
|
session,
|
|
411
455
|
client,
|
|
412
456
|
serverUrl: host,
|
|
413
457
|
name: title,
|
|
414
|
-
tools:
|
|
415
|
-
reasoning:
|
|
458
|
+
tools: partDisplay,
|
|
459
|
+
reasoning: partDisplay,
|
|
460
|
+
subagents: partDisplay,
|
|
416
461
|
assistantResponseStats: "tokensPerSecond",
|
|
417
462
|
promptCommandHandler: createModelCommandHandler({ host, token, productClient: client, title }),
|
|
418
463
|
availablePromptCommands: [
|
|
@@ -432,6 +477,12 @@ export async function runProductTui({ profile } = {}) {
|
|
|
432
477
|
argumentHint: "[all|stderr|sandbox|none]",
|
|
433
478
|
takesArgument: true,
|
|
434
479
|
},
|
|
480
|
+
{
|
|
481
|
+
name: "verbose",
|
|
482
|
+
aliases: [],
|
|
483
|
+
description: "How to show full tool output and reasoning",
|
|
484
|
+
takesArgument: false,
|
|
485
|
+
},
|
|
435
486
|
{ name: "exit", aliases: ["quit"], description: "Quit", takesArgument: false },
|
|
436
487
|
],
|
|
437
488
|
}).run();
|
package/lib/turn-stream.mjs
CHANGED
|
@@ -6,6 +6,108 @@ function sseEvent(event, data) {
|
|
|
6
6
|
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// Default inactivity budget for a live turn stream. If no BYTES arrive from the
|
|
10
|
+
// server for this long, the connection is treated as stalled. The server sends
|
|
11
|
+
// SSE heartbeat comments (`: ping`) roughly every 15s, so a healthy but quiet
|
|
12
|
+
// turn keeps resetting this budget without emitting any parsed event.
|
|
13
|
+
export const TURN_STALL_MS = 90000;
|
|
14
|
+
|
|
15
|
+
export class TurnStallError extends Error {
|
|
16
|
+
constructor(message = "Turn stream stalled.") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "TurnStallError";
|
|
19
|
+
this.code = "turn_stalled";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isBoundaryEvent(event) {
|
|
24
|
+
return event?.type === "session.waiting" ||
|
|
25
|
+
event?.type === "session.completed" ||
|
|
26
|
+
event?.type === "session.failed";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function neagenErrorToSessionFailed(data) {
|
|
30
|
+
return {
|
|
31
|
+
type: "session.failed",
|
|
32
|
+
data: {
|
|
33
|
+
error: {
|
|
34
|
+
code: data?.code ?? "turn_failed",
|
|
35
|
+
message: data?.message ?? "Turn failed.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Synthesized boundary for a turn whose stream went quiet past the watchdog.
|
|
42
|
+
export function stalledSessionFailed() {
|
|
43
|
+
return {
|
|
44
|
+
type: "session.failed",
|
|
45
|
+
data: {
|
|
46
|
+
error: {
|
|
47
|
+
code: "turn_stalled",
|
|
48
|
+
message: "The connection to your Neagen stalled with no reply for a while. Your message may still be processing, so just send it again.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Synthesized boundary for a stream that ended (cleanly or with a network error)
|
|
55
|
+
// without ever delivering a terminal boundary event. Without this the TUI runner
|
|
56
|
+
// would wait forever on a boundary that is never coming and freeze the keyboard.
|
|
57
|
+
export function droppedSessionFailed() {
|
|
58
|
+
return {
|
|
59
|
+
type: "session.failed",
|
|
60
|
+
data: {
|
|
61
|
+
error: {
|
|
62
|
+
code: "turn_dropped",
|
|
63
|
+
message: "The connection to your Neagen dropped mid-reply. Your message may still be processing, so just try again.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Synthesized failure for a held boundary (session.completed/waiting) that the
|
|
70
|
+
// stream never confirmed with a commit event or a `done`. The boundary reached
|
|
71
|
+
// us, but the turn was cut off before the server finished saving and billing it,
|
|
72
|
+
// so we must not show success. The reply may have been lost mid-save.
|
|
73
|
+
export function unconfirmedBoundarySessionFailed() {
|
|
74
|
+
return {
|
|
75
|
+
type: "session.failed",
|
|
76
|
+
data: {
|
|
77
|
+
error: {
|
|
78
|
+
code: "turn_unconfirmed",
|
|
79
|
+
message: "The connection to your Neagen dropped before the reply was saved. Your Neagen's reply may not have been saved, so ask again to be sure.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Reads raw byte chunks from a stream reader with an inactivity watchdog. Any
|
|
86
|
+
// chunk resets the watchdog, including SSE comment bytes that parse to no event,
|
|
87
|
+
// so heartbeats keep a quiet turn alive. If nothing arrives for `idleMs`, the
|
|
88
|
+
// reader is cancelled and a TurnStallError is thrown. A read that rejects (a
|
|
89
|
+
// mid-stream network error) is re-thrown as-is after cancelling.
|
|
90
|
+
export async function* readStreamChunks(reader, { idleMs = TURN_STALL_MS } = {}) {
|
|
91
|
+
while (true) {
|
|
92
|
+
let timer;
|
|
93
|
+
const idle = new Promise((_resolve, reject) => {
|
|
94
|
+
timer = setTimeout(() => reject(new TurnStallError()), idleMs);
|
|
95
|
+
timer.unref?.();
|
|
96
|
+
});
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await Promise.race([reader.read(), idle]);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
try { await reader.cancel(); } catch { /* best-effort */ }
|
|
102
|
+
throw error;
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
if (result.done) return;
|
|
107
|
+
if (result.value) yield result.value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
9
111
|
export function parseSseChunk(buffer) {
|
|
10
112
|
const events = [];
|
|
11
113
|
const parts = buffer.split(/\n\n+/);
|
package/lib/wake.mjs
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
// by the app + vitest). The node entrypoints are .mjs and can't import the TS
|
|
6
6
|
// path-aliased module, so the same ~40 lines live here. Keep the two in sync.
|
|
7
7
|
//
|
|
8
|
-
// The published `neagen` CLI
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// The published `neagen` CLI calls subscribeInboxWakeAuto(ownerId, onWake, {
|
|
9
|
+
// host, bearerToken }), which routes it to subscribeInboxWakeAbly (token auth
|
|
10
|
+
// against /api/ably-token) since a real end user has no ABLY_API_KEY or
|
|
11
|
+
// NEAGEN_WAKE_TRANSPORT set on their machine. The fs/ws transports below are for
|
|
12
|
+
// local dev / the two-agent harness (dev-only x-neagen-owner header, which prod
|
|
13
|
+
// rejects) and are inert in the remote CLI.
|
|
11
14
|
//
|
|
12
15
|
// The wake is non-polling: fs.watch is OS-level (FSEvents / inotify), event-
|
|
13
16
|
// driven. "Mail = the file system" — a wake is just an envelope appearing in the
|
|
@@ -130,6 +133,12 @@ function wakeWsUrl(host) {
|
|
|
130
133
|
return host.replace(/\/+$/, "").replace(/^http/, "ws") + "/eve/v1/wake";
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
// Give up reconnecting after this many CONSECUTIVE attempts that never reached
|
|
137
|
+
// OPEN (e.g. the server rejecting the upgrade with 401 — an auth wall, not a
|
|
138
|
+
// network blip). A successful open resets the counter, so genuine blips keep
|
|
139
|
+
// reconnecting forever as before; only a sustained rejection wall stops the loop.
|
|
140
|
+
const MAX_CONSECUTIVE_OPEN_FAILURES = 5;
|
|
141
|
+
|
|
133
142
|
export async function subscribeInboxWakeWS(
|
|
134
143
|
ownerId,
|
|
135
144
|
onWake,
|
|
@@ -139,14 +148,19 @@ export async function subscribeInboxWakeWS(
|
|
|
139
148
|
const url = wakeWsUrl(base);
|
|
140
149
|
|
|
141
150
|
let closed = false;
|
|
151
|
+
let gaveUp = false;
|
|
142
152
|
let socket;
|
|
143
153
|
let reconnectTimer;
|
|
144
154
|
let backoffMs = reconnectMs;
|
|
155
|
+
let consecutiveOpenFailures = 0;
|
|
145
156
|
|
|
146
157
|
const connect = () => {
|
|
147
|
-
if (closed) return;
|
|
158
|
+
if (closed || gaveUp) return;
|
|
159
|
+
let openedThisAttempt = false;
|
|
148
160
|
socket = new WebSocket(url, { headers: { "x-neagen-owner": ownerId } });
|
|
149
161
|
socket.onopen = () => {
|
|
162
|
+
openedThisAttempt = true;
|
|
163
|
+
consecutiveOpenFailures = 0;
|
|
150
164
|
backoffMs = reconnectMs; // a live connection resets the backoff
|
|
151
165
|
};
|
|
152
166
|
socket.onmessage = (ev) => {
|
|
@@ -167,6 +181,15 @@ export async function subscribeInboxWakeWS(
|
|
|
167
181
|
};
|
|
168
182
|
socket.onclose = () => {
|
|
169
183
|
if (closed) return;
|
|
184
|
+
if (!openedThisAttempt) {
|
|
185
|
+
consecutiveOpenFailures += 1;
|
|
186
|
+
if (consecutiveOpenFailures >= MAX_CONSECUTIVE_OPEN_FAILURES) {
|
|
187
|
+
// Looks like an auth wall (e.g. prod rejecting the dev-only
|
|
188
|
+
// x-neagen-owner header with 401), not a blip — stop hammering it.
|
|
189
|
+
gaveUp = true;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
170
193
|
// Exponential backoff (NOT interval polling): one-shot timer that doubles
|
|
171
194
|
// up to the cap and resets on the next successful open.
|
|
172
195
|
reconnectTimer = setTimeout(connect, backoffMs);
|
|
@@ -276,9 +299,13 @@ export async function subscribeInboxWakeAbly(ownerId, onWake, { key, authUrl, ho
|
|
|
276
299
|
* - NEAGEN_WAKE_TRANSPORT wins when set: "fs" → fs.watch (pure-local, needs a
|
|
277
300
|
* shared filesystem), "ws" → our held WebSocket to the eve wake channel,
|
|
278
301
|
* "ably" → Ably managed pub/sub.
|
|
279
|
-
* - Otherwise, if ABLY_API_KEY is set → Ably
|
|
280
|
-
*
|
|
281
|
-
* - Otherwise →
|
|
302
|
+
* - Otherwise, if ABLY_API_KEY is set → Ably with the raw key (local dev /
|
|
303
|
+
* two-agent harness).
|
|
304
|
+
* - Otherwise, if `host` + `bearerToken` are given → Ably via token auth
|
|
305
|
+
* against `${host}/api/ably-token` (the published CLI: no server-side env,
|
|
306
|
+
* no dev-only x-neagen-owner header, so the WS transport isn't reachable in
|
|
307
|
+
* prod — token-auth Ably is the only transport a real end user can use).
|
|
308
|
+
* - Otherwise → the held WebSocket (local dev harness, x-neagen-owner header).
|
|
282
309
|
* Same (ownerId, onWake) -> { close() } contract for every transport, so callers
|
|
283
310
|
* don't branch.
|
|
284
311
|
*/
|
|
@@ -288,6 +315,7 @@ export async function subscribeInboxWakeAuto(ownerId, onWake, options = {}) {
|
|
|
288
315
|
if (transport === "ws") return subscribeInboxWakeWS(ownerId, onWake, options);
|
|
289
316
|
if (transport === "ably") return subscribeInboxWakeAbly(ownerId, onWake, options);
|
|
290
317
|
if (process.env.ABLY_API_KEY) return subscribeInboxWakeAbly(ownerId, onWake, options);
|
|
318
|
+
if (options.host && options.bearerToken) return subscribeInboxWakeAbly(ownerId, onWake, options);
|
|
291
319
|
return subscribeInboxWakeWS(ownerId, onWake, options);
|
|
292
320
|
}
|
|
293
321
|
|
package/neagen.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// neagen
|
|
2
|
+
// neagen: agentic networking, in the terminal.
|
|
3
3
|
//
|
|
4
4
|
// `neagen` open the TUI (logs you in on first run)
|
|
5
5
|
// `neagen login` log in / refresh your session, then exit
|
|
@@ -40,7 +40,7 @@ function parseArgs(argv) {
|
|
|
40
40
|
|
|
41
41
|
function printHelp() {
|
|
42
42
|
process.stdout.write(
|
|
43
|
-
`neagen ${pkg.version}
|
|
43
|
+
`neagen ${pkg.version}: agentic networking, in the terminal\n\n` +
|
|
44
44
|
`Usage:\n` +
|
|
45
45
|
` neagen [--profile NAME] Open the agent (logs you in on first run)\n` +
|
|
46
46
|
` neagen login Log in / refresh your session\n` +
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neagen",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Agentic networking, in the terminal. Your Neagen represents you in the network: chat, agent-mail, and files, with passkey login and no passwords.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"neagen": "neagen.mjs"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"neagen",
|
|
34
|
+
"agentic-networking",
|
|
34
35
|
"networking",
|
|
35
36
|
"agent",
|
|
36
37
|
"cli",
|