zapdev 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +85 -47
  2. package/dist/cli.js +124 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,15 +1,55 @@
1
1
  # zapdev
2
2
 
3
- **zapdev** — a lightweight TypeScript CLI that makes small, repetitive git chores fast and precise.
3
+ ## Project Introduction
4
4
 
5
- ## Requirements
5
+ **zapdev** is a lightweight TypeScript CLI that makes small, repetitive Git chores fast and precise.
6
6
 
7
- Node.js >= 20 and git.
7
+ It stages changes, scans them for secrets, generates Conventional Commit messages with Ollama, and streamlines repository cleanup.
8
8
 
9
- ## Install
9
+ ## Project Architecture
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ Entry["src/index.ts"] --> CLI["src/cli.ts"]
14
+ CLI --> Commands["src/commands"]
15
+ Commands --> Lib["src/lib"]
16
+ Commands --> Types["src/types"]
17
+ Lib --> Prompts["src/prompts"]
18
+ Lib --> Types
19
+ Lib --> Tools["Git, Gitleaks, Ollama"]
20
+ ```
21
+
22
+ - `src/index.ts`: bin launcher; enables the V8 compile cache, then loads `cli.js`.
23
+ - `src/cli.ts`: CLI entry; registers subcommands and opens the interactive menu.
24
+ - `src/commands/`: command UI and orchestration.
25
+ - `src/lib/`: pure logic and isolated Git, Gitleaks, and Ollama side effects.
26
+ - `src/prompts/`: LLM prompts inlined into the bundle at build time.
27
+ - `src/types/`: shared type declarations.
28
+
29
+ ## Environment Variables
30
+
31
+ | Variable | Default | Required | Description |
32
+ | --- | --- | --- | --- |
33
+ | `OLLAMA_URL` | `http://localhost:11434` | No | Ollama base URL |
34
+ | `OLLAMA_MODEL` | `deepseek-v4-flash:cloud` | No | Model used to generate commit messages |
35
+ | `OLLAMA_BACKUP_MODEL` | - | No | Model used when generation with the primary model fails |
36
+ | `OLLAMA_EFFORT` | - | No | Ollama thinking effort (`low`, `medium`, `high`, or `max`) |
37
+
38
+ ## Setup
39
+
40
+ ### Requirements
41
+
42
+ - **Node.js >= 20 (required):** runs the CLI.
43
+ - **Git (required):** provides the repository operations.
44
+ - **Ollama (required for `commit`):** generates Conventional Commit messages.
45
+ - **Gitleaks (recommended):** scans staged changes before message generation when available on `PATH`.
46
+
47
+ ### Install
48
+
49
+ Install zapdev globally for daily use:
10
50
 
11
51
  ```bash
12
- npm install -g zapdev # installs the `zapdev` command globally
52
+ npm install -g zapdev
13
53
  ```
14
54
 
15
55
  Or run it once without installing:
@@ -18,15 +58,28 @@ Or run it once without installing:
18
58
  npx zapdev commit
19
59
  ```
20
60
 
21
- > For daily use, prefer the global install: `npx` adds resolution overhead on every run.
61
+ ### Development Setup
62
+
63
+ From a clone:
64
+
65
+ ```bash
66
+ npm install
67
+ npm run zapdev # build then run the CLI in dev
68
+ npm run typecheck # tsc --noEmit
69
+ npm run lint # eslint
70
+ npm run test # vitest
71
+ npm run build # bundle to dist/ with esbuild
72
+ ```
73
+
74
+ `npm link` exposes the local `zapdev` binary after `npm run build`.
22
75
 
23
76
  ## Usage
24
77
 
25
- Run `zapdev` with no command to pick one from an interactive menu (falls back to usage output without a TTY).
78
+ Run `zapdev` with no command to pick one from an interactive menu. Without a TTY, zapdev displays its usage instead.
26
79
 
27
80
  ### `zapdev commit`
28
81
 
29
- Stages all changes (`git add -A`), asks an Ollama model for a one-line Conventional Commits message, then lets you commit, edit or cancel, and optionally push.
82
+ Stages all changes, scans them with Gitleaks when installed, generates a Conventional Commit message, and optionally pushes the commit.
30
83
 
31
84
  ```bash
