senzu: v0.3.0
What's Changed release: v0.3.0 by @github-actions[bot] in https://github.com/aliou/senzu/pull/3 Full Changelog: https://github.com/aliou/senzu/compare/v0.2.0...v0.3.0
What's Changed release: v0.3.0 by @github-actions[bot] in https://github.com/aliou/senzu/pull/3 Full Changelog: https://github.com/aliou/senzu/compare/v0.2.0...v0.3.0
What's Changed release: v0.2.0 by @github-actions[bot] in https://github.com/aliou/senzu/pull/2 Full Changelog: https://github.com/aliou/senzu/compare/v0.1.0...v0.2.0
Release v0.7.0
What's Changed release: v0.1.0 by @github-actions[bot] in https://github.com/aliou/senzu/pull/1 New Contributors @github-actions[bot] made their first contribution in https://github.com/aliou/senzu/pull/1 Full Changelog: https://github.com/aliou/senzu/commits/v0.1.0
What's Changed release: v0.8.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/10 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.7.0...v0.8.0
Release @aliou/sesame-cli@0.11.0
Release v0.11.0
I think that’s a great summary of the things we’ve done to make agents more productive in our codebase: don’t make them guess.
I’ve been trying to articulate to friends and colleagues why I never seem to have my agent screw up that much and it boils down to this: not letting them guess. Whether it’s via skills or instructions, but also toolinge.g. plugins for your favorite linter that explain why something is prohibited. and scripts, having everything in front of your agent decreases its chances of doing things wrong.
Of course it’s hard work to keep all of this up to date, but I find it way more rewarding than running a random npx skills addNo shade! and crossing finger that it will solve all my problems.
My initial instinct was to go full near-airgap: remove the default route, run an SNI-based TLS proxy on the host, and only allow traffic to
api.telegram.organdopenrouter.ai.I built this with unbound returning fake DNS records pointing at the host bridge IP, nginx stream with
ssl_prereaddoing SNI-based forwarding, nftables dropping everything in the FORWARD chain.It worked, technically. But it was brittle. The proxy needed careful handling of dynamic IPs, and Openclaw’s error messages when DNS returned unexpected results were… unhelpful.
For this demo, I just said screw it and allowed everything (with a log). The VM has full internet access, but every DNS query and every new connection is visible from the host.
Continuing on my research around microvm, I discovered microvm.nix and yet another way of monitoring and/or locking down networking for vms that will be used by agents.
Minor Changes b4f5b84: Update Synthetic model catalog with Kimi K2.7 Code and remove delisted models.
Agents naturally gravitate toward the smallest stable abstraction. So, in an agent-first world, the most important question facing product teams is not what features and why, but instead how do I design the right primitive ?
Building great developer tools (e.g. primitives) has always been an exercise in exquisite taste and deep domain expertise because it requires balancing a set of competing, and often contradictory, forces: namely the tension between expressivity and simplicity. — The primitive is the product
Will be super interesting to see how companies adopt this and it will truly be a differentiator in adoption.
/tree and /spawn to investigate bugs Simple context management for when you don’t know what you’re looking for.
A VM that can reach the host’s local network is a VM that can reach things it has no business reaching. So QEMU doesn’t run in the host’s network namespace at all. We
unshareinto fresh user, net and mount namespaces first. Inside that namespace a small wrapper bind-mounts a resolv.conf pointing at127.0.0.1so that QEMU’s built-in slirp DNS isn’t used, then installs blackhole routes for every special-use IP range (RFC 6890, so private networks, link-local, loopback, etc.) before it execs QEMU.slirp4netnsthen provides the namespace’s outbound internet connection, with--disable-host-loopback, sandbox and seccomp all on. QEMU runs inside that namespace, and the guest’s network card is attached to QEMU’s own built-in user-mode networking. So every packet from the guest takes two hops: guest → QEMU’s slirp → the namespace’sslirp4netns→ the internet. The guest never sees the host’s network and the host’s network never sees the guest. All of this is done without needing any privileges!
I’ve been reading a lot about microVMs and Firecracker lately as I work on async agent side projectMore on that soon hopefully. and I find it super interesting how people tackel the same UX problems (in this case: locking down the network).
Release v0.2.0
Minor Changes 61e66dc: Filter disabled gateway providers out of proxy and dedicated flows. /api/providers lists every provider regardless of its disabled flag, so disabled providers and their models leaked into the proxy settings/onboarding checklist, the dedicated provider list…
this is a huge reason why i use pi.
i absolutely do not want my harness regularly changing behavior out from under me, including system prompt changes, on top of an already stochastic llm
Imagine I have /skill-1 and /skill-2, both with disable-model-invocation: true I tell Claude Code: “/skill-1, /skill-2 do XYZ”
Which of the skills will be invoked?
I’ll give you a clue - the behaviour was one thing 3 weeks ago, another thing 1 week ago, and back to the original today.
I’ve been playing around making my own harnessPi remains my main harness, but trying to think about how I work in this agent-looping world. recently, and also noticed how the behaviour of many Claude features seemingly seem to change every time I spin it up to test something. For my harness, I just decided on a simple principle: trying to keep the primitives (i.e. skills and mid-turn messages affect the agentMost of the time i want immediate steering, but I realized I do use follow-up quite a lot when leaving agents do their thing, especially when running in sandboxes and/or building prototypes where the result itself doesn’t matter much for me. ) consistent with how I expect agent to react.
introducing tau τ — an educational agent harness that teaches you how to build agent harnesses
This is so cool. I truly think we’re moving towards a world where writing/making a great agent harness ends up becoming accessible to most people because the knowledge is just readily available.
Minor Changes e86681a: Add a pythonToPython3 rule that rewrites bare python command invocations to python3 when enabled.
Minor Changes 69b7dba: Initial Cohere provider extension for Pi. Patch Changes 4a44d57: Align repository scaffolding and release workflow with pi-edgee.
Application scope and user experience: Sun Microsystems co-founder Bill Joy famously said: “No matter who you are, most of the smartest people work for someone else.” That applies with particular force to AI. The range of what agents can do — and the variety of industries, workflows, and real-world contexts they touch — is so vast that no single company can build the best solution for all of them. We see a near-infinite opportunity to rewrite the world using agentic AI as foundational infrastructure, and it will require many companies with focused effort.
Thinking a lot about this lately, especially around my own use. I want from using Anthropic models and tools exclusively a year ago to barely using them and instead using different models in different harnesses depending on the task at hand.
It’s very likely that I’m part of the minority of AI users in this case currently but I wonder how long this will become more common ground amongst the majority of users.
/aperture/config endpoint so non-admin Aperture grants can use the extensionus jurisdiction for Durable Objects Durable Objects now supports a new
usjurisdiction. Create namespaces restricted to the US to keep compute and storage local.
i feel like i have a aluminium hatit’s called a tinfoil hat dummass , but it seems like everyone is preparing for us/non-us tiered system around llm access.
@earendil-works/pi-coding-agent and @earendil-works/pi-tui from >=0.74.0 <1 to * to match the canonical Pi package convention in docs/packages.md. No runtime behavior changes; no post-0.74 APIs are usedRelease v0.18.0
pathAccess.allowedPaths from a flat string[] (trailing-slash convention) to an explicit { kind, path } discriminated array@aliou/pi-utils-settings to ^0.17.0 and switch migration warnings to its built-in Migration.message fieldWhat's Changed release: v0.7.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/9 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.6.1...v0.7.0
Release v0.5.0
Release v0.8.0
This article will sound like a schizo ramble, so prepare. To start off I want to state my true beliefs on the matter:
- LLMs are not conscious and won’t ever be.
- LLMs are inaccurate statistical machines.
- Open Source AI is good but must get better.
- I have no hatred, or malice for any closed source lab
- I respect what I see as my peers at OpenAI, Anthropic, xAI etc.
You might think, well if that’s the case what’s the problem? These models aren’t intelligent, they can’t really replace people, can they?
Yeah that’s my shit.
Release v0.10.0
been playing with exe.dev this weekend and was curious how they made ssh [vm-name].exe.dev just work, and it’s pretty cool: https://blog.exe.dev/ssh-host-header
we landed on a pretty good workflow for doing parallel work in OpenCode
this demo is with git worktrees but i also preview an alternative we’re working on at the end
this will be in 1.6.0
— @thdxr
Rift looks interesting.
Take a look, I really like how this is explained.
buildSchemaUrl callers to customize schema hosting URLs with baseUrl or template optionsonBeforeClose to intercept settings UI close requests before discarding draftsRelease v0.1.0
I like the idea of Claude’s workflow thingy but since it’s writing code, it keeps failing with these kind of errors:

