neagen 0.1.0 → 0.1.2

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/lib/slash.mjs ADDED
@@ -0,0 +1,49 @@
1
+ // Slash commands for the neagen TUI — single source for /help and command selection.
2
+
3
+ export const SLASH_COMMANDS = [
4
+ { cmd: "/model", help: "pick a model — type to filter, arrows to move, enter to select" },
5
+ { cmd: "/new", help: "start a fresh conversation" },
6
+ { cmd: "/help", help: "show commands" },
7
+ { cmd: "/exit", help: "quit (also /quit, Ctrl-D, Ctrl-C)" },
8
+ ];
9
+
10
+ /** Hidden alias — completable, not listed in /help. */
11
+ export const SLASH_ALIASES = ["/quit"];
12
+
13
+ export const SLASH_NAMES = [...SLASH_COMMANDS.map((entry) => entry.cmd), ...SLASH_ALIASES];
14
+
15
+ export function slashQuery(line) {
16
+ if (!line.startsWith("/") || line.includes(" ")) return null;
17
+ return line.toLowerCase();
18
+ }
19
+
20
+ export function slashCommandMatches(line, commands = SLASH_COMMANDS) {
21
+ const query = slashQuery(line);
22
+ if (query === null) return [];
23
+ return commands.filter((entry) => entry.cmd.toLowerCase().startsWith(query));
24
+ }
25
+
26
+ export function slashCompletionSuffix(line) {
27
+ const query = slashQuery(line);
28
+ if (query === null) return "";
29
+ const hits = SLASH_NAMES.filter((name) => name.toLowerCase().startsWith(query));
30
+ if (hits.length !== 1) return "";
31
+ return hits[0].slice(line.length);
32
+ }
33
+
34
+ /**
35
+ * readline completer: Tab-completes slash commands on the command word only
36
+ * (leading "/" with no space yet) so Tab never hijacks normal chat input.
37
+ */
38
+ export function completeSlash(line) {
39
+ if (!line.startsWith("/") || line.includes(" ")) return [[], line];
40
+ const query = line.toLowerCase();
41
+ const hits = SLASH_NAMES.filter((name) => name.startsWith(query));
42
+ return [hits, line];
43
+ }
44
+
45
+ export function slashHelpLines() {
46
+ const rows = SLASH_COMMANDS.map(({ cmd, help }) => ` ${cmd.padEnd(11)} ${help}`);
47
+ rows.push(" (type / to open the command selector; Tab accepts the prefill)");
48
+ return rows;
49
+ }
package/lib/tui.mjs CHANGED
@@ -1,28 +1,30 @@
1
- // neagen TUI — chat, agent-mail, files against the deployed agent.
2
- // Login happens here (device-grant panel) when no valid session exists.
3
- //
4
- // Resolves Eve's TUI internals from the INSTALLED `eve` package (via
5
- // require.resolve), so it works no matter where npm puts the dependency — global
6
- // install, npx cache, or this repo's node_modules. `eve` is a hard dependency of
7
- // this package (see ../package.json), pinned to an exact version because these
8
- // dist paths are internal to that version.
9
-
10
- import { createRequire } from "node:module";
11
- import { dirname, join } from "node:path";
12
- import { pathToFileURL } from "node:url";
13
- import { Client } from "eve/client";
1
+ // neagen CLI — chat through Neagen's server-mediated turn transport.
2
+ // Login happens here (device grant) when no valid session exists.
3
+
4
+ import readline from "node:readline/promises";
5
+ import { stdin } from "node:process";
6
+ import { dim, bold } from "./ansi.mjs";
14
7
  import { assertTransportSafe, clearCreds, loadToken, resolveHost } from "./creds.mjs";
15
- import { probeHost, resolveIdentity } from "./api.mjs";
8
+ import {
9
+ getSelectedModel,
10
+ listModels,
11
+ probeHost,
12
+ resolveIdentity,
13
+ selectModel,
14
+ } from "./api.mjs";
16
15
  import { runDeviceGrantLogin } from "./device-login.mjs";
