@niroai/niro 0.8.0 → 0.10.0
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/CHANGELOG.md +110 -0
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/commands/ab.js +198 -0
- package/src/commands/add-project.js +11 -11
- package/src/commands/add-repo.js +23 -23
- package/src/commands/build.js +5 -5
- package/src/commands/delete.js +3 -3
- package/src/commands/edit.js +11 -11
- package/src/commands/info.js +2 -2
- package/src/commands/init.js +108 -87
- package/src/commands/login.js +25 -5
- package/src/commands/projects.js +1 -1
- package/src/commands/release.js +81 -15
- package/src/commands/remove.js +5 -5
- package/src/commands/start.js +567 -0
- package/src/commands/takeover.js +105 -23
- package/src/commands/watch.js +35 -35
- package/src/index.js +57 -3
- package/src/lib/folderSync.js +13 -8
- package/src/lib/git.js +21 -3
- package/src/lib/moduleExcludeSuggestions.js +5 -5
- package/src/lib/nestedOutput.js +49 -0
- package/src/lib/onboardTarget.js +5 -5
- package/src/lib/prompt.js +3 -0
- package/src/lib/service.js +20 -12
- package/src/lib/spinner.js +28 -7
- package/src/lib/tempProject.js +25 -1
- package/src/lib/watchDaemon.js +9 -9
- package/src/mcp/server.js +7 -2
- package/src/mcp/setup.js +198 -160
- package/src/mcp/teardown.js +53 -53
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@niroai/niro`. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.10.0 — 2026-08-28
|
|
6
|
+
|
|
7
|
+
One command to get started, and a way to check what Niro is worth.
|
|
8
|
+
|
|
9
|
+
### Upgrading
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @niroai/niro@latest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Nothing extra is needed from 0.9.0. Every existing command works exactly as before; the new
|
|
16
|
+
ones sit alongside them.
|
|
17
|
+
|
|
18
|
+
**`niro start` replaces four steps with one.** Getting started used to mean learning a sequence
|
|
19
|
+
from a README: install, `niro login`, change into the right folder, `niro init`, then
|
|
20
|
+
`niro mcp install`. Each step was fine on its own; the sequence was the problem, because the
|
|
21
|
+
entry point was documentation and documentation is optional. One user ran `niro init` in their
|
|
22
|
+
home folder. Typing `niro` on its own printed an eighteen-command help listing, which is the
|
|
23
|
+
worst possible first screen for someone who knows none of them.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @niroai/niro start
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That signs you in, installs itself properly, shows you what it is about to index and asks
|
|
30
|
+
before uploading anything, builds the graph, connects your assistant, and offers to measure
|
|
31
|
+
the difference. Run it again in any other repo and it skips whatever is already done. It opens
|
|
32
|
+
by saying what Niro does and roughly how long this takes, and it asks whether you have an
|
|
33
|
+
access code before it installs anything, so nobody without one fills in a form that rejects
|
|
34
|
+
them at the end.
|
|
35
|
+
|
|
36
|
+
Two screens are worth calling out. Before uploading, it shows you the folder, the build files
|
|
37
|
+
it recognised, the module count and the git branch, and waits: the one screen worth reading
|
|
38
|
+
rather than skipping. And a folder it refuses, such as your home folder, now asks for a
|
|
39
|
+
different one instead of exiting, which is what made that mistake so easy to make.
|
|
40
|
+
|
|
41
|
+
`niro init`, `niro login` and `niro mcp install` are untouched. They are the scriptable path,
|
|
42
|
+
they take flags, and `niro start` simply calls them in order.
|
|
43
|
+
|
|
44
|
+
**`niro ab` measures what Niro changes.** Rather than take our word for it, Niro can run
|
|
45
|
+
*itself off* for half your sessions and show you the two halves side by side. While a window is
|
|
46
|
+
open each session lands in an "on" or "off" bucket by a deterministic split; the off half works
|
|
47
|
+
without Niro.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
niro ab enable # start a 14-day window (it states the scope and asks first)
|
|
51
|
+
niro ab report # the with-Niro / without-Niro comparison
|
|
52
|
+
niro ab disable # stop early
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It applies to your whole account, not just the repo you are standing in, and it says so before
|
|
56
|
+
starting. `niro start` offers it once, at the end of a first setup, and defaults to no.
|
|
57
|
+
|
|
58
|
+
**Setup prints about half as much.** A real run printed roughly 230 lines and only half were
|
|
59
|
+
ours. PM2's process table and generated systemd unit file, about 55 lines, are captured now and
|
|
60
|
+
shown only if something fails. The assistant list shows five clients and a count rather than all
|
|
61
|
+
fourteen with their config paths (`niro mcp install --list` for the rest). `niro init` stopped
|
|
62
|
+
telling you to run `niro mcp install` two seconds before the wizard did it for you.
|
|
63
|
+
|
|
64
|
+
**Output is indented consistently.** Printed lines used eight different indents across the CLI,
|
|
65
|
+
so nothing said what belonged to what. Everything now sits on one ladder: headings and step
|
|
66
|
+
markers, their messages one level in, detail one level further. Output from `niro init`, and
|
|
67
|
+
from the editor CLIs it shells out to, is folded into that ladder instead of printing flush
|
|
68
|
+
left in the middle of a step.
|
|
69
|
+
|
|
70
|
+
**Fixes.** The spinner and the upload progress bar wrote straight to the terminal and so
|
|
71
|
+
ignored indentation entirely. `niro mcp install --list` was added. Two em-dashes and one
|
|
72
|
+
invented URL were removed from text you read on screen.
|
|
73
|
+
|
|
74
|
+
## 0.9.0 — 2026-08-26
|
|
75
|
+
|
|
76
|
+
One change to how the client waits, and two fixes for cases where Niro and your machine
|
|
77
|
+
disagreed about what still exists.
|
|
78
|
+
|
|
79
|
+
### Upgrading
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install -g @niroai/niro@latest
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Nothing extra is needed from 0.8.0. Against an older server the waiting change simply does not
|
|
86
|
+
engage, because the client decides from the server's answer rather than from a version.
|
|
87
|
+
|
|
88
|
+
**Creating a working copy no longer times out.** It takes 44 to 60 seconds, and it used to run
|
|
89
|
+
inside a single request, against a gateway that gives up at 60. Three at once on 26 August all
|
|
90
|
+
came back as gateway error pages, and because such an error says nothing about whether the work
|
|
91
|
+
finished, the real cause underneath went unread for an afternoon. The client now asks the server
|
|
92
|
+
to do it in the background and waits for it to finish, showing progress. A dropped connection
|
|
93
|
+
while waiting is not treated as a failure, because the copy is still being made on the server; an
|
|
94
|
+
expired login is, because waiting cannot fix it.
|
|
95
|
+
|
|
96
|
+
**Discarding a working copy the server had already deleted.** A working copy can go away
|
|
97
|
+
without your machine being told: someone deletes it in the web UI, or the server reaps it
|
|
98
|
+
after it has sat idle for two weeks. The folder on your machine kept a registration pointing
|
|
99
|
+
at it, and `niro discard-temp-project`, which is what clears that registration, failed before
|
|
100
|
+
it got that far. The registration is also what makes `niro new-temp-project` in a second
|
|
101
|
+
worktree refuse, advising you to run discard in the other folder, so the advice named the one
|
|
102
|
+
command that could not work. Discard now treats a project the server says is missing as
|
|
103
|
+
nothing left to delete: it skips the server call and does the local half, pointing the folder
|
|
104
|
+
back at the original project. A server it could not reach, or one that refuses because the
|
|
105
|
+
copy belongs to another developer, still fails loudly and changes nothing, which matters
|
|
106
|
+
because the server answers both of those with the same status code as a deleted project.
|
|
107
|
+
|
|
108
|
+
**A detached checkout stopped Niro answering.** During `git bisect`, or after checking out a
|
|
109
|
+
tag, your repo is on no branch. `git rev-parse --abbrev-ref HEAD` does not fail there, it
|
|
110
|
+
prints the word `HEAD`, and Niro passed that on as though it were a branch name. Nothing
|
|
111
|
+
indexes a branch called `HEAD`, so the server treated every such question as coming from a
|
|
112
|
+
diverged branch and declined to answer it, for the whole bisect. Niro now reports no branch,
|
|
113
|
+
which is what the server already had a rule for, and answers from the indexed project.
|
|
114
|
+
|
|
5
115
|
## 0.8.0 — 2026-08-26
|
|
6
116
|
|
|
7
117
|
Needed before a server has the layered store turned on. On an older client the new
|
package/README.md
CHANGED
|
@@ -53,6 +53,23 @@ By default the CLI talks to Niro's cloud (`https://aiorchestrator.niroai.dev`) a
|
|
|
53
53
|
|
|
54
54
|
## 60-second setup
|
|
55
55
|
|
|
56
|
+
```bash
|
|
57
|
+
cd ~/code/my-service
|
|
58
|
+
npx @niroai/niro start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That is the whole thing. `niro start` signs you in (browser), installs itself properly,
|
|
62
|
+
shows you what it is about to index and asks before uploading anything, builds the graph,
|
|
63
|
+
connects your AI assistant, and finally offers to measure what difference Niro makes
|
|
64
|
+
(Scenario 12; optional, defaults to no). Run it again in any other repo; it skips whatever
|
|
65
|
+
is already done.
|
|
66
|
+
|
|
67
|
+
Niro is in private beta, so `start` asks for an **access code** first (the browser signup form
|
|
68
|
+
asks for it under that name). No code yet? Go to [www.niro.ai](https://www.niro.ai) and click
|
|
69
|
+
**Request Access**. Answering "no" stops the setup immediately, having installed nothing.
|
|
70
|
+
|
|
71
|
+
Prefer the individual commands (or scripting it)? They all still work, and take flags:
|
|
72
|
+
|
|
56
73
|
```bash
|
|
57
74
|
niro login # opens your browser to approve this device
|
|
58
75
|
cd ~/code/my-service
|
|
@@ -250,10 +267,46 @@ It does **not** sign you out — your `niro login` session in `~/.niro/credentia
|
|
|
250
267
|
|
|
251
268
|
---
|
|
252
269
|
|
|
270
|
+
## Scenario 12 — Measure what Niro actually changes
|
|
271
|
+
|
|
272
|
+
Rather than take our word for it, you can have Niro run **itself off** for half your sessions
|
|
273
|
+
and compare the two halves. While a window is open, each MCP session is put in an "on" or
|
|
274
|
+
"off" bucket by a deterministic 50/50 split; the "off" half works without Niro. `niro ab
|
|
275
|
+
report` then shows the two side by side.
|
|
276
|
+
|
|
277
|
+
Two things to know before you start one:
|
|
278
|
+
|
|
279
|
+
- It applies to your **whole account**, not just the repo you are standing in.
|
|
280
|
+
- It runs for a fixed window, **14 days** by default, and you can stop it at any time.
|
|
281
|
+
|
|
282
|
+
**Terminal**
|
|
283
|
+
```bash
|
|
284
|
+
niro ab # is a window open, and until when?
|
|
285
|
+
niro ab enable # start a 14-day window (states the scope, asks first)
|
|
286
|
+
niro ab enable --days 30 --name "Q3 trial"
|
|
287
|
+
niro ab enable --yes # skip the confirm (scripts / CI)
|
|
288
|
+
niro ab report # the with-Niro / without-Niro comparison
|
|
289
|
+
niro ab report --json # same, machine-readable
|
|
290
|
+
niro ab disable # stop early; every session uses Niro again
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The report compares session count, median session time, median tool calls per session, median
|
|
294
|
+
Niro tool calls, and median context size. A figure the server has not recorded reads
|
|
295
|
+
`not measured` rather than `0`, because `0` is a real result here: an off-bucket session
|
|
296
|
+
genuinely makes zero Niro tool calls.
|
|
297
|
+
|
|
298
|
+
`niro start` offers this once, at the end of your first setup, and defaults to no.
|
|
299
|
+
|
|
300
|
+
**Ask the agent**
|
|
301
|
+
> "Is Niro A/B measurement on?" / "Show me the A/B comparison." — the agent runs `niro ab`.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
253
305
|
## Command reference
|
|
254
306
|
|
|
255
307
|
| Command | What it does |
|
|
256
308
|
|---|---|
|
|
309
|
+
| `niro start [path]` | First-time setup end to end: sign in, onboard this folder, connect an assistant |
|
|
257
310
|
| `niro login` / `logout` / `whoami` | Authenticate / sign out / show current account |
|
|
258
311
|
| `niro init [path]` | Onboard a folder end-to-end (upload, project, env scan, build, watch) |
|
|
259
312
|
| `niro info [path]` | What Niro knows about this folder (repo, branch, project(s), status) |
|
|
@@ -267,9 +320,10 @@ It does **not** sign you out — your `niro login` session in `~/.niro/credentia
|
|
|
267
320
|
| `niro watch [path]` / `niro service …` | Foreground watch / manage the auto-sync daemon |
|
|
268
321
|
| `niro mcp install` | Connect an AI assistant + install the agent skills (one per run; `--client <id>` to skip the prompt, `--url <url>` for your own backend) |
|
|
269
322
|
| `niro mcp uninstall` | Disconnect an AI assistant — reverses `mcp install` (`--client <id>` / `--all`, `-y` to skip confirm) |
|
|
323
|
+
| `niro ab status\|enable\|disable\|report` | Run Niro off for half your sessions for a fixed window, then compare (`--days`, `--name`, `--yes`, `--json`) |
|
|
270
324
|
| `niro delete [project]` | Older whole-project delete (prefer `niro remove --project`) |
|
|
271
325
|
|
|
272
|
-
Add `--json` to `whoami`, `info`, `status`, `projects`, `build`, and `edit --list-env` for machine-readable output. Add `--help` to any command for its full flags.
|
|
326
|
+
Add `--json` to `whoami`, `info`, `status`, `projects`, `build`, `ab report`, and `edit --list-env` for machine-readable output. Add `--help` to any command for its full flags.
|
|
273
327
|
|
|
274
328
|
---
|
|
275
329
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `niro ab` — turn the A/B measurement window on or off, and read the result.
|
|
3
|
+
*
|
|
4
|
+
* What it measures: while a window is open, ai-assistant puts each MCP session in an "on" or
|
|
5
|
+
* "off" bucket by a deterministic 50/50 split of the session id. In the OFF bucket your
|
|
6
|
+
* assistant works WITHOUT Niro. Comparing the two buckets is the only way to see what Niro
|
|
7
|
+
* changes for you rather than take our word for it.
|
|
8
|
+
*
|
|
9
|
+
* Two things about the scope, both said out loud before anything is switched on:
|
|
10
|
+
* - It is per ACCOUNT, not per repo or per machine, so it reaches sessions beyond the
|
|
11
|
+
* folder you are standing in.
|
|
12
|
+
* - It runs for a fixed window (14 days by default) and can be stopped at any time.
|
|
13
|
+
*
|
|
14
|
+
* These endpoints live on ai-assistant, NOT the orchestrator every other command talks to,
|
|
15
|
+
* so the base URL is resolved through the MCP installer's own resolver rather than a second
|
|
16
|
+
* copy of that precedence. Deliberately no --url flag here, for the reason index.js refuses
|
|
17
|
+
* --api-url outside login: a per-command backend override reads one server and writes
|
|
18
|
+
* another. A mismatched pair fails as a 401 from the assistant rather than doing something
|
|
19
|
+
* quietly wrong.
|
|
20
|
+
*/
|
|
21
|
+
const api = require("../lib/api");
|
|
22
|
+
const credentials = require("../lib/credentials");
|
|
23
|
+
const color = require("../lib/color");
|
|
24
|
+
const prompt = require("../lib/prompt");
|
|
25
|
+
const { resolveMcpServerUrl } = require("../mcp/setup");
|
|
26
|
+
|
|
27
|
+
const ok = (s) => color.green(s);
|
|
28
|
+
const warn = (s) => color.yellow(s);
|
|
29
|
+
const fail = (s) => color.red(s);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_WINDOW_DAYS = 14;
|
|
32
|
+
|
|
33
|
+
/** The ai-assistant base for this session, by the same precedence `niro mcp install` uses. */
|
|
34
|
+
function assistantUrl() {
|
|
35
|
+
return resolveMcpServerUrl(null).url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function call(method, path, opts = {}) {
|
|
39
|
+
// Checked here rather than left to the server: without it an unauthenticated user gets a
|
|
40
|
+
// raw "GET /api/abtest/mode failed: Unauthorized (HTTP 401)", while every other command in
|
|
41
|
+
// the CLI says "Not logged in. Run `niro login` first." requireCreds throws the ENOAUTH
|
|
42
|
+
// that index.js already knows how to print.
|
|
43
|
+
credentials.requireCreds();
|
|
44
|
+
return api.request(method, path, { ...opts, apiUrl: assistantUrl() });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A run's window as a plain sentence, or null when there is no window. */
|
|
48
|
+
function describeWindow(run) {
|
|
49
|
+
if (!run || !run.window_start) return null;
|
|
50
|
+
const start = String(run.window_start).slice(0, 10);
|
|
51
|
+
const end = run.window_end ? String(run.window_end).slice(0, 10) : "no end date";
|
|
52
|
+
return `${start} to ${end}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isActive(run) {
|
|
56
|
+
return !!(run && run.status === "active");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function status() {
|
|
60
|
+
const run = await call("GET", "/api/abtest/mode");
|
|
61
|
+
if (!isActive(run)) {
|
|
62
|
+
console.log(`\n A/B measurement is ${color.bold("off")}.`);
|
|
63
|
+
console.log(` Start a ${DEFAULT_WINDOW_DAYS}-day window with ${color.cyan("niro ab enable")}.\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`\n A/B measurement is ${ok("on")}${run.name ? ` (${run.name})` : ""}.`);
|
|
67
|
+
const window = describeWindow(run);
|
|
68
|
+
if (window) console.log(` Window: ${window}`);
|
|
69
|
+
console.log(` Half your sessions are running without Niro so the two can be compared.`);
|
|
70
|
+
console.log(`\n ${color.cyan("niro ab report")} see the comparison`);
|
|
71
|
+
console.log(` ${color.cyan("niro ab disable")} stop early\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Starting a window changes how HALF of every future session behaves, account-wide, so the
|
|
76
|
+
* consequence is stated and confirmed rather than assumed from the fact that the command was
|
|
77
|
+
* typed. --yes is the scripted escape.
|
|
78
|
+
*/
|
|
79
|
+
async function enable(opts = {}) {
|
|
80
|
+
const days = parseDays(opts.days);
|
|
81
|
+
if (days === null) {
|
|
82
|
+
console.error(fail(` --days must be a whole number of days, got "${opts.days}".`));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const existing = await call("GET", "/api/abtest/mode").catch(() => null);
|
|
88
|
+
if (isActive(existing)) {
|
|
89
|
+
console.log(`\n ${warn("A/B measurement is already on.")}`);
|
|
90
|
+
const window = describeWindow(existing);
|
|
91
|
+
if (window) console.log(` Window: ${window}`);
|
|
92
|
+
console.log(` ${color.cyan("niro ab report")} for the comparison so far.\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!opts.yes) {
|
|
97
|
+
console.log(`\n This runs Niro ${color.bold("off")} for half your sessions for ${days} days,`);
|
|
98
|
+
console.log(" so the two halves can be compared. It applies to your whole account,");
|
|
99
|
+
console.log(" not just this repo, and you can stop it at any time.\n");
|
|
100
|
+
if (prompt.isNonInteractive()) {
|
|
101
|
+
console.error(fail(" No terminal to confirm on. Pass --yes if you mean it."));
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!(await prompt.confirm(" Start the measurement window?", false))) {
|
|
106
|
+
console.log("\n Left off. Nothing changed.\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const run = await call("POST", "/api/abtest/mode/enable", {
|
|
112
|
+
query: { windowDays: days, name: opts.name || undefined },
|
|
113
|
+
});
|
|
114
|
+
console.log(`\n ${ok("✓")} A/B measurement is on for ${days} days.`);
|
|
115
|
+
const window = describeWindow(run);
|
|
116
|
+
if (window) console.log(` Window: ${window}`);
|
|
117
|
+
console.log(` Come back with ${color.cyan("niro ab report")}, or stop early with ${color.cyan("niro ab disable")}.\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function disable() {
|
|
121
|
+
const existing = await call("GET", "/api/abtest/mode").catch(() => null);
|
|
122
|
+
if (!isActive(existing)) {
|
|
123
|
+
console.log(`\n A/B measurement was already off. Nothing changed.\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await call("POST", "/api/abtest/mode/disable");
|
|
127
|
+
console.log(`\n ${ok("✓")} A/B measurement stopped. Every session uses Niro again.`);
|
|
128
|
+
console.log(` The result so far is still readable with ${color.cyan("niro ab report")}.\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The comparison, as two columns.
|
|
133
|
+
*
|
|
134
|
+
* Rows are only printed when the server actually returned a number for them: a median that
|
|
135
|
+
* never arrived must read as absent, not as zero, because zero is a real and very different
|
|
136
|
+
* measurement here (an off-bucket session genuinely makes 0 Niro tool calls).
|
|
137
|
+
*/
|
|
138
|
+
async function report(opts = {}) {
|
|
139
|
+
const r = await call("GET", "/api/abtest/report", {
|
|
140
|
+
query: opts.runId ? { runId: opts.runId } : undefined,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (opts.json) {
|
|
144
|
+
console.log(JSON.stringify(r, null, 2));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const on = r.niro_on || {};
|
|
149
|
+
const off = r.niro_off || {};
|
|
150
|
+
|
|
151
|
+
console.log(`\n A/B comparison${r.run_name ? `: ${r.run_name}` : ""}`);
|
|
152
|
+
const window = describeWindow(r);
|
|
153
|
+
if (window) console.log(color.dim(` ${window}${r.active ? ", still running" : ", ended"}`));
|
|
154
|
+
|
|
155
|
+
if (!r.total_sessions) {
|
|
156
|
+
console.log(`\n No sessions recorded yet${r.message ? `: ${r.message}` : "."}`);
|
|
157
|
+
console.log(" Use your assistant for a while, then run this again.\n");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n ${pad("", 34)}${pad("with Niro", 14)}without Niro`);
|
|
162
|
+
console.log(` ${color.dim("-".repeat(60))}`);
|
|
163
|
+
row("Sessions", on.session_count, off.session_count);
|
|
164
|
+
row("Median session time (ms)", on.median_session_ms, off.median_session_ms);
|
|
165
|
+
row("Median tool calls per session", on.median_tool_calls, off.median_tool_calls);
|
|
166
|
+
row("Median Niro tool calls", on.median_niro_tool_calls, off.median_niro_tool_calls);
|
|
167
|
+
row("Median context (bytes)", on.median_context_bytes, off.median_context_bytes);
|
|
168
|
+
console.log(`\n ${r.total_sessions} sessions in total.`);
|
|
169
|
+
if (!r.active) console.log(color.dim(" This window has ended."));
|
|
170
|
+
console.log("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function row(label, a, b) {
|
|
174
|
+
console.log(` ${pad(label, 34)}${pad(fmt(a), 14)}${fmt(b)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function fmt(v) {
|
|
178
|
+
// Spelled out rather than a dash: this cell has to be distinguishable from 0 at a
|
|
179
|
+
// glance, and 0 is a real measurement here (an off-bucket session makes 0 Niro calls).
|
|
180
|
+
if (v === null || v === undefined) return color.dim("not measured");
|
|
181
|
+
return typeof v === "number" ? v.toLocaleString("en-GB") : String(v);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Pads on the VISIBLE length, so a dimmed cell still lines up with a plain one. */
|
|
185
|
+
function pad(s, width) {
|
|
186
|
+
const visible = String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
187
|
+
return String(s) + " ".repeat(Math.max(0, width - visible));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** null for anything that is not a positive whole number of days. */
|
|
191
|
+
function parseDays(raw) {
|
|
192
|
+
if (raw === undefined || raw === null || raw === "") return DEFAULT_WINDOW_DAYS;
|
|
193
|
+
const n = Number(raw);
|
|
194
|
+
if (!Number.isInteger(n) || n <= 0) return null;
|
|
195
|
+
return n;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { status, enable, disable, report, DEFAULT_WINDOW_DAYS, parseDays, pad, fmt };
|
|
@@ -47,7 +47,7 @@ async function addProject(opts = {}) {
|
|
|
47
47
|
localByGitId[entry.gitId] = entry;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
console.log(`\n
|
|
50
|
+
console.log(`\n ${color.bold("Available repos:")}`);
|
|
51
51
|
allRepos.forEach((repo, i) => {
|
|
52
52
|
const gitId = repo.id || repo.gitId;
|
|
53
53
|
const url = repo.repository_url || repo.repositoryUrl || "";
|
|
@@ -79,12 +79,12 @@ async function addProject(opts = {}) {
|
|
|
79
79
|
? color.dim(" last synced " + timeSince(local.lastSyncedAt))
|
|
80
80
|
: "";
|
|
81
81
|
|
|
82
|
-
console.log(`
|
|
82
|
+
console.log(` [${i + 1}] ${color.bold(displayName)} ${label}${syncInfo}`);
|
|
83
83
|
});
|
|
84
|
-
console.log(`
|
|
84
|
+
console.log(` [${allRepos.length + 1}] Register a new repo first (niro add repo)`);
|
|
85
85
|
|
|
86
86
|
const selectionRaw = await prompt.ask(
|
|
87
|
-
`\n
|
|
87
|
+
`\n Select repos to include (e.g. 1,3)`,
|
|
88
88
|
""
|
|
89
89
|
);
|
|
90
90
|
if (!selectionRaw.trim()) {
|
|
@@ -114,10 +114,10 @@ async function addProject(opts = {}) {
|
|
|
114
114
|
// on create below.
|
|
115
115
|
let alias;
|
|
116
116
|
while (true) {
|
|
117
|
-
const candidate = await prompt.ask("
|
|
117
|
+
const candidate = await prompt.ask(" Project name", defaultName);
|
|
118
118
|
if (!candidate) { console.error(color.red("Project name is required.")); return; }
|
|
119
119
|
if (await aliasTaken(candidate, opts)) {
|
|
120
|
-
console.log(color.yellow(`
|
|
120
|
+
console.log(color.yellow(` "${candidate}" already exists — pick another.`));
|
|
121
121
|
} else {
|
|
122
122
|
alias = candidate;
|
|
123
123
|
break;
|
|
@@ -130,7 +130,7 @@ async function addProject(opts = {}) {
|
|
|
130
130
|
gitIdToMapping[repo.id || repo.gitId] = {};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
console.log(`\n
|
|
133
|
+
console.log(`\n Creating project "${color.bold(alias)}"...`);
|
|
134
134
|
const project = await api.post("/api/projects", {
|
|
135
135
|
alias,
|
|
136
136
|
git_id_to_mapping: gitIdToMapping,
|
|
@@ -148,11 +148,11 @@ async function addProject(opts = {}) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
console.log(`
|
|
151
|
+
console.log(` ${color.green("✓")} Project created: ${color.bold(alias)}`);
|
|
152
152
|
|
|
153
|
-
const shouldBuild = await prompt.confirm("
|
|
153
|
+
const shouldBuild = await prompt.confirm(" Trigger a full index build now?", true);
|
|
154
154
|
if (shouldBuild) {
|
|
155
|
-
console.log(`
|
|
155
|
+
console.log(` Triggering build...`);
|
|
156
156
|
await api.post(
|
|
157
157
|
`/api/graph/${encodeURIComponent(projectId)}/build`,
|
|
158
158
|
{},
|
|
@@ -161,7 +161,7 @@ async function addProject(opts = {}) {
|
|
|
161
161
|
if (opts.watch !== false) {
|
|
162
162
|
await watchStatus(projectId);
|
|
163
163
|
} else {
|
|
164
|
-
console.log(`
|
|
164
|
+
console.log(` Build triggered. Run ${color.cyan("niro status")} to track progress.`);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
package/src/commands/add-repo.js
CHANGED
|
@@ -87,7 +87,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
87
87
|
const reuse = noConfirm
|
|
88
88
|
? true
|
|
89
89
|
: await prompt.confirm(
|
|
90
|
-
`
|
|
90
|
+
` This folder is already registered (gitId: ${color.dim(existing.gitId)}). Re-sync without re-registering?`,
|
|
91
91
|
true
|
|
92
92
|
);
|
|
93
93
|
if (reuse) {
|
|
@@ -101,7 +101,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
101
101
|
let branch = opts.branch || null;
|
|
102
102
|
|
|
103
103
|
if (gitInfo.repoRoot) {
|
|
104
|
-
console.log(`\n
|
|
104
|
+
console.log(`\n Detected a git repository.`);
|
|
105
105
|
let choice;
|
|
106
106
|
if (syncMode) {
|
|
107
107
|
choice = syncMode === "remote" ? "2" : "1";
|
|
@@ -109,14 +109,14 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
109
109
|
choice = "1"; // default to local sync without prompting
|
|
110
110
|
} else {
|
|
111
111
|
choice = await prompt.ask(
|
|
112
|
-
`
|
|
112
|
+
` Sync on: [1] local file changes [2] commits to remote`,
|
|
113
113
|
"1"
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
if (choice.trim() === "2") {
|
|
117
117
|
// Register as remote repo using the origin URL
|
|
118
118
|
if (!gitInfo.originUrl) {
|
|
119
|
-
console.error(color.red("
|
|
119
|
+
console.error(color.red(" No remote origin found. Cannot register as remote repo."));
|
|
120
120
|
process.exitCode = 1;
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
@@ -128,10 +128,10 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
128
128
|
const confirmed = (noConfirm || prompt.isNonInteractive())
|
|
129
129
|
? true
|
|
130
130
|
: await prompt.confirm(
|
|
131
|
-
`
|
|
131
|
+
` Detected branch: ${color.cyan(gitInfo.branch)}. Use this?`,
|
|
132
132
|
true
|
|
133
133
|
);
|
|
134
|
-
branch = confirmed ? gitInfo.branch : await prompt.ask("
|
|
134
|
+
branch = confirmed ? gitInfo.branch : await prompt.ask(" Branch name", "main");
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -141,10 +141,10 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
141
141
|
const displayName = opts.name
|
|
142
142
|
|| ((noConfirm || prompt.isNonInteractive())
|
|
143
143
|
? folderName
|
|
144
|
-
: await prompt.ask("
|
|
144
|
+
: await prompt.ask(" Repo display name", folderName));
|
|
145
145
|
|
|
146
146
|
// Build manifest
|
|
147
|
-
process.stdout.write(`\n
|
|
147
|
+
process.stdout.write(`\n Scanning ${color.cyan(targetDir)}...`);
|
|
148
148
|
const manifest = folderSync.buildManifest(targetDir);
|
|
149
149
|
process.stdout.write(` ${manifest.length} files found.\n`);
|
|
150
150
|
|
|
@@ -154,7 +154,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
154
154
|
let excludePatterns;
|
|
155
155
|
|
|
156
156
|
// Register first (server allocates workspace dir) so we can call /filter
|
|
157
|
-
console.log(`
|
|
157
|
+
console.log(` Registering repo with server...`);
|
|
158
158
|
const cliModuleExcludes = Array.isArray(opts.moduleExclude) ? opts.moduleExclude : [];
|
|
159
159
|
const cliFileExcludes = Array.isArray(opts.fileExclude) ? opts.fileExclude : [];
|
|
160
160
|
gitId = await agentApi.registerRepo({
|
|
@@ -164,7 +164,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
164
164
|
moduleExcludePatterns: cliModuleExcludes,
|
|
165
165
|
fileExcludePatterns: cliFileExcludes,
|
|
166
166
|
});
|
|
167
|
-
console.log(`
|
|
167
|
+
console.log(` gitId: ${color.dim(gitId)}`);
|
|
168
168
|
|
|
169
169
|
// Offer backend-suggested module exclusions (skipped if --defaults or
|
|
170
170
|
// --module-exclude already supplied or stdin is non-interactive).
|
|
@@ -172,31 +172,31 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
172
172
|
await offerModuleExcludeSuggestions(gitId, opts, cliModuleExcludes);
|
|
173
173
|
|
|
174
174
|
// Get filter recommendation
|
|
175
|
-
console.log(`
|
|
175
|
+
console.log(` Analyzing project structure...`);
|
|
176
176
|
const filterResult = await agentApi.filterManifest(gitId, manifest);
|
|
177
177
|
folderSync.warnOversizeFiles(manifest, filterResult, (m) => console.log(color.yellow(m)));
|
|
178
178
|
const includedCount = filterResult.included_count || filterResult.includedCount || 0;
|
|
179
179
|
const excludedCount = filterResult.excluded_count || filterResult.excludedCount || 0;
|
|
180
180
|
|
|
181
|
-
console.log(`\n
|
|
182
|
-
console.log(`
|
|
181
|
+
console.log(`\n ${color.green("Will sync:")} ${includedCount} files`);
|
|
182
|
+
console.log(` ${color.yellow("Excluded:")} ${excludedCount} files`);
|
|
183
183
|
|
|
184
184
|
const suggestedIncludes = filterResult.include_patterns || filterResult.includePatterns || [];
|
|
185
185
|
const suggestedExcludes = filterResult.exclude_patterns || filterResult.excludePatterns || [];
|
|
186
186
|
|
|
187
187
|
if (suggestedIncludes.length > 0) {
|
|
188
|
-
console.log(`
|
|
188
|
+
console.log(` Include patterns: ${color.dim(suggestedIncludes.join(", "))}`);
|
|
189
189
|
}
|
|
190
190
|
if (suggestedExcludes.length > 0) {
|
|
191
|
-
console.log(`
|
|
191
|
+
console.log(` Exclude patterns: ${color.dim(suggestedExcludes.join(", "))}`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const adjust = (noConfirm || prompt.isNonInteractive())
|
|
195
195
|
? false
|
|
196
|
-
: await prompt.confirm("
|
|
196
|
+
: await prompt.confirm(" Adjust patterns?", false);
|
|
197
197
|
if (adjust) {
|
|
198
|
-
const addIncludes = await prompt.ask("
|
|
199
|
-
const addExcludes = await prompt.ask("
|
|
198
|
+
const addIncludes = await prompt.ask(" Add include patterns (comma-separated, e.g. src/**)", "");
|
|
199
|
+
const addExcludes = await prompt.ask(" Add exclude patterns (comma-separated, e.g. docs/**)", "");
|
|
200
200
|
if (addIncludes) suggestedIncludes.push(...addIncludes.split(",").map(s => s.trim()).filter(Boolean));
|
|
201
201
|
if (addExcludes) suggestedExcludes.push(...addExcludes.split(",").map(s => s.trim()).filter(Boolean));
|
|
202
202
|
}
|
|
@@ -228,7 +228,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
228
228
|
excluded: excludePatterns,
|
|
229
229
|
});
|
|
230
230
|
} catch (err) {
|
|
231
|
-
console.log(color.yellow(`
|
|
231
|
+
console.log(color.yellow(` ⚠ Could not save patterns to server: ${err.message}`));
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -244,12 +244,12 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
244
244
|
async function addRemoteRepo(url, opts) {
|
|
245
245
|
const noConfirm = opts.confirm === false;
|
|
246
246
|
const branch = opts.branch
|
|
247
|
-
|| ((noConfirm || prompt.isNonInteractive()) ? "main" : await prompt.ask("
|
|
247
|
+
|| ((noConfirm || prompt.isNonInteractive()) ? "main" : await prompt.ask(" Branch", "main"));
|
|
248
248
|
const tokenId = opts.tokenId || null;
|
|
249
249
|
const cliModuleExcludes = Array.isArray(opts.moduleExclude) ? opts.moduleExclude : [];
|
|
250
250
|
const cliFileExcludes = Array.isArray(opts.fileExclude) ? opts.fileExclude : [];
|
|
251
251
|
|
|
252
|
-
console.log(`
|
|
252
|
+
console.log(` Registering remote repo: ${color.cyan(url)} (branch: ${branch})`);
|
|
253
253
|
const reqBody = { repository_url: url, branch, token_id: tokenId };
|
|
254
254
|
if (cliModuleExcludes.length > 0) reqBody.module_exclude_patterns = cliModuleExcludes;
|
|
255
255
|
if (cliFileExcludes.length > 0) reqBody.file_exclude_patterns = cliFileExcludes;
|
|
@@ -258,12 +258,12 @@ async function addRemoteRepo(url, opts) {
|
|
|
258
258
|
if (!rec || !rec.gitId) {
|
|
259
259
|
throw new Error(`Failed to register remote repo: ${(rec && rec.status) || "unknown error"}`);
|
|
260
260
|
}
|
|
261
|
-
console.log(`
|
|
261
|
+
console.log(` ${color.green("✓")} Remote repo registered. gitId: ${color.dim(rec.gitId)}`);
|
|
262
262
|
|
|
263
263
|
const { offerModuleExcludeSuggestions } = require("../lib/moduleExcludeSuggestions");
|
|
264
264
|
await offerModuleExcludeSuggestions(rec.gitId, opts, cliModuleExcludes);
|
|
265
265
|
|
|
266
|
-
console.log(`
|
|
266
|
+
console.log(` Run ${color.cyan("niro add project")} to attach it to a project.`);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/** Parse --sync-mode local|remote. Returns the mode, or null if unset. Throws on garbage. */
|