Hello! I'm Hong Minhee (洪 民憙), an open source software engineer in my late 30s, living in Seoul, Korea. I'm bisexual and non-binary (they/them), and an enthusiastic advocate of free/open source software and the fediverse.
I work full-time on @fedify, an ActivityPub server framework in TypeScript, funded by @sovtechfund. I'm also the creator of @hollo, a single-user ActivityPub microblog; @botkit, an ActivityPub bot framework; Hackers' Pub, a fediverse platform for software developers; and LogTape, a logging library for JavaScript and TypeScript.
I have a long interest in East Asian languages (CJK) and Unicode. I post mostly in English here, though occasionally in Japanese or in mixed-script Korean (國漢文混用體), a traditional writing style that interleaves Chinese characters with the native Korean alphabet. Wanting to write in that style was actually one of the reasons I joined the fediverse. Feel free to talk to me in English, Korean, Japanese, or even Literary Chinese!
安寧하세요! 저는 서울에 살고 있는 30代 後半의 오픈 소스 소프트웨어 엔지니어 洪民憙입니다. 兩性愛者(bisexual)이자 논바이너리(non-binary)이며, 自由·오픈 소스 소프트웨어(F/OSS)와 聯合宇宙(fediverse)의 熱烈한 支持者이기도 합니다.
STF(@sovtechfund)의 支援을 받아 TypeScript用 ActivityPub 서버 프레임워크 @fedify 開發에 專業으로 任하고 있습니다. 그 外에도 싱글 유저用 ActivityPub 마이크로블로그 @hollo, ActivityPub 봇 프레임워크 @botkit, 소프트웨어 開發者를 위한 聯合宇宙 플랫폼 Hackers' Pub, JavaScript·TypeScript用 로깅 라이브러리 LogTape 等의 製作者이기도 합니다.
東아시아 言語(이른바 CJK)와 Unicode에도 關心이 많습니다. 이 計定에서는 主로 英語로 포스팅하지만, 때때로 日本語나 國漢文混用體 韓國語로도 씁니다. 聯合宇宙에 오게 된 動機 中 하나가 바로 國漢文混用體로 글을 쓰고 싶었기 때문이기도 하고요. 韓國語, 英語, 日本語, 아니면 漢文으로도 말을 걸어주세요!
#Optique 0.3.0 is out with dependent options and flexible parser composition, shaped by feedback from @z9mb1's work migrating @fedify CLI from Cliffy to Optique.
We're releasing Optique 0.3.0 with several improvements that make building complex CLI applications more straightforward. This release focuses on expanding parser flexibility and improving the help system based on feedback from the community, particularly from the Fedify project's migration from Cliffy to Optique. Special thanks to @z9mb1 for her invaluable insights during this process.
What's new
Required Boolean flags with the new flag() parser for dependent options patterns
Flexible type defaults in withDefault() supporting union types for conditional CLI structures
Extended or() capacity now supporting up to 10 parsers (previously 5)
Enhanced merge() combinator that works with any object-producing parser, not just object()
Context-aware help using the new longestMatch() combinator
Version display support in both @optique/core and @optique/run
Structured output functions for consistent terminal formatting
Required Boolean flags with flag()
The new flag() parser creates Boolean flags that must be explicitly provided. While option() defaults to false when absent, flag() fails parsing entirely if not present. This subtle difference enables cleaner patterns for dependent options.
Consider a scenario where certain options only make sense when a mode is explicitly enabled:
import { flag, object, option, withDefault } from "@optique/core/parser";import { integer } from "@optique/core/valueparser";// Without the --advanced flag, these options aren't availableconst parser = withDefault( object({ advanced: flag("--advanced"), maxThreads: option("--threads", integer()), cacheSize: option("--cache-size", integer()) }), { advanced: false as const });// Usage:// myapp → { advanced: false }// myapp --advanced → Error: --threads and --cache-size required// myapp --advanced --threads 4 --cache-size 100 → Success
This pattern is particularly useful for confirmation flags (--yes-i-am-sure) or mode switches that fundamentally change how your CLI behaves.
Union types in withDefault()
Previously, withDefault() required the default value to match the parser's type exactly. Now it supports different types, creating union types that enable conditional CLI structures:
const conditionalParser = withDefault( object({ server: flag("-s", "--server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), { server: false as const });// Result type is now a union:// | { server: false }// | { server: true, port: number, host: string }
This change makes it much easier to build CLIs where different flags enable different sets of options, without resorting to complex or() chains.
More flexible merge() combinator
The merge() combinator now accepts any parser that produces object-like values. Previously limited to object() parsers, it now works with withDefault(), map(), and other transformative parsers:
const transformedConfig = map( object({ host: option("--host", string()), port: option("--port", integer()) }), ({ host, port }) => ({ endpoint: `${host}:${port}` }));const conditionalFeatures = withDefault( object({ experimental: flag("--experimental"), debugLevel: option("--debug-level", integer()) }), { experimental: false as const });// Can now merge different parser typesconst appConfig = merge( transformedConfig, // map() result conditionalFeatures, // withDefault() parser object({ // traditional object() verbose: option("-v", "--verbose") }));
This improvement came from recognizing that many parsers ultimately produce objects, and artificially restricting merge() to only object() parsers was limiting composition patterns.
Context-aware help with longestMatch()
The new longestMatch() combinator selects the parser that consumes the most input tokens. This enables sophisticated help systems where command --help shows help for that specific command rather than global help:
const normalParser = object({ help: constant(false), command: or( command("list", listOptions), command("add", addOptions) )});const contextualHelp = object({ help: constant(true), commands: multiple(argument(string())), helpFlag: flag("--help")});const cli = longestMatch(normalParser, contextualHelp);// myapp --help → Shows global help// myapp list --help → Shows help for 'list' command// myapp add --help → Shows help for 'add' command
The run() functions in both @optique/core/facade and @optique/run now use this pattern automatically, so your CLI gets context-aware help without any additional configuration.
Version display support
Both @optique/core/facade and @optique/run now support version display through --version flags and version commands. See the runners documentation for details:
// @optique/run - simple APIrun(parser, { version: "1.0.0", // Adds --version flag help: "both"});// @optique/core/facade - detailed controlrun(parser, "myapp", args, { version: { mode: "both", // --version flag AND version command value: "1.0.0", onShow: process.exit }});
The API follows the same pattern as help configuration, keeping things consistent and predictable.
Structured output functions
The new output functions in @optique/run provide consistent terminal formatting with automatic capability detection. Learn more in the messages documentation:
import { print, printError, createPrinter } from "@optique/run";import { message } from "@optique/core/message";// Standard output with automatic formattingprint(message`Processing ${filename}...`);// Error output to stderr with optional exitprintError(message`File ${filename} not found`, { exitCode: 1 });// Custom printer for specific needsconst debugPrint = createPrinter({ stream: "stderr", colors: true, maxWidth: 80});debugPrint(message`Debug: ${details}`);
These functions automatically detect terminal capabilities and apply appropriate formatting, making your CLI output consistent across different environments.
Breaking changes
While we've tried to maintain backward compatibility, there are a few changes to be aware of:
The help option in @optique/run no longer accepts "none". Simply omit the option to disable help.
Custom parsers implementing getDocFragments() need to update their signature to use DocState<TState> instead of direct state values.
The object() parser now uses greedy parsing, attempting to consume all matching fields in one pass. This shouldn't affect most use cases but may change parsing order in complex scenarios.
Upgrading to 0.3.0
To upgrade to Optique 0.3.0, update both packages:
These improvements came from real-world usage and community feedback. We're particularly interested in hearing how the new dependent options patterns work for your use cases, and whether the context-aware help system meets your needs.
As always, you can find complete documentation at optique.dev and file issues or suggestions on GitHub.
#Optique 0.3.0 is out with dependent options and flexible parser composition, shaped by feedback from @z9mb1's work migrating @fedify CLI from Cliffy to Optique.
We're releasing Optique 0.3.0 with several improvements that make building complex CLI applications more straightforward. This release focuses on expanding parser flexibility and improving the help system based on feedback from the community, particularly from the Fedify project's migration from Cliffy to Optique. Special thanks to @z9mb1 for her invaluable insights during this process.
What's new
Required Boolean flags with the new flag() parser for dependent options patterns
Flexible type defaults in withDefault() supporting union types for conditional CLI structures
Extended or() capacity now supporting up to 10 parsers (previously 5)
Enhanced merge() combinator that works with any object-producing parser, not just object()
Context-aware help using the new longestMatch() combinator
Version display support in both @optique/core and @optique/run
Structured output functions for consistent terminal formatting
Required Boolean flags with flag()
The new flag() parser creates Boolean flags that must be explicitly provided. While option() defaults to false when absent, flag() fails parsing entirely if not present. This subtle difference enables cleaner patterns for dependent options.
Consider a scenario where certain options only make sense when a mode is explicitly enabled:
import { flag, object, option, withDefault } from "@optique/core/parser";import { integer } from "@optique/core/valueparser";// Without the --advanced flag, these options aren't availableconst parser = withDefault( object({ advanced: flag("--advanced"), maxThreads: option("--threads", integer()), cacheSize: option("--cache-size", integer()) }), { advanced: false as const });// Usage:// myapp → { advanced: false }// myapp --advanced → Error: --threads and --cache-size required// myapp --advanced --threads 4 --cache-size 100 → Success
This pattern is particularly useful for confirmation flags (--yes-i-am-sure) or mode switches that fundamentally change how your CLI behaves.
Union types in withDefault()
Previously, withDefault() required the default value to match the parser's type exactly. Now it supports different types, creating union types that enable conditional CLI structures:
const conditionalParser = withDefault( object({ server: flag("-s", "--server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), { server: false as const });// Result type is now a union:// | { server: false }// | { server: true, port: number, host: string }
This change makes it much easier to build CLIs where different flags enable different sets of options, without resorting to complex or() chains.
More flexible merge() combinator
The merge() combinator now accepts any parser that produces object-like values. Previously limited to object() parsers, it now works with withDefault(), map(), and other transformative parsers:
const transformedConfig = map( object({ host: option("--host", string()), port: option("--port", integer()) }), ({ host, port }) => ({ endpoint: `${host}:${port}` }));const conditionalFeatures = withDefault( object({ experimental: flag("--experimental"), debugLevel: option("--debug-level", integer()) }), { experimental: false as const });// Can now merge different parser typesconst appConfig = merge( transformedConfig, // map() result conditionalFeatures, // withDefault() parser object({ // traditional object() verbose: option("-v", "--verbose") }));
This improvement came from recognizing that many parsers ultimately produce objects, and artificially restricting merge() to only object() parsers was limiting composition patterns.
Context-aware help with longestMatch()
The new longestMatch() combinator selects the parser that consumes the most input tokens. This enables sophisticated help systems where command --help shows help for that specific command rather than global help:
const normalParser = object({ help: constant(false), command: or( command("list", listOptions), command("add", addOptions) )});const contextualHelp = object({ help: constant(true), commands: multiple(argument(string())), helpFlag: flag("--help")});const cli = longestMatch(normalParser, contextualHelp);// myapp --help → Shows global help// myapp list --help → Shows help for 'list' command// myapp add --help → Shows help for 'add' command
The run() functions in both @optique/core/facade and @optique/run now use this pattern automatically, so your CLI gets context-aware help without any additional configuration.
Version display support
Both @optique/core/facade and @optique/run now support version display through --version flags and version commands. See the runners documentation for details:
// @optique/run - simple APIrun(parser, { version: "1.0.0", // Adds --version flag help: "both"});// @optique/core/facade - detailed controlrun(parser, "myapp", args, { version: { mode: "both", // --version flag AND version command value: "1.0.0", onShow: process.exit }});
The API follows the same pattern as help configuration, keeping things consistent and predictable.
Structured output functions
The new output functions in @optique/run provide consistent terminal formatting with automatic capability detection. Learn more in the messages documentation:
import { print, printError, createPrinter } from "@optique/run";import { message } from "@optique/core/message";// Standard output with automatic formattingprint(message`Processing ${filename}...`);// Error output to stderr with optional exitprintError(message`File ${filename} not found`, { exitCode: 1 });// Custom printer for specific needsconst debugPrint = createPrinter({ stream: "stderr", colors: true, maxWidth: 80});debugPrint(message`Debug: ${details}`);
These functions automatically detect terminal capabilities and apply appropriate formatting, making your CLI output consistent across different environments.
Breaking changes
While we've tried to maintain backward compatibility, there are a few changes to be aware of:
The help option in @optique/run no longer accepts "none". Simply omit the option to disable help.
Custom parsers implementing getDocFragments() need to update their signature to use DocState<TState> instead of direct state values.
The object() parser now uses greedy parsing, attempting to consume all matching fields in one pass. This shouldn't affect most use cases but may change parsing order in complex scenarios.
Upgrading to 0.3.0
To upgrade to Optique 0.3.0, update both packages:
These improvements came from real-world usage and community feedback. We're particularly interested in hearing how the new dependent options patterns work for your use cases, and whether the context-aware help system meets your needs.
As always, you can find complete documentation at optique.dev and file issues or suggestions on GitHub.
Recently, whenever I write or share (boost) a post on my hollo.social instance, the server consistently crashes immediately afterward, and I haven't found the cause yet. Since I haven't received similar reports from other Hollo users, I suspect it might be an issue with my deployment configuration…
I’m Jaeyeol, a software engineer who loves Neovim and Zed.
I see myself as a pragmatic builder — I focus on creating software that I’d genuinely want to use, made for people rather than for technology’s sake.
Beyond coding, I also experiment actively within developer communities, often initiating gatherings and exploring new ways for people to connect and share.
One of my current personal projects happens to be in the fediverse space:
cosmoslide — a slideshare-like service for sharing presentations across the fediverse.
It’s built with NestJS (@fedify/nestjs) for the backend, Next.js for the frontend (most of it is vibe-coded, so I’ll probably rewrite it later 😅)
The project is still in development, with an expected first release between late September and mid October.
It started as a personal experiment, but I’m excited to see how it may contribute to the broader fediverse ecosystem.
I enjoy meeting others who are curious about new ideas, whether in technology or in community.
I'm currently improving Optique's façade API, and in the process, I've added a few more parsers. Optique 0.3.0 might have some breaking changes on the API side. Of course, these changes will be thoroughly documented in the changelog.
We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as #BotKit now supports #Node.js alongside #Deno, making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred #JavaScript runtime while building #ActivityPub#bots with the same powerful BotKit APIs.
One of the most requested features has landed: poll support! You can now create interactive polls in your #bot messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major #fediverse platforms like Mastodon and Misskey.
The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.
For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.
This release also includes an important security update: we've upgraded to #Fedify 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.
@hongminhee I love what you’ve done with this library! I’m already pushing my coworkers to try to get them to adopt it. I’ve been frustrated by yargs for years.
>moving on AT Proto is much simpler for users than moving on ActivityPub, where you don't get to keep your posts currently
Can keep the posts too if your server implements FEP-ef61.
>So if the government decides to crack down on your fediverse instance, you're more screwed than if they crack down on Bluesky PBC or bluesky.social
In the worst case you just sign up on another Fediverse instance. The rest of the network is still there and all connections can be restored.
If Bluesky shuts down, it's over. They control almost all infrastructure. They control did:plc which is centralized. They have developers, moderators, and they have funding. Once all of that disappears, only a tiny group of power users remains who will burn out in a month.
New: SocialHub and the Substrate of Decentralised Networks
SocialHub, one of the primary forums to talk about the #fediverse and #ActivityPub, has been struggling how to continue the operation. Decentralised networks need a coordination layer, but how to build this in a decentralised manner?
The issue of SocialHub is an interesting one, because where we are today is an odd situation where you have activitypub developers fragmented across multiple collaborative channels.
Some discuss their issues on their respective repositories only
Some discuss on SocialHub
Some discuss on Matrix channels
However, the bottom line truth is as follows: every ActivityPub developer is on the fediverse, ergo why shouldn't ActivityPub-focused discussions take place on the fediverse as well?
Up until this year, SocialHub has been an island separate from the fediverse. I used this analogy in my talk at fedicon to describe how lonely starting a community can be.
To SocialHub's credit, they have created a community of ActivityPub developers that exists to this day, kudos to them! The question remains now whether SocialHub performing their function adequately — to bring together ActivityPub developers of all stripes.
That's a question worth exploring in and of itself.
Forem's site with logos on the left side of the page. Hovering over every logo shows a popover with more info about its "subforem" and a "Follow" button.