<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Automation</title><link>https://blog.iany.me/tags/automation/</link><description>Recent content in Automation «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Mon, 10 Aug 2026 12:45:09 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/automation/index.xml" rel="self" type="application/rss+xml"/><item><title>My AI-Assisted Development Workflow</title><link>https://blog.iany.me/2026/08/my-ai-assisted-development-workflow/</link><pubDate>Mon, 10 Aug 2026 12:45:09 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/08/my-ai-assisted-development-workflow/</guid><description>&lt;p&gt;AI agents write code fastest when the work is sliced small and a human stays on judgment. My workflow runs a four-step loop: plan the feature into units, hand the branch stack to the agent, review each PR myself, then let the agent address my comments.&lt;/p&gt;
&lt;p&gt;I wrote a separate guide on the branch-management mechanics: &lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;♯ A Practical Guide to gh-stack for AI-Assisted Development&lt;/a&gt;. That post covers worktrees, rebase habits, and JSON sync. This post is the loop around that tool.&lt;/p&gt;
&lt;h2 id="1-plan-and-split-into-units"&gt;1. Plan and split into units&lt;/h2&gt;
&lt;p&gt;Start with a written plan, not &amp;ldquo;build the feature.&amp;rdquo; The plan names the goal, records the product decisions already made, and breaks work into Implementation Units: U1, U2, U3, and so on.&lt;/p&gt;
&lt;p&gt;Each unit is small enough to fit one agent session and one human review. Specific, not vague. &amp;ldquo;Design the data model for board columns&amp;rdquo; is a unit. &amp;ldquo;Do the backend&amp;rdquo; is not. Dependencies between units are explicit – U3 depends on U2 because it queries the schema U2 defines.&lt;/p&gt;
&lt;p&gt;Before any code, I do a quick plan review across three axes: product (is this the right thing?), feasibility (can we build it this way?), and scope (are the units the right size?). Fixing a bad unit boundary in a markdown file takes a minute. Fixing it across five stacked PRs takes an afternoon.&lt;/p&gt;
&lt;p&gt;When the plan turns out to be wrong mid-implementation, I stop and revise it before writing more code. The plan is the source of truth, not the branches.&lt;/p&gt;
&lt;h2 id="2-let-ai-manage-branches-with-gh-stack"&gt;2. Let AI manage branches with gh-stack&lt;/h2&gt;
&lt;p&gt;Once the units are stable, I give the agent one prompt to build the stack:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Implement U1 and U2 from the plan. Use gh-stack to add a branch per unit. Include the Jira ticket number in branch names.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent creates branches, writes scoped commits, opens stacked PRs with each PR based on the unit below, and runs &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; when a lower layer changes. I do not hand-manage the branch chain day to day.&lt;/p&gt;
&lt;p&gt;One worktree per stack, one branch per unit. The &lt;a href="https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/"&gt;gh-stack guide&lt;/a&gt; has the full setup for worktrees, branch naming, and how to keep the JSON in sync.&lt;/p&gt;
&lt;h2 id="3-review-the-pr-and-leave-comments"&gt;3. Review the PR and leave comments&lt;/h2&gt;
&lt;p&gt;This step belongs to me.&lt;/p&gt;
&lt;p&gt;I read each stacked PR as a focused slice. Small units mean the diff is narrow enough to actually read. I leave inline comments or a review summary – whatever makes the problem clear. I do not fix code during the review pass.&lt;/p&gt;
&lt;p&gt;Good review comments say what is wrong, what I want instead, and which layer owns the change. Ambiguous comments waste the next agent session. The review is the quality gate; I treat it like one.&lt;/p&gt;
&lt;h2 id="4-let-ai-address-the-review-comments"&gt;4. Let AI address the review comments&lt;/h2&gt;
&lt;p&gt;I hand the PR back with a prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Address review comments on PR #42. Stay on the matching stack branch. Add new commits; don&amp;rsquo;t amend unless I ask.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent reads the threads, fixes issues on the correct layer, replies to comments, and resolves what it can. Anything that requires a product call comes back to me. After fixes on a lower layer, it rebases upstack.&lt;/p&gt;
&lt;p&gt;Then I re-review or approve and merge bottom-up. One loop per unit. Repeat.&lt;/p&gt;
&lt;h2 id="why-this-shape-works"&gt;Why this shape works&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plan and units&lt;/td&gt;
&lt;td&gt;Human + agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branches and PRs&lt;/td&gt;
&lt;td&gt;Agent + gh-stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;Human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address comments&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Agents do well with bounded units and explicit comment lists. They do worse at deciding where one unit ends and another begins, or at choosing between two product directions. The plan and the review are where those decisions happen – both belong to the human.&lt;/p&gt;
&lt;p&gt;The loop: plan → stack → review → fix.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/programming/">Programming</category><category domain="https://blog.iany.me/tags/tool/">Tool</category></item><item><title>A Practical Guide to gh-stack for AI-Assisted Development</title><link>https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/</link><pubDate>Thu, 30 Jul 2026 07:19:53 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/a-practical-guide-to-gh-stack-for-ai-assisted-development/</guid><description>&lt;p&gt;Stacked pull requests let you review a feature in logical slices rather than one enormous diff. The challenge is managing the branches, synchronizing PRs, and surviving rebases without breaking the chain. &lt;a href="https://github.github.com/gh-stack/"&gt;gh-stack&lt;/a&gt; handles the scaffolding. It tracks your branch stack in a local JSON file, generates PRs that base each one to its parent, and works on any GitHub repo regardless of whether your organization has enabled native stacked PR support.&lt;/p&gt;
&lt;p&gt;This is the workflow I use, including how an AI coding agent fits into it.&lt;/p&gt;
&lt;h2 id="start-with-a-worktree"&gt;Start with a Worktree&lt;/h2&gt;
&lt;p&gt;Before creating any branches, set up a dedicated Git worktree for the stack.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git worktree add ../project-login-stack login-stack-base
cd ../project-login-stack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;rsquo;s not required but my personal preference. Stacks and worktrees map one-to-one. This matters for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can switch between stacks by switching directories, not branches.&lt;/li&gt;
&lt;li&gt;Your shell&amp;rsquo;s working directory tells you exactly which stack you&amp;rsquo;re in.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="using-an-agent-to-build-the-stack"&gt;Using an Agent to Build the Stack&lt;/h2&gt;
&lt;p&gt;Stacked PRs only work well if the individual branches are focused. Broad, unfocused branches defeat the point. This is where AI agents help.&lt;/p&gt;
&lt;p&gt;When starting a feature, describe it to the agent and ask it to divide the work into discrete units. A typical breakdown might look like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Database layer&lt;/strong&gt; – schema and migrations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service layer&lt;/strong&gt; – business logic and data access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API layer&lt;/strong&gt; – endpoints and serializers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI layer&lt;/strong&gt; – frontend components&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The agent creates a branch for each, makes commits scoped to that layer, and stacks them in order. You end up with four focused PRs instead of one that mixes migrations, API code, and React components in the same diff.&lt;/p&gt;
&lt;h2 id="rebasing-without-breaking-the-stack"&gt;Rebasing Without Breaking the Stack&lt;/h2&gt;
&lt;p&gt;For day-to-day stack updates, use &lt;code&gt;gh stack rebase --upstack&lt;/code&gt;. Switch to any branch in the stack using &lt;code&gt;gh stack up/down/switch&lt;/code&gt;, add new commits, and &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; to reorganize the branch chain. Avoid &lt;code&gt;git commit --amend&lt;/code&gt; in this workflow – gh-stack cannot track amended commits.&lt;/p&gt;
&lt;p&gt;When you need to manually rebase the stack such as moving commits between branches in the stack, check out the topmost branch and rebase from there. With &lt;code&gt;rebase.updateRefs&lt;/code&gt; enabled, Git updates every intermediate branch ref as it replays commits down the chain. Starting from a middle branch leaves the branches above it with stale refs.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-gitconfig"&gt;# ~/.gitconfig or repo .gitconfig
[rebase]
updateRefs = true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;gh stack top # top of the stack
git rebase -i develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prefer &lt;code&gt;gh stack rebase --upstack&lt;/code&gt; for the common case. Reserve manual &lt;code&gt;git rebase&lt;/code&gt; from the top for when you need to move commits between branches in the stack.&lt;/p&gt;
&lt;h2 id="the-json-sync-problem"&gt;The JSON Sync Problem&lt;/h2&gt;
&lt;p&gt;gh-stack records branch metadata in &lt;code&gt;.git/gh-stack&lt;/code&gt;. When you squash or amend commits, or rebase from a middle layer, the SHA that gh-stack recorded no longer matches the branch’s actual HEAD. The JSON goes stale and breaks most features of &lt;code&gt;gh-stack&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The fix is to sync the JSON with the current state of your branches. Doing this manually means running &lt;code&gt;git log --oneline&lt;/code&gt; for each branch, reading the current SHAs, and editing the JSON to match. Not difficult, but tedious – especially across a deep stack.&lt;/p&gt;
&lt;p&gt;An AI assistant handles this well. Give it access to the repository and ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Read the current HEAD of each branch in my gh-stack and update &lt;code&gt;.git/gh-stack&lt;/code&gt; so each entry&amp;rsquo;s recorded SHA matches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="putting-it-together"&gt;Putting It Together&lt;/h2&gt;
&lt;p&gt;Stacked PRs reduce review friction. gh-stack makes stacking practical without requiring GitHub-side configuration. The four habits that make it work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One worktree per stack. Isolation by directory, not branch.&lt;/li&gt;
&lt;li&gt;Agent-planned stacks. Focused units make PRs reviewable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh stack rebase --upstack&lt;/code&gt; for routine updates. Manual &lt;code&gt;git rebase&lt;/code&gt; from the top when moving commits between branches in the stack.&lt;/li&gt;
&lt;li&gt;AI-assisted JSON sync. Fast recovery after a squash or amend.&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/tool/">Tool</category><category domain="https://blog.iany.me/tags/programming/">Programming</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category></item><item><title>Build an MCP with OAuth on Cloudflare Workers</title><link>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</link><pubDate>Fri, 24 Jul 2026 19:48:12 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/build-an-mcp-with-oauth-on-cloudflare-workers/</guid><description>&lt;p&gt;I wanted MCP clients to create Google Tasks for me. The Google Tasks API requires a Workspace-style setup I don&amp;rsquo;t have. IFTTT already bridges webhooks to Google Tasks—but a Maker webhook URL is itself the credential. Storing that key in local MCP config is a bad idea.&lt;/p&gt;
&lt;p&gt;The fix is a thin Cloudflare Worker plus &lt;strong&gt;Cloudflare Access Managed OAuth&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Worker holds the IFTTT key as a secret and speaks MCP over HTTP&lt;/li&gt;
&lt;li&gt;Access sits in front, handles OAuth login with your IdP&lt;/li&gt;
&lt;li&gt;Worker verifies the Access JWT (&lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;) before running tools&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full example: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;. Most of it was scaffolded by AI; the interesting part is the structure, not the TypeScript.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Tasks API&lt;/td&gt;
&lt;td&gt;Needs a Workspace-style setup I don&amp;rsquo;t want to pay for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFTTT Maker webhook&lt;/td&gt;
&lt;td&gt;Works, but the URL &lt;em&gt;is&lt;/em&gt; the credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP clients&lt;/td&gt;
&lt;td&gt;Expect remote HTTP tools with OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;MCP Client
→ Cloudflare Access (OAuth)
→ Worker (JWT)
→ IFTTT
→ Google Tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clients never see &lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;. They only hold a short-lived Access token after login.&lt;/p&gt;
&lt;h2 id="ifttt-side"&gt;IFTTT side&lt;/h2&gt;
&lt;p&gt;One applet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If&lt;/strong&gt;: Maker Webhooks → Receive a web request, event name &lt;code&gt;task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then&lt;/strong&gt;: Google Tasks → Create a task (title / notes / due from the payload)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Worker posts to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://maker.ifttt.com/trigger/task/json/with/key/{KEY}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with &lt;code&gt;{ &amp;quot;title&amp;quot;, &amp;quot;notes?&amp;quot;, &amp;quot;due?&amp;quot; }&lt;/code&gt;. The key never leaves Cloudflare.&lt;/p&gt;
&lt;h2 id="three-secrets"&gt;Three secrets&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-pwsh"&gt;npx wrangler secret put IFTTT_MAKER_TASK_KEY
npx wrangler secret put CF_ACCESS_TEAM_DOMAIN
npx wrangler secret put CF_ACCESS_AUD
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IFTTT_MAKER_TASK_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maker webhook key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_TEAM_DOMAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero Trust team domain (e.g. &lt;code&gt;myteam.cloudflareaccess.com&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CF_ACCESS_AUD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Access application audience tag (validates JWT &lt;code&gt;aud&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No custom authorize page, no signing keys you mint yourself. Access is the authorization server.&lt;/p&gt;
&lt;h2 id="cloudflare-access"&gt;Cloudflare Access&lt;/h2&gt;
&lt;p&gt;In the &lt;a href="https://one.dash.cloudflare.com/"&gt;Zero Trust dashboard&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Access&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;Add&lt;/strong&gt; → &lt;strong&gt;Self-hosted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Point the app at your Worker hostname (&lt;code&gt;*.workers.dev&lt;/code&gt; or a custom domain)&lt;/li&gt;
&lt;li&gt;Pick an IdP (Google, GitHub, email OTP, …) and an allow policy for yourself&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Managed OAuth (Beta)&lt;/strong&gt;—this is what MCP clients need for the OAuth dance&lt;/li&gt;
&lt;li&gt;Copy the team domain and Application Audience (AUD)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Access issues JWTs; MCP clients send them as &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt;. The Worker verifies with the team&amp;rsquo;s JWKS:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;const JWKS = createRemoteJWKSet(
new URL(`${issuer}/cdn-cgi/access/certs`),
);
await jwtVerify(token, JWKS, {
issuer,
...(env.CF_ACCESS_AUD ? { audience: env.CF_ACCESS_AUD } : {}),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;jose&lt;/code&gt; on Workers with &lt;code&gt;nodejs_compat&lt;/code&gt;.)&lt;/p&gt;
&lt;h2 id="worker-just-mcp--verify"&gt;Worker: just MCP + verify&lt;/h2&gt;
&lt;p&gt;The business logic is small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /&lt;/code&gt; or &lt;code&gt;POST /mcp&lt;/code&gt;—JSON-RPC (&lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;One tool: &lt;code&gt;create_google_task&lt;/code&gt; (&lt;code&gt;title&lt;/code&gt; required; &lt;code&gt;notes&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt; optional)&lt;/li&gt;
&lt;li&gt;Reject anything without a valid Access JWT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No OAuth endpoints in the Worker. Access owns login; the Worker only checks the assertion header.&lt;/p&gt;
&lt;h2 id="client-config"&gt;Client config&lt;/h2&gt;
&lt;p&gt;Point the client at the Worker URL and enable OAuth. Example structure (keys vary by client):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;mcp&amp;quot;: {
&amp;quot;google-tasks&amp;quot;: {
&amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
&amp;quot;url&amp;quot;: &amp;quot;https://google-task-mcp.yourdomain.com&amp;quot;
}
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client discovers OAuth via Access, opens the browser for your IdP, then attaches the Access JWT on MCP calls.&lt;/p&gt;
&lt;h2 id="why-this-structure-works-with-ai"&gt;Why this structure works with AI&lt;/h2&gt;
&lt;p&gt;The task decomposes cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MCP handler&lt;/strong&gt;—fixed JSON-RPC + one tool schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IFTTT &lt;code&gt;fetch&lt;/code&gt;&lt;/strong&gt;—secret from &lt;code&gt;env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access JWT verify&lt;/strong&gt;—JWKS + &lt;code&gt;aud&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrangler secrets&lt;/strong&gt;—nothing in git&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Describe the constraints (&amp;ldquo;don&amp;rsquo;t expose the IFTTT URL&amp;rdquo;, &amp;ldquo;Cloudflare Access OAuth&amp;rdquo;, &amp;ldquo;Worker MCP&amp;rdquo;) and get a deployable skeleton. Review JWT verification and secret handling; treat the tool body as glue.&lt;/p&gt;
&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Skip the official Tasks API if IFTTT already has the integration.&lt;/li&gt;
&lt;li&gt;Keep Maker keys in Worker secrets, not client config.&lt;/li&gt;
&lt;li&gt;Cloudflare Access Managed OAuth turns IdP login into MCP-friendly OAuth without requiring custom authorize/token endpoints.&lt;/li&gt;
&lt;li&gt;The Worker only needs to verify &lt;code&gt;Cf-Access-Jwt-Assertion&lt;/code&gt; and call IFTTT.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source: &lt;a href="https://github.com/doitian/google-task-ifttt-webhook-mcp"&gt;github.com/doitian/google-task-ifttt-webhook-mcp&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/api/">API</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/security/">Security</category><category domain="https://blog.iany.me/tags/tool/">Tool</category></item><item><title>Fix Claude Desktop PATH with CLAUDE_ENV_FILE</title><link>https://blog.iany.me/2026/07/fix-claude-desktop-path-with-claude-env-file/</link><pubDate>Wed, 01 Jul 2026 21:27:36 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/07/fix-claude-desktop-path-with-claude-env-file/</guid><description>&lt;p&gt;Claude Code runs tools and shell commands on your behalf. Launched from a terminal, the CLI inherits your shell environment and everything just works. But under &lt;strong&gt;Claude Desktop&lt;/strong&gt;, Claude Code is spawned from a GUI/desktop session that never sourced your interactive shell config—so if you rely on version managers like mise, asdf, or nix, the tools they install may not be on Claude&amp;rsquo;s &lt;code&gt;PATH&lt;/code&gt;. The fix is a single setting: &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;
&lt;p&gt;Set &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; in &lt;code&gt;.claude/settings.local.json&lt;/code&gt; to point at a shell file. Claude sources it before running any command.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;env&amp;quot;: {
&amp;quot;CLAUDE_ENV_FILE&amp;quot;: &amp;quot;.claude/mise.env&amp;quot;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That file is a plain shell snippet—&lt;code&gt;export&lt;/code&gt; statements, one per line:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;export PATH=&amp;quot;/home/ian/.local/share/mise/shims:/home/ian/project/node_modules/.bin:$PATH&amp;quot;
export DATABASE_URL=&amp;quot;postgres://localhost/db&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The path can be relative—it&amp;rsquo;s resolved from the project root—so &lt;code&gt;.claude/mise.env&lt;/code&gt; works on any machine and can be committed alongside the project. Write both, and the next Claude session picks up everything.&lt;/p&gt;
&lt;h2 id="why-not-just-put-path-in-settingsjson"&gt;Why not just put PATH in settings.json?&lt;/h2&gt;
&lt;p&gt;You can set &lt;code&gt;env.PATH&lt;/code&gt; directly in &lt;code&gt;settings.local.json&lt;/code&gt;, but there&amp;rsquo;s a catch: Claude does not support variable substitution like &lt;code&gt;${env:PATH}&lt;/code&gt;. You&amp;rsquo;d have to hardcode the full &lt;code&gt;PATH&lt;/code&gt;, which breaks the moment dependencies change or you move between machines.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; you get full shell semantics—&lt;code&gt;$PATH&lt;/code&gt; expands at runtime, conditionals work, and subshells are fair game.&lt;/p&gt;
&lt;h2 id="why-shell-init-doesnt-help-either-the-snapshot"&gt;Why shell init doesn&amp;rsquo;t help either: the snapshot&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s a deeper reason the obvious fixes fail. At session start, Claude Code freezes a &lt;strong&gt;shell snapshot&lt;/strong&gt; to &lt;code&gt;~/.claude/shell-snapshots/snapshot-zsh-*.sh&lt;/code&gt;, and the Bash tool sources that file before every command it runs.&lt;/p&gt;
&lt;p&gt;The snapshot&amp;rsquo;s &lt;em&gt;final&lt;/em&gt; line is &lt;code&gt;export PATH=&amp;lt;frozen value&amp;gt;&lt;/code&gt;, and that value is Claude Code&amp;rsquo;s own process &lt;code&gt;PATH&lt;/code&gt; captured at startup—not re-derived from your shell config. (You can spot it: it contains directories no rc file adds, like Claude&amp;rsquo;s own plugin and session &lt;code&gt;bin&lt;/code&gt; paths.) Because this &lt;code&gt;export PATH&lt;/code&gt; is the last thing sourced before each command, it clobbers anything your shell init did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mise activate&lt;/code&gt; in &lt;code&gt;.zshenv&lt;/code&gt;/&lt;code&gt;.zshrc&lt;/code&gt; runs, prepends the shims—then the snapshot&amp;rsquo;s trailing &lt;code&gt;export PATH&lt;/code&gt; overwrites it, burying the shims back where they started.&lt;/li&gt;
&lt;li&gt;Even setting &lt;code&gt;env.PATH&lt;/code&gt; in &lt;code&gt;settings.json&lt;/code&gt; as a plain subprocess variable loses, because the snapshot is sourced &lt;em&gt;after&lt;/em&gt; the process environment is set up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; wins because it&amp;rsquo;s sourced &lt;strong&gt;after&lt;/strong&gt; the snapshot. Its &lt;code&gt;export PATH=&amp;quot;…/shims:$PATH&amp;quot;&lt;/code&gt; is the last word, so the shims land first for every Bash tool call. That&amp;rsquo;s the whole trick: it&amp;rsquo;s not just convenient shell semantics—it&amp;rsquo;s the one hook that runs late enough to survive the snapshot.&lt;/p&gt;
&lt;h2 id="generating-the-env-file-automatically"&gt;Generating the env file automatically&lt;/h2&gt;
&lt;p&gt;Writing the env file by hand gets tedious. If you use mise, you can generate both files with a task—I vibe-coded &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/src/mise-tasks/preset-claude.js"&gt;&lt;code&gt;preset-claude.js&lt;/code&gt;&lt;/a&gt; for exactly this (it&amp;rsquo;s a custom mise task, not a built-in). It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads &lt;code&gt;env._.path&lt;/code&gt; from &lt;code&gt;mise.toml&lt;/code&gt; (so per-project &lt;code&gt;node_modules/.bin&lt;/code&gt; directories land on &lt;code&gt;PATH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Expands &lt;code&gt;{{config_root}}&lt;/code&gt; to absolute paths.&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;.claude/mise.env&lt;/code&gt; with shims, extra paths, and all mise env vars.&lt;/li&gt;
&lt;li&gt;Sets &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; in &lt;code&gt;.claude/settings.local.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mise run preset:claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One command, and Claude sees the exact same tools you do.&lt;/p&gt;
&lt;h2 id="what-about-other-version-managers"&gt;What about other version managers?&lt;/h2&gt;
&lt;p&gt;The pattern works for anything that can write a shell env file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;asdf&lt;/strong&gt;: &lt;code&gt;asdf direnv envrc &amp;gt; .claude/asdf.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nix&lt;/strong&gt;: &lt;code&gt;nix print-dev-env | grep ^export &amp;gt; .claude/nix.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;direnv&lt;/strong&gt;: &lt;code&gt;direnv export json | jq -r 'to_entries[] | &amp;quot;export \(.key)=\&amp;quot;\(.value)\&amp;quot;&amp;quot;' &amp;gt; .claude/direnv.env&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then point &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; at it and you&amp;rsquo;re set.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The file is sourced, not executed—shebang lines are ignored.&lt;/li&gt;
&lt;li&gt;Changes to &lt;code&gt;settings.local.json&lt;/code&gt; take effect on the next Claude session.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;CLAUDE_ENV_FILE&lt;/code&gt; is a relative path, it&amp;rsquo;s resolved from the project root.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small config file saves you from fighting invisible &lt;code&gt;PATH&lt;/code&gt; issues. Set it once, and Claude runs the same tools you do.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/dev-environment/">Dev Environment</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/tool/">Tool</category></item><item><title>Backup Ignored Files with Git Remote Branch</title><link>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</link><pubDate>Fri, 12 Dec 2025 03:22:58 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</guid><description>&lt;p&gt;When working with Git repositories, there are often files that need to be backed up but shouldn&amp;rsquo;t be committed to the main branch. These might include local development settings, IDE configuration files, personal notes, or development scripts that are specific to your workflow. The challenge is finding a way to back up these ignored files without polluting the main repository history.&lt;/p&gt;
&lt;h2 id="the-original-solution"&gt;The Original Solution&lt;/h2&gt;
&lt;p&gt;My original approach was to use a separate repository to store backup files for all repositories and create symbolic links. This worked, but had several drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backup files were disconnected from their source repositories, making it harder to track what belongs where&lt;/li&gt;
&lt;li&gt;Backing up new files required moving them to the backup repository and then creating symbolic links&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-better-approach-remote-branch"&gt;A Better Approach: Remote Branch&lt;/h2&gt;
&lt;p&gt;Instead of using a separate repository, we can use a remote branch within the same repository. This approach offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everything stays in one repository&lt;/li&gt;
&lt;li&gt;When you clone the repository, the backup branch comes with it&lt;/li&gt;
&lt;li&gt;Backup files are stored in a separate branch, keeping the main branch clean&lt;/li&gt;
&lt;li&gt;Full Git history for backup files, just like any other branch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-script"&gt;The Script&lt;/h2&gt;
&lt;p&gt;With AI assistance, I developed &lt;code&gt;git-store-file&lt;/code&gt;, a tool for managing ignored files by storing them in a remote branch (default: &lt;code&gt;origin/_store&lt;/code&gt;). This keeps backup files separate from your working branch without interfering with regular development. The script is available in &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/default/bin/git-store-file"&gt;Python&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;Download the script and make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;curl -o ~/bin/git-store-file https://raw.githubusercontent.com/doitian/dotfiles-public/master/default/bin/git-store-file
chmod +x ~/bin/git-store-file
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="usage"&gt;Usage&lt;/h3&gt;
&lt;p&gt;The script has four main commands: &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;restore&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="store-files"&gt;Store Files&lt;/h4&gt;
&lt;p&gt;Store one or more files to the remote branch, including files that are ignored by Git:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store a single file
git-store-file config.local.json
# Store multiple files
git-store-file config.local.json secrets.env
# Use a custom branch name
git-store-file --branch backup config.local.json
# Use a custom remote
git-store-file --remote upstream config.local.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the store command, the script will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a temporary Git index to avoid conflicts with your working directory&lt;/li&gt;
&lt;li&gt;Load the target branch&amp;rsquo;s current state into the temporary index&lt;/li&gt;
&lt;li&gt;Add the specified files using &lt;code&gt;git add -f&lt;/code&gt; to include ignored files&lt;/li&gt;
&lt;li&gt;Create a commit with the changes&lt;/li&gt;
&lt;li&gt;Display commit statistics and prompt for confirmation&lt;/li&gt;
&lt;li&gt;Push the commit to the remote branch&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="check-status"&gt;Check Status&lt;/h4&gt;
&lt;p&gt;Check which files stored in the remote branch have been modified in your local working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Check status
git-store-file status
# Show diff for modified files
git-store-file status --diff
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="restore-files"&gt;Restore Files&lt;/h4&gt;
&lt;p&gt;Restore files from the remote branch to your working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Restore a specific file
git-store-file restore config.local.json
# Restore all files from the branch
git-store-file restore
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="list-files"&gt;List Files&lt;/h4&gt;
&lt;p&gt;List all files stored in the remote branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="options"&gt;Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-b, --branch BRANCH&lt;/code&gt;: Specify the branch name (default: &lt;code&gt;_store&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r, --remote REMOTE&lt;/code&gt;: Specify the remote name (default: &lt;code&gt;origin&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h, --help&lt;/code&gt;: Show help message&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="examples"&gt;Examples&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store local configuration
git-store-file .env.local
# Check what's changed
git-store-file status -d
# Restore after cloning
git-store-file restore .env.local
# List all backed up files
git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="how-it-works"&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The script uses Git&amp;rsquo;s low-level plumbing commands to manipulate the repository without affecting your working directory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Temporary Index&lt;/strong&gt;: Uses the &lt;code&gt;GIT_INDEX_FILE&lt;/code&gt; environment variable to create a temporary index, completely isolated from your working directory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read Tree&lt;/strong&gt;: Loads the target branch&amp;rsquo;s tree structure into the temporary index&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Force Add&lt;/strong&gt;: Uses &lt;code&gt;git add -f&lt;/code&gt; to add files even if they&amp;rsquo;re listed in &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit Tree&lt;/strong&gt;: Creates a commit object directly using &lt;code&gt;git commit-tree&lt;/code&gt;, bypassing the normal commit workflow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct Push&lt;/strong&gt;: Pushes the commit hash directly to the remote branch reference&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This low-level approach ensures that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your working directory remains unchanged&lt;/li&gt;
&lt;li&gt;The main branch is never affected&lt;/li&gt;
&lt;li&gt;Ignored files can be stored without modifying &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The backup branch maintains full Git history&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;git-store-file&lt;/code&gt; script solves the problem of backing up ignored files in a clean, Git-native way. By leveraging a remote branch, it keeps everything in one repository while maintaining clear separation between your main codebase and backup files. Whether you&amp;rsquo;re managing local configurations, IDE settings, or personal development scripts, this tool provides a simple and reliable backup solution that integrates seamlessly with your existing Git workflow.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/backup/">Backup</category></item><item><title>Hosting Static Site on Cloudflare R2</title><link>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</link><pubDate>Wed, 22 Oct 2025 18:46:38 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</guid><description>&lt;p&gt;This post explains how to set up a static site on Cloudflare R2.&lt;/p&gt;
&lt;p&gt;I recently migrated my blog to Cloudflare R2, and the process went smoothly until I encountered R2&amp;rsquo;s lack of native support for rewriting URLs to &lt;code&gt;index.html&lt;/code&gt; files. This post explains how I resolved this issue.&lt;/p&gt;
&lt;h2 id="setting-up"&gt;Setting Up&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create the bucket.&lt;/li&gt;
&lt;li&gt;Add the custom domain &lt;code&gt;blog.iany.me&lt;/code&gt;. To redirect &lt;code&gt;iany.me&lt;/code&gt; and &lt;code&gt;www.iany.me&lt;/code&gt; to the blog, add these as custom domains as well. Since I host the domain in Cloudflare, the DNS record is automatically configured.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="publishing"&gt;Publishing&lt;/h2&gt;
&lt;p&gt;I use &lt;code&gt;rclone&lt;/code&gt; to deploy the site to R2. Below is the GitHub workflow I employ. Noting that the &lt;code&gt;fetch-depth&lt;/code&gt; option for &lt;code&gt;actions/checkout&lt;/code&gt; retrieves the complete repository history. This works with Hugo&amp;rsquo;s &lt;code&gt;--enableGitInfo&lt;/code&gt; flag to accurately determine article creation dates.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: Deploy Hugo Site to Cloudflare R2
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest Hugo
run: |
TAG=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest \
| grep '&amp;quot;tag_name&amp;quot;:' \
| head -1 \
| sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')
curl -L &amp;quot;https://github.com/gohugoio/hugo/releases/download/${TAG}/hugo_${TAG#v}_Linux-64bit.tar.gz&amp;quot; \
-o hugo.tar.gz
tar -xzf hugo.tar.gz hugo
sudo mv hugo /usr/local/bin/
hugo version
- name: Install rclone
run: |
curl https://rclone.org/install.sh | sudo bash
- name: Build site with Hugo
run: hugo --minify --enableGitInfo
- name: Configure rclone for Cloudflare R2
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
rclone config create r2 s3 \
provider Cloudflare \
access_key_id &amp;quot;${R2_ACCESS_KEY_ID}&amp;quot; \
secret_access_key &amp;quot;${R2_SECRET_ACCESS_KEY}&amp;quot; \
endpoint &amp;quot;${R2_ENDPOINT}&amp;quot; \
--quiet
- name: Deploy to Cloudflare R2
env:
R2_BUCKET: ${{ secrets.R2_BUCKET }}
run: |
rclone copy public/ r2:&amp;quot;${R2_BUCKET}&amp;quot; \
--checksum \
--no-traverse \
--verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rules"&gt;Rules&lt;/h2&gt;
&lt;p&gt;Go to the dashboard of the domain &lt;code&gt;iany.me&lt;/code&gt; and go to the section &lt;code&gt;Rules&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="redirect-rules"&gt;Redirect Rules&lt;/h3&gt;
&lt;p&gt;I have added 3 redirect rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When incoming requests have a hostname &lt;code&gt;iany.me&lt;/code&gt; or &lt;code&gt;www.iany.me&lt;/code&gt;, redirect to &lt;code&gt;concat(&amp;quot;https://blog.iany.me&amp;quot;, http.request.uri.path)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;https://blog.iany.me/*/index.html&lt;/code&gt; to &lt;code&gt;https://blog.iany.me/${1}/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;http://*&lt;/code&gt; to &lt;code&gt;https://${1}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although the Redirect Rules manual says if multiple rules make the same modification, the last executed rule wins, I have to put rule 2 before 3.&lt;/p&gt;
&lt;h3 id="url-rewrite-rules"&gt;URL Rewrite Rules&lt;/h3&gt;
&lt;p&gt;I added 2 rewrite rules for &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the request URL is &lt;code&gt;https://blog.iany.me&lt;/code&gt; or &lt;code&gt;https://blog.iany.me/&lt;/code&gt;, rewrite the URL path to &lt;code&gt;/index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the request URL matches &lt;code&gt;https://blog.iany.me/*/&lt;/code&gt;, rewrite the path to &lt;code&gt;${1}/index.html&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="cache-rules"&gt;Cache Rules&lt;/h3&gt;
&lt;p&gt;I added 2 cache rules for images and static content:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set cache TTL to 1 month for &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpg&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, and &lt;code&gt;svg&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Set cache TTL to 1 year for files in &lt;code&gt;/js&lt;/code&gt;, &lt;code&gt;/fonts&lt;/code&gt;, &lt;code&gt;/css&lt;/code&gt;, and &lt;code&gt;/uploads&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/ci/">CI</category><category domain="https://blog.iany.me/tags/automation/">Automation</category></item><item><title>Write to Any iOS Files Location via Working Copy</title><link>https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/</link><pubDate>Fri, 24 Apr 2020 21:14:17 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/</guid><description>&lt;p&gt;I use file system to manage my knowledge base. The &lt;a href="https://github.com/doitian/knowledge-base/"&gt;repository&lt;/a&gt; has a well defined directory structure. I have a bunch of scripts to help me creating these directories on the laptop. But in iOS, I have to create them manually. Recently, I have found out a way to work around it via Working Copy.&lt;/p&gt;
&lt;p&gt;The trick uses two features of Working Copy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2-way synchronized repository with any iOS Files location.&lt;/li&gt;
&lt;li&gt;The shortcut &lt;em&gt;Write Repository File&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shortcut allows writing file into any path in a repository, and the synchronized repository synchronizes changes back to the target iOS Files location.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Write to Files via Working Copy" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/write-to-files-via-working-copy_hu_9afd16b0ae602838.png" srcset="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/write-to-files-via-working-copy_hu_512138b46b9eaad5.png 400w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/write-to-files-via-working-copy_hu_aa3d7fbfb4ac5208.png 800w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/write-to-files-via-working-copy_hu_9e7916f9cf969714.png 1200w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/write-to-files-via-working-copy_hu_9afd16b0ae602838.png 1477w" sizes="(max-width: 1200px) 100vw, 1477px" /&gt;
&lt;figcaption &gt;Write to Files via Working Copy&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In the following I&amp;rsquo;ll show how to create a shortcut which saves files into an iCloud folder and sorts them into sub-directories by date.&lt;/p&gt;
&lt;p&gt;First create the target directory and set up the synchronized repository. I use &lt;code&gt;Desktop/Inbox&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Set Up Sync Repo in Working Copy" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_3095b87da92af2f4.jpg" srcset="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_7fda3cc432e7063a.jpg 400w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_62d8fe3cb98c4823.jpg 800w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_c40ba0adc1c4614d.jpg 1200w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_329a07dcf085759f.jpg 1600w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/set-up-sync-repo-in-working-copy_hu_3095b87da92af2f4.jpg 1960w" sizes="(max-width: 1600px) 100vw, 1960px" /&gt;
&lt;figcaption &gt;Set Up Sync Repo in Working Copy&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Then create the shortcut. It accepts the file from Share Sheet and saves it into the synchronized repository sorted by date. It is important to check the option &amp;ldquo;Create&amp;rdquo; in the &lt;em&gt;Write Repository File&lt;/em&gt; step.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Shortcut: Save Files by Date" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/shortcut-save-files-by-date_hu_2ef68996f341c0e7.jpg" srcset="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/shortcut-save-files-by-date_hu_c3b8177ce33cd5b1.jpg 400w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/shortcut-save-files-by-date_hu_eb865d4b1f65914b.jpg 800w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/shortcut-save-files-by-date_hu_7e54a7293804a5ba.jpg 1200w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/shortcut-save-files-by-date_hu_2ef68996f341c0e7.jpg 1325w" sizes="(max-width: 1200px) 100vw, 1325px" /&gt;
&lt;figcaption &gt;Shortcut: Save Files by Date&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now give it a try. Share a text file using the new created action.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Test Shortcut" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_e7e1b81611d53ee3.jpg" srcset="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_892594feb1e07da4.jpg 400w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_9d797af8afb2c487.jpg 800w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_d9be8049f655237.jpg 1200w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_7fc93bbd89f89200.jpg 1600w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/test-shortcut_hu_e7e1b81611d53ee3.jpg 1942w" sizes="(max-width: 1600px) 100vw, 1942px" /&gt;
&lt;figcaption &gt;Test Shortcut&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And the file appears in the &lt;code&gt;Desktop/Inbox&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="File Sorted into Inbox" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_d9683c4cd4404ec9.jpg" srcset="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_5005298fae4abcd1.jpg 400w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_e72e0678ea3e753a.jpg 800w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_6ff455e89458946.jpg 1200w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_17c8f9927956ea78.jpg 1600w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_16d6ed64435dc66.jpg 2000w, https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/file-sorted-into-inbox_hu_d9683c4cd4404ec9.jpg 2048w" sizes="(max-width: 2000px) 100vw, 2048px" /&gt;
&lt;figcaption &gt;File Sorted into Inbox&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;p&gt;This solution is not perfect and has two major drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It duplicates the files.&lt;/li&gt;
&lt;li&gt;It requires opening Working Copy to synchronize files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, it also has a bonus. I can backup the directory by git now.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/ios/">iOS</category></item><item><title>Save Web Articles via Inoreader</title><link>https://blog.iany.me/2020/03/save-web-articles-via-inoreader/</link><pubDate>Sat, 14 Mar 2020 17:02:25 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2020/03/save-web-articles-via-inoreader/</guid><description>&lt;p&gt;One of my favorite features of &lt;a href="https://www.inoreader.com/"&gt;Inoreader&lt;/a&gt; is saving the article to external services, such as Evernote, OneDrive or Google Drive. I can archive the articles I liked for future references.&lt;/p&gt;
&lt;p&gt;Inoreader even supports automatically fetching full text for truncated RSS feed. This is awesome to work with other web services. Many bookmarking and reading later services, such as Diigo, Instapaper and Pinboard, all provide RSS feed but without full text.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Automatically Fetch Full Content" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-fetches-full-text-squashed_hu_3f89695cf348133b.png" srcset="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-fetches-full-text-squashed_hu_9dedc0443ec44659.png 400w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-fetches-full-text-squashed_hu_ed595609dd430cf9.png 800w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-fetches-full-text-squashed_hu_3f89695cf348133b.png 997w" sizes="(max-width: 800px) 100vw, 997px" /&gt;
&lt;figcaption &gt;Automatically Fetch Full Content&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Inoreader Pro subscribers can save the articles automatically by creating rules. For example, I have saved all of my bookmarks into Evernote.&lt;/p&gt;
&lt;figure class="kg-image-card kg-width-fit"&gt;
&lt;img alt="Automatically Save Articles" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-rule-saves-article-squashed_hu_39a9a697ecfc5d81.png" srcset="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-rule-saves-article-squashed_hu_9479f887cf05c4a2.png 400w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-rule-saves-article-squashed_hu_ea67e9330e9165ac.png 800w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-rule-saves-article-squashed_hu_b2bb9b766243b037.png 1200w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/inoreader-rule-saves-article-squashed_hu_39a9a697ecfc5d81.png 1494w" sizes="(max-width: 1200px) 100vw, 1494px" /&gt;
&lt;figcaption &gt;Automatically Save Articles&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class="kg-image-card kg-width-full"&gt;
&lt;img alt="Saved Articles" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/evernote-saved-articles-squashed_hu_e1e9f118b7878c6e.png" srcset="https://blog.iany.me/2020/03/save-web-articles-via-inoreader/evernote-saved-articles-squashed_hu_abf7a4c03404ec7.png 400w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/evernote-saved-articles-squashed_hu_b897d6ddfc944b14.png 800w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/evernote-saved-articles-squashed_hu_d1a31eb25b156e5f.png 1200w, https://blog.iany.me/2020/03/save-web-articles-via-inoreader/evernote-saved-articles-squashed_hu_e1e9f118b7878c6e.png 1206w" sizes="(max-width: 1200px) 100vw, 1206px" /&gt;
&lt;figcaption &gt;Saved Articles&lt;/figcaption&gt;
&lt;/figure&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/knowledge-management/">Knowledge Management</category></item><item><title>Vim iTerm Launcher</title><link>https://blog.iany.me/2020/02/vim-iterm-launcher/</link><pubDate>Sat, 08 Feb 2020 16:48:30 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2020/02/vim-iterm-launcher/</guid><description>&lt;p&gt;I prefer using Vim in a terminal. I can switch to the shell with &lt;kbd&gt;Ctrl-Z&lt;/kbd&gt; and back with &lt;code&gt;fg&lt;/code&gt;. However it is hard to integrate a terminal command with other GUI tools, such as editing a file in Vim from Finder.&lt;/p&gt;
&lt;p&gt;In macOS, the default Terminal app and iTerm both supports automation. It&amp;rsquo;s easy to write a script to open a new terminal window and run a command like &amp;ldquo;vim file&amp;rdquo; in it. But I want to close the window after Vim quits. A quick work around is running the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim file; exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the problem is that if I suspend Vim via &lt;kbd&gt;Ctrl-Z&lt;/kbd&gt;, the terminal window is closed, because shell will continue to execute the next command when the process is suspended.&lt;/p&gt;
&lt;p&gt;After research and reading &lt;a href="https://stackoverflow.com/a/16215525/667158"&gt;a StackOverflow answer&lt;/a&gt;, I wrote &lt;a href="https://gist.github.com/doitian/0c8775e88ceed7bac44c4fb4287822d5"&gt;two scripts&lt;/a&gt; to launch Vim in iTerm. I also added features in &lt;code&gt;iterm-vim-wrapper&lt;/code&gt; to edit clipboard and empty scratch file in temporary directory.&lt;/p&gt;
&lt;p&gt;This is &lt;a href="https://github.com/doitian/assets/raw/master/2020/Vim%20File%20Action.alfredworkflow"&gt;an example&lt;/a&gt; which uses the scripts to create File Action in Alfred.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/macos/">macOS</category></item><item><title>How to Download Only Gmail Inbox</title><link>https://blog.iany.me/2020/02/download-only-gmail-inbox/</link><pubDate>Sun, 02 Feb 2020 12:15:10 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2020/02/download-only-gmail-inbox/</guid><description>&lt;p&gt;I prefer reading my mails in the Gmail web client directly. I rarely send new mails or replies. But when I do, I want to use PGP to encrypt or sign the mail. I have tried two extensions to use PGP in Gmail, FlowCrypt and Mailvelope. But both of them are far from a competent solution for me. GPG Mail in the GPG Suite for macOS is still my preferred way. However, GPG Mail is an Apple Mail plugin, which requires downloading mails first. Based on my scenarios, I only need to download the mails I left in the inbox. But the basic setup of Gmail in Apple Mail will download many mails in the background.&lt;/p&gt;
&lt;p&gt;I tried POP3. It has two issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The status is not synchronized with the server. I can configure Gmail to archive the mails which have been downloaded via POP3, but that means once I have downloaded mails, I must process them locally. This is a burden, since I only want to reply several mails in the client.&lt;/li&gt;
&lt;li&gt;POP3 does not observe the filter which auto archives new mails. The filtered emails still occur in the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IMAP is still the better choice. The real issue is indeed that Apple Mail uses remote IMAP mail boxes for Drafts, Sent, Junk, Trash and Archive by default. If it uses local mail boxes instead, the client will no longer try to synchronize this mail boxes.&lt;/p&gt;
&lt;p&gt;Following is the detailed instructions to setup a Gmail IMAP client which only downloads mails in the inbox.&lt;/p&gt;
&lt;p&gt;First go to the Gmail &lt;a href="https://mail.google.com/mail/u/0/#settings/labels"&gt;label settings&lt;/a&gt;. Disable all the &amp;ldquo;Show in IMAP&amp;rdquo; options except the one for inbox.&lt;/p&gt;
&lt;p&gt;Since Apple Mail does not allow configuring mailboxes for a Google account, I have to generate an &lt;a href="https://myaccount.google.com/apppasswords"&gt;app password&lt;/a&gt; to add the Gmail as an IMAP account. Open Mail app, add account and choose IMAP from &amp;ldquo;Other mail account…&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;After adding the new account, use local mail boxes in the Mailbox Behaviors settings pane. Leave the Archive mail box remote, since we cannot change it.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Apple Mail Mailboxes Behaviors Settings Pane" class="kg-image" loading="lazy" src="https://blog.iany.me/2020/02/download-only-gmail-inbox/apple-mail-mailboxes-behaviors_hu_5004a41c08c53fa3.png" srcset="https://blog.iany.me/2020/02/download-only-gmail-inbox/apple-mail-mailboxes-behaviors_hu_1f0da9588835395a.png 400w, https://blog.iany.me/2020/02/download-only-gmail-inbox/apple-mail-mailboxes-behaviors_hu_5004a41c08c53fa3.png 710w" sizes="(max-width: 400px) 100vw, 710px" /&gt;
&lt;figcaption &gt;Apple Mail Mailboxes Behaviors Settings Pane&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now Apple Mail will only download mails in the inbox. There is one last thing, if I archive a mail in the client, it will be labeled &lt;code&gt;[Imap]/Archive&lt;/code&gt; in Gmail, you may want to hide it from the label list and message like me in the &lt;a href="https://mail.google.com/mail/u/0/#settings/labels"&gt;labels settings&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/email/">Email</category><category domain="https://blog.iany.me/tags/power-tool/">Power Tool</category></item><item><title>Capture Anything using macOS Javascript automation</title><link>https://blog.iany.me/2017/03/capture-anything-using-macos-javascript-automation/</link><pubDate>Sun, 19 Mar 2017 17:54:08 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2017/03/capture-anything-using-macos-javascript-automation/</guid><description>&lt;p&gt;I have written many scripts to automate the work in macOS. This one is the most frequently used one. The script can capture the current selection in frontend most app in OmniFocus, and I can jump back to the app using URL.&lt;/p&gt;
&lt;p&gt;It only supports apps I frequently use. New app support can be added in &lt;code&gt;capturers&lt;/code&gt; map.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;var tasks = [];
var app = null;
// Uncomment following line to debug a specific app.
// var app = Application(&amp;quot;Evernote&amp;quot;);
(function(app, tasks) {
var bundleIdentifier;
if (app === null || app === undefined) {
var systemEvents = Application(&amp;quot;System Events&amp;quot;);
var bundleIdentifier = systemEvents.processes.whose({ frontmost: true })[0].bundleIdentifier();
app = Application(bundleIdentifier);
} else {
bundleIdentifier = app.id();
}
if (tasks === null || tasks === undefined) {
tasks = [];
}
var capturers = {
Safari: function(app, tasks) {
var tab = app.windows[0].currentTab();
tasks.push({ name: tab.name(), note: tab.url() });
},
Chromium: function(app, tasks) {
var tab = app.windows[0].activeTab();
tasks.push({ name: tab.title(), note: tab.url() });
},
Finder: function(app, tasks) {
app.selection().forEach(function(file) {
var name = file.name();
var url = file.url();
var directory = file.container().url();
tasks.push({ name: &amp;quot;[File] &amp;quot; + name, note: url + &amp;quot; in directory &amp;quot; + directory });
});
},
Contacts: function(app, tasks) {
app.selection().forEach(function(contact) {
tasks.push({ name: &amp;quot;Contact &amp;quot; + contact.name(), note: &amp;quot;addressbook://&amp;quot; + contact.id() });
});
},
Evernote: function(app, tasks) {
app.selection().forEach(function(note) {
var url = note.noteLink();
var title = note.title();
tasks.push({ name: &amp;quot;[Evernote] &amp;quot; + title, note: url });
});
},
Mail: function(app, tasks) {
app.selection().forEach(function(mail) {
var url = encodeURI(&amp;quot;message://&amp;lt;&amp;quot; + mail.messageId() + &amp;quot;&amp;gt;&amp;quot;);
var title = mail.sender() + &amp;quot;: &amp;quot; + mail.subject();
tasks.push({ name: title, note: url });
});
}
};
capturers[&amp;quot;Google Chrome&amp;quot;] = capturers.Chromium;
var fluidCapturer = function(app, tasks) {
var tab = app.browserWindows[0].selectedTab();
var name = &amp;quot;[&amp;quot; + app.name() + &amp;quot;] &amp;quot; + tab.title();
tasks.push({ name: name, note: tab.url() });
};
var defaultCapturer = function(app, tasks) {
app.includeStandardAdditions = true;
var url = encodeURI(&amp;quot;file://&amp;quot; + app.pathTo().toString());
tasks.push({ name: app.name(), note: url })
};
var capture = capturers[app.name()];
if (capture === undefined) {
if (bundleIdentifier.startsWith(&amp;quot;com.fluidapp.FluidApp.&amp;quot;)) {
capture = fluidCapturer;
} else {
capture = defaultCapturer;
}
}
capture(app, tasks);
return tasks;
})(app, tasks);
// Then send tasks to OmniFocus or other app which supports Applescript automation.
var of = Application(&amp;quot;OmniFocus&amp;quot;);
var inbox = of.quickEntry().inboxTasks;
tasks.forEach(function(task) {
inbox.push(of.Task(task));
});
of.quickEntry().open();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use &lt;a href="https://www.keyboardmaestro.com/main/"&gt;Keyboard Maestro&lt;/a&gt; to invoke the script using keyboard shortcut.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/macos/">macOS</category></item><item><title>Convert Web Page to Markdown using Ulysses or Bear</title><link>https://blog.iany.me/2016/11/convert-web-page-to-markdown-using-ulysses-or-bear/</link><pubDate>Sun, 27 Nov 2016 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2016/11/convert-web-page-to-markdown-using-ulysses-or-bear/</guid><description>&lt;details open disabled class="kg-card kg-callout kg-callout-info" data-callout-type="info"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-circle-info"&gt;&lt;/i&gt;
Updates
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2017-05-09&lt;/strong&gt;: The bear current version made clipper easier to use.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Ulysses way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select content in browser and copy.&lt;/li&gt;
&lt;li&gt;Open Ulysses and use menu &lt;code&gt;Edit &amp;gt; Paste from &amp;gt; Rich text&lt;/code&gt; to paste&lt;/li&gt;
&lt;li&gt;Select pasted text and use menu &lt;code&gt;Edit &amp;gt; Copy as &amp;gt; Markdown&lt;/code&gt; to copy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bear way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy page URL.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://bear.app/faq/X-callback-url%20Scheme%20documentation/#grab-url"&gt;/grab-url&lt;/a&gt; x-callback-url&lt;/li&gt;
&lt;li&gt;After the article is grabbed, use menu &lt;code&gt;Edit &amp;gt; Markdown&lt;/code&gt; to copy&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/macos/">macOS</category><category domain="https://blog.iany.me/tags/markdown/">Markdown</category><category domain="https://blog.iany.me/tags/note-taking/">Note Taking</category></item><item><title>Auto Toggle MacBook Internal Keyboard</title><link>https://blog.iany.me/2014/01/auto-toggle-macbook-internal-keyboard/</link><pubDate>Fri, 17 Jan 2014 00:00:00 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2014/01/auto-toggle-macbook-internal-keyboard/</guid><description>&lt;p&gt;I prefer using external keyboard with my MacBook. When no external monitors are used, a typical setup is placing the keyboard above the internal one, so I can still use the internal touchpad.&lt;/p&gt;
&lt;p&gt;But sometimes the external keyboard may press some keys of the internal keyboard. There is a &lt;a href="http://forums.macrumors.com/showthread.php?t=433407"&gt;solution&lt;/a&gt; to disable the internal keyboard, but it is tedious to run the command manually.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Disable, ignore the warning
sudo kextunload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/
# Enable
sudo kextload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fortunately, &lt;a href="http://www.keyboardmaestro.com/main/"&gt;Keyboard Maestro&lt;/a&gt; supports executing scripts when a USB device is attached or detached.&lt;/p&gt;
&lt;p&gt;I have created two macros to enable and disable internal keyboard.&lt;/p&gt;
&lt;p&gt;Before the macros can be used, you must setup sudoers to allow running the command without password.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo visudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And append following line to the file and save it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%admin ALL = NOPASSWD: /sbin/kextunload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext, /sbin/kextload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restart sudo session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo -K
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/keyboard-maestro/">Keyboard Maestro</category><category domain="https://blog.iany.me/tags/macos/">macOS</category></item></channel></rss>