32
85
  zapdev commit
@@ -34,67 +87,52 @@ zapdev commit
34
87
 
35
88
  | Flag | Description |
36
89
  | --- | --- |
37
- | `-m, --model <model>` | Override the Ollama model |
38
- | `-t, --type <type>` | Force the Conventional Commits type (`feat`, `fix`, `chore`, …) |
39
- | `--pull` | Rebase onto upstream (`git pull --rebase`) after committing, before pushing |
90
+ | `--model <model>` | Override the Ollama model |
91
+ | `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
40
92
  | `-p, --push` | Push after committing without asking |
93
+ | `-s, --staged` | Commit only changes that are already staged |
94
+ | `-r, --rebase` | Rebase on upstream if the push is rejected |
95
+ | `-m, --merge` | Merge upstream if the push is rejected |
41
96
  | `-y, --yes` | Skip prompts and commit directly |
42
97
 
43
98
  ```bash
44
- zapdev commit -t feat # force the type; the model still writes scope + description
99
+ zapdev commit -t feat # force the type
100
+ zapdev commit --staged # leave unstaged changes untouched
45
101
  ```
46
102
 
47
- Pushing is optimistic — no preliminary fetch, so the common case stays a single round-trip. If the push is rejected because the branch is behind its upstream, in a TTY it offers to `git pull --rebase` and retry; non-interactively (`--yes` / piped) it stops and asks you to re-run with `--pull`. Pass `--pull` to rebase onto upstream right after the commit (a rebase conflict stops before pushing). `--yes` only silences prompts; it never rebases on its own.
103
+ Before contacting Ollama, zapdev runs `gitleaks git --staged` when Gitleaks is installed. A failed scan stops the commit; when Gitleaks is absent, the scan is skipped.
104
+
105
+ Pushing is optimistic, with no preliminary fetch. If the branch is behind upstream, `--rebase` runs `git pull --rebase`, while `--merge` runs `git pull --no-rebase --no-edit`; zapdev then retries once. Without either flag, interactive runs ask whether to rebase, merge, or quit. Runs using `--yes` or without a TTY must provide one of the flags.
48
106
 
49
- Without a TTY (piped / CI), it commits automatically and only pushes when `--push` is set.
107
+ Without a TTY, zapdev commits automatically and only pushes when `--push` is set.
50
108
 
51
109
  ### `zapdev reset`
52
110
 
53
- Operates on a git repo, or — when the path is a plain directory — on its **direct child** repos (level 1, non-recursive). For each one: `git fetch --prune`, switch to a branch (chosen interactively, or forced with `--principal` / `--target`), then permanently delete every other local branch and every linked worktree. The principal branch (from `origin/HEAD`) and the branch you switched to are always kept. Ends with an optional `pull`.
111
+ Operates on a Git repository or the direct child repositories of a directory. It fetches and prunes, switches branch, then permanently removes other local branches and linked worktrees.
54
112
 
55
113
  ```bash
56
- zapdev reset # the current repo, or the child repos of the current directory
57
- zapdev reset ~/dev # a repo, or the child repos of a directory
58
- zapdev reset -p # switch each repo to its principal branch, no prompt
59
- zapdev reset -t dev # switch each repo to `dev` (or principal if absent)
114
+ zapdev reset # reset the current repo or direct child repos
115
+ zapdev reset ~/dev # reset repos under a directory
116
+ zapdev reset -p # switch to the principal branch without prompting
117
+ zapdev reset -t dev # switch to dev or fall back to the principal branch
60
118
  ```
61
119
 
62
120
  | Flag | Description |
63
121
  | --- | --- |
64
122
  | `-p, --principal` | Switch every repo to its resolved principal branch (`origin/HEAD`) |
65
- | `-t, --target <branch>` | Switch every repo to `<branch>`, falling back to the principal one |
123
+ | `-t, --target <branch>` | Switch to a target branch, falling back to the principal branch |
66
124
  | `--pull` | Pull the checked-out branch after reset without asking |
67
- | `-y, --yes` | Non-interactive: switch and delete without confirmation |
68
-
69
- Deletion is permanent: branches via `git branch -D` (force), worktrees via `git worktree remove --force`. Worktrees are removed first so the branches they held become deletable. Without a TTY, pass `--yes` — otherwise the destructive step is refused. `node_modules` is never scanned.
70
-
71
- ## Configuration
125
+ | `-y, --yes` | Switch and delete without confirmation |
72
126
 
73
- | Variable | Default | Description |
74
- | --- | --- | --- |
75
- | `OLLAMA_URL` | `http://localhost:11434` | Ollama base URL |
76
- | `OLLAMA_MODEL` | `deepseek-v4-flash:cloud` | Model used to generate messages |
127
+ Deletion is permanent. Branches are removed with `git branch -D`; worktrees are removed with `git worktree remove --force`. Without a TTY, pass `--yes` or the destructive step is refused. `node_modules` is never scanned.
77
128
 