In addition, the UI of Claude feels a bit overwhelming to me.

There’s also some strong validations around making the workflow deterministic and I wonder if it could still be handled? I’m also curious on why they’re not giving guidance to the model about this and/or linting workflow code to have this be caught before the harness runs the workflow.

What's Changed release: v0.2.0 by @github-actions[bot] in https://github.com/aliou/granola-cli/pull/2 Full Changelog: https://github.com/aliou/granola-cli/compare/v0.1.0...v0.2.0
What's Changed release: v0.1.0 by @github-actions[bot] in https://github.com/aliou/granola-cli/pull/1 New Contributors @github-actions[bot] made their first contribution in https://github.com/aliou/granola-cli/pull/1 Full Changelog: https://github.com/aliou/granola-cli/commits/v…
Missed that claude/opus has a fast mode so I had Pi make a quick extensionDon’t look too closely at that horrendous code that Kimi with reasoning disabled produced. to play with it. Below, a comparison with the vanilla Opus.
requiresReasoningContentOnAssistantMessages compat flag for reasoning modelsguardrails:action:prompted event that fires when guardrails shows an interactive prompt to the user, before the user has respondedMinor Changes ea5dab6: Catch await expressions buried directly inside call arguments. Patch Changes a53d66e: Fix pi-no-node-exec to flag multi-name child_process spawning imports while allowing non-spawning imports.
What's Changed release: v0.6.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/7 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.5.0...v0.6.0
Release @aliou/vite-plugin-obsidian@0.2.0
Closing my open tabs and finally read Mikker’s post. Borrowed his review workflow but wanted to have it in a Ghostty split (thanks to AppleScript).
I then had Kimi and GLM figure it out and make me an extension.
Release v0.1.0
Been using variations of this prompt every time I notice a gap in my knowledge. Choosing a starting point that I broadly understand and then throwing stuff I literally don’t know about works super well for me.

