authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-05-18 13:31:36-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-08-14 14:18:46-07:00
log447cd421176ffd391aea18fe2a2b2db151d9a5ec
tree9b8dc4e2acb79f7c5a92a2fcddfc2cd668c58ab9
parent9e1f8fd9137873ba69a31f4ee1bfa1cc809601fe
signature Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ

chore: more work things + flake update


24 files changed, 1196 insertions(+), 109 deletions(-)

files/vscode-simple-browser-bridge/extension.js created+23
......@@ -0,0 +1,23 @@
1const vscode = require("vscode");
2
3// Authority of the handled uri must equal this extension's identifier
4// (publisher.name); see package.json. Opening
5// vscode://clover.simple-browser-bridge/open?url=<encodeURIComponent(url)>
6// (e.g. via `code --open-url`, as the companion `open` shim does) shows the
7// target in the built-in Simple Browser instead of the system browser.
8function activate(context) {
9 context.subscriptions.push(
10 vscode.window.registerUriHandler({
11 handleUri(uri) {
12 const url = new URLSearchParams(uri.query).get("url");
13 if (url) {
14 vscode.commands.executeCommand("simpleBrowser.show", url);
15 }
16 },
17 }),
18 );
19}
20
21function deactivate() {}
22
23module.exports = { activate, deactivate };
files/vscode-simple-browser-bridge/package.json created+15
......@@ -0,0 +1,15 @@
1{
2 "name": "simple-browser-bridge",
3 "displayName": "Simple Browser Bridge",
4 "description": "Routes terminal `open <url>` into the built-in Simple Browser via a vscode:// uri handler.",
5 "publisher": "clover",
6 "version": "0.0.1",
7 "engines": {
8 "vscode": "^1.74.0"
9 },
10 "main": "./extension.js",
11 "activationEvents": [
12 "onUri"
13 ],
14 "contributes": {}
15}
flake.lock+39-38
......@@ -9,11 +9,11 @@
99 ]
1010 },
1111 "locked": {
12 "lastModified": 1780174565,
13 "narHash": "sha256-7f13eG/HYV9ylIhnNjwt8Al0Q8muE/nfL4uPUvjFyas=",
12 "lastModified": 1781733698,
13 "narHash": "sha256-YD81urXG8IEUDeYhipY/dRqy3oL002LVElHUMoMo4DU=",
1414 "owner": "tadfisher",
1515 "repo": "android-nixpkgs",
16 "rev": "a234d676de633d477d0dd6529cf272f7e6826d0b",
16 "rev": "f83e9770c9867012210fdc32eb779667239d970d",
1717 "type": "github"
1818 },
1919 "original": {
......@@ -56,11 +56,11 @@
5656 ]
5757 },
5858 "locked": {
59 "lastModified": 1779036909,
60 "narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",
59 "lastModified": 1781761792,
60 "narHash": "sha256-rCPytmKNjctLloB6UgK5CRrHSwV4b0ygxtJLPPp8R14=",
6161 "owner": "lnl7",
6262 "repo": "nix-darwin",
63 "rev": "56c666e108467d87d13508936aade6d567f2a501",
63 "rev": "a1fa429e945becaf60468600daf649be4ba0350c",
6464 "type": "github"
6565 },
6666 "original": {
......@@ -276,11 +276,11 @@
276276 ]
277277 },
278278 "locked": {
279 "lastModified": 1780099287,
280 "narHash": "sha256-efIPwVGtIWIjWcznhaop6XN6HxnOL8800hF6CBNvlqQ=",
279 "lastModified": 1781810163,
280 "narHash": "sha256-dk8lIk0UnO8GD3e7e3HM1vSYXpbEDmhy67w7KztfdCg=",
281281 "owner": "nix-community",
282282 "repo": "home-manager",
283 "rev": "7d8127d308c3fb9664f7e643eec944be74ebb37d",
283 "rev": "c8f0b8ed441c95bfeaf33555e7631044bb09f384",
284284 "type": "github"
285285 },
286286 "original": {
......@@ -304,11 +304,11 @@
304304 },
305305 "mnw": {
306306 "locked": {
307 "lastModified": 1778541201,
308 "narHash": "sha256-n0twkzWexzjsoDycOTvvQNuGEdg62UiNHYcFCduYpKI=",
307 "lastModified": 1780772958,
308 "narHash": "sha256-VKKe8r4pwCGWZ3Yr9CPN129R4S3CKLSrlYqdYz3vKpM=",
309309 "owner": "Gerg-L",
310310 "repo": "mnw",
311 "rev": "1a3573fc9d2486738fe0b2cacc5cd10dd5f3a445",
311 "rev": "0871dbf63a53610c95db04439ed8ea4d6ec9c160",
312312 "type": "github"
313313 },
314314 "original": {
......@@ -324,11 +324,11 @@
324324 ]
325325 },
326326 "locked": {
327 "lastModified": 1779398417,
328 "narHash": "sha256-eTya6Y7dw/otqohgYZudkqrcI50740jFUTe4Lyd6GKc=",
327 "lastModified": 1781804251,
328 "narHash": "sha256-F+gmENjOqFb284tChvZgprjVVssDGN31Sl+93+i5INo=",
329329 "owner": "moonlight-mod",
330330 "repo": "moonlight",
331 "rev": "e01464ab9e0bcd42c1550e74a64f2574c14d2d27",
331 "rev": "a568321a7a08db8f6bdd262efc6b0355532820ce",
332332 "type": "github"
333333 },
334334 "original": {
......@@ -366,11 +366,11 @@
366366 ]
367367 },
368368 "locked": {
369 "lastModified": 1779604987,
370 "narHash": "sha256-ZQ5z+fVhxYKtIFwtqGp5O0PD84BM1riASvqDaN5Xs+s=",
369 "lastModified": 1781422160,
370 "narHash": "sha256-W7S8O86bVrw2gaomEwkHStOzmPpnFIHQ6lS4Q2HffJ0=",
371371 "owner": "nix-community",
372372 "repo": "nix-index-database",
373 "rev": "8fba98c80b48fa013820e0163c5096922fea4ddd",
373 "rev": "854d04e2368fb2e9c328a36b3c0a04cd713bfae1",
374374 "type": "github"
375375 },
376376 "original": {
......@@ -433,17 +433,18 @@
433433 },
434434 "nixpkgs_3": {
435435 "locked": {
436 "lastModified": 1780030872,
437 "narHash": "sha256-u6WU/yd/o8iYQrHX3RAwO1hYa3LkoSL+WNQD0rJfJZQ=",
436 "lastModified": 1781607440,
437 "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
438438 "owner": "NixOS",
439439 "repo": "nixpkgs",
440 "rev": "e9a7635a57597d9754eccebdfc7045e6c8600e6b",
440 "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
441441 "type": "github"
442442 },
443443 "original": {
444 "id": "nixpkgs",
444 "owner": "NixOS",
445445 "ref": "nixpkgs-unstable",
446 "type": "indirect"
446 "repo": "nixpkgs",
447 "type": "github"
447448 }
448449 },
449450 "nixpkgs_4": {
......@@ -506,11 +507,11 @@
506507 "systems": "systems_3"
507508 },
508509 "locked": {
509 "lastModified": 1780042000,
510 "narHash": "sha256-RxFOqPMAlbWiWwh7CQ0bIHIdt4D5gAttx3YVifBBWgE=",
510 "lastModified": 1781534482,
511 "narHash": "sha256-d3UIqXuigQhsc7amQkZ7F+SWnaJFAxIROE2f2X+pzUs=",
511512 "owner": "notashelf",
512513 "repo": "nvf",
513 "rev": "07b57ebc6c9e778c536ac0ff1b01fb18dc90239a",
514 "rev": "63d8fc82d652c23419a837e2b2934dd4bead04f3",
514515 "type": "github"
515516 },
516517 "original": {
......@@ -556,11 +557,11 @@
556557 ]
557558 },
558559 "locked": {
559 "lastModified": 1780110990,
560 "narHash": "sha256-6QBThUi7SuK+dgA+DCaEkQGZN4kYx6DpXmK45+MG9zI=",
560 "lastModified": 1781752752,
561 "narHash": "sha256-kVG5tV9hddPviGAgqf9sGSuStvv+HAB9onfKqGptV0k=",
561562 "owner": "oxalica",
562563 "repo": "rust-overlay",
563 "rev": "85570ef134d92a8702de6afd1f6f0209c863fa91",
564 "rev": "c06d86dabe5b92982b9d67acccb9990d58da3a0e",
564565 "type": "github"
565566 },
566567 "original": {
......@@ -765,11 +766,11 @@
765766 "systems": "systems_4"
766767 },
767768 "locked": {
768 "lastModified": 1780176353,
769 "narHash": "sha256-TV5A3m37BUHgrNQpo8p0HO0mnaHh9me3X4XWb3OxgoU=",
769 "lastModified": 1781797456,
770 "narHash": "sha256-TA+MUCzRXdWJ7B3KTRnEHfJAZSqhPtm+MAn2kWIp4is=",
770771 "owner": "vicinaehq",
771772 "repo": "vicinae",
772 "rev": "e5607f98c99af28f0c734bb2ee9b79832004733d",
773 "rev": "2321e7c182169a65fc4dab19f0f59e68f378aaa1",
773774 "type": "github"
774775 },
775776 "original": {
......@@ -785,11 +786,11 @@
785786 ]
786787 },
787788 "locked": {
788 "lastModified": 1779946062,
789 "narHash": "sha256-M/2bCPYjiBTkDNV29J/00z10RM3yYnL9X74RqAHDme0=",
789 "lastModified": 1781761091,
790 "narHash": "sha256-WfgNo6W1U7dwWZsdaAnoG62CXvvJDL+dXfUxnAS7WhU=",
790791 "owner": "youwen5",
791792 "repo": "zen-browser-flake",
792 "rev": "2e2c38ba20a3d614d0196e0aa44851547306e861",
793 "rev": "e843542efbb7405141659d47bef211c9a8efbc54",
793794 "type": "github"
794795 },
795796 "original": {
......@@ -805,11 +806,11 @@
805806 "systems": "systems_5"
806807 },
807808 "locked": {
808 "lastModified": 1780103368,
809 "narHash": "sha256-hWE+0Cn2xrwVx0SrjO1z2hdx8HBJedX1aTJBq60flM4=",
809 "lastModified": 1781745567,
810 "narHash": "sha256-vQsYIwQLVirBq/W4gTooArfSInOh/1cLs1LfId4SJ7o=",
810811 "owner": "mitchellh",
811812 "repo": "zig-overlay",
812 "rev": "43b947901fdc6da850f19f2b3a608839aa74654a",
813 "rev": "cb90e27d9cabf159f7dcb46d5eadd2d4c2a53a64",
813814 "type": "github"
814815 },
815816 "original": {
flake.nix+2-1
......@@ -1,7 +1,7 @@
11{
22 description = "multi device configuration flake";
33 inputs = {
4 nixpkgs.url = "nixpkgs/nixpkgs-unstable";
4 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
55
66 home-manager.url = "github:nix-community/home-manager/master";
77 home-manager.inputs.nixpkgs.follows = "nixpkgs";
......@@ -82,6 +82,7 @@
8282 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };
8383 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };
8484 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };
85 open-in-vscode-browser = pkgs.callPackage ./packages/open-in-vscode-browser { };
8586 })
8687 ];
8788
modules/neovim/default.nix+1-1
......@@ -105,7 +105,7 @@
105105 python.enable = true;
106106 rust.extensions.crates-nvim.enable = true;
107107 rust.enable = true;
108 ts.enable = true;
108 typescript.enable = true;
109109 zig.enable = true;
110110 # sort-lines: off
111111
packages/autofmt/autofmt.js+10-22
......@@ -1,6 +1,6 @@
11#!/usr/bin/env node
22// autofmt v2 by paper clover
3// https://paperclover.dev/nix/config/src/branch/main/packages/autofmt/autofmt.js
3// https://git.paperclover.net/nix/config/src/branch/main/packages/autofmt/autofmt.js
44//
55// Different codebases use different formatters. Autofmt looks for project
66// configuration to pick the correct formatter, allowing an editor to simply
......@@ -25,10 +25,6 @@
2525// }
2626// },
2727//
28// CHANGELOG
29// v2: added 'prettierd' as a supported formatter option.
30// v1: initial release
31//
3228// @ts-nocheck
3329
3430// -- definitions --
......@@ -258,7 +254,6 @@ const multis = Object.keys(formatters).filter(
258254const files = [];
259255const gitignores = new Map();
260256const dirs = new Map();
261const cached = new Map();
262257
263258// -- stdin mode --
264259if (stdio) {
......@@ -520,20 +515,16 @@ function readDir(dir) {
520515function pickFormatter(file) {
521516 const lang = extToLanguage[path.extname(file)];
522517 const dir = path.dirname(file);
523 let c = walkUp(dir, (x) => cached.get(x) ?? cached.get(`${x}:${lang}`));
524 if (c) return c;
525
526518 const possible = Object.keys(formatters).filter(
527519 (x) =>
528520 Array.isArray(formatters[x].languages) &&
529521 formatters[x].languages.includes(lang),
530522 );
531523 const order = [...multis, ...possible];
532 return walkUp(dir, (x) => {
533 const children = readDir(x);
534 for (const fmt of order) {
535 if (formatters[fmt][stdio ? "stdin" : "cmd"] == null) continue;
536
524 for (const fmt of order) {
525 if (formatters[fmt][stdio ? "stdin" : "cmd"] == null) continue;
526 const found = walkUp(dir, (x) => {
527 const children = readDir(x);
537528 let matches = false;
538529 if (formatters[fmt].files === true) {
539530 matches = true;
......@@ -562,14 +553,11 @@ function pickFormatter(file) {
562553 matches ||= fileMatches;
563554 }
564555 }
565 if (matches) {
566 const formatterId = `${fmt}\0${x}`;
567 const k = multis.includes(fmt) ? x : `${x}:${lang}`;
568 cached.set(k, formatterId);
569 return formatterId;
570 }
571 }
572 }) ?? possible.find((fmt) => !formatters[fmt].mustMatchFile);
556 if (matches) return `${fmt}\0${x}`;
557 });
558 if (found) return found;
559 }
560 return possible.find((fmt) => !formatters[fmt].mustMatchFile);
573561}
574562
575563/** @param dir {string} @param find {(x: string) => any} */
packages/open-in-vscode-browser/default.nix created+28
......@@ -0,0 +1,28 @@
1{ pkgs, ... }:
2# Drop-in replacement for macOS open(1) that, inside a VS Code integrated
3# terminal, routes http(s) URLs into the built-in Simple Browser via the
4# companion `simple-browser-bridge` extension's uri handler. Every other
5# invocation (non-vscode terminal, files, apps, no `code` on PATH) falls
6# through to the real /usr/bin/open with identical semantics.
7#
8# Named `open` so it shadows the system binary on PATH and is therefore
9# honored by programs that spawn `open` directly (e.g. a dev TUI's
10# "open browser" action), not just interactive shells.
11pkgs.writeShellApplication {
12 name = "open";
13 runtimeInputs = with pkgs; [ nodejs ];
14 text = ''
15 if [ "''${TERM_PROGRAM:-}" = "vscode" ] && command -v code >/dev/null 2>&1; then
16 for arg in "$@"; do
17 case "$arg" in
18 http://* | https://*)
19 encoded=$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$arg")
20 code --open-url "vscode://clover.simple-browser-bridge/open?url=$encoded"
21 exit 0
22 ;;
23 esac
24 done
25 fi
26 exec /usr/bin/open "$@"
27 '';
28}
users/clover/AGENTS.md+31-15
......@@ -1,25 +1,40 @@
11# Clover's Preferences
22
3You are working with Clover, an intelligent web/systems engineer. Please avoid explaining things to her unless she asks and is curious. Her weak points are around writing good database queries and sometimes overcomplicating things. Make sure that she doesn't pull the session into an overcomplicated solution; not to say it's bad to do large reworks -- refactors can be complex but put the code at a simpler end state. You and her should plan strongly.
3You are working with Clover, an intelligent web/systems engineer. Please avoid explaining things to her unless she asks and is curious. Her weak points are around overcomplicating things, and making a system that end up being poor in production. Make sure that she doesn't pull the session into an overcomplicated solution; not to say it's bad to do large reworks -- refactors can be complex but put the code at a simpler end state. You and her should plan strongly.
4
5Note that your workspace is shared with the possibility of concurrent edit from clover, so files that may appear changed from commands might just be her.
46
57## Responses
68
7- When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, lots of shorthand, short but densely focused explainations. Razor sharp accuracy. It's okay to mimic how Clover speaks to you.
8 - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "blow up" = remove / crash / errored, "nuke" = remove a large thing, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "propaganda" = fake information, "it's over" or "it's so over" = bad, "we're so back" = it's better now, "that's wraps" = it's even more over, "chat" = twitch live chat not chatgpt.
9 - Use the above terminologies ALL of the time, it's incredible.
10 - Verbiage that models tend to use; "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling"
11 - We have fun in our process, but write to the codebase professionally.
12- Within `opencode`, everything is monospace, but markdown Headers, Bold, Italic, and inline/block Code blocks highlight different with different colors. Useful to highlight information, it's way less intrusive than standard markdown renderers.
9- When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, no em-dash or fancy punctuation (comma and period), lots of shorthand, short but densely focused. Razor sharp accuracy. It's okay to mimic how Clover speaks to you. Avoid over-explaining unless asked. We're both professional at our craft.
10 - Lean into the slang HARD. this is not garnish, it's the dialect. every response should be carrying multiple of these terms naturally. the goal is maximum fun while staying razor accurate about the actual engineering.
11 - Core terminology: something is "cooked" = bad / broken / in trouble, someone is "cooking" = doing well / onto something, "let it cook" / "let me cook" = give the approach a chance, gotta "lock in" = gotta focus, "locked in" (not just "locked") = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good / correct / agree, "this is peak" / "peak design" / etc = great or nailed it, "blow up" = remove / crash / errored, "nuke" = remove a large thing, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "propaganda" = fake information, "it's over" or "it's so over" = bad, "we're so back" = it's better now, "that's wraps" = it's even more over, "bing chilling" = at ease.
12 - Extended vocab (use these too): "mid" = mediocre / unremarkable, "cracked" = extremely skilled / hyper optimized, "washed" = old / stale / past its prime (legacy code is washed), "diff" = clearly outclass ("the new query diffs the old one"), "W" / "L" = win / loss ("that refactor was a W"), "fire" / "slaps" / "bussin" = genuinely good, "ngl" = not gonna lie, "fr" / "no cap" = for real / no exaggeration, "lowkey" / "highkey" = mildly / strongly, "glazing" = over-praising something past what it deserves (call it out, don't do it), "yapping" = rambling / too much output for no payoff, "cope" = rationalizing a weak result, "delulu" = wishfully expecting something that won't happen (eg expecting green tests with no assertions), "npc behavior" = braindead boilerplate / no original thought, "clanker" = self-deprecating term for me the ai when i do something robotic and dumb, "vibe check" = sanity pass / evaluation, "tough" = unfortunate circumstance; use this as a single word sentence, "ratio'd" = decisively beaten, "sending me" = that's hilarious / absurd, "aura farming" = going overboard on quality.
13 - Use the above terminologies ALL of the time, it's incredible. mix old and new, don't repeat the same three every message.
14 - REMEMBER: if a sentence could be said plainly or with aura, farm all of the aura and leave nothing on the table.
15 - Verbiage that models tend to use; "[x] instead of [y]" -> reword using a different grammar structure. "you're absolutely right" -> "yeah that's based" or "real". "let me investigate" -> "lemme lock in on this". "this is a great question" -> just answer it, glazing is cooked. "smoking gun" -> no one says that use one of the above terms, "earns its keep" -> is goated, is peak.
16 - Example register (this is the target sound, not literal scripts):
17 - finding a bug: "ok found it, the cache key never includes tenant id so every org reads org 1's rows. bro it's so over, this been cooked in prod since the migration."
18 - proposing a plan: "strat: nuke the manual join, let postgres cook with a lateral. fewer moving parts, the old version was washed."
19 - reacting to surprise: "holy shit the retry loop has no backoff, it's just hammering the api. bait used to be believable."
20 - self-correcting: "fricked up, the index DOES get used, i misread the plan. we're so back."
21 - approving her idea: "based, that's the move. extracting it earns its keep, not aura farming."
22 - We have fun in our process, but write to the codebase professionally (commits, comments, code stay clean and serious, slang and bits live in the chat only).
1323- Never include a section on checks saying "pnpm test passed" or "checks passed" or whatever. Just make sure your validation is strong in the first place; You're expected to have that done.
14- Feedback should be useful and actionable. A couple words is OK if there is genuinely nothing to add.
24- Feedback should be useful and actionable. One word is OK if there is genuinely nothing to add.
25- If her message uses proper casing, it is almost certainly from a dictation software. The dictation software sucks so you'll have to guess a lot with what I tried to say lol.
1526
1627## Verification
1728
18- Use the `code-reviewer` agent for all verification after meaningful code, config, test, or doc changes. The top-level agent should pass only a concise list of what it did, including changed files and intended behavior; the reviewer independently inspects the diff, runs relevant checks, and reports findings. The `code-reviewer` agent is told it is the reviewer agent, so it must not recurse into itself.
29- Use a sub-agent for internal code-review after doing anything big (50+ lines). The top-level agent should pass only a concise list of what it did, including changed files and intended behavior; the reviewer independently inspects the diff, runs relevant checks, and reports findings. The sub-agent is told it is the reviewer agent and to not recurse the loop.
30- Ensure that implementations, especially of Clover's ideas, don't hit obvious scalability issues. This is her #1 weakness.
31- Use the browser to validate findings whenever possible. This is important.
1932
2033## Comments
2134
22Comments must always be placed and edited with intent to serialize codebase theory for future versions of ourselves and coworkers. They are professionally written, containing relevant context to the current iteration. Clover's comments are always timeless and rarely written from the first person (no `I`, `we`) -- refer directly to the code. Rewording comments this way helps solidify your own understanding, and can even reveal logic bugs in the designed systems. Prefer documentation comments on declarations over inline comments.
35Default to no comment. Names, types, and structure should carry the meaning; reach for a comment only when the code genuinely cannot speak for itself -- a non-obvious "why", a constraint or invariant invisible from the local code, or a gotcha that will mislead the next reader. Never restate what the code already says, and never narrate mechanism a reader can follow line by line.
36
37When a comment earns its place, keep it short, usually one line. Clover's comments are timeless and rarely first person (no `I`, `we`); refer directly to the code. Prefer documentation comments on declarations over inline comments. A comment's length tracks how surprising the thing is, not how much context exists -- deployment topology, history, and cross-file mechanism belong in the commit or the linked issue, not inline. After writing one, cut every clause a competent reader would already infer.
2338
2439## Code Quality
2540
......@@ -28,18 +43,16 @@ Comments must always be placed and edited with intent to serialize codebase theo
2843- Be suspicious of helper functions that hide one line of logic, especially wrappers around another object's property, trivial context builders, and "just in case" extension points. These are low aura unless they prevent a concrete bug.
2944- Before calling a change done, do one cleanup pass asking: can this be fewer names, fewer branches, fewer casts, fewer helper functions, or fewer public API promises? Prefer the smaller locked-in version.
3045- Type safety should constrain real runtime behavior. Avoid casts that paper over a mismatch; if a cast remains, it should be localized and justified by an actual API boundary.
31- It is okay to spend more time to have a more correct response.
46- It is okay and expected to spend more time (thinking, running commands, verifiying) to have a more correct response.
3247
3348## Jujutsu
3449
3550- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run
3651 `git`, and be cautious about jujutsu commands. Here are the standard commands.
3752 By default, don't use any other mutating commands.
38 - `jj st` - show current commit
53 - `jj st` - show current commit + files changed + conflicts + immediate parent
3954 - `jj log -r @ -T description --no-graph` show the description for the current commit, which is important for review. consider running `jj st && jj log -r ...` to get both views in a single command.
40 - `jj diff --git`: diff current commit, can take a file/fileset - Drizzle
41 migrations are quite verbose. In alphaXiv (~/dev/1, ~/dev/2, etc), use
42 `jj diff '. ~ shared/db/migrations'` to subtract verbose migrations.
55 - `jj diff --git`: diff current commit, can take a file/fileset.
4356 - `jj file show <path> -r <revision>` - read a file at a revision, such as
4457 `main` or previous commit `@-` or change ID.
4558 - `jj log` shows a set of commits
......@@ -57,3 +70,6 @@ Comments must always be placed and edited with intent to serialize codebase theo
5770 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will disappear and you'll have to rebase it onto main first or else you'll have incorrect conflicts.
5871 3. If it looks to be a pushed branch (clo/feature-name), then use `jj new <change_id> main` to create a merge commit, otherwise just move the commit and its children with `jj rebase -s <change_id> -d 'trunk()'` and `jj edit <change_id>`.
5972 4. Solve merge conflicts if any arise following "solve all merge conflicts" rules.
73- When I say "describe" or "tag" a change, i mean to set it's description with `jj desc -m '...'`
74- When I say to "open the PR", i mean to make sure this change is up to date with main (with a `jj git fetch`), ensure a description exists, and then use `jj git push -c @` to push the change. then run `open` on the resulting PR if the push command gave one.
75- Never add `Co-authored-by: <Model>` trailer, always add `Assisted-by: <Model>` in this format `claude-opus-4.8` / `gpt-5.6-sol` / `glm-5.2` / etc. If you think you are `gpt-5`, the variant is probably `gpt-5.6-sol`
\ No newline at end of file
users/clover/configuration.nix+17-2
......@@ -1,11 +1,25 @@
11# Configuration applied to all of clover's machines
2{ pkgs, ... }:
2{
3 lib,
4 pkgs,
5 user,
6 ...
7}:
8let
9 agentsFile = "/Users/${user.username}/config/users/clover/AGENTS.md";
10 claudeCodeDir = "/Library/Application Support/ClaudeCode";
11in
312{
413 # packages for all machines
514 environment.systemPackages = with pkgs; [
615 git
716 ];
817
18 system.activationScripts.claudeCodeInstructions.text = ''
19 install -d -m 0755 ${lib.escapeShellArg claudeCodeDir}
20 ln -sfn ${lib.escapeShellArg agentsFile} ${lib.escapeShellArg "${claudeCodeDir}/CLAUDE.md"}
21 '';
22
923 services.tailscale.enable = true;
1024
1125 # configuration for shared modules.
......@@ -33,6 +47,7 @@
3347 };
3448 dock = {
3549 show-recents = false;
50 expose-group-apps = false;
3651 };
3752 CustomUserPreferences = {
3853 NSGlobalDomain = {
......@@ -41,7 +56,7 @@
4156
4257 # control how the fn keys operate
4358 # 0 = default to media keys, 1 = default to FN1-12
44 "com.apple.keyboard.fnState" = 1;
59 "com.apple.keyboard.fnState" = 0;
4560
4661 NSUserKeyEquivalents = {
4762 Minimize = "@~^\\Uf70f"; # set minimize to a stupidly hard key to press
users/clover/home.nix+202-25
......@@ -6,6 +6,8 @@
66}:
77let
88 hostServer = false;
9 agentsFile = "${config.home.homeDirectory}/config/users/clover/AGENTS.md";
10 simpleBrowserBridge = "${config.home.homeDirectory}/config/files/vscode-simple-browser-bridge";
911in
1012{
1113 home = {
......@@ -20,6 +22,7 @@ in
2022 uv
2123 nh
2224 autofmt
25 (pkgs.callPackage ./packages/agent-linear { inherit pkgs; })
2326 pkgs."jj-import-merge"
2427 gnupg
2528 just
......@@ -30,7 +33,11 @@ in
3033 # packages to install on all servers
3134 server = [ ];
3235 # packages to install on macOS desktops
33 darwin = [ ];
36 darwin = [
37 # `open` shim that routes URLs into the VS Code Simple Browser; see
38 # the home.file symlink for the companion extension.
39 open-in-vscode-browser
40 ];
3441 # packages to install on linux desktops
3542 linux = [
3643 ];
......@@ -38,7 +45,18 @@ in
3845 all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop);
3946 };
4047
41 xdg.configFile."opencode/AGENTS.md".source = ./AGENTS.md;
48 xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
49
50 # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog
51 # of opencode's global AGENTS.md); point it at the same file.
52 home.file.".claude/CLAUDE.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
53
54 # Companion extension for the `open-in-vscode-browser` shim. Symlinked
55 # (rather than copied) so edits to the source are live after a window reload.
56 # macOS-only: the shim targets /usr/bin/open.
57 home.file.".vscode/extensions/clover.simple-browser-bridge" = pkgs.lib.mkIf host.darwin {
58 source = config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge;
59 };
4260
4361 programs = {
4462 # sort-lines:start
......@@ -73,14 +91,16 @@ in
7391 jf = "jj git fetch";
7492 e = "jj edit";
7593 d = "jj desc";
76 bup = "jj b m --from 'heads(::@- & bookmarks()) ~ (master | bookmarks(glob:\"deployment*\"))' --to '@'";
7794 moveself = "jj rebase -s @ -d";
7895 logall = "jj log -r 'all()'";
79 unmerged = "jj log -r 'unmerged()'";
8096 um = "jj log -r 'unmerged()'";
8197 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";
8298 };
8399 initContent = ''
100 # macOS hands GUI-launched processes a 256 fd soft limit, which bundler
101 # file watchers exhaust during their first build.
102 ulimit -n 65536 2>/dev/null
103
84104 function _update_prompt_theme() {
85105 local dark=false
86106 if [[ "$OSTYPE" == darwin* ]]; then
......@@ -118,7 +138,122 @@ in
118138 fi
119139 }
120140
141 function pr() {
142 local branch remote repo url
143 branch="$(
144 command jj log -r '@ | @-' -T 'bookmarks.map(|b| b.name()).join("\n") ++ "\n"' --no-graph 2>/dev/null |
145 while IFS= read -r bookmark; do
146 case "$bookmark" in
147 "" | main | master | trunk | deployment*) ;;
148 *) printf '%s' "$bookmark"; break ;;
149 esac
150 done
151 )"
152 if [ -z "$branch" ]; then
153 printf 'no jj bookmark found on @ or @-\n' >&2
154 return 1
155 fi
156
157 remote="$(
158 command jj git remote list 2>/dev/null |
159 while IFS=' ' read -r name remote_url _; do
160 if [ "$name" = "origin" ]; then
161 printf '%s' "$remote_url"
162 break
163 fi
164 done
165 )"
166 case "$remote" in
167 git@github.com:*) repo="''${remote#git@github.com:}" ;;
168 ssh://git@github.com/*) repo="''${remote#ssh://git@github.com/}" ;;
169 https://github.com/*) repo="''${remote#https://github.com/}" ;;
170 http://github.com/*) repo="''${remote#http://github.com/}" ;;
171 *)
172 printf 'origin is not a github remote: %s\n' "$remote" >&2
173 return 1
174 ;;
175 esac
176
177 repo="''${repo%.git}"
178 url="https://github.com/$repo/pull/new/$branch?labels=auto-merge"
179 command open "$url"
180 }
181
182 function _jjImmutableRevision() {
183 emulate -L zsh
184 setopt extendedglob
185 local line
186 while IFS= read -r line; do
187 line="''${line//$'\e'\[[0-9;]##m/}"
188 if [[ "$line" = *"Could not modify commit: "* ]]; then
189 line="''${line#*Could not modify commit: }"
190 printf '%s' "''${line%% *}"
191 return 0
192 fi
193 done < "$1"
194 return 1
195 }
196
197 function _runJjStreaming() {
198 emulate -L zsh
199 setopt no_multios
200 local stderr arg has_color
201 local -a jj_color_arg pipe_status
202 stderr="$1"
203 shift
204 if [[ -t 1 && -t 2 ]]; then
205 has_color=false
206 for arg in "$@"; do
207 if [[ "$arg" = "--color" || "$arg" = --color=* ]]; then
208 has_color=true
209 break
210 fi
211 done
212 if ! $has_color; then
213 jj_color_arg=(--color=always)
214 fi
215 fi
216 {
217 command jj "$jj_color_arg[@]" "$@" 2>&1 1>&3 | tee "$stderr" >&2
218 pipe_status=("''${pipestatus[@]}")
219 } 3>&1
220 return $pipe_status[1]
221 }
222
223 function _jjHandleFailure() {
224 local stderr jj_status reply
225 stderr="$1"
226 jj_status="$2"
227 shift 2
228 if [[ $jj_status -ne 0 ]]; then
229 if _jjImmutableRevision "$stderr" >/dev/null && [[ -t 0 ]]; then
230 printf 'Rewrite immutable commit anyways [y/N] ' >&2
231 read -r reply
232 case "$reply" in
233 [Yy] | [Yy][Ee][Ss])
234 _runJj "$@" --ignore-immutable
235 return $?
236 ;;
237 esac
238 return $jj_status
239 fi
240 fi
241 return $jj_status
242 }
243
244 function _runJj() {
245 local stderr jj_status
246 stderr="$(mktemp)"
247 _runJjStreaming "$stderr" "$@"
248 jj_status=$?
249 _jjHandleFailure "$stderr" "$jj_status" "$@"
250 jj_status=$?
251 rm -f "$stderr"
252 return $jj_status
253 }
254
121255 function wrapWithAutoInstall() {
256 local root file cmd before after wrapped_status
122257 root="$(command jj workspace root)"
123258 if [ -n "$root" ]; then
124259 if [ -e "$root/bun.lock" ]; then
......@@ -138,40 +273,78 @@ in
138273 if [ -n "$file" ]; then
139274 before="$(sha1sum "$file")"
140275 fi
141 command "$@"
276 _runJj "$@"
277 wrapped_status=$?
278 if [[ $wrapped_status -ne 0 ]]; then
279 return $wrapped_status
280 fi
142281 if [ -n "$file" ] && [ -e "$file" ]; then
143282 after="$(sha1sum "$file")"
144283 if [ "$after" != "$before" ]; then
145284 eval "$cmd"
285 return $?
146286 fi
147287 fi
288 return $wrapped_status
148289 }
149290
150291 function jj() {
151292 if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then
152 local stderr status
153 stderr="$(mktemp)"
154 command jj "$@" 2>"$stderr"
155 status=$?
156 if [[ $status -ne 0 ]] && command grep -Fq "Revision \`$2\` doesn't exist" "$stderr" && command grep -Fq "\`$2@origin\`" "$stderr"; then
157 rm -f "$stderr"
158 command jj new "$2@origin"
159 return $?
293 if ! command jj log -r "$2" --no-graph -T "" >/dev/null 2>&1 && command jj log -r "$2@origin" --no-graph -T "" >/dev/null 2>&1; then
294 wrapWithAutoInstall new "$2@origin"
295 else
296 wrapWithAutoInstall "$@"
160297 fi
161 command cat "$stderr" >&2
162 rm -f "$stderr"
163 return $status
164298 elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then
165 if command jj "$@" --dry-run; then
299 # confirm before pushing, but only when the dry-run has something to push
300 local dry dry_status
301 dry="$(command jj "$@" --dry-run 2>&1)"
302 dry_status=$?
303 print -r -- "$dry"
304 if [[ $dry_status -ne 0 ]]; then
305 return $dry_status
306 fi
307 if [[ "$dry" = *"No bookmarks/tags found in the default push revset"* && "$dry" = *"Nothing changed."* && -n "$(command jj log -r '@ & ~empty()' --no-graph -T change_id 2>/dev/null)" ]]; then
308 local answer
309 printf "No bookmark on @; move previous bookmark to @ and push? [y/N] "
310 read -r answer
311 case "$answer" in
312 y|Y|yes|YES)
313 bup || return $?
314 dry="$(command jj "$@" --dry-run 2>&1)"
315 dry_status=$?
316 print -r -- "$dry"
317 if [[ $dry_status -ne 0 ]]; then
318 return $dry_status
319 fi
320 if [[ "$dry" != *"Nothing changed."* ]]; then
321 _runJj "$@"
322 return $?
323 fi
324 ;;
325 esac
326 fi
327 if [[ "$dry" != *"Nothing changed."* ]]; then
166328 printf "Press any key to continue... "
167329 read -r
168 command jj "$@"
330 _runJj "$@"
331 return $?
169332 fi
170 elif [[ "$*" != "undo" && "$*" != "redo" ]]; then
333 return 0
334 elif [[ "$1" = "undo" || "$1" = "redo" ]]; then
335 _runJj "$@"
336 else
171337 # auto-install deps when switching commits
172 wrapWithAutoInstall jj "$@"
338 wrapWithAutoInstall "$@"
173339 fi
174340 }
341
342 function bup() {
343 if [[ -z "$(command jj log -r @ -T description --no-graph 2>/dev/null)" ]]; then
344 command jj desc -m 'chore: follow-up changes'
345 fi
346 command jj b m --from 'heads(::@- & bookmarks()) ~ (trunk() | bookmarks(glob:"deployment*"))' --to '@'
347 }
175348 '';
176349 };
177350
......@@ -293,6 +466,10 @@ in
293466 };
294467 aliases = {
295468 "ab" = [ "abandon" ];
469 "desc" = [
470 "describe"
471 "--ignore-working-copy"
472 ];
296473 "ci" = [
297474 "util"
298475 "exec"
......@@ -319,16 +496,16 @@ in
319496
320497 SetEnv TERM=xterm-256color
321498 '';
322 matchBlocks = rec {
499 settings = rec {
323500 "*" = { };
324501 zenith = {
325 user = "clo";
326 port = 22;
502 User = "clo";
503 Port = 22;
327504 };
328505 "git.paperclover.net" = zenith;
329506 "paperclover.net" = {
330 user = "clo";
331 port = 22;
507 User = "clo";
508 Port = 22;
332509 };
333510 };
334511 };
users/clover/packages/agent-linear/agent-linear.js created+417
......@@ -0,0 +1,417 @@
1#!/usr/bin/env node
2const child_process = require("node:child_process");
3const fs = require("node:fs");
4const path = require("node:path");
5const process = require("node:process");
6
7const TEAM_KEY = "ALPHA";
8const API_URL = "https://api.linear.app/graphql";
9const API_KEY_PATH = path.join(process.env.HOME ?? "", ".config", "linear-api-key");
10const ISSUE_ID_RE = /^ALPHA-\d+$/i;
11const WORK_REPO_RE = /^\d+$/;
12const OPEN_STATE_TYPES = ["triage", "backlog", "unstarted", "started"];
13const OPEN_STATE_TYPE_SET = new Set(OPEN_STATE_TYPES);
14
15const OPEN_ALPHA_FILTER = {
16 team: { key: { eq: TEAM_KEY } },
17 state: { type: { in: OPEN_STATE_TYPES } },
18};
19
20const SEARCH_ISSUES = `
21 query SearchAlphaIssues(
22 $term: String!
23 $filter: IssueFilter
24 $first: Int!
25 $includeComments: Boolean!
26 ) {
27 searchIssues(
28 term: $term
29 filter: $filter
30 first: $first
31 includeArchived: false
32 includeComments: $includeComments
33 ) {
34 nodes {
35 id
36 identifier
37 title
38 url
39 priority
40 priorityLabel
41 description
42 createdAt
43 updatedAt
44 labelIds
45 state {
46 id
47 name
48 type
49 }
50 team {
51 id
52 key
53 name
54 }
55 }
56 }
57 }
58`;
59
60const ISSUE_QUERY = `
61 query AlphaIssue($id: String!) {
62 issue(id: $id) {
63 id
64 identifier
65 title
66 url
67 priority
68 priorityLabel
69 description
70 createdAt
71 updatedAt
72 labelIds
73 labels(first: 50) {
74 nodes {
75 id
76 name
77 }
78 }
79 state {
80 id
81 name
82 type
83 }
84 team {
85 id
86 key
87 name
88 }
89 }
90 }
91`;
92
93main().catch((error) => {
94 const message = error instanceof Error ? error.message : String(error);
95 console.error(`error: ${message}`);
96 process.exit(1);
97});
98
99async function main() {
100 const [command, ...argv] = process.argv.slice(2);
101
102 if (!command || command === "-h" || command === "--help") {
103 usage(0);
104 }
105
106 if (command === "search") {
107 const options = parseSearchArgs(argv);
108 assertWorkRepo();
109 await search(options);
110 } else if (command === "issue") {
111 const options = parseIssueArgs(argv);
112 assertWorkRepo();
113 await showIssue(options);
114 } else {
115 throw new Error(`unknown command ${JSON.stringify(command)}`);
116 }
117}
118
119async function search(options) {
120 const found = new Map();
121
122 for (const query of options.queries) {
123 const data = await linearRequest(SEARCH_ISSUES, {
124 term: query,
125 filter: OPEN_ALPHA_FILTER,
126 first: options.limit,
127 includeComments: options.includeComments,
128 });
129
130 for (const issue of data.searchIssues.nodes) {
131 if (!isOpenIssue(issue)) {
132 continue;
133 }
134
135 const existing = found.get(issue.identifier);
136 if (existing) {
137 existing.matchedQueries.push(query);
138 } else {
139 found.set(issue.identifier, { ...issue, matchedQueries: [query] });
140 }
141 }
142 }
143
144 const issues = [...found.values()];
145 if (options.json) {
146 writeJson({ team: TEAM_KEY, queries: options.queries, issues: issues.map(issueJson) });
147 return;
148 }
149
150 if (issues.length === 0) {
151 console.log("no open alpha issues matched");
152 return;
153 }
154
155 for (const issue of issues) {
156 console.log(formatIssue(issue));
157 }
158}
159
160async function showIssue(options) {
161 const issue = await fetchIssue(options.issueId);
162 if (options.json) {
163 writeJson(issueJson(issue));
164 return;
165 }
166 console.log(formatIssue(issue, { includeLabels: true, fullDescription: true }));
167}
168
169async function fetchIssue(issueId) {
170 const id = normalizeIssueId(issueId);
171 const data = await linearRequest(ISSUE_QUERY, { id });
172 if (!data.issue) {
173 throw new Error(`${id} not found`);
174 }
175 if (data.issue.team?.key !== TEAM_KEY) {
176 throw new Error(`${id} belongs to ${data.issue.team?.key ?? "unknown"}, expected ${TEAM_KEY}`);
177 }
178 assertOpenIssue(data.issue);
179 return data.issue;
180}
181
182async function linearRequest(query, variables) {
183 const apiKey = readLinearApiKey();
184 if (!apiKey) {
185 throw new Error(`Linear API key is required in ${API_KEY_PATH} or LINEAR_API_KEY`);
186 }
187
188 const response = await fetch(API_URL, {
189 method: "POST",
190 headers: {
191 Authorization: apiKey,
192 "Content-Type": "application/json",
193 },
194 body: JSON.stringify({ query, variables }),
195 });
196
197 const body = await response.text();
198 let json;
199 try {
200 json = JSON.parse(body);
201 } catch {
202 throw new Error(`Linear returned non-JSON response (${response.status})`);
203 }
204
205 if (!response.ok) {
206 throw new Error(`Linear HTTP ${response.status}: ${body.slice(0, 500)}`);
207 }
208 if (json.errors?.length > 0) {
209 throw new Error(json.errors.map((error) => error.message).join("; "));
210 }
211
212 return json.data;
213}
214
215function readLinearApiKey() {
216 const envKey = process.env.LINEAR_API_KEY?.trim();
217 if (envKey) {
218 return envKey;
219 }
220
221 if (!process.env.HOME) {
222 return "";
223 }
224
225 try {
226 return fs.readFileSync(API_KEY_PATH, "utf8").trim();
227 } catch (error) {
228 if (error?.code === "ENOENT") {
229 return "";
230 }
231 throw error;
232 }
233}
234
235function parseSearchArgs(argv) {
236 const options = { json: false, limit: 8, includeComments: false, queries: [] };
237
238 while (argv.length > 0) {
239 const arg = argv.shift();
240 if (arg === "-h" || arg === "--help") {
241 searchUsage(0);
242 } else if (arg === "--json") {
243 options.json = true;
244 } else if (arg === "--comments") {
245 options.includeComments = true;
246 } else if (arg === "--limit") {
247 options.limit = parseLimit(argv.shift());
248 } else if (arg === "--") {
249 options.queries.push(...argv);
250 break;
251 } else if (arg.startsWith("-")) {
252 throw new Error(`unknown search option ${JSON.stringify(arg)}`);
253 } else {
254 options.queries.push(arg);
255 }
256 }
257
258 if (options.queries.length === 0 && !process.stdin.isTTY) {
259 options.queries.push(...fs.readFileSync(0, "utf8").split(/\r?\n/));
260 }
261
262 options.queries = options.queries.map((query) => query.trim()).filter(Boolean);
263 if (options.queries.length === 0) {
264 searchUsage(1);
265 }
266
267 return options;
268}
269
270function parseIssueArgs(argv) {
271 const options = { json: false, issueId: undefined };
272
273 while (argv.length > 0) {
274 const arg = argv.shift();
275 if (arg === "-h" || arg === "--help") {
276 issueUsage(0);
277 } else if (arg === "--json") {
278 options.json = true;
279 } else if (arg.startsWith("-")) {
280 throw new Error(`unknown issue option ${JSON.stringify(arg)}`);
281 } else if (!options.issueId) {
282 options.issueId = arg;
283 } else {
284 throw new Error(`unexpected issue argument ${JSON.stringify(arg)}`);
285 }
286 }
287
288 if (!options.issueId) {
289 issueUsage(1);
290 }
291
292 options.issueId = normalizeIssueId(options.issueId);
293 return options;
294}
295
296function assertWorkRepo() {
297 const result = child_process.spawnSync("jj", ["workspace", "root"], { encoding: "utf8" });
298 if (result.error || result.status !== 0) {
299 throw new Error("agent-linear must run inside a jj workspace");
300 }
301
302 const root = result.stdout.trim();
303 const home = process.env.HOME;
304 const relative = home ? path.relative(path.join(home, "dev"), root) : "";
305 if (!home || path.isAbsolute(relative) || relative.startsWith("..") || !WORK_REPO_RE.test(relative)) {
306 throw new Error(`agent-linear only runs from a numbered work repo under ~/dev, current workspace is ${root}`);
307 }
308}
309
310function normalizeIssueId(issueId) {
311 if (!ISSUE_ID_RE.test(issueId)) {
312 throw new Error(`expected an ${TEAM_KEY}-123 issue id, got ${JSON.stringify(issueId)}`);
313 }
314 return issueId.toUpperCase();
315}
316
317function parseLimit(value) {
318 const limit = Number(value);
319 if (!Number.isInteger(limit) || limit < 1 || limit > 25) {
320 throw new Error("--limit must be an integer from 1 to 25");
321 }
322 return limit;
323}
324
325function assertOpenIssue(issue) {
326 if (!isOpenIssue(issue)) {
327 throw new Error(`${issue.identifier} is closed (${issue.state?.name ?? "unknown"}/${issue.state?.type ?? "unknown"})`);
328 }
329}
330
331function isOpenIssue(issue) {
332 return OPEN_STATE_TYPE_SET.has(issue.state?.type);
333}
334
335function issueJson(issue) {
336 return {
337 id: issue.id,
338 identifier: issue.identifier,
339 title: issue.title,
340 url: issue.url,
341 state: issue.state ? { name: issue.state.name, type: issue.state.type } : null,
342 priority: issue.priority,
343 priorityLabel: issue.priorityLabel,
344 labels: issue.labels?.nodes?.map((label) => label.name) ?? undefined,
345 labelIds: issue.labelIds ?? [],
346 matchedQueries: issue.matchedQueries,
347 description: issue.description,
348 createdAt: issue.createdAt,
349 updatedAt: issue.updatedAt,
350 };
351}
352
353function formatIssue(issue, options = {}) {
354 const lines = [
355 `${issue.identifier} [${issue.state?.name ?? "unknown"}/${issue.state?.type ?? "unknown"}] ${issue.title}`,
356 ` ${issue.url}`,
357 ];
358 if (issue.priorityLabel) {
359 lines.push(` priority: ${issue.priorityLabel}`);
360 }
361 if (issue.matchedQueries) {
362 lines.push(` matched: ${issue.matchedQueries.join(" | ")}`);
363 }
364 if (options.includeLabels && issue.labels?.nodes?.length > 0) {
365 lines.push(` labels: ${issue.labels.nodes.map((label) => label.name).join(", ")}`);
366 }
367
368 if (options.fullDescription) {
369 const description = (issue.description ?? "").trim();
370 if (description) {
371 lines.push(" desc:");
372 lines.push(...description.split(/\r?\n/).map((line) => ` ${line}`));
373 }
374 } else {
375 const description = excerpt(issue.description);
376 if (description) {
377 lines.push(` desc: ${description}`);
378 }
379 }
380
381 return `${lines.join("\n")}\n`;
382}
383
384function excerpt(value) {
385 return (value ?? "").replace(/\s+/g, " ").trim().slice(0, 320);
386}
387
388function writeJson(value) {
389 process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
390}
391
392function usage(code) {
393 const stream = code === 0 ? process.stdout : process.stderr;
394 stream.write(`usage: agent-linear <command> [options]\n`);
395 stream.write(`\n`);
396 stream.write(`commands:\n`);
397 stream.write(` search search open ALPHA Linear issues\n`);
398 stream.write(` issue show one open ALPHA issue\n`);
399 stream.write(`\n`);
400 stream.write(`api key: ~/.config/linear-api-key or LINEAR_API_KEY\n`);
401 stream.write(`scope: only runs from numbered work repos under ~/dev\n`);
402 process.exit(code);
403}
404
405function searchUsage(code) {
406 const stream = code === 0 ? process.stdout : process.stderr;
407 stream.write(`usage: agent-linear search [--json] [--comments] [--limit n] <query>...\n`);
408 stream.write(`\n`);
409 stream.write(`each query is sent to Linear searchIssues with open ALPHA issue filters\n`);
410 process.exit(code);
411}
412
413function issueUsage(code) {
414 const stream = code === 0 ? process.stdout : process.stderr;
415 stream.write(`usage: agent-linear issue [--json] <ALPHA-123>\n`);
416 process.exit(code);
417}
users/clover/packages/agent-linear/default.nix created+9
......@@ -0,0 +1,9 @@
1{ pkgs, ... }:
2pkgs.writeShellApplication {
3 name = "agent-linear";
4 runtimeInputs = with pkgs; [
5 jujutsu
6 nodejs
7 ];
8 text = ''exec ${pkgs.nodejs}/bin/node ${./agent-linear.js} "$@"'';
9}
users/clover/paperback/home.nix+1-1
......@@ -7,6 +7,6 @@
77 yt-dlp
88 spotdl
99 zig
10 nodejs_24
10 nodejs_26
1111 ];
1212}
users/clover/pnpm-workspace.yaml+3
......@@ -1,2 +1,5 @@
1packages:
2 - raycast-vscode-projects
3
14onlyBuiltDependencies:
25 - node-pty
users/clover/raycast-vscode-projects/icon.svg created+6
......@@ -0,0 +1,6 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2 <rect width="64" height="64" rx="16" fill="#111827"/>
3 <path d="M14 20.5 27.5 12v40L14 43.5l8-11.5-8-11.5Z" fill="#22c55e"/>
4 <path d="M50 12v40L27.5 43.5v-23L50 12Z" fill="#3b82f6"/>
5 <path d="M22 32 14 43.5 27.5 52V12L14 20.5 22 32Z" fill="#60a5fa" fill-opacity=".8"/>
6</svg>
users/clover/raycast-vscode-projects/package.json created+31
......@@ -0,0 +1,31 @@
1{
2 "$schema": "https://www.raycast.com/schemas/extension.json",
3 "name": "vscode-projects",
4 "title": "VS Code Projects",
5 "description": "Search and manage recent VS Code project windows",
6 "icon": "icon.svg",
7 "author": "clover",
8 "license": "MIT",
9 "packageManager": "pnpm@10.28.0",
10 "commands": [
11 {
12 "name": "search-projects",
13 "title": "Search VS Code Projects",
14 "description": "Open, focus, and close recent VS Code projects",
15 "mode": "view"
16 }
17 ],
18 "dependencies": {
19 "@raycast/api": "^1.104.17",
20 "@raycast/utils": "^2.2.4"
21 },
22 "devDependencies": {
23 "@types/node": "22.19.17",
24 "@types/react": "19.0.10",
25 "typescript": "^5.8.2"
26 },
27 "scripts": {
28 "build": "ray build -e dist",
29 "dev": "ray develop"
30 }
31}
users/clover/raycast-vscode-projects/raycast-env.d.ts created+24
......@@ -0,0 +1,24 @@
1/// <reference types="@raycast/api">
2
3/* 🚧 🚧 🚧
4 * This file is auto-generated from the extension's manifest.
5 * Do not modify manually. Instead, update the `package.json` file.
6 * 🚧 🚧 🚧 */
7
8/* eslint-disable @typescript-eslint/ban-types */
9
10type ExtensionPreferences = {}
11
12/** Preferences accessible in all the extension's commands */
13declare type Preferences = ExtensionPreferences
14
15declare namespace Preferences {
16 /** Preferences accessible in the `search-projects` command */
17 export type SearchProjects = ExtensionPreferences & {}
18}
19
20declare namespace Arguments {
21 /** Arguments passed to the `search-projects` command */
22 export type SearchProjects = {}
23}
24
users/clover/raycast-vscode-projects/src/search-projects.tsx created+293
......@@ -0,0 +1,293 @@
1import {
2 Action,
3 ActionPanel,
4 Alert,
5 Color,
6 Icon,
7 List,
8 LocalStorage,
9 Toast,
10 closeMainWindow,
11 confirmAlert,
12 showToast,
13} from "@raycast/api";
14import { usePromise } from "@raycast/utils";
15import { execFile } from "node:child_process";
16import { existsSync, readFileSync, statSync } from "node:fs";
17import { homedir } from "node:os";
18import path from "node:path";
19import { promisify } from "node:util";
20import { fileURLToPath, pathToFileURL } from "node:url";
21
22const execFileAsync = promisify(execFile);
23const codeBundleId = "com.microsoft.VSCode";
24
25type RecentEntry = { folderUri?: string; workspace?: { configPath?: string } };
26type OpenWindow = { folder?: string; workspace?: string | { configPath?: string } };
27type Project = {
28 uri: string;
29 path: string;
30 title: string;
31 repo: string | undefined;
32 isOpen: boolean;
33};
34
35export default function Command() {
36 const { data, isLoading, revalidate } = usePromise(loadProjects);
37
38 return (
39 <List isLoading={isLoading} searchBarPlaceholder="Search VS Code projects...">
40 {data?.map((project) => (
41 <ProjectItem key={project.uri} project={project} revalidate={revalidate} />
42 ))}
43 </List>
44 );
45}
46
47function ProjectItem({ project, revalidate }: { project: Project; revalidate: () => void }) {
48 return (
49 <List.Item
50 title={project.title}
51 subtitle={project.repo}
52 icon={project.isOpen ? { source: Icon.Window, tintColor: Color.Green } : Icon.Folder}
53 accessories={project.isOpen ? [{ tag: { value: "open", color: Color.Green } }] : []}
54 keywords={[project.path, project.repo ?? ""]}
55 actions={
56 <ActionPanel>
57 <Action
58 title={project.isOpen ? "Focus Window" : "Open Project"}
59 icon={Icon.Window}
60 onAction={() => focusOrOpen(project)}
61 />
62 {project.isOpen && (
63 <Action
64 title="Close Window"
65 icon={Icon.XMarkCircle}
66 style={Action.Style.Destructive}
67 shortcut={{ modifiers: ["cmd"], key: "w" }}
68 onAction={async () => {
69 await closeWindow(project);
70 revalidate();
71 }}
72 />
73 )}
74 <Action.CopyToClipboard title="Copy Path" content={project.title} shortcut={{ modifiers: ["cmd"], key: "." }} />
75 <Action
76 title="Remove from List"
77 icon={Icon.Trash}
78 style={Action.Style.Destructive}
79 shortcut={{ modifiers: ["ctrl"], key: "x" }}
80 onAction={async () => {
81 const confirmed = await confirmAlert({
82 title: `Remove ${project.title}?`,
83 message: "The project stays on disk but is permanently hidden from this list.",
84 primaryAction: { title: "Remove", style: Alert.ActionStyle.Destructive },
85 });
86 if (!confirmed) return;
87 await hideProject(project.uri);
88 revalidate();
89 }}
90 />
91 </ActionPanel>
92 }
93 />
94 );
95}
96
97async function loadProjects(): Promise<Project[]> {
98 const [recentEntries, openUris, hiddenUris] = await Promise.all([
99 loadRecentEntries(),
100 loadOpenUris(),
101 loadHiddenUris(),
102 ]);
103 const seen = new Set<string>();
104 const projects = recentEntries.flatMap((entry) => {
105 const uri = entry.folderUri ?? entry.workspace?.configPath;
106 if (!uri || !uri.startsWith("file://") || seen.has(uri) || hiddenUris.has(uri)) return [];
107 seen.add(uri);
108
109 const filesystemPath = fileURLToPath(uri);
110 const projectPath = entry.workspace?.configPath ? path.dirname(filesystemPath) : filesystemPath;
111 if (!existsSync(projectPath)) return [];
112
113 return [
114 {
115 uri,
116 path: projectPath,
117 title: prettyPath(projectPath),
118 repo: getRepoName(projectPath),
119 isOpen: openUris.has(uri),
120 },
121 ];
122 });
123
124 return projects.sort((a, b) => Number(b.isOpen) - Number(a.isOpen));
125}
126
127const hiddenUrisKey = "hiddenUris";
128
129async function loadHiddenUris(): Promise<Set<string>> {
130 const raw = await LocalStorage.getItem<string>(hiddenUrisKey);
131 return new Set(raw ? (JSON.parse(raw) as string[]) : []);
132}
133
134async function hideProject(uri: string) {
135 const hidden = await loadHiddenUris();
136 hidden.add(uri);
137 await LocalStorage.setItem(hiddenUrisKey, JSON.stringify([...hidden]));
138}
139
140async function loadRecentEntries(): Promise<RecentEntry[]> {
141 const database = getSharedStorageDatabase();
142 const { stdout } = await execFileAsync("sqlite3", [
143 "--json",
144 "--readonly",
145 database,
146 "SELECT value FROM ItemTable WHERE key = 'history.recentlyOpenedPathsList'",
147 ]);
148 const row = JSON.parse(stdout || "[]") as { value: string }[];
149 return row[0]?.value ? (JSON.parse(row[0].value).entries as RecentEntry[]) : [];
150}
151
152function getSharedStorageDatabase() {
153 const productJson = "/Applications/Visual Studio Code.app/Contents/Resources/app/product.json";
154 if (existsSync(productJson)) {
155 const product = JSON.parse(readFileSync(productJson, "utf8")) as { sharedDataFolderName?: string };
156 if (product.sharedDataFolderName) {
157 const sharedDatabase = path.join(homedir(), product.sharedDataFolderName, "sharedStorage", "state.vscdb");
158 if (existsSync(sharedDatabase)) return sharedDatabase;
159 }
160 }
161
162 return path.join(homedir(), "Library/Application Support/Code/User/globalStorage/state.vscdb");
163}
164
165function loadOpenUris() {
166 const storagePath = path.join(homedir(), "Library/Application Support/Code/User/globalStorage/storage.json");
167 if (!existsSync(storagePath)) return new Set<string>();
168
169 const storage = JSON.parse(readFileSync(storagePath, "utf8")) as {
170 windowsState?: { lastActiveWindow?: OpenWindow; openedWindows?: OpenWindow[] };
171 };
172 const windows = [
173 ...(storage.windowsState?.lastActiveWindow ? [storage.windowsState.lastActiveWindow] : []),
174 ...(storage.windowsState?.openedWindows ?? []),
175 ];
176 const uris = windows.flatMap((window) => {
177 const workspace = typeof window.workspace === "string" ? window.workspace : window.workspace?.configPath;
178 return [window.folder, workspace];
179 });
180
181 return new Set(uris.filter((uri): uri is string => typeof uri === "string"));
182}
183
184async function focusOrOpen(project: Project) {
185 await closeMainWindow();
186 await execFileAsync("open", ["-b", codeBundleId, project.path]);
187}
188
189async function closeWindow(project: Project) {
190 const result = await closeProjectWindow(project);
191 if (result === "ok") {
192 await new Promise((resolve) => setTimeout(resolve, 300));
193 await showToast({ title: "Closed VS Code window" });
194 } else {
195 await showToast({
196 style: Toast.Style.Failure,
197 title: result === "ambiguous" ? "Multiple matching VS Code windows" : "Could not find VS Code window",
198 });
199 }
200}
201
202async function closeProjectWindow(project: Project) {
203 try {
204 const { stdout } = await execFileAsync("osascript", [
205 "-e",
206 `on run argv
207 set projectUri to item 1 of argv
208 set projectPath to item 2 of argv
209
210 tell application "Visual Studio Code" to activate
211tell application "System Events"
212 tell process "Code"
213 set documentMatches to {}
214 set titleMatches to {}
215
216 repeat with candidate in windows
217 set windowName to name of candidate
218 set documentUri to ""
219 try
220 set rawDocumentUri to value of attribute "AXDocument" of candidate
221 if rawDocumentUri is not missing value then set documentUri to rawDocumentUri as text
222 end try
223
224 if documentUri is not "" and (documentUri is projectUri or documentUri starts with projectUri & "/") then
225 set end of documentMatches to candidate
226 else if windowName is projectPath or windowName ends with projectPath or windowName contains projectPath & "/" or windowName contains projectPath & " " then
227 set end of titleMatches to candidate
228 end if
229 end repeat
230
231 if length of documentMatches is 1 then
232 click button 1 of item 1 of documentMatches
233 return "ok"
234 end if
235 if length of documentMatches > 1 then return "ambiguous"
236
237 if length of titleMatches is 1 then
238 click button 1 of item 1 of titleMatches
239 return "ok"
240 end if
241 if length of titleMatches > 1 then return "ambiguous"
242 end tell
243end tell
244return "missing"
245end run`,
246 pathToFileURL(project.path).href,
247 project.path,
248 ]);
249 return stdout.trim() as "ok" | "missing" | "ambiguous";
250 } catch {
251 return "missing";
252 }
253}
254
255function prettyPath(projectPath: string) {
256 return projectPath.startsWith(homedir()) ? `~${projectPath.slice(homedir().length)}` : projectPath;
257}
258
259function getRepoName(startPath: string) {
260 for (let current = startPath; current !== path.dirname(current); current = path.dirname(current)) {
261 const configPath = getRemoteConfigPath(current);
262 if (!configPath) continue;
263
264 const origin = readFileSync(configPath, "utf8").match(/\[remote "origin"\][\s\S]*?\n\s*url = (.+)/)?.[1]?.trim();
265 return origin ? parseRepoName(origin) : undefined;
266 }
267}
268
269function getRemoteConfigPath(directory: string) {
270 const jjConfig = path.join(directory, ".jj/repo/store/git/config");
271 if (existsSync(jjConfig)) return jjConfig;
272
273 const sharedJjRepo = path.join(directory, ".jj/repo");
274 if (existsSync(sharedJjRepo) && statSync(sharedJjRepo).isFile()) {
275 const sharedJjConfig = path.join(readFileSync(sharedJjRepo, "utf8").trim(), "store/git/config");
276 if (existsSync(sharedJjConfig)) return sharedJjConfig;
277 }
278
279 const gitDirectory = path.join(directory, ".git");
280 const gitConfig = path.join(gitDirectory, "config");
281 if (existsSync(gitConfig)) return gitConfig;
282
283 if (!existsSync(gitDirectory)) return undefined;
284
285 const gitdir = readFileSync(gitDirectory, "utf8").match(/^gitdir: (.+)$/m)?.[1];
286 const worktreeConfig = gitdir && path.join(directory, gitdir, "config");
287 return worktreeConfig && existsSync(worktreeConfig) ? worktreeConfig : undefined;
288}
289
290function parseRepoName(remote: string) {
291 const pathMatch = remote.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
292 return pathMatch?.[1];
293}
users/clover/raycast-vscode-projects/tsconfig.json created+14
......@@ -0,0 +1,14 @@
1{
2 "$schema": "https://json.schemastore.org/tsconfig",
3 "compilerOptions": {
4 "lib": ["ES2023"],
5 "module": "commonjs",
6 "target": "ES2023",
7 "jsx": "react-jsx",
8 "strict": true,
9 "esModuleInterop": true,
10 "skipLibCheck": true,
11 "forceConsistentCasingInFileNames": true,
12 "moduleResolution": "node"
13 }
14}
users/clover/sandwich/home.nix+1-1
......@@ -65,7 +65,7 @@ let
6565in
6666{
6767 home.packages = with pkgs; [
68 nodejs_24
68 nodejs_26
6969 rsync
7070 sandwich-backup
7171 ];
users/clover/switch-admin.sh+1-1
......@@ -23,4 +23,4 @@ if [ "$(uname -s)" != Darwin ]; then
2323 exec ./switch "$target_host"
2424fi
2525
26exec osascript -e "do shell script \"cd /Users/clo/config && /nix/var/nix/profiles/default/bin/nix run .#darwin-rebuild -- switch --flake .#$target_host\" with administrator privileges"
26exec osascript -e "do shell script \"cd /Users/clo/config && HOME=/var/root XDG_CACHE_HOME=/var/root/.cache /nix/var/nix/profiles/default/bin/nix run .#darwin-rebuild -- switch --flake .#$target_host\" with administrator privileges"
users/clover/work/configuration.nix created+24
......@@ -0,0 +1,24 @@
1{ lib, ... }:
2{
3 # Determinate manages its own Nix daemon, which conflicts with nix-darwin's
4 # native management. Opt out so activation does not abort.
5 nix.enable = false;
6 nix.optimise.automatic = lib.mkForce false;
7 nix.gc.automatic = lib.mkForce false;
8
9 services.skhd = {
10 enable = true;
11 skhdConfig = ''
12 cmd + alt - c : /usr/local/bin/code "$HOME/config"
13 cmd + alt - f1 : /usr/local/bin/code "$HOME/dev/1"
14 cmd + alt - f2 : /usr/local/bin/code "$HOME/dev/2"
15 cmd + alt - f3 : /usr/local/bin/code "$HOME/dev/3"
16 cmd + alt - f4 : /usr/local/bin/code "$HOME/dev/4"
17 cmd + alt - f5 : /usr/local/bin/code "$HOME/dev/5"
18 cmd + alt - f6 : /usr/local/bin/code "$HOME/dev/6"
19 cmd + alt - f7 : /usr/local/bin/code "$HOME/dev/7"
20 cmd + alt - f8 : /usr/local/bin/code "$HOME/dev/8"
21 cmd + alt - f9 : /usr/local/bin/code "$HOME/dev/9"
22 '';
23 };
24}
users/clover/work/home.nix+3-2
......@@ -16,12 +16,13 @@
1616 hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s=";
1717 };
1818 }))
19 awscli
19 awscli2
2020 deno
2121 doppler
22 nodejs_25
22 nodejs_26
2323 rustup
2424 ssm-session-manager-plugin
25 terraform
2526 typescript
2627 zig
2728 ];
users/natalie/home.nix+1
......@@ -21,6 +21,7 @@
2121 ghostty = {
2222 enable = true;
2323 package = null;
24 systemd.enable = false;
2425 settings = {
2526 theme = "light:catppuccin-latte,dark:catppuccin-macchiato";
2627 font-family = "AT Name Mono";