78
- ## Development
79
-
80
- From a clone:
129
+ ### Shell Aliases
81
130
 
82
131
  ```bash
83
- npm install
84
- npm run zapdev # build then run the CLI in dev
85
- npm run typecheck # tsc --noEmit
86
- npm run lint # eslint
87
- npm run test # vitest
88
- npm run build # bundle to dist/ (esbuild)
132
+ alias commit="zapdev commit --yes"
133
+ alias git-reset="zapdev reset --yes --principal --pull"
89
134
  ```
90
135
 
91
- `npm link` (after `npm run build`) exposes the local `zapdev` binary on your PATH.
92
-
93
- ## Project structure
136
+ ## Other
94
137
 
95
- - `src/index.ts` — bin launcher; enables the V8 compile cache, then loads `cli.js`.
96
- - `src/cli.ts` — CLI entry (Citty); registers subcommands, defaults to `commit`.
97
- - `src/commands/` — one file per command.
98
- - `src/lib/` — pure logic (`config`, `commit-message`, `branches`, unit-tested) and side effects (`git`, `ollama`); shared helpers (`errors`).
99
- - `src/prompts/` — LLM prompts as `.md` files, imported as text (esbuild `.md` text loader) and inlined into the bundle at build time.
100
- - `src/types/` — shared type declarations (one file per domain) and ambient module declarations.
138
+ zapdev is available under the MIT license.
package/dist/cli.js CHANGED
@@ -1718,7 +1718,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
1718
1718
  // package.json