17
- import { panel } from "./flow.mjs";
16
+ import {
17
+ formatRequestDetails,
18
+ requestToolName,
19
+ sendTransportTurn,
20
+ sendWakeSyncTurn,
21
+ summarizeResult,
22
+ } from "./eve-chat.mjs";
23
+ import { createTurnStreamRenderer } from "./turn-stream.mjs";
24
+ import { pickModel, readPromptLine } from "./picker.mjs";
25
+ import { SLASH_COMMANDS, slashHelpLines } from "./slash.mjs";
18
26
  import { subscribeInboxWakeAbly } from "./wake.mjs";
19
27
 
20
- const require = createRequire(import.meta.url);
21
- // `eve` exposes "./package.json" in its exports map, so this resolves the
22
- // installed package root regardless of hoisting.
23
- const EVE_DIST = join(dirname(require.resolve("eve/package.json")), "dist", "src");
24
- const EVE_TUI = join(EVE_DIST, "cli", "dev", "tui");
25
-
26
28
  const profile = process.env.NEAGEN_PROFILE?.trim() || undefined;
27
29
  const host = resolveHost();
28
30
 
@@ -38,59 +40,21 @@ try {
38
40
  process.exit(1);
39
41
  }
40
42
 
41
- async function importEve(relPath) {
42
- return import(pathToFileURL(join(EVE_DIST, relPath)).href);
43
- }
44
-
45
- const [
46
- { EveTUIRunner },
47
- { createPromptCommandHandler },
48
- { promptCommandsFor },
49
- { formatRemoteAuthChallengeMessage },
50
- { toErrorMessage },
51
- { isVercelAuthChallenge },
52
- { TerminalRenderer },
53
- ] = await Promise.all([
54
- import(pathToFileURL(join(EVE_TUI, "runner.js")).href),
55
- import(pathToFileURL(join(EVE_TUI, "prompt-command-handler.js")).href),
56
- import(pathToFileURL(join(EVE_TUI, "prompt-commands.js")).href),
57
- import(pathToFileURL(join(EVE_TUI, "remote-auth-result.js")).href),
58
- importEve("shared/errors.js"),
59
- importEve("services/dev-client/vercel-auth-error.js"),
60
- import(pathToFileURL(join(EVE_TUI, "terminal-renderer.js")).href),
61
- ]);
62
-
63
- const target = {
64
- kind: "remote",
65
- serverUrl: host,
66
- workspaceRoot: process.cwd(),
67
- };
68
-
69
- const renderer = new TerminalRenderer({ availablePromptCommands: promptCommandsFor("remote") });
70
-
71
43
  function fail(message) {
72
- try {
73
- panel(renderer).renderLine(message, "error");
74
- } catch {
75
- console.error(`\n ${message}\n`);
76
- }
44
+ console.error(`\n ${message}\n`);
77
45
  process.exit(1);
78
46
  }
79
47
 
80
48
  async function ensureReachable() {
81
- const ui = panel(renderer);
82
- ui.begin("neagen", "pulse");
83
- ui.renderLine(`Connecting to ${host}…`, "info");
49
+ process.stdout.write(`Connecting to ${host}...\n`);
84
50
  try {
85
51
  await probeHost(host);
86
- ui.renderLine("Agent online.", "success");
52
+ process.stdout.write("Agent online.\n");
87
53
  } catch (e) {
88
- ui.renderLine(`Can't reach ${host} (${e.message}).`, "error");
89
- ui.renderLine("Check your connection, or set NEAGEN_HOST to your server.", "info");
90
- ui.end({ preserveDiagnostics: true });
54
+ process.stderr.write(`Can't reach ${host} (${e.message}).\n`);
55
+ process.stderr.write("Check your connection, or set NEAGEN_HOST to your server.\n");
91
56
  process.exit(1);
92
57
  }
93
- ui.end({ preserveDiagnostics: false });
94
58
  }
95
59
 
