niahere 0.2.13 → 0.2.14
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 +7 -2
- package/package.json +1 -1
- package/skills/github-link-repo-explorer/SKILL.md +104 -0
- package/skills/image-generation/SKILL.md +121 -0
- package/skills/image-generation/scripts/generate_image.py +401 -0
- package/skills/llms-txt/SKILL.md +141 -0
- package/skills/pr-reviewer/SKILL.md +187 -0
- package/src/chat/engine.ts +73 -10
- package/src/chat/repl.ts +176 -11
- package/src/cli/index.ts +112 -5
- package/src/db/models/session.ts +38 -0
- package/src/types/engine.ts +2 -1
|
@@ -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)
|
package/src/chat/engine.ts
CHANGED
|
@@ -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.
|
|
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
|
|
125
|
+
return input.pattern ? `searching for "${truncate(input.pattern, 35)}"` : "searching code";
|
|
119
126
|
case "Glob":
|
|
120
|
-
return input.pattern ? `finding
|
|
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,
|
|
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 ? `
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
236
|
+
console.log(`\n${DIM}bye${RESET}`);
|
|
72
237
|
engine.close();
|
|
73
238
|
await closeDb();
|
|
74
239
|
process.exit(0);
|