I want to learn more and understand what’s fine tuning for llm models, what’s lora, how does https://thinkingmachines.ai/ ‘s products fit in this etc. to do so, i want to use coding agent traces as training data, from hugging face (see https://huggingface.co/changelog/agent-trace-viewer ) , in particular traces from pi.
let’s assume i know nothing about what is finetuning and how it works. let’s go through this blog post https://leoniemonigatti.com/blog/fine-tuning-lfm2-5-1-2b-instruct-with-grpo.html and quizz me on it to see what is my current understanding. use the current directory as scratchpad / notes. download the markdown of the article here so I/we can annotate it . curl it using this url
https://markdown.new/https://leoniemonigatti.com/blog/fine-tuning-lfm2-5-1-2b-instruct-with-grpo.html
New TIL: I figured out how to use my LLM CLI tool in a shebang line, which means you can write executable scripts in English, or hook up more complex scripts with a snippet of YAML template
— @simonw
It’s becoming a meme at this point, but here’s a Pi extension to do the same:
Also watch GLM-5.1 completely overthink while trying to speed up the video and add a fast-forward overlay.
Current status.
Currently cleaning up a backlog of issues and PRs in a client’s repo and was tired of alt-tabbing between Pi and my browser, so made a quick Pi extension to autocomplete GitHub PRs and issues.
Never going to use this ever again, but might be useful to other people so sharing it as a gist.
@aliou/pi-utils-ui panels while preserving the existing pi-utils-settings component APIsWas about to make a Pi extension to communicate between sessions, but actually only needed Amp’s read thread tool.

