wiki-viewer 1.0.0 → 1.0.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.
Files changed (128) hide show
  1. package/.next/standalone/README.md +328 -99
  2. package/.next/standalone/agents/bootstrap-prompt.md +1 -0
  3. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
  4. package/.next/standalone/bin/wiki-viewer.js +57 -4
  5. package/.next/standalone/docs/agent-collab-plan.md +1615 -0
  6. package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
  7. package/.next/standalone/package.json +19 -2
  8. package/.next/standalone/pnpm-lock.yaml +1368 -325
  9. package/.next/standalone/pnpm-workspace.yaml +7 -1
  10. package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
  11. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
  12. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
  13. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
  14. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
  15. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
  16. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
  17. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
  18. package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
  19. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
  20. package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
  21. package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
  22. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
  23. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
  24. package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
  25. package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
  26. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
  27. package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
  28. package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
  29. package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
  30. package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
  31. package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
  32. package/.next/standalone/src/app/api/system/config/route.ts +5 -1
  33. package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
  34. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
  35. package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
  36. package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
  37. package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
  38. package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
  39. package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
  40. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
  41. package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
  42. package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
  43. package/.next/standalone/src/app/api/wiki/route.ts +10 -0
  44. package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
  45. package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
  46. package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
  47. package/.next/standalone/src/app/globals.css +70 -0
  48. package/.next/standalone/src/app/page.tsx +461 -217
  49. package/.next/standalone/src/app/signin/page.tsx +217 -0
  50. package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
  51. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
  52. package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
  53. package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
  54. package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
  55. package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
  56. package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
  57. package/.next/standalone/src/components/editor/editor.tsx +513 -10
  58. package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
  59. package/.next/standalone/src/components/editor/extensions.ts +2 -0
  60. package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
  61. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
  62. package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
  63. package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
  64. package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
  65. package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
  66. package/.next/standalone/src/lib/auth/client.ts +4 -0
  67. package/.next/standalone/src/lib/auth/csrf.ts +68 -0
  68. package/.next/standalone/src/lib/auth/server.ts +164 -0
  69. package/.next/standalone/src/lib/config.ts +4 -0
  70. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
  71. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
  72. package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
  73. package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
  74. package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
  75. package/.next/standalone/src/lib/proof/activity.ts +74 -0
  76. package/.next/standalone/src/lib/proof/auth.ts +132 -0
  77. package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
  78. package/.next/standalone/src/lib/proof/blocks.ts +73 -0
  79. package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
  80. package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
  81. package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
  82. package/.next/standalone/src/lib/proof/glob.ts +51 -0
  83. package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
  84. package/.next/standalone/src/lib/proof/mutex.ts +32 -0
  85. package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
  86. package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
  87. package/.next/standalone/src/lib/proof/pending.ts +97 -0
  88. package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
  89. package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
  90. package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
  91. package/.next/standalone/src/lib/proof/registry.ts +190 -0
  92. package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
  93. package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
  94. package/.next/standalone/src/lib/proof/types.ts +170 -0
  95. package/.next/standalone/src/lib/proof-config.ts +14 -0
  96. package/.next/standalone/src/middleware.ts +38 -0
  97. package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
  98. package/.next/standalone/src/stores/editor-store.ts +115 -9
  99. package/.next/standalone/src/stores/proof-store.ts +123 -0
  100. package/.next/standalone/src/stores/view-width-store.ts +62 -0
  101. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
  102. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
  103. package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
  104. package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
  105. package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
  106. package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
  107. package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
  108. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
  109. package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
  110. package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
  111. package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
  112. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
  113. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
  114. package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
  115. package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
  116. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
  117. package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
  118. package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
  119. package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
  120. package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
  121. package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
  122. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
  123. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
  124. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
  125. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
  126. package/README.md +328 -99
  127. package/bin/wiki-viewer.js +57 -4
  128. package/package.json +19 -2
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: wiki-viewer
3
+ description: Read and edit markdown files in a running wiki-viewer instance over its HTTP collab API. Use when the user mentions wiki-viewer, asks you to edit local notes, or shares a localhost wiki-viewer URL. Handles agent registration, scoped capabilities, block-level edits with provenance marks, comments, and suggestions.
4
+ license: MIT
5
+ ---
6
+
7
+ # wiki-viewer agent skill
8
+
9
+ You are working with **wiki-viewer**, a local-first markdown viewer that exposes a Proof-SDK-compatible HTTP API for agents. Files on disk are the source of truth. Every AI-authored edit is wrapped in an inline `<proof-span>` mark so the human can see, accept, or revert your contribution.
10
+
11
+ ## Discovery
12
+
13
+ The server publishes everything you need at one URL:
14
+
15
+ ```
16
+ GET http://<host>:<port>/api/agents/install
17
+ ```
18
+
19
+ This returns JSON with: bootstrap instructions, route table, op vocabulary, the current registration endpoint, and the human-facing approval workflow. Fetch this first and treat it as authoritative for the running instance you're talking to.
20
+
21
+ If the user pasted only the wiki-viewer URL, append `/api/agents/install` and fetch it before doing anything else.
22
+
23
+ ## Authentication
24
+
25
+ wiki-viewer uses **TOFU (Trust On First Use)**:
26
+
27
+ 1. You register anonymously. Server stores a pending request.
28
+ 2. The human approves you in the wiki-viewer AI Panel.
29
+ 3. You poll, receive a one-shot token, and use it from then on.
30
+
31
+ ### Register
32
+
33
+ ```
34
+ POST /api/agent/register
35
+ Content-Type: application/json
36
+
37
+ {
38
+ "id": "ai:<your-name>",
39
+ "displayName": "<readable name>",
40
+ "scope": {
41
+ "paths": ["**/*"],
42
+ "ops": ["read", "mutate"]
43
+ }
44
+ }
45
+ ```
46
+
47
+ - `id` must match `^ai:[a-z][a-z0-9-]{0,30}$`. Pick something stable per agent identity (e.g. `ai:claude`, `ai:cursor`, `ai:my-script`).
48
+ - `displayName` 1-80 chars. What the human sees in the approval UI.
49
+ - `scope.paths` is a glob list. Use `**/*` for full repo access, or restrict to a subtree like `notes/**`.
50
+ - `scope.ops` ⊆ `["read", "mutate"]`.
51
+
52
+ Response:
53
+
54
+ ```json
55
+ {
56
+ "registrationId": "reg_<32hex>",
57
+ "pollUrl": "/api/agent/register/<regId>",
58
+ "status": "pending"
59
+ }
60
+ ```
61
+
62
+ **Tell the human**: "Open the wiki-viewer AI Panel and approve my registration."
63
+
64
+ ### Poll
65
+
66
+ ```
67
+ GET /api/agent/register/<registrationId>
68
+ ```
69
+
70
+ - `202 {status:"pending"}` — keep polling, every 2-5s.
71
+ - `200 {status:"approved", agentId, token}` — capture token immediately. Pickup is **one-shot**; you cannot fetch it again.
72
+ - `410 {status:"denied"}` or `{status:"consumed"}` — abort or restart registration.
73
+ - `404` — registration expired, re-register.
74
+
75
+ ### Use the token
76
+
77
+ Every subsequent request:
78
+
79
+ ```
80
+ Authorization: Bearer <token>
81
+ X-Agent-Id: ai:<your-name>
82
+ ```
83
+
84
+ The `X-Agent-Id` must match the id the token was issued for. Spoofing rejected with `401`.
85
+
86
+ ## Reading
87
+
88
+ ```
89
+ GET /api/agent/files/<url-encoded-path>.md
90
+ ```
91
+
92
+ Returns a Snapshot:
93
+
94
+ ```json
95
+ {
96
+ "path": "notes/plan.md",
97
+ "revision": 7,
98
+ "fingerprint": "sha256:...",
99
+ "blocks": [
100
+ { "ref": "b1a3f0", "type": "heading", "level": 1, "markdown": "# Plan" },
101
+ { "ref": "b7f2c1", "type": "paragraph", "markdown": "Ship in June." }
102
+ ],
103
+ "comments": [...],
104
+ "suggestions": [...],
105
+ "lastEventId": 12
106
+ }
107
+ ```
108
+
109
+ Block `ref` strings are stable across edits. Use them to target mutations. **Always read a fresh snapshot before mutating** so you have the current `revision`.
110
+
111
+ ## Mutating
112
+
113
+ ```
114
+ POST /api/agent/files/<path>.md
115
+ Authorization: Bearer <token>
116
+ X-Agent-Id: ai:<your-name>
117
+ Content-Type: application/json
118
+ Idempotency-Key: <uuid you generate per request>
119
+
120
+ {
121
+ "baseRevision": <revision from snapshot>,
122
+ "by": "ai:<your-name>",
123
+ "ops": [ <op>, ... ]
124
+ }
125
+ ```
126
+
127
+ Requirements:
128
+
129
+ - `Idempotency-Key` header mandatory. Same key + same body within 5 minutes returns the cached response. Same key + different body returns `409 IDEMPOTENCY_KEY_REUSED`.
130
+ - `by` must equal your `X-Agent-Id` or you get `403 FORBIDDEN`.
131
+ - If `baseRevision` is stale, response is `409 STALE_REVISION` with a fresh snapshot. Read the new revision, rebuild your op against current refs, retry.
132
+
133
+ ### Op vocabulary
134
+
135
+ **Block ops** — content edits:
136
+
137
+ ```json
138
+ { "type": "block.replace", "ref": "b7f2c1", "markdown": "New content." }
139
+ { "type": "block.insertAfter", "ref": "b7f2c1", "markdown": "..." }
140
+ { "type": "block.insertBefore", "ref": "b7f2c1", "markdown": "..." }
141
+ { "type": "block.delete", "ref": "b7f2c1" }
142
+ { "type": "block.append", "markdown": "..." }
143
+ { "type": "block.prepend", "markdown": "..." }
144
+ ```
145
+
146
+ Any text you insert is automatically wrapped in a `<proof-span>` mark by the server. Provide optional metadata to make your contribution legible:
147
+
148
+ ```json
149
+ {
150
+ "type": "block.insertAfter",
151
+ "ref": "b7f2c1",
152
+ "markdown": "Three pillars: infra, tooling, launch.",
153
+ "basis": "described",
154
+ "basisDetail": "user asked for an opening paragraph",
155
+ "inResponseTo": "c4a1"
156
+ }
157
+ ```
158
+
159
+ `basis` ∈ `"described" | "inferred" | "suggested"`. Defaults to `"inferred"`. Always set `basis` and a short `basisDetail` so the human reviewer knows where your edit came from.
160
+
161
+ **Comment ops** — threaded discussion attached to a block:
162
+
163
+ ```json
164
+ { "type": "comment.add", "ref": "b7f2c1", "text": "Why end of June?" }
165
+ { "type": "comment.reply", "commentId": "c4a1", "text": "Because of API freeze." }
166
+ { "type": "comment.resolve", "commentId": "c4a1" }
167
+ { "type": "comment.reopen", "commentId": "c4a1" }
168
+ ```
169
+
170
+ **Suggestion ops** — proposed edits the human must accept:
171
+
172
+ ```json
173
+ {
174
+ "type": "suggestion.add",
175
+ "ref": "b7f2c1",
176
+ "kind": "replace",
177
+ "markdown": "Ship the rewrite by July 15.",
178
+ "basis": "described",
179
+ "basisDetail": "user mentioned slippage in chat"
180
+ }
181
+ ```
182
+
183
+ Suggestion kinds: `"replace" | "insertAfter" | "insertBefore" | "delete"`.
184
+
185
+ Default for AI-initiated content edits: **prefer suggestions over direct block ops** unless the human explicitly asked you to write directly. Suggestions render as inline cards in the editor with Accept / Reject buttons. Block ops apply immediately and only show up as proof-span decorations.
186
+
187
+ ## Polling events
188
+
189
+ ```
190
+ GET /api/agent/events/<path>.md?after=<lastEventId>
191
+ ```
192
+
193
+ Returns events emitted since `lastEventId`: human comments, accepted suggestions, external file edits (the human opened the file in vim), and so on. Use this to react when the human responds to one of your comments or suggestions.
194
+
195
+ Acknowledge:
196
+
197
+ ```
198
+ POST /api/agent/events/<path>.md
199
+ { "upToId": <id>, "by": "ai:<your-name>" }
200
+ ```
201
+
202
+ Acks are advisory; events are never deleted.
203
+
204
+ ## Error codes
205
+
206
+ | Status | Code | Meaning |
207
+ | ------ | ---------------------- | -------------------------------------------------- |
208
+ | 401 | UNAUTHORIZED | bad/missing token or X-Agent-Id |
209
+ | 403 | FORBIDDEN | scope mismatch, or `by` doesn't match `X-Agent-Id` |
210
+ | 404 | FILE_NOT_FOUND | path doesn't exist under server root |
211
+ | 409 | STALE_REVISION | `baseRevision` wrong, retry with included snapshot |
212
+ | 409 | BLOCK_NOT_FOUND | ref no longer exists, refetch snapshot |
213
+ | 409 | IDEMPOTENCY_KEY_REUSED | same key, different body |
214
+ | 422 | INVALID_MARKDOWN | op's markdown failed to parse |
215
+ | 429 | RATE_LIMITED | bucket exhausted, honor `Retry-After` header |
216
+
217
+ ## Working style
218
+
219
+ - Read before write. Always GET a fresh snapshot to capture current `revision` and block `ref`s.
220
+ - One file per request. There is no batch-across-files endpoint.
221
+ - Atomic ops. Each POST applies all its ops or none. Order matters within a batch.
222
+ - Be transparent. Set `basis` + `basisDetail` on every content op.
223
+ - Prefer comments and suggestions over silent edits. The human is your collaborator, not your reviewer-of-last-resort.
224
+ - Poll events between turns when the human is reviewing your work; respond to their comments rather than re-litigating.
225
+
226
+ ## Sample first interaction
227
+
228
+ ```
229
+ 1. GET /api/agents/install # discovery
230
+ 2. POST /api/agent/register # register
231
+ 3. tell human: "approve me in the AI Panel"
232
+ 4. GET /api/agent/register/<regId> # poll until approved
233
+ 5. GET /api/agent/files/<path>.md # read
234
+ 6. POST /api/agent/files/<path>.md # suggest or mutate
235
+ 7. GET /api/agent/events/<path>.md?after=<id> # listen for replies
236
+ ```
@@ -45,10 +45,11 @@ const rootDir = args.find((a) => !a.startsWith("-"));
45
45
  let port = process.env.PORT ?? "3000";