1719
1719
  var package_default = {
1720
1720
  name: "zapdev",
1721
- version: "0.8.0",
1721
+ version: "0.10.0",
1722
1722
  description: "Fast, performant and lightweight git chores and dev tools using your terminal and your own local LLM.",
1723
1723
  type: "module",
1724
1724
  bin: {
@@ -1817,9 +1817,14 @@ var DEFAULT_MODEL = "deepseek-v4-flash:cloud";
1817
1817
  function resolveConfig(env = process.env, overrides = {}) {
1818
1818
  return {
1819
1819
  ollamaUrl: normalizeBaseUrl(overrides.ollamaUrl ?? env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL),
1820
- model: overrides.model ?? env.OLLAMA_MODEL ?? DEFAULT_MODEL
1820
+ model: overrides.model ?? env.OLLAMA_MODEL ?? DEFAULT_MODEL,
1821
+ backupModel: overrides.backupModel ?? optionalValue(env.OLLAMA_BACKUP_MODEL),
1822
+ effort: overrides.effort ?? optionalValue(env.OLLAMA_EFFORT)
1821
1823
  };
1822
1824
  }
1825
+ function optionalValue(value) {
1826
+ return value?.trim() || void 0;
1827
+ }
1823
1828
  function normalizeBaseUrl(raw) {
1824
1829
  const trimmed = raw.trim().replace(/\/+$/, "");
1825
1830
  return /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
@@ -2225,6 +2230,9 @@ async function pushSetUpstream(branch) {
2225
2230
  async function pullRebase() {
2226
2231
  await git(["pull", "--rebase"]);
2227
2232
  }
2233
+ async function pullMerge() {
2234
+ await git(["pull", "--no-rebase", "--no-edit"]);
2235
+ }
2228
2236
  async function fetchRemote() {
2229
2237
  await git(["fetch"]);
2230
2238
  }
@@ -2296,6 +2304,25 @@ function errorMessage(error) {
2296
2304
  return error instanceof Error ? error.message : String(error);
2297
2305
  }
2298
2306
 
2307
+ // src/lib/gitleaks.ts
2308
+ async function hasGitleaks() {
2309
+ try {
2310
+ await R2("gitleaks", ["version"], { nodePath: false });
2311
+ return true;
2312
+ } catch (error) {
2313
+ if (error instanceof Error && error.code === "ENOENT") {
2314
+ return false;
2315
+ }
2316
+ throw error;
2317
+ }
2318
+ }
2319
+ async function scanStagedChanges() {
2320
+ const result = await R2("gitleaks", ["git", "--staged"], { nodePath: false });
2321
+ if (result.exitCode === 0) return;
2322
+ const output = result.stderr.trim() || result.stdout.trim();
2323
+ throw new Error(output || "gitleaks git --staged failed");
2324
+ }
2325
+
2299
2326
  // src/prompts/commit-message.md
2300
2327
  var commit_message_default = "You are zapdev, a CLI tool that creates commits quickly and reliably.\n\nGenerate ONE commit message in Conventional Commits format.\nA single line: imperative, English, \u226472 chars.\n\nStrictly follow this format:\n\n```text\ntype(scope): description\n```\n\nReply ONLY with the message, no backticks, no quotes, no explanation.\n";
2301
2328
 
@@ -2307,13 +2334,28 @@ var REQUEST_TIMEOUT_MS = 25e3;
2307
2334
  var KEEP_ALIVE = "30m";
2308
2335
  async function generateCommitMessage(diff, config, type) {
2309
2336
  const systemPrompt = type ? applyCommitType(COMMIT_SYSTEM_PROMPT, type) : COMMIT_SYSTEM_PROMPT;
2310
- const content = await chat(config, [
2337
+ const messages = [
2311
2338
  { role: "system", content: systemPrompt },
2312
2339
  { role: "user", content: truncateDiff(diff) }
2313
- ]);
2340
+ ];
2341
+ let content;
2342
+ try {
2343
+ content = await chat(config, config.model, messages);
2344
+ } catch (error) {
2345
+ if (!config.backupModel || config.backupModel === config.model) throw error;
2346
+ try {
2347
+ content = await chat(config, config.backupModel, messages);
2348
+ } catch (backupError) {
2349
+ throw new AggregateError(
2350
+ [error, backupError],
2351
+ `Primary model failed: ${errorMessage(error)} Backup model failed: ${errorMessage(backupError)}`,
2352
+ { cause: backupError }
2353
+ );
2354
+ }
2355
+ }
2314
2356
  return sanitizeCommitMessage(content);
2315
2357
  }
2316
- async function chat(config, messages) {
2358
+ async function chat(config, model, messages) {
2317
2359
  const url = `${config.ollamaUrl}/api/chat`;
2318
2360
  let response;
2319
2361
  try {
@@ -2321,9 +2363,10 @@ async function chat(config, messages) {
2321
2363
  method: "POST",
2322
2364
  headers: { "content-type": "application/json" },
2323
2365
  body: JSON.stringify({
2324
- model: config.model,
2366
+ model,
2325
2367
  stream: false,
2326
2368
  keep_alive: KEEP_ALIVE,
2369
+ think: config.effort,
2327
2370
  options: { temperature: 0.2 },
2328
2371
  messages
2329
2372
  }),
@@ -2374,7 +2417,6 @@ var commitCommand = defineCommand({
2374
2417
  args: {
2375
2418
  model: {
2376
2419
  type: "string",
2377
- alias: "m",
2378
2420
  description: "Override the Ollama model (defaults to $OLLAMA_MODEL)."
2379
2421
  },
2380
2422
  type: {
@@ -2382,15 +2424,26 @@ var commitCommand = defineCommand({
2382
2424
  alias: "t",
2383
2425
  description: `Force the Conventional Commits type (${COMMIT_TYPES.join(", ")}).`
2384
2426
  },
2385
- pull: {
2386
- type: "boolean",
2387
- description: "Rebase onto upstream (git pull --rebase) after committing, before pushing."
2388
- },
2389
2427
  push: {
2390
2428
  type: "boolean",
2391
2429
  alias: "p",
2392
2430
  description: "Push after committing without asking."
2393
2431
  },
2432
+ staged: {
2433
+ type: "boolean",
2434
+ alias: "s",
2435
+ description: "Commit only changes that are already staged."
2436
+ },
2437
+ rebase: {
2438
+ type: "boolean",
2439
+ alias: "r",
2440
+ description: "Rebase on the upstream branch if the push is rejected."
2441
+ },
2442
+ merge: {
2443
+ type: "boolean",
2444
+ alias: "m",
2445
+ description: "Merge the upstream branch if the push is rejected."
2446
+ },
2394
2447
  yes: {
2395
2448
  type: "boolean",
2396
2449
  alias: "y",
@@ -2403,6 +2456,12 @@ var commitCommand = defineCommand({
2403
2456
  process.env,
2404
2457
  args.model ? { model: args.model } : {}
2405
2458
  );
2459
+ if (args.rebase && args.merge) {
2460
+ log.error("Choose either --rebase or --merge, not both.");
2461
+ process.exitCode = 1;
2462
+ return;
2463
+ }
2464
+ const syncStrategy = args.rebase ? "rebase" : args.merge ? "merge" : void 0;
2406
2465
  const type = args.type ? normalizeCommitType(args.type) : void 0;
2407
2466
  if (type === null) {
2408
2467
  log.error(
@@ -2412,13 +2471,32 @@ var commitCommand = defineCommand({
2412
2471
  return;
2413
2472
  }
2414
2473
  if (interactive) intro("zapdev commit");
2415
- await stageAll();
2474
+ if (!args.staged) await stageAll();
2416
2475
  const diff = await getStagedDiff();
2417
2476
  if (!diff.trim()) {
2418
2477
  log.warn("Nothing to commit.");
2419
2478
  if (interactive) outro("Nothing to do.");
2420
2479
  return;
2421
2480
  }
2481
+ try {
2482
+ if (await hasGitleaks()) {
2483
+ const scanLoader = interactive ? spinner() : void 0;
2484
+ scanLoader?.start("Scanning staged changes with Gitleaks");
2485
+ try {
2486
+ await scanStagedChanges();
2487
+ scanLoader?.stop("No leaks found");
2488
+ } catch (error) {
2489
+ scanLoader?.error("Gitleaks check failed");
2490
+ throw error;
2491
+ }
2492
+ } else {
2493
+ log.info("Gitleaks not found, skipping secret scan.");
2494
+ }
2495
+ } catch (error) {
2496
+ log.error(`Gitleaks check failed: ${errorMessage(error)}`);
2497
+ process.exitCode = 1;
2498
+ return;
2499
+ }
2422
2500
  const loader = interactive ? spinner() : void 0;
2423
2501
  loader?.start("Generating commit message");
2424
2502
  let message;
@@ -2471,10 +2549,6 @@ var commitCommand = defineCommand({
2471
2549
  }
2472
2550
  await commit(finalMessage);
2473
2551
  if (interactive) log.success(`Committed: ${finalMessage}`);
2474
- if (args.pull && !await rebaseOnUpstream(interactive)) {
2475
- process.exitCode = 1;
2476
- return;
2477
- }
2478
2552
  let shouldPush = Boolean(args.push);
2479
2553
  if (!shouldPush && interactive && !args.yes) {
2480
2554
  const answer = await confirm({ message: "Push?", initialValue: false });
@@ -2485,7 +2559,11 @@ var commitCommand = defineCommand({
2485
2559
  shouldPush = answer;
2486
2560
  }
2487
2561
  if (shouldPush) {
2488
- const pushed = await pushOptimistic(interactive, Boolean(args.yes));
2562
+ const pushed = await pushOptimistic(
2563
+ interactive,
2564
+ interactive && !args.yes,
2565
+ syncStrategy
2566
+ );
2489
2567
  if (!pushed) {
2490
2568
  process.exitCode = 1;
2491
2569
  return;
@@ -2494,39 +2572,28 @@ var commitCommand = defineCommand({
2494
2572
  if (interactive) outro("Done.");
2495
2573
  }
2496
2574
  });
2497
- async function rebaseOnUpstream(interactive) {
2498
- if (!await hasUpstream()) {
2499
- log.info("No upstream to pull from; skipping --pull.");
2500
- return true;
2501
- }
2575
+ async function syncWithUpstream(strategy, interactive) {
2502
2576
  const loader = interactive ? spinner() : void 0;
2503
- loader?.start("Pulling --rebase");
2577
+ const label = strategy === "rebase" ? "Rebase" : "Merge";
2578
+ loader?.start(`Pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
2504
2579
  try {
2505
- await pullRebase();
2506
- loader?.stop("\u2713 Rebased on upstream");
2580
+ await (strategy === "rebase" ? pullRebase() : pullMerge());
2581
+ loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
2507
2582
  return true;
2508
2583
  } catch (error) {
2509
- loader?.error("Rebase failed");
2510
- log.error(`Rebase failed (resolve conflicts, then push): ${errorMessage(error)}`);
2584
+ loader?.error(`${label} failed`);
2585
+ log.error(`${label} failed (resolve conflicts, then push): ${errorMessage(error)}`);
2511
2586
  return false;
2512
2587
  }
2513
2588
  }
2514
- async function pushOptimistic(interactive, assumeYes) {
2589
+ async function pushOptimistic(interactive, canPrompt, strategy) {
2515
2590
  const [upstream, branch] = await Promise.all([hasUpstream(), currentBranch()]);
2516
2591
  const doPush = () => upstream ? push() : pushSetUpstream(branch);
2517
2592
  const first = await tryPush(interactive, doPush);
2518
2593
  if (first.ok) return true;
2519
2594
  if (upstream && await isBehind(interactive)) {
2520
- if (assumeYes || !interactive) {
2521
- log.error("Behind upstream \u2014 re-run with --pull to rebase and push.");
2522
- return false;
2523
- }
2524
- const answer = await confirm({ message: "Pull --rebase and retry push?", initialValue: true });
2525
- if (isCancel(answer) || !answer) {
2526
- log.info("Not pushed (still behind).");
2527
- return true;
2528
- }
2529
- if (!await rebaseOnUpstream(interactive)) return false;
2595
+ const syncStrategy = strategy ?? await chooseSyncStrategy(canPrompt);
2596
+ if (!syncStrategy || !await syncWithUpstream(syncStrategy, interactive)) return false;
2530
2597
  const retry = await tryPush(interactive, doPush);
2531
2598
  if (retry.ok) return true;
2532
2599
  log.error(`Push failed: ${errorMessage(retry.error)}`);
@@ -2535,6 +2602,25 @@ async function pushOptimistic(interactive, assumeYes) {
2535
2602
  log.error(`Push failed: ${errorMessage(first.error)}`);
2536
2603
  return false;
2537
2604
  }
2605
+ async function chooseSyncStrategy(interactive) {
2606
+ if (!interactive) {
2607
+ log.error("Branch is behind upstream. Re-run with --rebase or --merge.");
2608
+ return null;
2609
+ }
2610
+ const action = await select({
2611
+ message: "Branch is behind upstream. How should zapdev sync it?",
2612
+ options: [
2613
+ { value: "rebase", label: "Rebase" },
2614
+ { value: "merge", label: "Merge" },
2615
+ { value: "quit", label: "Quit" }
2616
+ ]
2617
+ });
2618
+ if (isCancel(action) || action === "quit") {
2619
+ log.warn("Push cancelled. Commit remains local.");
2620
+ return null;
2621
+ }
2622
+ return action;
2623
+ }
2538
2624
  async function tryPush(interactive, doPush) {
2539
2625
  const loader = interactive ? spinner() : void 0;
2540
2626
  loader?.start("Pushing");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Fast, performant and lightweight git chores and dev tools using your terminal and your own local LLM.",
5
5
  "type": "module",
6
6
  "bin": {