maqcli 0.8.0 → 0.9.1

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.
@@ -67,6 +67,16 @@ export interface OnboardingResult {
67
67
  * knowledge files, so it is directly testable.
68
68
  */
69
69
  export declare function applyOnboarding(choices: OnboardingChoices): OnboardingResult;
70
+ /** Render a 9-digit key large and spaced, boxed for terminal visibility (spec: "large terminal display"). */
71
+ export declare function renderKeyBox(key: string): string;
72
+ /**
73
+ * Dependency-free arrow-key-navigable menu (spec Phase 1: "arrow-key
74
+ * navigable, styled selection highlight" — no inquirer/blessed). Renders each
75
+ * option, redraws the highlighted one on ↑/↓, and resolves its index on
76
+ * Enter. Falls back to nothing here — callers that need non-TTY behavior
77
+ * should check `process.stdin.isTTY` first, same as the rest of the launcher.
78
+ */
79
+ export declare function renderMenu(options: string[], selected: number): string;
70
80
  /**
71
81
  * Run the guided launcher. Returns 0 on success. In a non-interactive context
72
82
  * (piped stdin), it prints guidance and returns without blocking.
@@ -163,6 +163,68 @@ export function applyOnboarding(choices) {
163
163
  return { config: cfg, knowledgePath, headroom, authKey: generateAuthKey() };
164
164
  }
165
165
  /* ----------------------------- interactive ----------------------------- */
166
+ /** Render a 9-digit key large and spaced, boxed for terminal visibility (spec: "large terminal display"). */
167
+ export function renderKeyBox(key) {
168
+ const spaced = key.split("").join(" ");
169
+ const width = spaced.length + 4;
170
+ const bar = "─".repeat(width);
171
+ return [`┌${bar}┐`, `│ ${spaced} │`, `└${bar}┘`].join("\n");
172
+ }
173
+ /**
174
+ * Dependency-free arrow-key-navigable menu (spec Phase 1: "arrow-key
175
+ * navigable, styled selection highlight" — no inquirer/blessed). Renders each
176
+ * option, redraws the highlighted one on ↑/↓, and resolves its index on
177
+ * Enter. Falls back to nothing here — callers that need non-TTY behavior
178
+ * should check `process.stdin.isTTY` first, same as the rest of the launcher.
179
+ */
180
+ export function renderMenu(options, selected) {
181
+ return options
182
+ .map((opt, i) => (i === selected ? ` \x1b[38;5;196m❱ ${opt}\x1b[0m` : ` ${opt}`))
183
+ .join("\n");
184
+ }
185
+ async function arrowMenu(options) {
186
+ let selected = 0;
187
+ const render = () => {
188
+ process.stdout.write(renderMenu(options, selected) + "\n");
189
+ };
190
+ render();
191
+ return new Promise((resolve) => {
192
+ const stdin = process.stdin;
193
+ const wasRaw = stdin.isRaw;
194
+ stdin.setRawMode?.(true);
195
+ stdin.resume();
196
+ stdin.setEncoding("utf8");
197
+ const redraw = () => {
198
+ process.stdout.write(`\x1b[${options.length}A`); // move cursor back up
199
+ render();
200
+ };
201
+ const onData = (chunk) => {
202
+ if (chunk === "\u0003") {
203
+ // Ctrl-C
204
+ cleanup();
205
+ process.exit(130);
206
+ }
207
+ if (chunk === "\r" || chunk === "\n") {
208
+ cleanup();
209
+ resolve(selected);
210
+ return;
211
+ }
212
+ if (chunk === "\u001b[A" || chunk === "k") {
213
+ selected = (selected - 1 + options.length) % options.length;
214
+ redraw();
215
+ }
216
+ else if (chunk === "\u001b[B" || chunk === "j") {
217
+ selected = (selected + 1) % options.length;
218
+ redraw();
219
+ }
220
+ };
221
+ const cleanup = () => {
222
+ stdin.off("data", onData);
223
+ stdin.setRawMode?.(Boolean(wasRaw));
224
+ };
225
+ stdin.on("data", onData);
226
+ });
227
+ }
166
228
  function ask(rl, q) {
167
229
  return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
168
230
  }
@@ -191,14 +253,22 @@ export async function runLauncher(cwd) {
191
253
  }
192
254
  }
