niahere 0.2.13 → 0.2.15

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.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: llms-txt
3
+ description: Expert guidance for creating, improving, and maintaining llms.txt/llms-full.txt for LLM-aware content indexing and AI retrieval.
4
+ argument-hint: "[path] [goal]"
5
+ license: MIT
6
+ metadata:
7
+ author: aman
8
+ version: "1.0.0"
9
+ ---
10
+
11
+ # llms.txt Skill
12
+
13
+ Use this skill when the user asks to create, review, improve, or scale `llms.txt`/`llms-full.txt` for a site, docs portal, or product.
14
+
15
+ ## Goals
16
+
17
+ - Explain what `llms.txt` is and when to use it.
18
+ - Help write high-signal, low-noise link collections for AI readers.
19
+ - Improve existing files with ranking/order, scope, freshness, and maintainability changes.
20
+ - Provide tooling and review checks to prevent low-quality output.
21
+
22
+ ## Runtime Scope
23
+
24
+ - Works for any static or dynamic site.
25
+ - Can be used alongside SEO artifacts (`robots.txt`, `sitemap.xml`, `robots` metadata); it is **not** a replacement.
26
+ - Use when the ask is about discoverability for AI systems, docs quality for retrieval, or reducing crawl noise for model context.
27
+
28
+ ## What `llms.txt` Is
29
+
30
+ - A curated, human-readable index intended for machine readers.
31
+ - A concise map of authoritative pages, organized by topic.
32
+ - A guidance file, not a permission file:
33
+ - It does not grant permission or block crawling.
34
+ - It signals what content is high value.
35
+
36
+ ## Who This Helps
37
+
38
+ - Content/site owners who want AI systems to prioritize reliable pages.
39
+ - Product teams building LLM-powered assistants/crawlers.
40
+ - Internal teams maintaining docs, knowledge bases, and API references.
41
+ - External integrators that consume public docs and need stable entry points.
42
+
43
+ ## Why It Helps
44
+
45
+ - Reduces ambiguity by exposing intent: what should be read first.
46
+ - Improves consistency of summaries and question-answering quality from your site.
47
+ - Helps new models/tools avoid outdated, low-value, and duplicate pages.
48
+ - Supports faster onboarding for AI agents and copilots that consume your site.
49
+
50
+ ## Core Rules
51
+
52
+ 1. Keep the file short and opinionated.
53
+ 2. Use a stable order: high-level entry first, then deeper pages.
54
+ 3. Group by purpose (`Overview`, `Getting Started`, `Core`, `API`, `Projects`, etc.).
55
+ 4. Use clear one-line descriptions for each link.
56
+ 5. Prefer canonical URLs and remove dead/redirected links.
57
+ 6. Mark lower-priority links as optional.
58
+ 7. Update with every meaningful content change.
59
+
60
+ ## Standard Authoring Pattern
61
+
62
+ ```
63
+ # Site or Product Name
64
+
65
+ > One-line description of what the site provides.
66
+
67
+ ## Overview
68
+ - [Home](https://example.com/) : What this website is and who it serves.
69
+ - [About](https://example.com/about) : Core context and mission.
70
+
71
+ ## Core documentation
72
+ - [Getting Started](https://example.com/docs/getting-started) : Setup and onboarding path.
73
+ - [Concepts](https://example.com/docs/concepts) : Key ideas and mental models.
74
+
75
+ ## Projects / Products
76
+ - [Project Index](https://example.com/projects) : Curated project list.
77
+ - [Featured Project](https://example.com/projects/featured-project) : High-priority example.
78
+
79
+ ## Optional
80
+ - [Blog](https://example.com/blog) : Current essays; useful but not required for basic understanding.
81
+ ```
82
+
83
+ ## Writing `llms.txt` (Step-by-step)
84
+
85
+ 1. Define audience and primary task (e.g., onboarding, evaluation, API usage, portfolio review).
86
+ 2. Select 10-30 high-signal URLs only.
87
+ 3. Add required top-level sections:
88
+ - `#` title
89
+ - `##` grouped headings
90
+ - bullet list links with short purpose text
91
+ 4. Order by usefulness for first-pass understanding.
92
+ 5. Mark noisy or secondary pages under `## Optional`.
93
+ 6. Validate all links and prune stale pages.
94
+ 7. Track version updates in repo changelog or notes.
95
+
96
+ ## Improve Existing `llms.txt`
97
+
98
+ - Remove dead pages and broken links.
99
+ - Consolidate repeated or overlapping pages.
100
+ - Move outdated material to `llms-full.txt` if too large for primary file.
101
+ - Keep first section reserved for decision-making pages.
102
+ - Add or refresh descriptions when APIs/features move.
103
+ - Add explicit entry for changelogs or release notes if they affect understanding.
104
+ - Re-rank links to surface most important pages first.
105
+
106
+ ## Validation Checklist
107
+
108
+ - File is plain text/markdown and accessible at `/llms.txt`.
109
+ - No marketing fluff; every bullet is actionable/identifying.
110
+ - Descriptions are factual and specific.
111
+ - URLs are absolute, canonical, and reachable.
112
+ - No duplicate links across sections.
113
+ - Total size is practical (start lean, grow with scale).
114
+
115
+ ## Next.js / Vercel Example
116
+
117
+ - Add file at `public/llms.txt`.
118
+ - Keep it in git with your content updates.
119
+ - Optional: generate periodically from a docs manifest if your site grows quickly.
120
+
121
+ ## Common Mistakes
122
+
123
+ - Treating it as SEO replacement.
124
+ - Adding a giant full list of all pages.
125
+ - Using vague descriptions like "click here".
126
+ - Outdated links after page moves.
127
+ - Mixing private/internal URLs with public consumption targets.
128
+
129
+ ## Additional Files
130
+
131
+ - Optional: `llms-full.txt` for exhaustive references only when needed.
132
+ - Optional: `llms-<area>.txt` variants for domain-specific sections.
133
+
134
+ ## Resources to Explore
135
+
136
+ - llms.txt proposal: https://llmstxt.org/
137
+ - llms.txt repository: https://github.com/AnswerDotAI/llms-txt
138
+ - llms.txt parser/usage notes: https://github.com/AnswerDotAI/llms-txt/blob/main/README.md
139
+ - AI content discoverability baseline (model context quality): https://www.sitemaps.org/protocol.html
140
+ - Crawling guidance reference: https://developers.google.com/search/docs/crawling-indexing/overview
141
+ - Robots standard: https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt
@@ -0,0 +1,187 @@
1
+ ---
2
+ name: pr-reviewer
3
+ description: Review pull requests and code diffs for correctness, design, security, performance, and style. Use when asked to "review this PR", "review my changes", "check this diff", or before merging code. Language-aware — adapts to the project's stack, idioms, and conventions.
4
+ ---
5
+
6
+ # PR Reviewer
7
+
8
+ Structured code review for pull requests and diffs. Adapts to the project's language, framework, and conventions by reading project documentation before reviewing code.
9
+
10
+ ## Quick Start
11
+
12
+ 1. Gather context (project docs, language, conventions).
13
+ 2. Read the full diff.
14
+ 3. Run the review passes below.
15
+ 4. Output structured findings.
16
+
17
+ ## Step 1: Gather Project Context
18
+
19
+ Before looking at any code, read these files if they exist:
20
+
21
+ - `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` — project rules, code style, conventions
22
+ - `README.md` — project purpose, architecture overview
23
+ - `.editorconfig`, linter configs (`.eslintrc`, `pyproject.toml`, `rustfmt.toml`, etc.)
24
+ - `CONTRIBUTING.md` — contribution guidelines
25
+
26
+ From these, extract:
27
+ - **Language & framework** (TypeScript/Bun, Python/Django, Rust, Go, etc.)
28
+ - **Code style rules** (naming, imports, formatting)
29
+ - **Testing expectations** (unit, integration, what framework)
30
+ - **Architecture patterns** (module layout, type organization)
31
+ - **Any explicit review criteria** the project defines
32
+
33
+ ## Step 2: Get the Diff
34
+
35
+ ```bash
36
+ # For uncommitted changes
37
+ git diff
38
+
39
+ # For staged changes
40
+ git diff --cached
41
+
42
+ # For a branch vs main
43
+ git diff main...HEAD
44
+
45
+ # For a specific PR (GitHub)
46
+ gh pr diff <number>
47
+ ```
48
+
49
+ Read all changed files in full (not just the diff) when context is needed to understand the change.
50
+
51
+ ## Step 3: Review Passes
52
+
53
+ Run these passes in order. Each pass focuses on one concern — don't mix them.
54
+
55
+ ### Pass 1: Intent & Design
56
+
57
+ - Does the change do what it claims? (PR title/description vs actual diff)
58
+ - Is the approach right? Could this be simpler?
59
+ - Is this the right place for this code? (module boundaries, separation of concerns)
60
+ - Over-engineering check: is code more generic than needed? Solving future problems?
61
+ - Are there unnecessary changes? (unrelated refactors, formatting-only changes mixed in)
62
+
63
+ ### Pass 2: Correctness & Logic
64
+
65
+ - Off-by-one errors, boundary conditions, null/undefined handling
66
+ - Race conditions in async/concurrent code
67
+ - State mutations — are they safe? Expected?
68
+ - Error paths — what happens when things fail?
69
+ - Data flow — does data transform correctly through the pipeline?
70
+ - Are edge cases handled? (empty inputs, large inputs, unicode, timezone, etc.)
71
+
72
+ ### Pass 3: Language Idioms & Best Practices
73
+
74
+ Adapt to the project's language. Apply that language's conventions:
75
+
76
+ **TypeScript/JavaScript:**
77
+ - Proper typing (no unnecessary `any`, correct generics, discriminated unions)
78
+ - Async/await over raw promises, proper error propagation
79
+ - Immutability preferences, const over let
80
+ - Node.js/Bun API usage (streams, buffers, path handling)
81
+
82
+ **Python:**
83
+ - PEP 8, Pythonic idioms (comprehensions, context managers, generators)
84
+ - Type hints where the project uses them
85
+ - Proper exception hierarchy, avoid bare `except:`
86
+ - f-strings over `.format()` or `%`
87
+
88
+ **Go:**
89
+ - Error handling patterns (check errors, don't ignore)
90
+ - Naming conventions (exported vs unexported, receiver names)
91
+ - Goroutine safety, channel usage, context propagation
92
+ - Avoid unnecessary interfaces
93
+
94
+ **Rust:**
95
+ - Ownership and borrowing correctness
96
+ - Error handling (`Result`/`Option`, avoid `.unwrap()` in library code)
97
+ - Clippy-clean idioms, iterator chains over manual loops
98
+ - Lifetime annotations only when needed
99
+
100
+ **Other languages:** Apply equivalent idiomatic standards. When unsure, check what the existing codebase does.
101
+
102
+ ### Pass 4: Security
103
+
104
+ - Input validation at system boundaries (user input, API payloads, file uploads)
105
+ - SQL injection, XSS, command injection, path traversal
106
+ - Auth/authz checks — are they in the right place? Can they be bypassed?
107
+ - Secrets — no hardcoded keys, tokens, passwords
108
+ - Dependency changes — are new deps trusted? Pinned versions?
109
+ - OWASP Top 10 for web-facing code
110
+
111
+ ### Pass 5: Performance
112
+
113
+ - N+1 queries, missing indexes, unbounded queries
114
+ - Unnecessary allocations in hot paths
115
+ - Missing pagination for list endpoints
116
+ - Large payloads loaded into memory
117
+ - Blocking operations in async contexts
118
+ - Caching opportunities (or cache invalidation bugs)
119
+
120
+ ### Pass 6: Testing
121
+
122
+ - Are new code paths tested?
123
+ - Do tests actually assert behavior (not just "doesn't crash")?
124
+ - Edge cases covered? Error paths?
125
+ - Test names describe the scenario, not the implementation
126
+ - No test pollution (shared mutable state between tests)
127
+ - Missing tests flagged as an issue, not ignored
128
+
129
+ ### Pass 7: Documentation & Naming
130
+
131
+ - Are names self-documenting? (variables, functions, types)
132
+ - Complex logic has comments explaining *why*, not *what*
133
+ - Public APIs have documentation
134
+ - Misleading names or outdated comments
135
+ - Breaking changes documented
136
+
137
+ ## Step 4: Output Format
138
+
139
+ Structure the review as:
140
+
141
+ ```markdown
142
+ ## PR Review: <title or summary>
143
+
144
+ ### Summary
145
+ <1-2 sentences on what the PR does and overall assessment>
146
+
147
+ ### Critical (must fix)
148
+ - **[file:line]** Description of the issue
149
+ - Why it matters
150
+ - Suggested fix
151
+
152
+ ### Important (should fix)
153
+ - **[file:line]** Description
154
+
155
+ ### Suggestions (nice to have)
156
+ - **[file:line]** Description
157
+
158
+ ### Positive
159
+ - Call out good patterns, clean abstractions, solid test coverage
160
+ ```
161
+
162
+ **Severity guide:**
163
+ - **Critical:** Bugs, security issues, data loss risks, broken functionality
164
+ - **Important:** Design issues, missing tests, performance problems, convention violations
165
+ - **Suggestions:** Style improvements, alternative approaches, minor cleanups
166
+ - **Positive:** Things done well — always include at least one
167
+
168
+ ## Decision Points
169
+
170
+ - If the diff is > 500 lines: split review by file/module, note that the PR might benefit from being broken up
171
+ - If no project docs exist: infer conventions from the existing codebase (read 2-3 similar files)
172
+ - If the PR has no description: note it, then infer intent from the diff
173
+ - If you're unsure about a pattern: flag it as a question, not a demand
174
+
175
+ ## Anti-patterns to Avoid in Reviews
176
+
177
+ - Don't bikeshed on style that a linter should catch
178
+ - Don't rewrite the PR in your head — review what's there
179
+ - Don't block on personal preference when the code is correct
180
+ - Don't ignore test quality — bad tests are worse than no tests
181
+ - Don't review with only the diff — read surrounding code for context
182
+
183
+ ## References
184
+
185
+ - [Google Engineering Practices — What to Look For](https://google.github.io/eng-practices/review/reviewer/looking-for.html)
186
+ - [Google — The Standard of Code Review](https://google.github.io/eng-practices/review/reviewer/standard.html)
187
+ - [Augment — 40 Questions Before You Approve](https://www.augmentcode.com/guides/code-review-checklist-40-questions-before-you-approve)
@@ -103,31 +103,88 @@ function truncate(s: string, max: number): string {
103
103
  }
104
104
 
105
105
  function formatToolUse(tool: string, input: any): string {
106
- if (!input || typeof input !== "object") return tool;
106
+ if (!input || typeof input !== "object") return tool.toLowerCase();
107
107
 
108
108
  switch (tool) {
109
+ // File operations
109
110
  case "Bash":
110
- return input.command ? `$ ${truncate(input.command, 50)}` : "running command";
111
+ return input.description
112
+ ? truncate(input.description, 60)
113
+ : input.command ? `$ ${truncate(input.command, 55)}` : "running command";
111
114
  case "Read":
112
115
  return input.file_path ? `reading ${basename(input.file_path)}` : "reading file";
113
116
  case "Edit":
114
117
  return input.file_path ? `editing ${basename(input.file_path)}` : "editing file";
115
118
  case "Write":
116
119
  return input.file_path ? `writing ${basename(input.file_path)}` : "writing file";
120
+ case "NotebookEdit":
121
+ return input.file_path ? `editing notebook ${basename(input.file_path)}` : "editing notebook";
122
+
123
+ // Search operations
117
124
  case "Grep":
118
- return input.pattern ? `searching: ${truncate(input.pattern, 40)}` : "searching";
125
+ return input.pattern ? `searching for "${truncate(input.pattern, 35)}"` : "searching code";
119
126
  case "Glob":
120
- return input.pattern ? `finding: ${truncate(input.pattern, 40)}` : "finding files";
127
+ return input.pattern ? `finding ${truncate(input.pattern, 40)}` : "finding files";
128
+ case "ToolSearch":
129
+ return input.query ? `looking up tool: ${truncate(input.query, 40)}` : "searching tools";
130
+
131
+ // Agent & task operations
121
132
  case "Agent":
133
+ return input.description ? `⟩ ${truncate(input.description, 55)}` : "running agent";
122
134
  case "Task":
123
- return input.description || input.prompt?.slice(0, 40) || "running agent";
135
+ return input.description || input.prompt?.slice(0, 50) || "running task";
136
+ case "TaskCreate":
137
+ return input.description ? `starting: ${truncate(input.description, 45)}` : "creating task";
138
+ case "TaskGet":
139
+ case "TaskOutput":
140
+ return "checking task progress";
141
+ case "TaskList":
142
+ return "listing tasks";
143
+ case "TaskStop":
144
+ return "stopping task";
145
+ case "TaskUpdate":
146
+ return "updating task";
147
+ case "SendMessage":
148
+ return input.to ? `messaging ${truncate(String(input.to), 30)}` : "sending message";
149
+
150
+ // Web operations
124
151
  case "WebFetch":
125
- return input.url ? `fetching ${truncate(input.url, 50)}` : "fetching";
152
+ return input.url ? `fetching ${truncate(input.url, 50)}` : "fetching url";
126
153
  case "WebSearch":
127
- return input.query ? `searching: ${truncate(input.query, 40)}` : "searching web";
154
+ return input.query ? `web search: ${truncate(input.query, 40)}` : "searching the web";
155
+
156
+ // Planning & workflow
157
+ case "EnterPlanMode":
158
+ return "entering plan mode";
159
+ case "ExitPlanMode":
160
+ return "exiting plan mode";
161
+ case "EnterWorktree":
162
+ return "creating worktree";
163
+ case "ExitWorktree":
164
+ return "leaving worktree";
165
+
166
+ // Skill & todo
167
+ case "Skill":
168
+ return input.skill ? `using /${truncate(input.skill, 40)}` : "invoking skill";
169
+ case "TodoWrite":
170
+ case "TodoRead":
171
+ return tool === "TodoWrite" ? "updating checklist" : "reading checklist";
172
+
173
+ // LSP
174
+ case "LSP":
175
+ return input.command ? `lsp: ${truncate(input.command, 50)}` : "querying language server";
176
+
177
+ // MCP tools (plugin_name__tool_name pattern)
128
178
  default: {
129
- const val = input.command || input.pattern || input.query || input.file_path || input.description || "";
130
- return val ? `${tool} ${truncate(String(val), 50)}` : tool;
179
+ // Handle MCP tools like mcp__playwright__browser_navigate
180
+ if (tool.startsWith("mcp__")) {
181
+ const parts = tool.split("__");
182
+ const action = parts[parts.length - 1]?.replace(/_/g, " ") || tool;
183
+ const val = input.url || input.selector || input.text || input.value || "";
184
+ return val ? `${action}: ${truncate(String(val), 40)}` : action;
185
+ }
186
+ const val = input.description || input.command || input.pattern || input.query || input.file_path || "";
187
+ return val ? `${tool.toLowerCase()}: ${truncate(String(val), 50)}` : tool.toLowerCase();
131
188
  }
132
189
  }
133
190
  }
@@ -148,7 +205,13 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
148
205
  const systemPrompt = buildSystemPrompt("chat", channel);
149
206
  const cwd = homedir();
150
207
 
151
- let sessionId = resume ? await Session.getLatest(room) : null;
208
+ let sessionId: string | null = null;
209
+ if (typeof resume === "string") {
210
+ // Specific session ID provided
211
+ sessionId = resume;
212
+ } else if (resume) {
213
+ sessionId = await Session.getLatest(room);
214
+ }
152
215
 
153
216
  // Verify session file exists on disk before attempting resume
154
217
  if (sessionId && !sessionFileExists(sessionId, cwd)) {
package/src/chat/repl.ts CHANGED
@@ -4,8 +4,115 @@ import { runMigrations } from "../db/migrate";
4
4
  import { closeDb } from "../db/connection";
5
5
  import { getMcpServers, setMcpServers } from "../mcp";
6
6
  import { createNiaMcpServer } from "../mcp/server";
7
+ import { Session } from "../db/models";
8
+ import { relativeTime } from "../utils/format";
7
9
 
8
- export async function startRepl(resume = false): Promise<void> {
10
+ // ANSI helpers
11
+ const DIM = "\x1b[2m";
12
+ const BOLD = "\x1b[1m";
13
+ const CYAN = "\x1b[36m";
14
+ const RESET = "\x1b[0m";
15
+ const CLEAR_LINE = "\x1b[2K\r";
16
+
17
+ // Braille spinner frames
18
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
19
+
20
+ class StatusLine {
21
+ private frame = 0;
22
+ private timer: ReturnType<typeof setInterval> | null = null;
23
+ private text = "";
24
+ private active = false;
25
+
26
+ start(initialText = "thinking") {
27
+ this.text = initialText;
28
+ this.active = true;
29
+ this.frame = 0;
30
+ this.render();
31
+ this.timer = setInterval(() => {
32
+ this.frame = (this.frame + 1) % SPINNER.length;
33
+ this.render();
34
+ }, 80);
35
+ }
36
+
37
+ update(text: string) {
38
+ this.text = text;
39
+ if (this.active) this.render();
40
+ }
41
+
42
+ stop() {
43
+ if (this.timer) {
44
+ clearInterval(this.timer);
45
+ this.timer = null;
46
+ }
47
+ if (this.active) {
48
+ process.stderr.write(CLEAR_LINE);
49
+ }
50
+ this.active = false;
51
+ }
52
+
53
+ private render() {
54
+ const spinner = SPINNER[this.frame];
55
+ process.stderr.write(`${CLEAR_LINE}${DIM} ${spinner} ${this.text}${RESET}`);
56
+ }
57
+ }
58
+
59
+ function truncatePreview(text: string, max: number): string {
60
+ const oneline = text.replace(/\n/g, " ").trim();
61
+ return oneline.length > max ? oneline.slice(0, max) + "…" : oneline;
62
+ }
63
+
64
+ async function pickSession(): Promise<string | null> {
65
+ const sessions = await Session.getRecent("terminal", 10);
66
+ if (sessions.length === 0) {
67
+ console.log(`${DIM}no previous sessions${RESET}\n`);
68
+ return null;
69
+ }
70
+
71
+ const now = new Date();
72
+ console.log(`\n${DIM}recent sessions:${RESET}\n`);
73
+
74
+ for (let i = 0; i < sessions.length; i++) {
75
+ const s = sessions[i];
76
+ const age = relativeTime(new Date(s.updatedAt), now);
77
+ const preview = s.preview ? truncatePreview(s.preview, 50) : "empty session";
78
+ const msgs = `${s.messageCount} msg${s.messageCount !== 1 ? "s" : ""}`;
79
+ console.log(` ${BOLD}${i + 1}${RESET} ${preview} ${DIM}${msgs} · ${age}${RESET}`);
80
+ }
81
+
82
+ console.log(`\n ${DIM}n${RESET} start new session`);
83
+ console.log();
84
+
85
+ return new Promise<string | null>((resolve) => {
86
+ const rl = readline.createInterface({
87
+ input: process.stdin,
88
+ output: process.stdout,
89
+ });
90
+
91
+ rl.question(`${DIM}select [1-${sessions.length}, n]:${RESET} `, (answer) => {
92
+ rl.close();
93
+ const trimmed = answer.trim().toLowerCase();
94
+
95
+ if (trimmed === "n" || trimmed === "new") {
96
+ resolve(null);
97
+ return;
98
+ }
99
+
100
+ const idx = parseInt(trimmed, 10);
101
+ if (idx >= 1 && idx <= sessions.length) {
102
+ resolve(sessions[idx - 1].id);
103
+ } else if (trimmed === "" && sessions.length > 0) {
104
+ // Default: most recent session
105
+ resolve(sessions[0].id);
106
+ } else {
107
+ resolve(null);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ export type ChatMode = "continue" | "new" | "pick";
114
+
115
+ export async function startRepl(mode: ChatMode = "continue"): Promise<void> {
9
116
  try {
10
117
  await runMigrations();
11
118
  } catch (err) {
@@ -23,15 +130,30 @@ export async function startRepl(resume = false): Promise<void> {
23
130
  } catch {}
24
131
  }
25
132
 
133
+ // Determine session to use
134
+ let resume: boolean | string = false;
135
+
136
+ if (mode === "pick") {
137
+ const picked = await pickSession();
138
+ if (picked) {
139
+ resume = picked;
140
+ }
141
+ } else if (mode === "continue") {
142
+ resume = true;
143
+ }
144
+
26
145
  const engine = await createChatEngine({ room: "terminal", channel: "terminal", resume, mcpServers: getMcpServers() });
27
146
 
28
- console.log(resume && engine.sessionId ? "Resumed previous session." : "New chat session started.");
29
- console.log('Type your message and press Enter. Type "exit" or Ctrl+C to quit.\n');
147
+ // Welcome
148
+ const isResumed = engine.sessionId && resume;
149
+ const sessionNote = isResumed ? "resumed" : "new session";
150
+ console.log(`\n${DIM}nia chat${RESET} ${DIM}(${sessionNote})${RESET}`);
151
+ console.log(`${DIM}type /exit to quit${RESET}\n`);
30
152
 
31
153
  const rl = readline.createInterface({
32
154
  input: process.stdin,
33
155
  output: process.stdout,
34
- prompt: "you > ",
156
+ prompt: `${BOLD}>${RESET} `,
35
157
  });
36
158
 
37
159
  rl.prompt();
@@ -44,31 +166,74 @@ export async function startRepl(resume = false): Promise<void> {
44
166
  return;
45
167
  }
46
168
 
47
- const exitCommands = [".exit", ".quit", "exit", "quit"];
169
+ const exitCommands = ["/exit", "/quit", ".exit", ".quit", "exit", "quit"];
48
170
  if (exitCommands.includes(input.toLowerCase())) {
49
171
  rl.close();
50
172
  return;
51
173
  }
52
174
 
53
- process.stdout.write("\n");
175
+ const status = new StatusLine();
176
+ status.start("thinking");
177
+
178
+ let streamedLength = 0;
179
+ let responseStarted = false;
54
180
 
55
181
  try {
56
182
  const { result, costUsd, turns } = await engine.send(input, {
57
- onActivity(status) {
58
- process.stdout.write(`\x1b[2m ${status}\x1b[0m\n`);
183
+ onStream(textSoFar) {
184
+ // Stream response text as it arrives
185
+ if (!responseStarted) {
186
+ status.stop();
187
+ process.stdout.write("\n");
188
+ responseStarted = true;
189
+ }
190
+ const newText = textSoFar.slice(streamedLength);
191
+ if (newText) {
192
+ process.stdout.write(newText);
193
+ streamedLength = textSoFar.length;
194
+ }
195
+ },
196
+ onActivity(activityText) {
197
+ if (!responseStarted) {
198
+ status.update(activityText);
199
+ }
59
200
  },
60
201
  });
61
- console.log(`nia > ${result.trim()}\n`);
202
+
203
+ // If streaming didn't fire (e.g. tool-only turns), print the result
204
+ if (!responseStarted && result.trim()) {
205
+ status.stop();
206
+ process.stdout.write(`\n${result.trim()}`);
207
+ } else if (responseStarted) {
208
+ // Print any remaining text that wasn't streamed
209
+ const remaining = result.slice(streamedLength);
210
+ if (remaining.trim()) {
211
+ process.stdout.write(remaining);
212
+ }
213
+ } else {
214
+ status.stop();
215
+ }
216
+
217
+ // Cost line
218
+ const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
219
+ const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
220
+ const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
221
+ if (meta) {
222
+ process.stdout.write(`\n${DIM}${meta}${RESET}`);
223
+ }
224
+
225
+ process.stdout.write("\n\n");
62
226
  } catch (err) {
227
+ status.stop();
63
228
  const msg = err instanceof Error ? err.message : String(err);
64
- console.error(`[error] ${msg}\n`);
229
+ console.error(`\n${DIM}error:${RESET} ${msg}\n`);
65
230
  }
66
231
 
67
232
  rl.prompt();
68
233
  });
69
234
 
70
235
  rl.on("close", async () => {
71
- console.log("\nBye!");
236
+ console.log(`\n${DIM}bye${RESET}`);
72
237
  engine.close();
73
238
  await closeDb();
74
239
  process.exit(0);