feat: a partner in the vault — context, navigation, folders, canvases, bases, and a gate this repo can fail #7

Merged
Joel merged 4 commits from svc-remy/partner-in-crime into main 2026-09-04 10:17:45 +00:00 AGit
Member

Companion to the Connor clean-slate PR. Full description follows in the first comment.

Companion to the Connor clean-slate PR. Full description follows in the first comment.
Joel, 2026-09-03: "double check that he has full capabilities now with
interacting with obsidian? Because that's been a constant thorn where he
claims he's done the thing only to find out that it's not done. … can it
navigate or link to files for one click by the user to see where Connor is
at? or detect what page the user is on?" And: "I really want full
partnership capabilities of working with obsidian with it."

The thorn was structural, and half of it was on this side of the wire.
Reads always echoed the plugin's real reply; WRITES answered "written:
<path>" as a bare string the service threw away before narrating its own
request back. A write that landed somewhere else, or wrote nothing, read
identically to one that worked. Every mutating tool now answers JSON — the
path it wrote, the byte count, the new mtime — and the service builds
Connor's words out of that.

Everything else here is a capability that did not exist:

  WHERE JOEL IS. active-leaf-change and a debounced editor-change push a
  `context` frame (path, kind, cursor line, the other open files, this
  vault's display name); get_context answers the same on demand. The PATH
  travels, never the note — the one exception is a passage he has selected
  in a note that already syncs, whose words are in Connor's memory anyway.
  One settings toggle turns the whole thing off.

  WHERE CONNOR IS. `reveal` opens a note in his Obsidian, at a heading or a
  line. The panel gets a "where is connor?" button over the service's
  activity ledger (click to open), a "Reveal Connor's last note" command,
  and a status strip — link state, last sync, last error — because every
  failure this plugin swallowed used to be a console.warn nobody read.

  STRUCTURE (Joel: "the ability to manage folders as well - like setting up
  new folder structures"). mkdir, move through fileManager so wikilinks
  follow, tree, rmdir for an EMPTY folder only, and trash into Obsidian's
  recoverable bin — never a permanent delete. Every destination goes
  through the same folder gate a write does.

  CANVASES AND BASES (Joel: "any chance we can get capabilities in there on
  those?"). read_meta plus write(if_mtime) give the service a guarded
  read→transform→write lane, so those transforms happen in tested code and
  the model never retypes Joel's document; a file he edited meanwhile is
  REFUSED inside Vault.process(). base_rows evaluates the simple filter
  forms over metadataCache and NAMES the parts it could not apply —
  checked, not assumed: the public API exposes no Bases engine.

  ONE COMMAND AT A TIME. `command` runs an Obsidian command id, but only
  from an allowlist kept in these settings, which ships empty. Nothing
  Connor says can add to it.

Auth: the signed-in probe prefers GET /auth/me (Connor's own OIDC relying
party) and falls back to the old /vault/session on anything but a 401, so
plugin and service upgrade on their own schedules; sign-in opens
/auth/login?next=/ in the panel and sign-out calls POST /auth/logout first.

Logic that could be wrong in an interesting way is written as exported PURE
functions (momentFormat, parseBase, compileFilter, ago) — this repo has no
test runner and its gate is the compile, so at minimum the testable parts
are isolated and callable the day one arrives.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MZsJcRh1Mw4GF7atvSRCyK
One bump for the whole lane. The description says what the plugin now is
rather than what it was: he does not only file notes here, he reads,
writes, edits, organises and navigates — notes, folders, canvases and
bases.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MZsJcRh1Mw4GF7atvSRCyK
The README described a plugin that filed notes. It now describes one that
works in the vault — and, just as importantly, says where to LOOK when
something is wrong (the panel's status strip: link state, last sync, last
error), because Joel's whole complaint was being handed claims he could not
check.

Also records the build contract in the repo itself: `npm ci && npm run
build` runs tsc first, and the compile IS this plugin's gate. A past
release shipped "green" with a TypeScript error because only the service's
Python suite had run.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MZsJcRh1Mw4GF7atvSRCyK
test: a gate this repo can fail — node:test, src/lib, and CI
All checks were successful
gate / gate (pull_request) Successful in 3m36s
e11bf3c0b8
Joel: "obsidian-connor has no test framework / ci, if you can, try and
change that so that it can be tested in some way when we make commits,
that keeps tripping connor up."

It tripped him up in a specific way. Connor builds this plugin in an
isolated clone and ships the result as a PR; his runner only knew how to
run `python3 -m unittest`, which a TypeScript plugin has none of. So
"the tests passed" meant "there were no tests", and a build with a
compile error in it shipped green. This is half the fix — the repo's own
definition of green. The other half is his runner learning to detect and
run it.

THE RUNNER, with zero new runtime deps and no new dev deps: Node's
built-in node:test + node:assert. esbuild.test.mjs (esbuild is already
here for the plugin) bundles test/*.test.ts to test/.build/*.mjs, and
`npm test` runs `node --test` over them. The `obsidian` import is
ALIASED to test/stubs/obsidian.ts rather than marked external — the real
module exists only inside Electron — with one piece of real behaviour in
it, normalizePath, because a write rule for "Connor/" has to govern
"Connor/x.md" for the permit tests to mean anything.

  npm run check   tsc -noEmit -skipLibCheck, over src/ AND test/
  npm run build   check, then the bundle
  npm test        bundle the tests, then node --test
  npm run gate    all three. This is what CI runs and what green means.

MADE TESTABLE. src/main.ts had a "PURE HELPERS" block whose own comment
said the logic was written as pure functions "directly callable the day a
runner is added". That day; and the same treatment for the logic that
had not been split out. Eleven modules under src/lib, one per decision,
none touching `app`, the network or Obsidian's runtime:

  link.ts     the held link's SSE framing, frame decode, reply encode
  feed.ts     the change feed's seq arithmetic, coalescing, batch reading
  permits.ts  folder sync flags and write permits, nearest-rule-wins
  base.ts     the .base reader and filter evaluator
  edits.ts    the write/modify refusals and the exact bytes written
  paths.ts    the wire path denylist
  tree.ts     the folder walk
  session.ts  the /auth/me -> /vault/session probe fallback
  mcp.ts      the loopback server's screening and JSON-RPC framing
  knock.ts    the knock URL check
  format.ts   moment tokens, "3m ago", the daily-note path

main.ts imports from them instead of repeating them, so what the tests
pin is what runs. main.ts is 375 lines shorter and the bundle is
byte-verified to contain each moved function.

115 tests. They pin the things that fail INVISIBLY in production: an SSE
frame split across chunk boundaries, a seq hole read as a gap, a skipped
event resent unnumbered, a permit override cascading, a rename that
moves the mirror instead of re-embedding, if_mtime refusing a clobber, an
ambiguous find writing nothing, a 401 from /auth/me NOT falling through
to the old probe, an Origin header refused at the loopback door.

BEHAVIOUR IS UNCHANGED by the move. Where a test expectation and the
code disagreed, the code won and the test was rewritten to say what the
code does — three times. One of those is left as a named KNOWN GAP in
test/base.test.ts: `status == "a" && due < "2026"` compiles to a
comparison against one long literal instead of landing in `unsupported`.
It matches nothing, so no note is wrongly included, but the filter is
counted as supported — the one thing that module promises never to do.
Pinned rather than fixed, so a fix has to come past the test and update
the tool description with it.

The one deliberate change: KnockModal now refuses a knock URL that is not
a URL, is a bare site root, or is not http(s), instead of storing it. A
mistyped knock fails later as an unexplained 401 in a component that has
no idea a knock exists.

CI: .forgejo/workflows/gate.yml, modelled on Connor's — self-hosted
runner, actions/checkout@v4, `npm ci` then `npm run gate`. The job is
named so the status context is exactly "gate / gate (pull_request)",
which is what branch protection requires. engines pinned to node >=20
(the host runs v22.23.1, npm 10.9.8).

main.js, node_modules/ and test/.build/ stay untracked. manifest version
left at 0.4.0 as the lane set it.

Verified: `npm ci && npm run gate` green locally, 115/115.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MZsJcRh1Mw4GF7atvSRCyK
Joel approved these changes 2026-09-04 10:17:40 +00:00
Joel merged commit 2ed0088429 into main 2026-09-04 10:17:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ZSDev/obsidian-connor!7
No description provided.