Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6ad2d22
feat(url-encoder): add URL encoder/decoder tool (#137)
SaiThihan Oct 10, 2026
18e8bf9
fix(url-encoder): show an error instead of crashing on a lone surroga…
AnushreeKasturi Oct 11, 2026
b53e6ed
docs: add the Discord link for contributors (#140)
anurag629 Oct 11, 2026
ee78f5a
docs: add ground rules for AI tools, spam, reviews and goodies (#141)
anurag629 Oct 11, 2026
e37037a
chore: align the package version with the latest release (0.6.2) (#142)
anurag629 Oct 11, 2026
2800c8c
Quote YAML strings that parsers read as numbers or dates (#143)
jay23606 Oct 11, 2026
aa01d9b
docs: let contributors with 3 merged PRs hold two issues (#144)
anurag629 Oct 11, 2026
16b3958
ci: check that a PR's linked issue is assigned to its author (#146)
anurag629 Oct 12, 2026
cc43266
ci: drop the manual trigger from the claim check (#147)
anurag629 Oct 12, 2026
6e05309
ci: check that a PR says how it was tested and whether AI helped (#148)
anurag629 Oct 12, 2026
c63aec5
fix: reject invalid UTF-8 in Base64 text decoding (#150)
kudala-bharani Oct 13, 2026
5e99227
feat: add env-driven search-engine verification meta tags (#26) (#149)
bhushan4work Oct 13, 2026
af3feb8
docs: say that discord-bot PRs count for the October program too (#151)
anurag629 Oct 13, 2026
38f8359
feat: add green and fuchsia accent colors for new tools (#152)
anurag629 Oct 13, 2026
13f190f
fix: merge array items into one interface in JSON to TypeScript (#154)
prakashNalla Oct 13, 2026
b0f0838
chore: ignore local tooling notes (#156)
anurag629 Oct 13, 2026
bdfb90a
fix: prefix TypeScript interface names starting with a digit with an …
CreatorguyS Oct 14, 2026
cd53d67
docs: add performance baseline audit and methodology (#28) (#164)
bhushan4work Oct 14, 2026
b540e52
fix: write empty objects and arrays inside arrays on the dash line in…
prakashNalla Oct 14, 2026
ab11292
fix(json-to-typescript): reserve root alias name when JSON is a top-l…
CreatorguyS Oct 15, 2026
beeba37
fix: make Base64 drop zones keyboard accessible (#174)
kudala-bharani Oct 15, 2026
569e47c
chore(deps): bump @opennextjs/cloudflare from 1.20.8 to 1.20.10 (#171)
dependabot[bot] Oct 15, 2026
ba863cb
chore(deps): bump next from 16.3.8 to 16.4.0 (#169)
dependabot[bot] Oct 15, 2026
ba5a49b
chore(deps-dev): bump the dev-dependencies group with 3 updates (#170)
dependabot[bot] Oct 15, 2026
836e38d
chore: add .editorconfig (#158)
anurag629 Oct 15, 2026
67f065f
docs: ask for bug fix tests that fail without the fix (#175)
anurag629 Oct 15, 2026
a4f13f6
chore: test tool registry and metadata invariants (#176)
KingEmma7 Oct 15, 2026
130df47
fix: correct JSON formatter clear shortcut and share platform detecti…
bhushan4work Oct 15, 2026
782b464
fix: preserve exact numbers in JSON tree view (#179)
kudala-bharani Oct 15, 2026
b3b7c95
chore(deps): override glob under @node-minify/core to 10.5.0 (#177)
anurag629 Oct 15, 2026
f7442d3
docs: tests can use the @/ alias now (#180)
anurag629 Oct 15, 2026
7ad22cb
chore: version 0.7.0 (#182)
anurag629 Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

# upstream font license files, kept byte for byte as published
[OFL-ibm-plex-*.txt]
end_of_line = crlf
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ body:
- JSON formatter
- JWT decoder
- Base64
- URL encoder/decoder
- Invoice generator
- Shell (theme, command palette, header)
- Other / not sure
Expand Down
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ contact_links:
- name: CODERCOPS
url: https://www.codercops.com
about: Learn about the team behind toolbelt or get in touch about a project.
- name: Ask a question on Discord
url: https://discord.gg/mpGdJyjaRQ
about: Chat with the maintainers and other contributors. Issue claims still happen on GitHub.
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Release PRs go `develop` → `production` and are merged as a merge commit

Closes #

## How I tested this

<!-- Required. What you ran or clicked through to check this works. -->

## AI assistance

<!-- For contributor PRs. Write "none", or name the tool and what you used it for. See CONTRIBUTING.md. -->

## Type of change

- [ ] Bug fix
Expand All @@ -20,8 +28,10 @@ Closes #

## Checklist

- [ ] I was assigned the linked issue
- [ ] `npm run lint && npm run test && npm run build` passes locally
- [ ] Logic changes live in `lib/` and have a test in `lib/__tests__/`
- [ ] For a bug fix: the new test fails without my change
- [ ] No analytics, trackers, or calls that send user data off-device
- [ ] Works in both light and dark themes (if UI changed)
- [ ] Inputs have labels and it works with the keyboard alone (if UI changed)
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/claim-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Claim check

# A contributor's PR counts only for an issue that was assigned to them first
# (see CONTRIBUTING.md). This check says so on the PR itself, so nobody has to
# wait for a review to find out.
#
# It uses pull_request_target so fork PRs get an answer without a maintainer
# approving the run. That is safe here: the job never checks out or runs the
# PR's code, and its token can only read.
on:
pull_request_target:
branches: [develop]
types: [opened, edited, reopened, synchronize]

permissions:
issues: read
pull-requests: read

concurrency:
group: claim-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
assigned:
name: Linked issue is assigned to the author
# Maintainers and bots don't claim issues.
if: >-
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- name: Check the linked issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

pr=$(gh api graphql \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F pr="$PR" \
-f query='query($owner: String!, $name: String!, $pr: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
author { login }
body
closingIssuesReferences(first: 20) {
nodes { number repository { nameWithOwner } assignees(first: 20) { nodes { login } } }
}
}
}
}' \
--jq '.data.repository.pullRequest')
author=$(jq -r '.author.login // ""' <<<"$pr")

# The issues GitHub will close when this PR merges ("Closes #12" in the description).
linked=$(jq --arg repo "$REPO" '[ .closingIssuesReferences.nodes[] | {
ref: (if .repository.nameWithOwner == $repo then "#\(.number)" else "\(.repository.nameWithOwner)#\(.number)" end),
assignees: [.assignees.nodes[].login]
} ]' <<<"$pr")

# GitHub can take a moment to record those links on a new PR, so when it
# has none, read "Closes #12" from the description here.
if [ "$(jq length <<<"$linked")" -eq 0 ]; then
numbers=$(jq -r '[ (.body // "") | scan("(?i)\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s+#([0-9]+)") | .[0] ] | unique | .[]' <<<"$pr")
for number in $numbers; do
# A number can also belong to a PR, or to nothing at all. Those don't count.
if issue=$(gh api "repos/$REPO/issues/$number" --jq 'select(.pull_request == null) | {ref: "#\(.number)", assignees: [.assignees[].login]}' 2>/dev/null) && [ -n "$issue" ]; then
linked=$(jq --argjson issue "$issue" '. + [$issue]' <<<"$linked")
fi
done
fi

fail() {
echo "::error title=$1::$2"
printf '### %s\n\n%s\n' "$1" "$2" >> "$GITHUB_STEP_SUMMARY"
exit 1
}

if [ "$(jq length <<<"$linked")" -eq 0 ]; then
fail "No linked issue" "This PR isn't linked to an issue. Add \"Closes #12\" to the description, with the number of the issue that was assigned to you. If you haven't claimed one yet, comment on the issue and wait to be assigned (CONTRIBUTING.md has the rules)."
fi

# Every linked issue has to be assigned to the PR author.
missing=$(jq -r --arg author "$author" '
[ .[]
| select(all(.assignees[]; ascii_downcase != ($author | ascii_downcase)))
| .ref ]
| join(", ")' <<<"$linked")

if [ -n "$missing" ]; then
fail "Issue not assigned to you" "$missing isn't assigned to @$author. Comment on the issue and wait for a maintainer to assign it before you open a PR (CONTRIBUTING.md has the rules). Once it's assigned to you, this check passes the next time it runs: push a commit or edit the PR description."
fi

echo "Assigned to @$author: $(jq -r '[.[].ref] | join(", ")' <<<"$linked")"
106 changes: 106 additions & 0 deletions .github/workflows/description-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Description check

# Every contributor PR says how it was tested and whether AI tools helped (see
# CONTRIBUTING.md). This check says so on the PR itself while one of the two is
# missing or still empty, so nobody has to wait for a review to find out.
#
# It uses pull_request_target so fork PRs get an answer without a maintainer
# approving the run. That is safe here: the job never checks out or runs the
# PR's code, and its token can only read.
on:
pull_request_target:
branches: [develop]
types: [opened, edited, reopened, synchronize]

permissions:
pull-requests: read

concurrency:
group: description-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
sections:
name: Says how it was tested and whether AI helped
# Maintainers and bots are exempt.
if: >-
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- name: Check the description
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

# Read the description as it is now, so a re-run sees later edits.
gh api "repos/$REPO/pulls/$PR" --jq '.body // ""' > body.md

python3 - <<'PY'
import os
import re
import sys

body = open("body.md", encoding="utf-8").read()
# The hints in the PR template are HTML comments. They aren't an answer.
body = re.sub(r"<!--.*?-->", "", body, flags=re.S)
lines = body.splitlines()

# Section name, the headings we accept for it, and what to write there.
WANTED = [
(
"How I tested this",
r"how i tested|\btest(ed|ing|s)?\b|\bverif|\bvalidat|\bqa\b",
"what you ran or clicked through to check that this works",
),
(
"AI assistance",
r"\bai[ -]?(assist|usage|use|disclos|tool|help|generated)|\b(use|usage) of ai\b|^\W*ai\W*$",
'write "none", or name the tool and what you used it for',
),
]


def is_heading(line):
"""A Markdown heading, or a bold line used as one."""
return bool(re.match(r"\s{0,3}#{1,6}\s", line) or re.fullmatch(r"\s*(\*\*|__).+(\*\*|__)\s*:?\s*", line))


def answer(pattern):
"""The text under the first matching heading, or after a "Label: answer" line. None when there is neither."""
for i, line in enumerate(lines):
if is_heading(line) and re.search(pattern, line, re.I):
below = []
for following in lines[i + 1 :]:
if is_heading(following):
break
below.append(following)
return "\n".join(below).strip()
label, colon, inline = line.partition(":")
if colon and len(label) < 60 and re.search(pattern, label, re.I) and inline.strip(" *_"):
return inline.strip(" *_")
return None


problems = []
for name, pattern, hint in WANTED:
found = answer(pattern)
if found is None:
problems.append(f'The description has no "{name}" section. Add one: {hint}.')
elif not found:
problems.append(f'The "{name}" section is empty. Fill it in: {hint}.')

if not problems:
print("Both sections are filled in.")
sys.exit(0)

tail = "Edit the PR description and this check runs again (CONTRIBUTING.md has the rules)."
for problem in problems:
print(f"::error title=PR description::{problem} {tail}")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write("### PR description\n\n" + "\n".join(f"- {p}" for p in problems) + f"\n\n{tail}\n")
sys.exit(1)
PY
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ public/version.txt

# Generated by scripts/og-assets.mjs before build/dev.
generated/

# local tooling notes
CLAUDE.md
AGENTS.md
54 changes: 48 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,55 @@ We're taking part in Hacktoberfest this year, and first-time contributors are ve

1. Pick an open issue labeled [`hacktoberfest`](https://github.com/codercops/toolbelt/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest+no%3Aassignee) or [`good first issue`](https://github.com/codercops/toolbelt/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+no%3Aassignee) that nobody is assigned to. Want to build a whole new tool? Look at the [`new tool`](https://github.com/codercops/toolbelt/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+tool%22) issues.
2. Comment on it to ask for it. I'll assign it to you, usually within a day. Please don't start on an issue that's assigned to someone else.
3. Take one issue at a time. Once your PR is merged, grab the next one.
3. Take one issue at a time. Once your PR is merged, grab the next one. After 3 merged PRs with us you can hold two issues at once.
4. If you're assigned and there's no PR or update for 5 days, I'll free the issue up for someone else. Just comment if you need more time.
5. Open your PR against `develop` and put `Closes #<issue number>` in the description.
5. Open your PR against `develop` and put `Closes #<issue number>` in the description. A check on the PR ("Claim check") fails if that issue isn't assigned to you. Another one ("Description check") fails while the "How I tested this" or "AI assistance" section of the description is missing or empty.

PRs count for Hacktoberfest once they're merged or labeled `hacktoberfest-accepted`. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the `spam` label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code.
PRs count for Hacktoberfest once they're merged or labeled `hacktoberfest-accepted`. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the `spam` label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code. The full rules are under [Ground rules](#ground-rules) below.

Stuck? Ask on the issue. Questions are welcome.
Stuck? Ask on the issue, or in `#oss-help` on our [Discord](https://discord.gg/mpGdJyjaRQ). Questions are welcome. Claims still happen on the issue, so the assignment stays visible to everyone.

## Ground rules

### AI tools

You can use AI tools (Copilot, ChatGPT, Claude, Cursor and so on). Two conditions:

1. Say so in the PR. If a tool wrote or rewrote more than autocomplete-sized pieces, fill in the "AI assistance" line in the PR template: which tool, and what you used it for.
2. You own every line. Run it, test it, and be ready to explain it in review. If you can't explain a change, the PR gets closed.

PRs that are unreviewed AI output (code that doesn't run, invented APIs, a description that doesn't match the diff, unrelated rewrites) are treated as spam.

### Spam

These get closed and labeled `spam`:

- a PR for an issue that isn't assigned to you, or that isn't linked to an issue at all
- formatting, whitespace or typo-only changes nobody asked for
- one fix split into several PRs
- a copy of someone else's PR
- claiming a pile of issues at once, or bulk and automated submissions
- untested generated code

Two spam PRs and you won't be assigned more issues this month, and you're out of the CODERCOPS goodies.

### Reviews

Every PR needs a "How I tested this" line: what you ran or clicked through to check it works.

For a bug fix, check that your new test fails without the fix: undo the change in the source, run the tests, then put it back. A test that passes either way doesn't catch anything.

I review most PRs within a few days. Please don't ping repeatedly or DM maintainers about a review. If three days pass with no reply, one follow-up comment on the PR is fine.

### Goodies

The top 5 contributors this October get a CODERCOPS T-shirt and stickers. You need at least 5 merged PRs labeled `hacktoberfest-accepted` across chatcops, ogcops, toolbelt and discord-bot, merged between Oct 1 and Oct 31 (UTC), and ties go to whoever got there first. To keep it fair:

- only PRs for issues that were assigned to you count
- a fix split into several PRs counts once
- what counts is the maintainers' call, and that call is final

These goodies come from CODERCOPS through our own open source program. They're separate from Hacktoberfest, so anything Hacktoberfest itself gives out is a different thing with its own rules.

## Getting set up

Expand Down Expand Up @@ -56,12 +98,12 @@ npm run lint && npm run test && npm run build
## Adding a tool

1. Add a typed entry at the end of the `TOOLS` array in `lib/tools.ts`, filling every field of the `Tool` interface.
- Pick `accentVar` from the theme variables in `app/globals.css` (`--cyan`, `--amber`, `--rose`, `--violet`, `--sky`, `--lime`) and set `accentHex` to that variable's dark-theme value.
- Pick `accentVar` from the accent variables in `app/globals.css` (`--cyan`, `--amber`, `--violet`, `--sky`, `--lime`, `--green`, `--fuchsia`) and set `accentHex` to that variable's dark-theme value. Take one that no tool uses yet. If they're all taken, reuse the one the fewest tools have. Don't use `--rose`: it's the color of error messages.
- `heroDim` must be `""` or part of `heroHeading`.
- Write 3 or 4 FAQs. Keep `metaTitle` under 60 characters and `metaDescription` around 150 to 160.
- Keep `features` and `ogDescription` to plain Latin text; the social card font has no arrows or symbols.
2. Create `app/(tools)/<slug>/page.tsx`, a `<Name>Client.tsx` client component, and `opengraph-image.tsx`. Copying `page.tsx` and `opengraph-image.tsx` from `app/(tools)/base64/` and changing the slug is the easiest start.
3. Put pure logic in `lib/<name>.ts` with tests in `lib/__tests__/<name>.test.ts`. Use relative imports in tests; vitest has no `@/` alias.
3. Put pure logic in `lib/<name>.ts` with tests in `lib/__tests__/<name>.test.ts`. Tests can import with `@/` like the app does.
4. Add the tool to the "Which tool?" dropdown in `.github/ISSUE_TEMPLATE/bug_report.yml`.

The home page, metadata, JSON-LD, sitemap, manifest and command palette pick the new tool up from the registry automatically. Several people may be adding tools at the same time, so rebase on `develop` before asking for review.
Expand Down
Loading