reasoningEffortMap to thinkingLevelMap per Pi 0.72.0 API. Replace session_switch event with session_start. Swap @sinclair/typebox for typebox. Add Kimi K2.5 model. Set reasoning: false for Llama 3.3 70B/v1/models on session start and re-registers the provider with live data (including pricing, capabilities, and limits from the new API metadata). A notification is shown when live models differ from the cache. Falls back to the hardcoded cache if the fetch failssrc/lib/, introduces ApertureRuntime class with dependency injection, replaces lifecycle hooks with session_start + onSync callback pattern, and adds provider unregistration with user notificationstreamSimple wrapper that sends x-session-id header with the Pi session ID. This groups all requests from the same Pi session together in the Aperture dashboardmodels from registerProvider call. Rely on the baseUrl-override path instead, which preserves built-in model definitions (reasoning, compat, thinking levels) and only updates the endpoint URLCan’t help but re-read this every time I write “just” somewhere.
ctx.ui.custom() returns undefined, with fallback to ctx.ui.select()Frame to extend Container from pi-tui, inheriting addChild, removeChild, and clear. borderColor is now required. Children are no longer accepted via constructor options — use addChild insteadMinor Changes eb09413: Add no-empty-catch plugin: disallows empty catch blocks Patch Changes 526fb68: Bump Biome peer dependency to >=2.5.0 ef2d41d: Add biome-plugins-setup skill for configuring plugins in consumer projects
r key binding to the quotas command to refetch and refresh quota data without closing the panel~-based patterns match correctly and expand to the current user’s home directory during blocking and existence checksdisplayToStorageValue helper. The default change handler now stores raw strings as-is instead of coercing “on”/“off”/“enabled”/“disabled” to booleans. Use onSettingChange to convert display values to the correct storage typesMinor Changes c72c019: Add the no-buried-await plugin to disallow burying await inside parentheses. This catches patterns like return (await foo()) || "" and Boolean(await foo()) and encourages awaiting the value first before using it in a separate expression.
Banger.
Two ways to connect Pi to a custom model endpoint, using Synthetic as an example
linkup_web_fetch outputs in tool results and save the full fetched content to a temp filelimit parameter to linkup_web_search so agents can cap returned result countbash.sourceMode with deterministic override-bash and composed-bash routing for rewrite featuresWhat's Changed release: v0.5.0 by @github-actions[bot] in https://github.com/aliou/linear-cli/pull/7 Full Changelog: https://github.com/aliou/linear-cli/compare/v0.4.0...v0.5.0
Minor Changes 4f2e6c0: Add pi-no-node-exec plugin: disallows importing from child_process in pi extensions. Use pi.exec() instead.
FuzzySelector via searchThreshold (default 7). When item count is at or below the threshold, it now renders a simple Up/Down/Enter list without a search input while keeping callbacks and currentValue pre-selection behavior consistentWhat's Changed release: v0.4.0 by @github-actions[bot] in https://github.com/aliou/linear-cli/pull/6 Full Changelog: https://github.com/aliou/linear-cli/compare/v0.3.0...v0.4.0
Needed to scaffold a project this morning with an AGENTS.md and liked how Claude Code’s new /init looked, so had Pi figure out how it works and make its own version.
Another one-time use extension.
@aliou/pi-extension-dev to @aliou/pi-dev-kitWhat's Changed release: v0.3.0 by @github-actions[bot] in https://github.com/aliou/linear-cli/pull/5 Full Changelog: https://github.com/aliou/linear-cli/compare/v0.2.2...v0.3.0
pi config can enable or disable them individually.pi config loading, shared command registration, and dynamic Linkup system prompt guidance. Closes #17.The models are getting better and better which makes it easy to forget to not delegate your thinking.
What's Changed release: v0.5.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/6 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.4.1...v0.5.0
Finally got around to reading Linear’s Agent interaction guidelines and did a quick implementation with Pi. It was pretty simple to set up and I kinda like the UX!
Take a look at the code on GitHub.
Minor Changes 9231b94: Add a new no-ts-import-extension plugin that disallows .ts extensions in import and re-export paths.
What's Changed release: v0.2.0 by @github-actions[bot] in https://github.com/aliou/fastmail-cli/pull/3 Full Changelog: https://github.com/aliou/fastmail-cli/compare/v0.1.1...v0.2.0
Had to update Ghostty to fix that annoying auto-scroll bug but didn’t want to lose my open tabs. Asked Pi/GPT-5.4 to dump and restore my tabs and splits. It mostly worked and I’m never going to reuse this.
Full Changelog: https://github.com/aliou/linear-cli/commits/v0.1.0
buildSchemaUrl helper and schemaUrl option for ConfigLoader. When set, save() injects $schema as the first key and load() strips it from parsed configenvFiles to a new policies system with per-rule protection levels (noAccess, readOnly, none), add migration from old config fields, and replace the old env hook with a general policies hook@aliou/pi-utils-settings to latest 0.5.x, clarify fallback behavior in README/AGENTS, ignore .pi/settings.json, and ensure migrated configs write the current schema version without lexicographic version comparisons/ps:dock, /ps:focus, /ps:logs commands. Add deprecated /process:* commands. Replace status widget with log dock. Preserve ANSI colors. Fix duplicate notifications. Use proper ThemeColor typewrite action to write to process stdinextraTabs support to registerSettingsCommand so extensions can render non-scope top-level tabs (for example, an Examples tab) after scope tabsSettingsTheme that works as both SettingsListTheme and full pi `ThemeSettingsDetailEditor component for focused second-level settings editingWhat's Changed release: v0.4.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/3 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.3.0...v0.4.0
Minor Changes 0a00dc1: Add a new no-inner-types plugin that disallows type and interface declarations inside function bodies.
Got absolutely screwed by Opus thinking it was running on Linux instead of macOS while debugging some CLI stuff with tmux.
But the session of Opus figuring out what happened is pretty fascinating:
Also created a small tool that uses BM25 to find content in previous sessions. It’s super useful to just say “I was running a Pi session at the same time, could it be related?” This is completely borrowed from Amp.
What made me switch coding agents and why I stayed
deep boolean with depth enum on web-answer tool for consistency with web-searchallowImportingTsExtensions and setting noEmit: falseWhat's Changed Updating @aliou/sesame to version 0.3.0 by @github-actions[bot] in https://github.com/aliou/sesame/pull/6 Full Changelog: https://github.com/aliou/sesame/compare/v0.2.0...v0.3.0
What's Changed release: v0.3.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/2 Full Changelog: https://github.com/aliou/preview-markdown/compare/v0.2.0...v0.3.0
What's Changed release: v0.1.0 by @github-actions[bot] in https://github.com/aliou/sesame/pull/1 New Contributors @github-actions[bot] made their first contribution in https://github.com/aliou/sesame/pull/1 Full Changelog: https://github.com/aliou/sesame/commits/v0.1.0
Minor Changes fcd49ce: Correct version bump: new plugins are minor releases, not patches. This bump acknowledges that the no-emojis plugin added in 0.2.1 was a feature addition.
Minor Changes 04b0993: Add no-js-import-extension plugin that disallows .js extensions in import and re-export paths Patch Changes b900cf9: Bundle biome-gritql-plugins skill for agents working with this package
Maintenance matters more than setup: keeping context fresh as codebases evolve
Very meta: wanted to use Kimi K2.5 via Synthetic, so I had Pi set up a one-off extension that creates the provider and defines its model. Then reloaded Pi and asked Kimi to create the Synthetic provider.
Release pi-linkup@0.1.0
Another one, but this one probably more for me than other people: guardrails!
Fun extension I made for Pi: processes. Let your agent handle running processes in the background and be notified when they finish / die etc.
Ported by beloved jellybeans-mono for Pi, both dark and light!


What's Changed release: v0.1.0 by @github-actions[bot] in https://github.com/aliou/fastmail-cli/pull/1 New Contributors @github-actions[bot] made their first contribution in https://github.com/aliou/fastmail-cli/pull/1 Full Changelog: https://github.com/aliou/fastmail-cli/commit…
Your AGENTS.md is not enough. Leverage deterministic tools to enforce rules
What's Changed release: v0.2.0 by @github-actions[bot] in https://github.com/aliou/preview-markdown/pull/1 New Contributors @github-actions[bot] made their first contribution in https://github.com/aliou/preview-markdown/pull/1 Full Changelog: https://github.com/aliou/preview-mar…
Progressive disclosure for coding agents: load context only when needed
Full Changelog: https://github.com/aliou/preview-markdown/commits/v0.1.0
The new wave of coding models needs less hand-holding than you think
How coding agents changed the way I worked
Fixing nvim-treesitter after the master-to-main rewrite
I needed to quickly investigate some data and used the following pattern to do so. Using the model of an e-commerce store as example, here’s what I did.
First, setup a cube.js defining the dbType:
module.exports = {
dbType: 'duckdb',
}
Then, define the driverFactory, and configure the access to the GCS bucket:
const driverFactory = async () => {
const { DuckDBDriver } = require('@cubejs-backend/duckdb-driver');
return new DuckDBDriver({
database: ':memory:',
initSql: `
INSTALL httpfs;
LOAD httpfs;
CREATE SECRET (
TYPE gcs,
KEY_ID 'YOUR_KEY_ID',
SECRET 'YOUR_SECRET_ID'
);
`,
});
}
Finally, add to initSql the commands to setup our views and access our data:
CREATE VIEW orders AS
SELECT * FROM read_parquet('gs://tmp-bucket/orders/*.parquet');
CREATE VIEW products AS
SELECT * FROM read_parquet('gs://tmp-bucket/products/*.parquet');
CREATE VIEW line_items AS
SELECT * FROM read_parquet('gs://tmp-bucket/line_items/*.parquet');
CREATE VIEW users AS
SELECT * FROM read_parquet('gs://tmp-bucket/users/*.parquet');
CREATE VIEW inventory_transactions AS
SELECT * FROM read_parquet('gs://tmp-bucket/inventory/*.parquet');
Views, Not Tables: I specifically used CREATE VIEW instead of CREATE TABLE AS to avoid loading all the data into memory. Views just store the query definition - DuckDB streams the Parquet data chunk by chunk when you actually query it. With tables, DuckDB would copy everything into its internal format, eating up memory or disk space.
Zero Infrastructure: The :memory: database means no provisioning. Each Cube process gets a fresh DuckDB instance that reads directly from existing Parquet files.
Direct Access: No ETL pipeline, no data loading. The read_parquet() function streams data directly from GCS, and DuckDB’s columnar engine only fetches the columns needed for each query.
Wildcard Patterns: The /*.parquet pattern lets you treat multiple files as a single table without any preprocessing. Perfect for data that’s already partitioned by date or other dimensions.
In this example, you could run complex queries across orders, products, line items, and inventory - joining across millions of rows within seconds. What would normally require hours of data pipeline setup can be done immediately.
DuckDB’s aggressive caching means the first query might take a few seconds to fetch metadata, but subsequent queries run pretty much instantly even on larger datasets.
Benefits:
Drawbacks:
:memory: means state vanishes on restartThis pattern works for any quick data investigation. Got Parquet files? Throw them in GCS, point Cube at them with this config, and you’re analyzing data in minutes. For more permanent setups, swap :memory: for a file path to persist the DuckDB catalog between restarts.
Your data lake is already an OLAP database. You just need DuckDB to unlock it.
Continuing my work on cube-records, I wanted to support joined field names like orders.total or users.email - matching exactly how the underlying Cube names them. Instead of manually defining these combinations, TypeScript’s template literal types generate them automatically.
Here’s a simplified version of the Cube schema:
// User defines their schema
interface Schema {
users: {
fields: { id: string; email: string };
joins: ['orders'];
};
orders: {
fields: { total: number; status: string };
joins: [];
};
}
Then, we use a template literal type to defined our joined fields:
// Template literal magic
type JoinedFields<T extends keyof Schema> =
Schema[T]['joins'][number] extends infer Join
? Join extends keyof Schema
? `${Join}.${keyof Schema[Join]['fields'] & string}`
: never
: never;
Finally, we can also use the type to create our own custom type.
// TypeScript now knows these fields exist:
type UserJoinedFields = JoinedFields<'users'>;
// Result: 'orders.total' | 'orders.status'
The infer keyword extracts each join name, then template literals compose the dot-notation paths.
With the schema defined above and our new type, we can define a Query type like the following:
type Query<T extends keyof Schema> = {
model: T;
fields: (Schema[T]['fields'] & JoinedFields<T>)[];
}
And in our code, we can declare a query serenely thanks to the type system preventing us from writing a query with invalid fields:
const query: Query = {
model: 'users',
fields: [
'email', // ✓ users field
'orders.total', // ✓ joined field
'orders.status', // ✓ joined field
'invalid.field' // ✗ Type error
]
};
The real implementation has more conditionals for safety, but the core idea remains the same: extract the join names, then use template literals to compose the field paths.
Benefits:
Drawbacks:
This pattern shines when you control the schema and need to generate predictable string patterns. It’s less suitable for user-defined schemas or when you need runtime validation anyway. The sweet spot is internal APIs where you want compiler-enforced consistency between your types and runtime behavior.
What you don’t write is as important as what you do. No manual string unions. No keeping field lists in sync. Add a new join relationship, and TypeScript immediately knows about every possible field combination.
When Ghostty came out, I wrote this blog post which contained a few screenshots. I forgot how I managed to have both light and dark mode work seamlessly in Eleventy, so I figured I’d write it down for future me or anyone that sees this.
I use an Eleventy shortcode that conditionally renders different images based on theme preference. The basic version wraps images in figure tags with captions:
config.addShortcode('image', (src: string, alt: string, withDarkMode = false) => {
const lastDotIndex = src.lastIndexOf('.');
const basePath = src.slice(0, lastDotIndex);
const extension = src.slice(lastDotIndex);
const lightPath = `${basePath}-light${extension}`;
const darkPath = `${basePath}-dark${extension}`;
return `<figure>${
withDarkMode
? `<img class="not-dark:hidden" src="${darkPath}" alt="${alt}">` +
`<img class="dark:hidden" src="${lightPath}" alt="${alt}">`
: `<img src="${src}" alt="${alt}">`
}<figcaption>${alt}</figcaption></figure>`;
});
The images would swap instantly when toggling themes, which felt jarring. Adding Tailwind’s transition utilities creates a cross-fade effect:
withDarkMode
? `<img class="not-dark:hidden not-dark:opacity-0 dark:opacity-100 starting:dark:opacity-0 transition-discrete transition-opacity duration-1000" src="${darkPath}" alt="${alt}">` +
`<img class="dark:hidden dark:opacity-0 not-dark:opacity-100 starting:not-dark:opacity-0 transition-discrete transition-opacity duration-1000" src="${lightPath}" alt="${alt}">`
: `<img src="${src}" alt="${alt}">`
The starting: modifiers prevent the fade animation on initial page load, which I borrowed from how Tailwind’s own docs handle theme switching.
Now {% raw %}{% image "ghostty-screenshot.png" "Terminal with ligatures" true %}{% endraw %} automatically looks for ghostty-screenshot-light.png and ghostty-screenshot-dark.png, then smoothly transitions between them when you toggle themes.
Benefits:
Drawbacks:
This approach works well for static sites where you control every image. For dynamic content or high-traffic sites, you’d want lazy loading for the hidden variant or CSS filters for automatic adjustments.
Figured how to create custom shortcode after reading this article by Anh.
While working on @general-dexterity/cube-records, I needed a way to defineOr use codegen with another library. my domain-specific Cube models in a way that would provide type-safe autocompletion. The solution was ended up pretty simple: global interface augmentation.
The lib defines an empty global interface that users can augment with their own cube definitions. However, after publishing, I discovered that tsup was optimizing the empty interface into a type alias during the build:
// What we write
export interface CubeRecordMap {}
// What tsup outputs with dts: true
export type CubeRecordMap = {}
// Now augmentation fails
declare global {
interface CubeRecordMap { // Error: can't augment a type alias
users: { /* ... */ }
}
}
Adding a dummy property prevents this optimization:
export interface CubeRecordMap {
__empty: {
measures: {};
dimensions: {};
joins: [];
};
}
That __empty property isn’t arbitrary - it ensures the interface remains augmentable through the entire build pipelineThis is specifically a build tool optimization when generating declaration files, not a TypeScript language limitation. .
Users can now extend the interface in their projects:
declare global {
interface CubeRecordMap {
users: {
measures: { count: { type: number } };
dimensions: {
id: { type: string };
email: { type: string };
};
joins: readonly ['orders'];
};
// ... other models and views
}
}
The library extracts type-safe cube names and fields:
type CubeRecordName = keyof CubeRecordMap;
type CubeRecordMeasure<T extends CubeRecordName> =
keyof CubeRecordMap[T]['measures'] & string;
Benefits:
Drawbacks:
This isn’t just for my personal benefitsEven though it was pretty fun to try to figure this out. , this pattern provides domain-specific autocompletion without runtime cost. Instead of searching through Cube models for field names, TypeScript provides instant feedback as you type.
While writing my Ghostty blog post, I wanted syntax highlighting for the configuration snippets. Since Shiki doesn’t support Ghostty’s config format, I added a custom language definition.
const ghostty = {
name: 'ghostty',
scopeName: 'source.ghostty',
fileTypes: ['ghostty'],
repository: {},
patterns: [
{
include: '#strings',
},
{
name: 'comment.line.number-sign.ghostty',
match: '^\\s*#.*$',
},
{
match: '\\b(font-family|font-feature)\\b',
name: 'keyword.other.ghostty',
},
],
};
I only defined patterns for comments (#) and the font keywords I was actually using. The grammar follows TextMate rules but doesn’t try to implement the full Ghostty spec - just enough for my examples.
highlighter = await createHighlighter({
themes: ['github-dark', 'github-light'],
langs: ['ruby', 'javascript', 'json', 'elixir', 'typescript', ghostty],
});
Enabling Monaspace’s stylistic sets for code ligatures in Ghostty
Parsing structured strings with Ruby’s named regex captures instead of split
Making Ruby value objects look like constants by overriding inspect
Serializing value objects into ActiveRecord models with the Attributes API
.gitignore, .git/info/exclude, and core.excludesfile
Building a custom Ecto type for Postgres timestamp ranges
A Rails source spelunking find: custom validators can be namespaced with ::
Setting up a global .gitignore to keep editor and OS files out of every repo