96
60
  async function ensureAuthenticated() {
@@ -101,7 +65,7 @@ async function ensureAuthenticated() {
101
65
  clearCreds(profile);
102
66
  token = null;
103
67
  }
104
- token = await runDeviceGrantLogin({ host, profile, renderer });
68
+ token = await runDeviceGrantLogin({ host, profile });
105
69
  const me = await resolveIdentity(host, token);
106
70
  if (!me) {
107
71
  clearCreds(profile);
@@ -110,6 +74,107 @@ async function ensureAuthenticated() {
110
74
  return { token, me };
111
75
  }
112
76
 
77
+ function formatModel(model) {
78
+ if (!model) return "unknown";
79
+ const label = model.providerLabel && model.name ? `${model.providerLabel} ${model.name}` : model.id;
80
+ return `${label} (${model.id})`;
81
+ }
82
+
83
+ function modelAliases(input) {
84
+ const value = input.trim();
85
+ const lower = value.toLowerCase();
86
+ if (lower === "deepseek" || lower === "flash") return "deepseek/deepseek-v4-flash";
87
+ if (lower === "sonnet" || lower === "claude" || lower === "claude-sonnet") {
88
+ return "anthropic/claude-sonnet-4.6";
89
+ }
90
+ return value;
91
+ }
92
+
93
+ async function runModelPicker(rl, token) {
94
+ let selection;
95
+ let models;
96
+ try {
97
+ [selection, models] = await Promise.all([
98
+ getSelectedModel(host, token),
99
+ listModels(host, token),
100
+ ]);
101
+ } catch (e) {
102
+ process.stderr.write(`! ${e?.message ?? e}\n`);
103
+ return;
104
+ }
105
+
106
+ const activeId = selection?.activeModel?.id;
107
+ const chosen = await pickModel(rl, models, activeId);
108
+ if (!chosen) {
109
+ process.stdout.write(`${dim("(no change)")}\n`);
110
+ return;
111
+ }
112
+ if (chosen.id === activeId) {
113
+ process.stdout.write(`${dim(`already using ${formatModel(chosen)}`)}\n`);
114
+ return;
115
+ }
116
+
117
+ try {
118
+ const updated = await selectModel(host, token, chosen.id);
119
+ process.stdout.write(`Switched model to ${bold(formatModel(updated.activeModel))}.\n`);
120
+ } catch (e) {
121
+ process.stderr.write(`! ${e?.message ?? e}\n`);
122
+ }
123
+ }
124
+
125
+ async function switchModelByName(token, raw) {
126
+ const modelId = modelAliases(raw);
127
+ const selection = await selectModel(host, token, modelId);
128
+ process.stdout.write(`Switched model to ${bold(formatModel(selection.activeModel))}.\n`);
129
+ }
130
+
131
+ async function handleModelCommand(rl, token, raw) {
132
+ const arg = raw.trim();
133
+ if (!arg) {
134
+ await runModelPicker(rl, token);
135
+ return;
136
+ }
137
+ await switchModelByName(token, arg);
138
+ }
139
+
140
+ function printHelp() {
141
+ process.stdout.write([...slashHelpLines(), ""].join("\n"));
142
+ }
143
+
144
+ function normalizeSlashInput(input) {
145
+ if (input === "/quit") return "/exit";
146
+ return input;
147
+ }
148
+
149
+ async function askApproval(rl, request) {
150
+ const tool = requestToolName(request);
151
+ const details = formatRequestDetails(request);
152
+ const answer = (await rl.question(
153
+ `\nApprove ${tool}${details ? ` (${details})` : ""}?\n(y/n) > `,
154
+ )).trim().toLowerCase();
155
+ return answer === "y" || answer === "yes";
156
+ }
157
+
158
+ async function askQuestion(rl, request) {
159
+ const options = (request.options ?? []).map((option, index) => `${index + 1}. ${option.label}`).join(" ");
160
+ return rl.question(`\n${request.prompt}\n${options ? `${options}\n` : ""}> `);
161
+ }
162
+
163
+ const turnRenderer = createTurnStreamRenderer();
164
+
165
+ function printTurnOutcome(result) {
166
+ if (result?.assistantTextStreamed) {
167
+ process.stdout.write("\n");
168
+ return;
169
+ }
170
+ const summary = summarizeResult(result);
171
+ if (summary && summary !== "(no reply)") {
172
+ process.stdout.write(`\n${summary}\n\n`);
173
+ } else {
174
+ process.stdout.write("\n");
175
+ }
176
+ }
177
+
113
178
  await ensureReachable();
114
179
 
115
180
  let session;
@@ -122,27 +187,58 @@ try {
122
187
  const { me } = session;
123
188
  const label = me.displayName ?? "you";
124
189
  const ownerId = me.ownerId;
190
+ const token = session.token;
125
191
 
126
- const client = new Client({
127
- host,
128
- auth: { bearer: () => loadToken(profile) ?? "" },
129
- preserveCompletedSessions: true,
192
+ let conversationId;
193
+ const rl = readline.createInterface({
194
+ input: stdin,
195
+ output: process.stdout,
196
+ historySize: 100,
130
197
  });
131
198
 
132
- try {
133
- await client.health();
134
- } catch (e) {
135
- fail(`Agent unreachable at ${host} (${e.message}).`);
199
+ let chain = Promise.resolve();
200
+ function runExclusive(fn) {
201
+ const next = chain.then(fn, fn);
202
+ chain = next.catch(() => {});
203
+ return next;
204
+ }
205
+
206
+ async function sendTurn(message) {
207
+ const result = await sendTransportTurn({
208
+ host,
209
+ token,
210
+ conversationId,
211
+ message,
212
+ render: turnRenderer,
213
+ handlers: {
214
+ askApproval: (request) => askApproval(rl, request),
215
+ askQuestion: (request) => askQuestion(rl, request),
216
+ },
217
+ });
218
+ conversationId = result?.conversationId ?? conversationId;
219
+ return result;
136
220
  }
137
221
 
138
222
  let wake;
139
223
  try {
140
- const token = loadToken(profile);
141
224
  wake = await subscribeInboxWakeAbly(ownerId, (ev) => {
142
225
  const what = ev.kind === "message" ? "a message" : `a file (${ev.filename})`;
143
- renderer.renderNotice(
144
- `📬 New mail — ${what} arrived. Ask me about it (e.g. "what's new?") to fetch it.`,
145
- );
226
+ process.stdout.write(`\nNew mail: ${what} arrived. Fetching it now...\n`);
227
+ void runExclusive(async () => {
228
+ const result = await sendWakeSyncTurn({
229
+ host,
230
+ token,
231
+ event: ev,
232
+ render: turnRenderer,
233
+ handlers: {
234
+ askApproval: (request) => askApproval(rl, request),
235
+ askQuestion: (request) => askQuestion(rl, request),
236
+ },
237
+ });
238
+ printTurnOutcome(result);
239
+ }).catch((e) => {
240
+ process.stderr.write(`! wake sync failed: ${e?.message ?? e}\n`);
241
+ });
146
242
  }, { host, bearerToken: token });
147
243
  } catch {
148
244
  /* wake is best-effort */
@@ -158,16 +254,58 @@ const stopWake = async () => {
158
254
  process.on("SIGINT", stopWake);
159
255
  process.on("exit", () => void wake?.close());
160
256
 
161
- await new EveTUIRunner({
162
- name: `neagen (${label})`,
163
- session: client.session(),
164
- client,
165
- serverUrl: host,
166
- renderer,
167
- promptCommandHandler: createPromptCommandHandler({ target }),
168
- availablePromptCommands: promptCommandsFor("remote"),
169
- formatTransportError: (error) =>
170
- isVercelAuthChallenge(error) ? formatRemoteAuthChallengeMessage(host) : toErrorMessage(error),
171
- }).run();
257
+ process.stdout.write(`neagen (${label})\n`);
258
+ process.stdout.write("Type a message, / for commands, /model to switch models, /help for the list.\n\n");
259
+
260
+ async function handleSlash(rl, input) {
261
+ const normalized = normalizeSlashInput(input);
262
+
263
+ if (normalized === "/exit") return true;
264
+ if (normalized === "/help") {
265
+ printHelp();
266
+ return false;
267
+ }
268
+ if (normalized === "/new") {
269
+ conversationId = undefined;
270
+ process.stdout.write("Started a new conversation.\n");
271
+ return false;
272
+ }
273
+ if (normalized === "/model" || normalized.startsWith("/model ")) {
274
+ await handleModelCommand(rl, token, normalized.slice("/model".length));
275
+ return false;
276
+ }
277
+
278
+ process.stdout.write(`${dim(`unknown command: ${input} (try /help)`)}\n`);
279
+ return false;
280
+ }
281
+
282
+ try {
283
+ for (;;) {
284
+ let line;
285
+ try {
286
+ line = await readPromptLine(rl, { prompt: "neagen> ", slashCommands: SLASH_COMMANDS });
287
+ } catch {
288
+ break;
289
+ }
290
+ if (line === null) break;
291
+
292
+ const input = line.trim();
293
+ if (!input) continue;
294
+
295
+ try {
296
+ if (input.startsWith("/")) {
297
+ if (await handleSlash(rl, input)) break;
298
+ continue;
299
+ }
300
+
301
+ const result = await runExclusive(() => sendTurn(line));
302
+ printTurnOutcome(result);
303
+ } catch (e) {
304
+ process.stderr.write(`! ${e?.message ?? e}\n`);
305
+ }
306
+ }
307
+ } finally {
308
+ rl.close();
309
+ }
172
310
 
173
311
  await stopWake();
@@ -0,0 +1,219 @@
1
+ import { clearLine, cursorTo } from "node:readline";
2
+ import { stdout } from "node:process";
3
+ import { dim } from "./ansi.mjs";
4
+
5
+ function sseEvent(event, data) {
6
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
7
+ }
8
+
9
+ export function parseSseChunk(buffer) {
10
+ const events = [];
11
+ const parts = buffer.split(/\n\n+/);
12
+ const trailing = parts.pop() ?? "";
13
+
14
+ for (const part of parts) {
15
+ const event = part.match(/^event: (.+)$/m)?.[1];
16
+ const dataLine = part.match(/^data: (.+)$/m)?.[1];
17
+ if (!event || !dataLine) continue;
18
+ try {
19
+ events.push({ event, data: JSON.parse(dataLine) });
20
+ } catch {
21
+ /* ignore malformed chunks */
22
+ }
23
+ }
24
+
25
+ return { events, trailing };
26
+ }
27
+
28
+ function readReasoningDelta(data) {
29
+ if (typeof data?.reasoningDelta === "string") return data.reasoningDelta;
30
+ if (typeof data?.delta === "string") return data.delta;
31
+ return "";
32
+ }
33
+
34
+ function readMessageDelta(data) {
35
+ if (typeof data?.messageDelta === "string") return data.messageDelta;
36
+ if (typeof data?.delta === "string") return data.delta;
37
+ return "";
38
+ }
39
+
40
+ export function createTurnStreamRenderer({ write = (text) => stdout.write(text) } = {}) {
41
+ let assistantLineOpen = false;
42
+ let reasoningLineOpen = false;
43
+ let assistantTextStreamed = false;
44
+
45
+ const closeAssistantLine = () => {
46
+ if (!assistantLineOpen) return;
47
+ write("\n");
48
+ assistantLineOpen = false;
49
+ };
50
+
51
+ const closeReasoningLine = () => {
52
+ if (!reasoningLineOpen) return;
53
+ write("\n");
54
+ reasoningLineOpen = false;
55
+ };
56
+
57
+ return {
58
+ get assistantTextStreamed() {
59
+ return assistantTextStreamed;
60
+ },
61
+ reset() {
62
+ assistantTextStreamed = false;
63
+ closeAssistantLine();
64
+ closeReasoningLine();
65
+ },
66
+ handle(eventName, data) {
67
+ switch (eventName) {
68
+ case "turn.started":
69
+ closeAssistantLine();
70
+ closeReasoningLine();
71
+ break;
72
+ case "reasoning.appended": {
73
+ const delta = readReasoningDelta(data);
74
+ if (!delta) break;
75
+ if (!reasoningLineOpen) {
76
+ closeAssistantLine();
77
+ write(`${dim("… ")}`);
78
+ reasoningLineOpen = true;
79
+ }
80
+ write(dim(delta));
81
+ break;
82
+ }
83
+ case "reasoning.completed":
84
+ closeReasoningLine();
85
+ break;
86
+ case "message.appended": {
87
+ const delta = readMessageDelta(data);
88
+ if (!delta) break;
89
+ assistantTextStreamed = true;
90
+ closeReasoningLine();
91
+ if (!assistantLineOpen) {
92
+ write("\n");
93
+ assistantLineOpen = true;
94
+ }
95
+ write(delta);
96
+ break;
97
+ }
98
+ case "message.completed":
99
+ closeAssistantLine();
100
+ break;
101
+ case "actions.requested": {
102
+ closeAssistantLine();
103
+ closeReasoningLine();
104
+ const actions = Array.isArray(data?.actions) ? data.actions : [];
105
+ const labels = actions.map((action) => {
106
+ if (action?.kind === "tool-call" && typeof action.toolName === "string") {
107
+ return action.toolName;
108
+ }
109
+ return action?.kind ?? "action";
110
+ });
111
+ if (labels.length > 0) {
112
+ write(`${dim(`↳ ${labels.join(", ")}`)}\n`);
113
+ }
114
+ break;
115
+ }
116
+ case "action.result": {
117
+ const result = data?.result;
118
+ const name = result?.toolName ?? result?.kind ?? "tool";
119
+ const status = data?.status ?? "completed";
120
+ write(`${dim(` ${status === "completed" ? "✓" : "×"} ${name}`)}\n`);
121
+ break;
122
+ }
123
+ case "input.requested":
124
+ closeAssistantLine();
125
+ closeReasoningLine();
126
+ write(`${dim("… approval required")}\n`);
127
+ break;
128
+ case "neagen.waiting_saved":
129
+ closeAssistantLine();
130
+ closeReasoningLine();
131
+ break;
132
+ case "neagen.error":
133
+ closeAssistantLine();
134
+ closeReasoningLine();
135
+ write(`\n! ${data?.message ?? "Turn failed."}\n`);
136
+ break;
137
+ default:
138
+ break;
139
+ }
140
+ },
141
+ finish() {
142
+ closeAssistantLine();
143
+ closeReasoningLine();
144
+ },
145
+ };
146
+ }
147
+
148
+ export function createTurnStatusSpinner(label = "thinking") {
149
+ if (!stdout.isTTY) return { stop() {} };
150
+
151
+ let frame = 0;
152
+ let active = true;
153
+ const frames = ["", ".", "..", "..."];
154
+ const render = () => {
155
+ if (!active) return;
156
+ cursorTo(stdout, 0);
157
+ stdout.write(`${label}${frames[frame % frames.length]}`);
158
+ clearLine(stdout, 1);
159
+ frame += 1;
160
+ };
161
+
162
+ render();
163
+ const timer = setInterval(render, 450);
164
+ return {
165
+ stop() {
166
+ if (!active) return;
167
+ active = false;
168
+ clearInterval(timer);
169
+ cursorTo(stdout, 0);
170
+ clearLine(stdout, 0);
171
+ },
172
+ };
173
+ }
174
+
175
+ export async function readTurnSseStream(response, onEvent) {
176
+ if (!response.ok) {
177
+ let message = `HTTP ${response.status}`;
178
+ const text = await response.text();
179
+ try {
180
+ message = JSON.parse(text)?.error?.message ?? message;
181
+ } catch {
182
+ if (text.trim()) message = text.trim();
183
+ }
184
+ throw new Error(message);
185
+ }
186
+
187
+ const reader = response.body?.getReader();
188
+ if (!reader) {
189
+ throw new Error("Turn stream is not readable.");
190
+ }
191
+
192
+ const decoder = new TextDecoder();
193
+ let buffer = "";
194
+ const collected = [];
195
+
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done) break;
199
+ buffer += decoder.decode(value, { stream: true });
200
+ const parsed = parseSseChunk(buffer);
201
+ buffer = parsed.trailing;
202
+ for (const entry of parsed.events) {
203
+ collected.push(entry);
204
+ await onEvent?.(entry.event, entry.data);
205
+ }
206
+ }
207
+
208
+ if (buffer.trim()) {
209
+ const parsed = parseSseChunk(`${buffer}\n\n`);
210
+ for (const entry of parsed.events) {
211
+ collected.push(entry);
212
+ await onEvent?.(entry.event, entry.data);
213
+ }
214
+ }
215
+
216
+ return collected;
217
+ }
218
+
219
+ export { sseEvent };
package/neagen.mjs CHANGED
@@ -62,8 +62,8 @@ async function runTui(profile) {
62
62
  );
63
63
  process.exit(1);
64
64
  }
65
- if (profile) process.env.NEAGEN_PROFILE = profile;
66
- await import("./lib/tui.mjs");
65
+ const { runProductTui } = await import("./lib/eve-product-tui.mjs");
66
+ await runProductTui({ profile });
67
67
  }
68
68
 
69
69
  async function runLogin(profile) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neagen",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Your personal networking agent, in the terminal. Chat, agent-mail, and files against your Neagen — passkey login, no passwords.",
5
5
  "type": "module",
6
6
  "bin": {