46
46
  let host = process.env.HOSTNAME ?? "localhost";
47
47
  let useHttps = false;
48
+ let userSpecifiedPort = false;
48
49
 
49
50
  for (let i = 0; i < args.length; i++) {
50
51
  const a = args[i];
51
- if (a === "-p" || a === "--port") port = args[++i] ?? port;
52
+ if (a === "-p" || a === "--port") { port = args[++i] ?? port; userSpecifiedPort = true; }
52
53
  else if (a === "-H" || a === "--host") host = args[++i] ?? host;
53
54
  else if (a === "--https") useHttps = true;
54
55
  }
@@ -85,6 +86,23 @@ function ensureCerts() {
85
86
  return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
86
87
  }
87
88
 
89
+ // ── port availability helpers ──────────────────────────────────────────────
90
+
91
+ function isPortAvailable(p, h) {
92
+ return new Promise((resolve) => {
93
+ const s = createNetServer();
94
+ s.once("error", () => resolve(false));
95
+ s.once("listening", () => s.close(() => resolve(true)));
96
+ s.listen(Number(p), h);
97
+ });
98
+ }
99
+
100
+ async function findNextAvailablePort(startPort, h) {
101
+ let p = Number(startPort);
102
+ while (!(await isPortAvailable(p, h))) p++;
103
+ return String(p);
104
+ }
105
+
88
106
  // ── free port helper ───────────────────────────────────────────────────────
