summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:43:08 -0300
committerGabriel Schneider <[email protected]>2026-08-03 09:54:39 -0300
commit5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch)
treef12f4b700fff4ff6f7e818d2a542672ba0b8d76e /main.go
parenta9263daee9413c5eff3c1f1bebcd224442fd8685 (diff)
downloadnotevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz
notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly what vrsite did, and "notevi read/grep/note/query" is unchanged. The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the description "private: notevi log". The pre-rebrand names are still recognized, so an old repository opens and reads; it is renamed in place on the first write, or up front with "notevi migrate DIR...". That rename cannot be a single mv inside jj run. jj only auto-tracks a *new* file in the run working copy below a size limit it does not take from the command line, so writing a whole log under a name the change has never held is silently dropped while jj reports success. ensureLogFile creates the file empty first and lets every later byte be a modification of a tracked file, which snapshots at any size; that also fixes the same latent bug when importing a large legacy vr-log.jsonl. Adds a bem-te-vi mark (favicon and nav brand) and a README. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'main.go')
-rw-r--r--main.go84
1 files changed, 68 insertions, 16 deletions
diff --git a/main.go b/main.go
index 1d263cb..2924d1c 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,14 @@
-// vr — file reader / note taker for coding agents, logging every call.
+// notevi — read code, take notes, read the trace back; every call is logged.
//
-// Each operation appends one JSON line to a private jj sidecar recording who read what:
+// Named for the bem-te-vi, the kiskadee that announces itself all day and keeps
+// a bright crown patch hidden under plain feathers: the notes are there the
+// whole time, tucked out of the project's way in a private jj sidecar.
+//
+// Each operation appends one JSON line to that sidecar recording who read what:
// session id, agent, model, reasoning effort, working dir, full jj change and
// commit ids, file and line range. Reads and greps are relative to a jj
// revision (default @, the working copy) — the tool assumes a jj repo.
+// "notevi web" serves the same log as a browsable site.
//
// Agent identity comes from the caller's environment (Claude Code exports
// CLAUDE_CODE_SESSION_ID, AI_AGENT, CLAUDE_EFFORT into tool subprocesses); when
@@ -29,7 +34,8 @@ import (
"strings"
"time"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
+ "0x4200.cafe/notevi/web"
)
type Entry struct {
@@ -76,6 +82,11 @@ func main() {
fmt.Print(queryDoc)
return
}
+ // web serves the log rather than appending to it, and brings its own flags.
+ if os.Args[1] == "web" {
+ web.Main(os.Args[2:])
+ return
+ }
detect()
switch os.Args[1] {
case "read":
@@ -86,36 +97,43 @@ func main() {
cmdNote(os.Args[2:])
case "query":
cmdQuery(os.Args[2:])
+ case "migrate":
+ cmdMigrate(os.Args[2:])
default:
usage()
}
}
func usage() {
- fmt.Fprint(os.Stderr, `vr — file reader / note taker for coding agents; logs every call.
+ fmt.Fprint(os.Stderr, `notevi — read code and take notes; logs every call, hides them in a sidecar.
usage:
- vr read [-r REV] FILE[:START[-END]]
+ notevi read [-r REV] FILE[:START[-END]]
print a file line-numbered, whole or a line range, at a jj revision
- vr grep [-r REV] PATTERN [PATH]
+ notevi grep [-r REV] PATTERN [PATH]
search the working copy (rg) or another revision (jj file list/show);
scope non-@ greps with PATH, they read every file through jj
- vr note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...
+ notevi note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...
record a note about a file range or a kind of thing (struct, enum,
const, ...), optionally replying to a log-wide note number; ids count
up from 1 per session
- vr query [-session S] [-op OP] [-file SUBSTR] [-n N]
+ notevi query [-session S] [-op OP] [-file SUBSTR] [-n N]
print logged entries, oldest first
+ notevi web [-repo DIR] [-out DIR] ...
+ browse the trace: repo tree, coverage, notes; "notevi web -h" for more
+ notevi migrate [DIR...]
+ rename a pre-notevi sidecar in place; run once per old repository
REV is any jj revset, default @ (working copy). The JSONL log lives in the
-private jj sidecar named by the repo-local revset alias vr_log.
-"vr -doc" prints the short agent-facing usage doc; "vr -doc-query" the query one.
+private jj sidecar named by the repo-local revset alias notevi_log.
+"notevi -doc" prints the short agent-facing usage doc; "notevi -doc-query" the
+query one.
`)
os.Exit(2)
}
func fatal(format string, a ...any) {
- fmt.Fprintf(os.Stderr, "vr: "+format+"\n", a...)
+ fmt.Fprintf(os.Stderr, "notevi: "+format+"\n", a...)
os.Exit(1)
}
@@ -252,7 +270,7 @@ func jjOut(args ...string) string {
}
// --ignore-working-copy skips the snapshot: change ids are stable across
-// snapshots, and this keeps `vr note`/`vr grep` working in sandboxes that
+// snapshots, and this keeps `notevi note`/`notevi grep` working in sandboxes that
// block .git writes (codex workspace-write). read/grep -r snapshot anyway
// via their own jj file show/list calls.
func revisionIDs(rev string) (change, commit string) {
@@ -290,7 +308,7 @@ func cmdRead(args []string) {
rev := fs.String("r", "@", "jj revision to read at")
fs.Parse(args)
if fs.NArg() != 1 {
- fatal("usage: vr read [-r REV] FILE[:START[-END]]")
+ fatal("usage: notevi read [-r REV] FILE[:START[-END]]")
}
file, start, end := parseFileRange(fs.Arg(0))
@@ -320,7 +338,7 @@ func cmdGrep(args []string) {
rev := fs.String("r", "@", "jj revision to search at")
fs.Parse(args)
if fs.NArg() < 1 || fs.NArg() > 2 {
- fatal("usage: vr grep [-r REV] PATTERN [PATH]")
+ fatal("usage: notevi grep [-r REV] PATTERN [PATH]")
}
pattern, path := fs.Arg(0), fs.Arg(1)
@@ -372,7 +390,7 @@ func cmdNote(args []string) {
fs.Parse(args)
text := strings.Join(fs.Args(), " ")
if text == "" {
- fatal("usage: vr note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...")
+ fatal("usage: notevi note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...")
}
if *reply < 0 {
fatal("reply note must be positive")
@@ -392,7 +410,7 @@ func cmdNote(args []string) {
fmt.Printf("note %d\n", e.NoteID)
}
-// hasNote uses the same numbering as vrsite permalinks: note operations count
+// hasNote uses the same numbering as the web permalinks: note operations count
// from one over the append-only log, independent of their writers' sessions.
func hasNote(entries []Entry, id int) bool {
n := 0
@@ -486,6 +504,40 @@ func cmdQuery(args []string) {
}
}
+// cmdMigrate renames pre-notevi sidecars. It takes directories rather than only
+// working on the current one so that a machine full of old repositories can be
+// brought over in one run.
+func cmdMigrate(args []string) {
+ fs := flag.NewFlagSet("migrate", flag.ExitOnError)
+ fs.Parse(args)
+ dirs := fs.Args()
+ if len(dirs) == 0 {
+ dirs = []string{gDir}
+ }
+ failed := false
+ for _, dir := range dirs {
+ s, err := sidecar.Open(dir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "notevi: %s: %v\n", dir, err)
+ failed = true
+ continue
+ }
+ migrated, err := s.Migrate()
+ switch {
+ case err != nil:
+ fmt.Fprintf(os.Stderr, "notevi: %s: %v\n", s.Root(), err)
+ failed = true
+ case migrated:
+ fmt.Printf("%s: renamed to %s\n", s.Root(), s)
+ default:
+ fmt.Printf("%s: nothing to rename\n", s.Root())
+ }
+ }
+ if failed {
+ os.Exit(1)
+ }
+}
+
func fileRange(e Entry) string {
s := e.File
if e.Start > 0 {