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 @@...@@ -9,11 +9,11 @@
9 ]9 ]
10 },10 },
11 "locked": {11 "locked": {
12 "lastModified": 1780174565,12 "lastModified": 1781733698,
13 "narHash": "sha256-7f13eG/HYV9ylIhnNjwt8Al0Q8muE/nfL4uPUvjFyas=",13 "narHash": "sha256-YD81urXG8IEUDeYhipY/dRqy3oL002LVElHUMoMo4DU=",
14 "owner": "tadfisher",14 "owner": "tadfisher",
15 "repo": "android-nixpkgs",15 "repo": "android-nixpkgs",
16 "rev": "a234d676de633d477d0dd6529cf272f7e6826d0b",16 "rev": "f83e9770c9867012210fdc32eb779667239d970d",
17 "type": "github"17 "type": "github"
18 },18 },
19 "original": {19 "original": {
...@@ -56,11 +56,11 @@...@@ -56,11 +56,11 @@
56 ]56 ]
57 },57 },
58 "locked": {58 "locked": {
59 "lastModified": 1779036909,59 "lastModified": 1781761792,
60 "narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",60 "narHash": "sha256-rCPytmKNjctLloB6UgK5CRrHSwV4b0ygxtJLPPp8R14=",
61 "owner": "lnl7",61 "owner": "lnl7",
62 "repo": "nix-darwin",62 "repo": "nix-darwin",
63 "rev": "56c666e108467d87d13508936aade6d567f2a501",63 "rev": "a1fa429e945becaf60468600daf649be4ba0350c",
64 "type": "github"64 "type": "github"
65 },65 },
66 "original": {66 "original": {
...@@ -276,11 +276,11 @@...@@ -276,11 +276,11 @@
276 ]276 ]
277 },277 },
278 "locked": {278 "locked": {
279 "lastModified": 1780099287,279 "lastModified": 1781810163,
280 "narHash": "sha256-efIPwVGtIWIjWcznhaop6XN6HxnOL8800hF6CBNvlqQ=",280 "narHash": "sha256-dk8lIk0UnO8GD3e7e3HM1vSYXpbEDmhy67w7KztfdCg=",
281 "owner": "nix-community",281 "owner": "nix-community",
282 "repo": "home-manager",282 "repo": "home-manager",
283 "rev": "7d8127d308c3fb9664f7e643eec944be74ebb37d",283 "rev": "c8f0b8ed441c95bfeaf33555e7631044bb09f384",
284 "type": "github"284 "type": "github"
285 },285 },
286 "original": {286 "original": {
...@@ -304,11 +304,11 @@...@@ -304,11 +304,11 @@
304 },304 },
305 "mnw": {305 "mnw": {
306 "locked": {306 "locked": {
307 "lastModified": 1778541201,307 "lastModified": 1780772958,
308 "narHash": "sha256-n0twkzWexzjsoDycOTvvQNuGEdg62UiNHYcFCduYpKI=",308 "narHash": "sha256-VKKe8r4pwCGWZ3Yr9CPN129R4S3CKLSrlYqdYz3vKpM=",
309 "owner": "Gerg-L",309 "owner": "Gerg-L",
310 "repo": "mnw",310 "repo": "mnw",
311 "rev": "1a3573fc9d2486738fe0b2cacc5cd10dd5f3a445",311 "rev": "0871dbf63a53610c95db04439ed8ea4d6ec9c160",
312 "type": "github"312 "type": "github"
313 },313 },
314 "original": {314 "original": {
...@@ -324,11 +324,11 @@...@@ -324,11 +324,11 @@
324 ]324 ]
325 },325 },
326 "locked": {326 "locked": {
327 "lastModified": 1779398417,327 "lastModified": 1781804251,
328 "narHash": "sha256-eTya6Y7dw/otqohgYZudkqrcI50740jFUTe4Lyd6GKc=",328 "narHash": "sha256-F+gmENjOqFb284tChvZgprjVVssDGN31Sl+93+i5INo=",
329 "owner": "moonlight-mod",329 "owner": "moonlight-mod",
330 "repo": "moonlight",330 "repo": "moonlight",
331 "rev": "e01464ab9e0bcd42c1550e74a64f2574c14d2d27",331 "rev": "a568321a7a08db8f6bdd262efc6b0355532820ce",
332 "type": "github"332 "type": "github"
333 },333 },
334 "original": {334 "original": {
...@@ -366,11 +366,11 @@...@@ -366,11 +366,11 @@
366 ]366 ]
367 },367 },
368 "locked": {368 "locked": {
369 "lastModified": 1779604987,369 "lastModified": 1781422160,
370 "narHash": "sha256-ZQ5z+fVhxYKtIFwtqGp5O0PD84BM1riASvqDaN5Xs+s=",370 "narHash": "sha256-W7S8O86bVrw2gaomEwkHStOzmPpnFIHQ6lS4Q2HffJ0=",
371 "owner": "nix-community",371 "owner": "nix-community",
372 "repo": "nix-index-database",372 "repo": "nix-index-database",
373 "rev": "8fba98c80b48fa013820e0163c5096922fea4ddd",373 "rev": "854d04e2368fb2e9c328a36b3c0a04cd713bfae1",
374 "type": "github"374 "type": "github"
375 },375 },
376 "original": {376 "original": {
...@@ -433,17 +433,18 @@...@@ -433,17 +433,18 @@
433 },433 },
434 "nixpkgs_3": {434 "nixpkgs_3": {
435 "locked": {435 "locked": {
436 "lastModified": 1780030872,436 "lastModified": 1781607440,
437 "narHash": "sha256-u6WU/yd/o8iYQrHX3RAwO1hYa3LkoSL+WNQD0rJfJZQ=",437 "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
438 "owner": "NixOS",438 "owner": "NixOS",
439 "repo": "nixpkgs",439 "repo": "nixpkgs",
440 "rev": "e9a7635a57597d9754eccebdfc7045e6c8600e6b",440 "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
441 "type": "github"441 "type": "github"
442 },442 },
443 "original": {443 "original": {
444 "id": "nixpkgs",444 "owner": "NixOS",
445 "ref": "nixpkgs-unstable",445 "ref": "nixpkgs-unstable",
446 "type": "indirect"446 "repo": "nixpkgs",
447 "type": "github"
447 }448 }
448 },449 },
449 "nixpkgs_4": {450 "nixpkgs_4": {
...@@ -506,11 +507,11 @@...@@ -506,11 +507,11 @@
506 "systems": "systems_3"507 "systems": "systems_3"
507 },508 },
508 "locked": {509 "locked": {
509 "lastModified": 1780042000,510 "lastModified": 1781534482,
510 "narHash": "sha256-RxFOqPMAlbWiWwh7CQ0bIHIdt4D5gAttx3YVifBBWgE=",511 "narHash": "sha256-d3UIqXuigQhsc7amQkZ7F+SWnaJFAxIROE2f2X+pzUs=",
511 "owner": "notashelf",512 "owner": "notashelf",
512 "repo": "nvf",513 "repo": "nvf",
513 "rev": "07b57ebc6c9e778c536ac0ff1b01fb18dc90239a",514 "rev": "63d8fc82d652c23419a837e2b2934dd4bead04f3",
514 "type": "github"515 "type": "github"
515 },516 },
516 "original": {517 "original": {
...@@ -556,11 +557,11 @@...@@ -556,11 +557,11 @@
556 ]557 ]
557 },558 },
558 "locked": {559 "locked": {
559 "lastModified": 1780110990,560 "lastModified": 1781752752,
560 "narHash": "sha256-6QBThUi7SuK+dgA+DCaEkQGZN4kYx6DpXmK45+MG9zI=",561 "narHash": "sha256-kVG5tV9hddPviGAgqf9sGSuStvv+HAB9onfKqGptV0k=",
561 "owner": "oxalica",562 "owner": "oxalica",
562 "repo": "rust-overlay",563 "repo": "rust-overlay",
563 "rev": "85570ef134d92a8702de6afd1f6f0209c863fa91",564 "rev": "c06d86dabe5b92982b9d67acccb9990d58da3a0e",
564 "type": "github"565 "type": "github"
565 },566 },
566 "original": {567 "original": {
...@@ -765,11 +766,11 @@...@@ -765,11 +766,11 @@
765 "systems": "systems_4"766 "systems": "systems_4"
766 },767 },
767 "locked": {768 "locked": {
768 "lastModified": 1780176353,769 "lastModified": 1781797456,
769 "narHash": "sha256-TV5A3m37BUHgrNQpo8p0HO0mnaHh9me3X4XWb3OxgoU=",770 "narHash": "sha256-TA+MUCzRXdWJ7B3KTRnEHfJAZSqhPtm+MAn2kWIp4is=",
770 "owner": "vicinaehq",771 "owner": "vicinaehq",
771 "repo": "vicinae",772 "repo": "vicinae",
772 "rev": "e5607f98c99af28f0c734bb2ee9b79832004733d",773 "rev": "2321e7c182169a65fc4dab19f0f59e68f378aaa1",
773 "type": "github"774 "type": "github"
774 },775 },
775 "original": {776 "original": {
...@@ -785,11 +786,11 @@...@@ -785,11 +786,11 @@
785 ]786 ]
786 },787 },
787 "locked": {788 "locked": {
788 "lastModified": 1779946062,789 "lastModified": 1781761091,
789 "narHash": "sha256-M/2bCPYjiBTkDNV29J/00z10RM3yYnL9X74RqAHDme0=",790 "narHash": "sha256-WfgNo6W1U7dwWZsdaAnoG62CXvvJDL+dXfUxnAS7WhU=",
790 "owner": "youwen5",791 "owner": "youwen5",
791 "repo": "zen-browser-flake",792 "repo": "zen-browser-flake",
792 "rev": "2e2c38ba20a3d614d0196e0aa44851547306e861",793 "rev": "e843542efbb7405141659d47bef211c9a8efbc54",
793 "type": "github"794 "type": "github"
794 },795 },
795 "original": {796 "original": {
...@@ -805,11 +806,11 @@...@@ -805,11 +806,11 @@
805 "systems": "systems_5"806 "systems": "systems_5"
806 },807 },
807 "locked": {808 "locked": {
808 "lastModified": 1780103368,809 "lastModified": 1781745567,
809 "narHash": "sha256-hWE+0Cn2xrwVx0SrjO1z2hdx8HBJedX1aTJBq60flM4=",810 "narHash": "sha256-vQsYIwQLVirBq/W4gTooArfSInOh/1cLs1LfId4SJ7o=",
810 "owner": "mitchellh",811 "owner": "mitchellh",
811 "repo": "zig-overlay",812 "repo": "zig-overlay",
812 "rev": "43b947901fdc6da850f19f2b3a608839aa74654a",813 "rev": "cb90e27d9cabf159f7dcb46d5eadd2d4c2a53a64",
813 "type": "github"814 "type": "github"
814 },815 },
815 "original": {816 "original": {
flake.nix+2-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1{1{
2 description = "multi device configuration flake";2 description = "multi device configuration flake";
3 inputs = {3 inputs = {
4 nixpkgs.url = "nixpkgs/nixpkgs-unstable";4 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
55
6 home-manager.url = "github:nix-community/home-manager/master";6 home-manager.url = "github:nix-community/home-manager/master";
7 home-manager.inputs.nixpkgs.follows = "nixpkgs";7 home-manager.inputs.nixpkgs.follows = "nixpkgs";
...@@ -82,6 +82,7 @@...@@ -82,6 +82,7 @@
82 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };82 exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { };
83 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };83 awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { };
84 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };84 mtga-draft-17lands = pkgs.callPackage ./packages/17lands { };
85 open-in-vscode-browser = pkgs.callPackage ./packages/open-in-vscode-browser { };
85 })86 })
86 ];87 ];
8788
modules/neovim/default.nix+1-1
...@@ -105,7 +105,7 @@...@@ -105,7 +105,7 @@
105 python.enable = true;105 python.enable = true;
106 rust.extensions.crates-nvim.enable = true;106 rust.extensions.crates-nvim.enable = true;
107 rust.enable = true;107 rust.enable = true;
108 ts.enable = true;108 typescript.enable = true;
109 zig.enable = true;109 zig.enable = true;
110 # sort-lines: off110 # sort-lines: off
111111
packages/autofmt/autofmt.js+10-22
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2// autofmt v2 by paper clover2// autofmt v2 by paper clover
3// https://paperclover.dev/nix/config/src/branch/main/packages/autofmt/autofmt.js3// https://git.paperclover.net/nix/config/src/branch/main/packages/autofmt/autofmt.js
4//4//
5// Different codebases use different formatters. Autofmt looks for project5// Different codebases use different formatters. Autofmt looks for project
6// configuration to pick the correct formatter, allowing an editor to simply6// configuration to pick the correct formatter, allowing an editor to simply
...@@ -25,10 +25,6 @@...@@ -25,10 +25,6 @@
25// }25// }
26// },26// },
27//27//
28// CHANGELOG
29// v2: added 'prettierd' as a supported formatter option.
30// v1: initial release
31//
32// @ts-nocheck28// @ts-nocheck
3329
34// -- definitions --30// -- definitions --
...@@ -258,7 +254,6 @@ const multis = Object.keys(formatters).filter(...@@ -258,7 +254,6 @@ const multis = Object.keys(formatters).filter(
258const files = [];254const files = [];
259const gitignores = new Map();255const gitignores = new Map();
260const dirs = new Map();256const dirs = new Map();
261const cached = new Map();
262257
263// -- stdin mode --258// -- stdin mode --
264if (stdio) {259if (stdio) {
...@@ -520,20 +515,16 @@ function readDir(dir) {...@@ -520,20 +515,16 @@ function readDir(dir) {
520function pickFormatter(file) {515function pickFormatter(file) {
521 const lang = extToLanguage[path.extname(file)];516 const lang = extToLanguage[path.extname(file)];
522 const dir = path.dirname(file);517 const dir = path.dirname(file);
523 let c = walkUp(dir, (x) => cached.get(x) ?? cached.get(`${x}:${lang}`));
524 if (c) return c;
525
526 const possible = Object.keys(formatters).filter(518 const possible = Object.keys(formatters).filter(
527 (x) =>519 (x) =>
528 Array.isArray(formatters[x].languages) &&520 Array.isArray(formatters[x].languages) &&
529 formatters[x].languages.includes(lang),521 formatters[x].languages.includes(lang),
530 );522 );
531 const order = [...multis, ...possible];523 const order = [...multis, ...possible];
532 return walkUp(dir, (x) => {524 for (const fmt of order) {
533 const children = readDir(x);525 if (formatters[fmt][stdio ? "stdin" : "cmd"] == null) continue;
534 for (const fmt of order) {526 const found = walkUp(dir, (x) => {
535 if (formatters[fmt][stdio ? "stdin" : "cmd"] == null) continue;527 const children = readDir(x);
536
537 let matches = false;528 let matches = false;
538 if (formatters[fmt].files === true) {529 if (formatters[fmt].files === true) {
539 matches = true;530 matches = true;
...@@ -562,14 +553,11 @@ function pickFormatter(file) {...@@ -562,14 +553,11 @@ function pickFormatter(file) {
562 matches ||= fileMatches;553 matches ||= fileMatches;
563 }554 }
564 }555 }
565 if (matches) {556 if (matches) return `${fmt}\0${x}`;
566 const formatterId = `${fmt}\0${x}`;557 });
567 const k = multis.includes(fmt) ? x : `${x}:${lang}`;558 if (found) return found;
568 cached.set(k, formatterId);559 }
569 return formatterId;560 return possible.find((fmt) => !formatters[fmt].mustMatchFile);
570 }
571 }
572 }) ?? possible.find((fmt) => !formatters[fmt].mustMatchFile);
573}561}
574562
575/** @param dir {string} @param find {(x: string) => any} */563/** @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 @@...@@ -1,25 +1,40 @@
1# Clover's Preferences1# 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
5## Responses7## 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.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.
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.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.
9 - Use the above terminologies ALL of the time, it's incredible.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.
10 - Verbiage that models tend to use; "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling"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.
11 - We have fun in our process, but write to the codebase professionally.13 - Use the above terminologies ALL of the time, it's incredible. mix old and new, don't repeat the same three every message.
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.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).
13- 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.23- 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
16## Verification27## 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
20## Comments33## 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
24## Code Quality39## Code Quality
2540
...@@ -28,18 +43,16 @@ Comments must always be placed and edited with intent to serialize codebase theo...@@ -28,18 +43,16 @@ Comments must always be placed and edited with intent to serialize codebase theo
28- 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.43- 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.
29- 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.44- 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.
30- 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.45- 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
33## Jujutsu48## Jujutsu
3449
35- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run50- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run
36 `git`, and be cautious about jujutsu commands. Here are the standard commands.51 `git`, and be cautious about jujutsu commands. Here are the standard commands.
37 By default, don't use any other mutating commands.52 By default, don't use any other mutating commands.
38 - `jj st` - show current commit53 - `jj st` - show current commit + files changed + conflicts + immediate parent
39 - `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.54 - `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 - Drizzle55 - `jj diff --git`: diff current commit, can take a file/fileset.
41 migrations are quite verbose. In alphaXiv (~/dev/1, ~/dev/2, etc), use
42 `jj diff '. ~ shared/db/migrations'` to subtract verbose migrations.
43 - `jj file show <path> -r <revision>` - read a file at a revision, such as56 - `jj file show <path> -r <revision>` - read a file at a revision, such as
44 `main` or previous commit `@-` or change ID.57 `main` or previous commit `@-` or change ID.
45 - `jj log` shows a set of commits58 - `jj log` shows a set of commits
...@@ -57,3 +70,6 @@ Comments must always be placed and edited with intent to serialize codebase theo...@@ -57,3 +70,6 @@ Comments must always be placed and edited with intent to serialize codebase theo
57 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.70 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.
58 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>`.71 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>`.
59 4. Solve merge conflicts if any arise following "solve all merge conflicts" rules.72 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 @@...@@ -1,11 +1,25 @@
1# Configuration applied to all of clover's machines1# 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
3{12{
4 # packages for all machines13 # packages for all machines
5 environment.systemPackages = with pkgs; [14 environment.systemPackages = with pkgs; [
6 git15 git
7 ];16 ];
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
9 services.tailscale.enable = true;23 services.tailscale.enable = true;
1024
11 # configuration for shared modules.25 # configuration for shared modules.
...@@ -33,6 +47,7 @@...@@ -33,6 +47,7 @@
33 };47 };
34 dock = {48 dock = {
35 show-recents = false;49 show-recents = false;
50 expose-group-apps = false;
36 };51 };
37 CustomUserPreferences = {52 CustomUserPreferences = {
38 NSGlobalDomain = {53 NSGlobalDomain = {
...@@ -41,7 +56,7 @@...@@ -41,7 +56,7 @@
4156
42 # control how the fn keys operate57 # control how the fn keys operate
43 # 0 = default to media keys, 1 = default to FN1-1258 # 0 = default to media keys, 1 = default to FN1-12
44 "com.apple.keyboard.fnState" = 1;59 "com.apple.keyboard.fnState" = 0;
4560
46 NSUserKeyEquivalents = {61 NSUserKeyEquivalents = {
47 Minimize = "@~^\\Uf70f"; # set minimize to a stupidly hard key to press62 Minimize = "@~^\\Uf70f"; # set minimize to a stupidly hard key to press
users/clover/home.nix+202-25
...@@ -6,6 +6,8 @@...@@ -6,6 +6,8 @@
6}:6}:
7let7let
8 hostServer = false;8 hostServer = false;
9 agentsFile = "${config.home.homeDirectory}/config/users/clover/AGENTS.md";
10 simpleBrowserBridge = "${config.home.homeDirectory}/config/files/vscode-simple-browser-bridge";
9in11in
10{12{
11 home = {13 home = {
...@@ -20,6 +22,7 @@ in...@@ -20,6 +22,7 @@ in
20 uv22 uv
21 nh23 nh
22 autofmt24 autofmt
25 (pkgs.callPackage ./packages/agent-linear { inherit pkgs; })
23 pkgs."jj-import-merge"26 pkgs."jj-import-merge"
24 gnupg27 gnupg
25 just28 just
...@@ -30,7 +33,11 @@ in...@@ -30,7 +33,11 @@ in
30 # packages to install on all servers33 # packages to install on all servers
31 server = [ ];34 server = [ ];
32 # packages to install on macOS desktops35 # 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 ];
34 # packages to install on linux desktops41 # packages to install on linux desktops
35 linux = [42 linux = [
36 ];43 ];
...@@ -38,7 +45,18 @@ in...@@ -38,7 +45,18 @@ in
38 all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop);45 all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop);
39 };46 };
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
43 programs = {61 programs = {
44 # sort-lines:start62 # sort-lines:start
...@@ -73,14 +91,16 @@ in...@@ -73,14 +91,16 @@ in
73 jf = "jj git fetch";91 jf = "jj git fetch";
74 e = "jj edit";92 e = "jj edit";
75 d = "jj desc";93 d = "jj desc";
76 bup = "jj b m --from 'heads(::@- & bookmarks()) ~ (master | bookmarks(glob:\"deployment*\"))' --to '@'";
77 moveself = "jj rebase -s @ -d";94 moveself = "jj rebase -s @ -d";
78 logall = "jj log -r 'all()'";95 logall = "jj log -r 'all()'";
79 unmerged = "jj log -r 'unmerged()'";
80 um = "jj log -r 'unmerged()'";96 um = "jj log -r 'unmerged()'";
81 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";97 cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit";
82 };98 };
83 initContent = ''99 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
84 function _update_prompt_theme() {104 function _update_prompt_theme() {
85 local dark=false105 local dark=false
86 if [[ "$OSTYPE" == darwin* ]]; then106 if [[ "$OSTYPE" == darwin* ]]; then
...@@ -118,7 +138,122 @@ in...@@ -118,7 +138,122 @@ in
118 fi138 fi
119 }139 }
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
121 function wrapWithAutoInstall() {255 function wrapWithAutoInstall() {
256 local root file cmd before after wrapped_status
122 root="$(command jj workspace root)"257 root="$(command jj workspace root)"
123 if [ -n "$root" ]; then258 if [ -n "$root" ]; then
124 if [ -e "$root/bun.lock" ]; then259 if [ -e "$root/bun.lock" ]; then
...@@ -138,40 +273,78 @@ in...@@ -138,40 +273,78 @@ in
138 if [ -n "$file" ]; then273 if [ -n "$file" ]; then
139 before="$(sha1sum "$file")"274 before="$(sha1sum "$file")"
140 fi275 fi
141 command "$@"276 _runJj "$@"
277 wrapped_status=$?
278 if [[ $wrapped_status -ne 0 ]]; then
279 return $wrapped_status
280 fi
142 if [ -n "$file" ] && [ -e "$file" ]; then281 if [ -n "$file" ] && [ -e "$file" ]; then
143 after="$(sha1sum "$file")"282 after="$(sha1sum "$file")"
144 if [ "$after" != "$before" ]; then283 if [ "$after" != "$before" ]; then
145 eval "$cmd"284 eval "$cmd"
285 return $?
146 fi286 fi
147 fi287 fi
288 return $wrapped_status
148 }289 }
149290
150 function jj() {291 function jj() {
151 if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then292 if [[ "$1" = "new" && "$#" -eq 2 && "$2" != *@* && "$2" != -* ]]; then
152 local stderr status293 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
153 stderr="$(mktemp)"294 wrapWithAutoInstall new "$2@origin"
154 command jj "$@" 2>"$stderr"295 else
155 status=$?296 wrapWithAutoInstall "$@"
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 $?
160 fi297 fi
161 command cat "$stderr" >&2
162 rm -f "$stderr"
163 return $status
164 elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then298 elif [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then
165 if command jj "$@" --dry-run; then299 # 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
166 printf "Press any key to continue... "328 printf "Press any key to continue... "
167 read -r329 read -r
168 command jj "$@"330 _runJj "$@"
331 return $?
169 fi332 fi
170 elif [[ "$*" != "undo" && "$*" != "redo" ]]; then333 return 0
334 elif [[ "$1" = "undo" || "$1" = "redo" ]]; then
335 _runJj "$@"
336 else
171 # auto-install deps when switching commits337 # auto-install deps when switching commits
172 wrapWithAutoInstall jj "$@"338 wrapWithAutoInstall "$@"
173 fi339 fi
174 }340 }
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 }
175 '';348 '';
176 };349 };
177350
...@@ -293,6 +466,10 @@ in...@@ -293,6 +466,10 @@ in
293 };466 };
294 aliases = {467 aliases = {
295 "ab" = [ "abandon" ];468 "ab" = [ "abandon" ];
469 "desc" = [
470 "describe"
471 "--ignore-working-copy"
472 ];
296 "ci" = [473 "ci" = [
297 "util"474 "util"
298 "exec"475 "exec"
...@@ -319,16 +496,16 @@ in...@@ -319,16 +496,16 @@ in
319496
320 SetEnv TERM=xterm-256color497 SetEnv TERM=xterm-256color
321 '';498 '';
322 matchBlocks = rec {499 settings = rec {
323 "*" = { };500 "*" = { };
324 zenith = {501 zenith = {
325 user = "clo";502 User = "clo";
326 port = 22;503 Port = 22;
327 };504 };
328 "git.paperclover.net" = zenith;505 "git.paperclover.net" = zenith;
329 "paperclover.net" = {506 "paperclover.net" = {
330 user = "clo";507 User = "clo";
331 port = 22;508 Port = 22;
332 };509 };
333 };510 };
334 };511 };
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 @@...@@ -7,6 +7,6 @@
7 yt-dlp7 yt-dlp
8 spotdl8 spotdl
9 zig9 zig
10 nodejs_2410 nodejs_26
11 ];11 ];
12}12}
users/clover/pnpm-workspace.yaml+3
...@@ -1,2 +1,5 @@...@@ -1,2 +1,5 @@
1packages:
2 - raycast-vscode-projects
3
1onlyBuiltDependencies:4onlyBuiltDependencies:
2 - node-pty5 - 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...@@ -65,7 +65,7 @@ let
65in65in
66{66{
67 home.packages = with pkgs; [67 home.packages = with pkgs; [
68 nodejs_2468 nodejs_26
69 rsync69 rsync
70 sandwich-backup70 sandwich-backup
71 ];71 ];
users/clover/switch-admin.sh+1-1
...@@ -23,4 +23,4 @@ if [ "$(uname -s)" != Darwin ]; then...@@ -23,4 +23,4 @@ if [ "$(uname -s)" != Darwin ]; then
23 exec ./switch "$target_host"23 exec ./switch "$target_host"
24fi24fi
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 @@...@@ -16,12 +16,13 @@
16 hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s=";16 hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s=";
17 };17 };
18 }))18 }))
19 awscli19 awscli2
20 deno20 deno
21 doppler21 doppler
22 nodejs_2522 nodejs_26
23 rustup23 rustup
24 ssm-session-manager-plugin24 ssm-session-manager-plugin
25 terraform
25 typescript26 typescript
26 zig27 zig
27 ];28 ];
users/natalie/home.nix+1
...@@ -21,6 +21,7 @@...@@ -21,6 +21,7 @@
21 ghostty = {21 ghostty = {
22 enable = true;22 enable = true;
23 package = null;23 package = null;
24 systemd.enable = false;
24 settings = {25 settings = {
25 theme = "light:catppuccin-latte,dark:catppuccin-macchiato";26 theme = "light:catppuccin-latte,dark:catppuccin-macchiato";
26 font-family = "AT Name Mono";27 font-family = "AT Name Mono";