193
255
  async function drive(rl, cwd) {
194
- line("How do you want to start?");
195
- line(" [1] Connect to Mobile — pair your phone as a control surface");
196
- line(" [2] AI Mode — set up the intelligence layer");
197
- line(" [0] Exit to command help");
198
- const path = (await ask(rl, "\n> ")) || "2";
199
- if (path === "0")
256
+ line("How do you want to start? (↑/↓ then Enter, or type a number)");
257
+ const options = ["Connect to Mobile — pair your phone as a control surface", "AI Mode — set up the intelligence layer", "Exit to command help"];
258
+ let idx;
259
+ if (typeof process.stdin.setRawMode === "function") {
260
+ idx = await arrowMenu(options);
261
+ }
262
+ else {
263
+ line(" [1] " + options[0]);
264
+ line(" [2] " + options[1]);
265
+ line(" [0] " + options[2]);
266
+ const path = (await ask(rl, "\n> ")) || "2";
267
+ idx = path === "0" ? 2 : path === "1" ? 0 : 1;
268
+ }
269
+ if (idx === 2)
200
270
  return 1;
201
- if (path === "1")
271
+ if (idx === 0)
202
272
  return await connectMobile(rl);
203
273
  return await aiMode(rl, cwd);
204
274
  }
@@ -256,8 +326,10 @@ async function aiMode(rl, cwd) {
256
326
  line(` Headroom model : ${result.headroom?.model ?? "heuristic-local"} (${headroomAuto ? "auto" : "manual"})`);
257
327
  line(` Permissions : ${permissionMode}`);
258
328
  line(` Knowledge doc : ${result.knowledgePath}`);
259
- line(` Auth key : ${result.authKey}`);
260
329
  line("─────────────────────────────────────────────");
330
+ line("");
331
+ line(renderKeyBox(result.authKey));
332
+ line(" ↑ session auth key — auto-passed to the browser, or type it if asked");
261
333
  const open = (await ask(rl, "\nLaunch the browser UI now? [Y/n] ")).toLowerCase();
262
334
  if (open === "" || open === "y" || open === "yes") {
263
335
  return await launchUi(result.authKey);
@@ -343,7 +415,8 @@ async function connectMobile(rl) {
343
415
  line(" 1. On this machine, MAQ will run its daemon (loopback + a tunnel/tailnet for remote).");
344
416
  line(" 2. In the MAQ phone app, pair using the details below.");
345
417
  line("");
346
- line(` Auth key (pairing PIN): ${authKey}`);
418
+ line(renderKeyBox(authKey));
419
+ line(" ↑ pairing PIN — copy this into the app");
347
420
  line("");
348
421
  line("Starting the daemon now with: maq serve");
349
422
  line(" (the app pairs with host/port + the token the daemon prints)");
@@ -353,11 +426,14 @@ async function connectMobile(rl) {
353
426
  async function launchUi(authKey) {
354
427
  // Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
355
428
  const { createDaemon } = await import("../server/daemon.js");
356
- const daemon = createDaemon({ token: authKey, version: "0.8.0" });
429
+ const daemon = createDaemon({ token: authKey, version: "0.9.1" });
357
430
  try {
358
431
  const { host, port } = await daemon.listen();
359
- const url = `http://${host}:${port}/`;
360
- line(`\nmaq daemon listening on ${url}`);
432
+ // Seamless handoff (spec Phase 3): the key rides in the URL so the
433
+ // browser UI never has to prompt for it. The page reads ?key= once and
434
+ // keeps it in sessionStorage only — it is not persisted or logged again.
435
+ const url = `http://${host}:${port}/?key=${authKey}`;
436
+ line(`\nmaq daemon listening on http://${host}:${port}`);
361
437
  line(`auth key/token: ${authKey}`);
362
438
  openBrowser(url);
363
439
  line("Opened your browser. Press Ctrl-C here to stop the daemon.");
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ import { runOrchestration } from "./core/orchestrator.js";
46
46
  import { performUpdate, installedGlobalVersion } from "./core/update.js";
47
47
  import { checkProtectedPath, scanForInjection, securityLog, securityRules, } from "./core/security.js";
48
48
  import { readFileSync, statSync } from "node:fs";
49
- const VERSION = "0.8.0";
49
+ const VERSION = "0.9.1";
50
50
  async function main(argv) {
51
51
  const [command, ...rest] = argv;
52
52
  switch (command) {
@@ -52,7 +52,7 @@ export function createDaemon(opts = {}) {
52
52
  const host = opts.host ?? process.env.MAQ_HOST ?? "127.0.0.1";
53
53
  const port = opts.port ?? Number(process.env.MAQ_PORT ?? 7717);
54
54
  const token = opts.token ?? process.env.MAQ_TOKEN ?? generateToken();
55
- const version = opts.version ?? "0.8.0";
55
+ const version = opts.version ?? "0.9.1";
56
56
  const corsOrigin = opts.corsOrigin ?? process.env.MAQ_CORS_ORIGIN;
57
57
  const registry = opts.registry ?? new SessionRegistry();
58
58
  const interactive = new InteractiveRegistry();
@@ -1,26 +1,28 @@
1
1
  /**
2
- * webui.ts — the self-contained control surface the daemon serves at `/app`
3
- * (and `/`). Zero build step, zero dependencies: one HTML document with
4
- * inline CSS + vanilla JS. It renders the SAME normalized event stream the
2
+ * webui.ts — the self-contained "Megalodon" control surface the daemon serves
3
+ * at `/app` (and `/`). Zero build step, zero dependencies: one HTML document
4
+ * with inline CSS + vanilla JS. It renders the SAME normalized event stream the
5
5
  * mobile app consumes, so there is no second source of truth.
6
6
  *
7
- * Design system: dark OLED canvas, two elevation surfaces, one signal accent
8
- * (teal "alive/running", not the generic AI purple/blue gradient). Type:
9
- * Space Grotesk for the brand mark + section labels (used sparingly), Inter
10
- * for UI/body, JetBrains Mono for the console/data surfaces. Signature
11
- * element: a live depth-of-work TIMELINE phases render as connected nodes
12
- * that fill in as work progresses, with orchestration rounds (parallel/loop/
13
- * safe) nesting as a sub-rail beneath the active phase — replacing static
14
- * chips with something that actually shows depth of work over time.
7
+ * This is a from-spec rebuild (maq_cli_ui_spec Phases 4-6). The recommended
8
+ * SPA stack (React + Monaco + Framer + react-resizable-panels) is deliberately
9
+ * NOT used: the daemon ships one static asset with no build pipeline, so the
10
+ * same UX is delivered dependency-free
11
+ * - real drag-resizable 3-panel shell (custom pointer-drag splitters)
12
+ * - universal preview: JSON tree / code+line-numbers / markdown / sandboxed
13
+ * HTML / images (replaces Monaco + react-markdown + react-json-view)
14
+ * - send button with a toggle-up mode picker popover (Single/Parallel/Loop/
15
+ * Safe), mode icon on the button face, selection persisted
16
+ * - orchestration visualizations derived from the event stream (parallel
17
+ * round dashboard / loop progress / safe split→merge→validate rail)
18
+ * - custom animated cursor follower + micro-interactions (CSS, not Framer)
15
19
  *
16
- * Layout: a named-grid-area app shell. Sidebar collapses to a drawer under
17
- * 900px; the preview panel becomes a full-screen sheet under 1200px; the
18
- * composer sticks to the bottom on narrow viewports. Motion is 150/220ms
19
- * ease-out and fully disabled under prefers-reduced-motion.
20
+ * Theme (Megalodon): deep matte black base, controlled blue-white accents (not
21
+ * neon), sparse red for status/danger, white primary text, no gradients except
22
+ * intentional depth cues. Everything references CSS custom-property tokens.
20
23
  *
21
24
  * Auth: the daemon requires a Bearer token for /v1/*. The page reads it from
22
- * ?token=… or a prompt, keeps it in memory only, and streams SSE via fetch +
23
- * ReadableStream (so the token rides in the Authorization header, never the
24
- * URL).
25
+ * ?key= / ?token= or a prompt, keeps it in memory only, and streams SSE via
26
+ * fetch + ReadableStream (token in the Authorization header, never the URL).
25
27
  */
26
28
  export declare function webuiHtml(version: string): string;