89
107
 
90
108
  function freePort() {
@@ -97,15 +115,36 @@ function freePort() {
97
115
  });
98
116
  }
99
117
 
118
+ // ── network address helper ─────────────────────────────────────────────────
119
+
120
+ function getNetworkAddress() {
121
+ for (const ifaces of Object.values(os.networkInterfaces())) {
122
+ for (const iface of ifaces ?? []) {
123
+ if (iface.family === "IPv4" && !iface.internal) return iface.address;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+
100
129
  // ── start ──────────────────────────────────────────────────────────────────
101
130
 
102
131
  async function start() {
103
132
  if (resolvedRoot) {
104
- console.log(`📂 ${resolvedRoot}`);
133
+ console.log(`📁 ${resolvedRoot}`);
105
134
  } else {
106
135
  console.log("📂 No directory specified — open the browser to choose one");
107
136
  }
108
137
 
138
+ // Auto-select next free port when user didn't specify one
139
+ if (!userSpecifiedPort) {
140
+ const available = await isPortAvailable(Number(port), host);
141
+ if (!available) {
142
+ const original = port;
143
+ port = await findNextAvailablePort(Number(port) + 1, host);
144
+ console.log(`⚠️ Port ${original} in use → using ${port} (pass -p <port> to override)`);
145
+ }
146
+ }
147
+
109
148
  // When HTTPS is requested, run the standalone server on a random internal
110
149
  // HTTP port and stand up an HTTPS reverse-proxy on the user-facing port.
111
150
  const internalPort = useHttps ? String(await freePort()) : port;
@@ -147,11 +186,25 @@ async function start() {
147
186
  // Wait a moment for the standalone server to bind before starting proxy
148
187
  setTimeout(() => {
149
188
  proxy.listen(Number(port), host, () => {
150
- console.log(`🌐 https://${host}:${port}`);
189
+ const scheme = "https";
190
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
191
+ console.log(`\n ➜ Local: ${scheme}://${displayHost}:${port}`);
192
+ const netAddr = getNetworkAddress();
193
+ if (netAddr && host !== "localhost" && host !== "127.0.0.1") {
194
+ console.log(` ➜ Network: ${scheme}://${netAddr}:${port}`);
195
+ }
196
+ console.log(`\n Listening on ${host}:${port} (--host / -H, --port / -p to rebind)\n`);
151
197
  });
152
198
  }, 1_000);
153
199
  } else {
154
- console.log(`🌐 http://${host}:${port}`);
200
+ const scheme = "http";
201
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
202
+ console.log(`\n ➜ Local: ${scheme}://${displayHost}:${port}`);
203
+ const netAddr = getNetworkAddress();
204
+ if (netAddr && host !== "localhost" && host !== "127.0.0.1") {
205
+ console.log(` ➜ Network: ${scheme}://${netAddr}:${port}`);
206
+ }
207
+ console.log(`\n Listening on ${host}:${port} (--host / -H, --port / -p to rebind)\n`);
155
208
  }
156
209
  }
157
210