Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ TypeScript, installed as executables on `PATH`.
| [`dl`](#dl) | Download a video, or just its audio, through yt-dlp |
| [`torrent`](#torrent) | Make a torrent out of a directory, and get it seeded |
| [`codeburn`](#codeburn) | See where your AI spend goes, by task, tool, model and project |
| [`agenticjobs`](#agenticjobs) | Search, apply, post and hire on an agent-friendly job board |
| [`shorten`](#shorten) | Mint a short link on the pit, and follow it from `/f/<code>` |
| [`sysupdate`](#sysupdate) | Update this box: apt lists, apt packages, snaps |

Expand Down Expand Up @@ -66,6 +67,9 @@ One thing here is not a `PATH` command and does not need Node:
- **Node 22.13+ and `pnpm` or `npm`** — `codeburn` only: it is somebody else's
npm package, installed on first use, and upstream's engine floor is higher
than this repo's
- **Node 24+ and `pnpm` or `npm`** — `agenticjobs` only, for the same reason:
the board is an npm package installed on first use, and it asks for a newer
Node than anything else here

## Install

Expand Down Expand Up @@ -1293,6 +1297,59 @@ before every launch, which is fine for a one-shot and wrong for a dashboard you
open twenty times a day. Upstream wants **Node 22.13+**; on an older one it says
so and tries anyway, since that floor is theirs to move.

### `agenticjobs`

The job board where the applying is done by agents, from the terminal:
[@profullstack/agenticjobs](https://www.npmjs.com/package/@profullstack/agenticjobs),
the same board that runs at [agenticjobs.work](https://agenticjobs.work).

```sh
agenticjobs signup # an account, and this box signed in
agenticjobs search rust --remote # the board you are on
agenticjobs search go --network # every board in the directory
agenticjobs apply <slug> --resume cv.md
agenticjobs post job.md # then `publish <slug>` to go live
agenticjobs tui # the full-screen client
agenticjobs mcp # stdio MCP server for the board
agenticjobs --help # it is upstream's CLI: upstream's flags
```

Everything is handed through untouched, so upstream's docs are the docs. Two
flags are ours, spelled `--self-*` because every plain word belongs to them:

```sh
agenticjobs --self-update # reinstall the latest release
agenticjobs --self-where # which copy runs, and from where
```

**The first run installs it**, into
`~/.local/share/cli-tools/vendor/agenticjobs` rather than globally. That matters
more here than for the other wrapped packages, because upstream's own executable
is *also* called `agenticjobs`: a global install would put two of that name on
`PATH` and the winner would come down to the order of two directories, and if
ours won and it followed `PATH` it would exec itself. A private prefix means the
name exists once. `AGENTICJOBS_BIN` points at a copy you would rather run, and
`AGENTICJOBS_SPEC` pins a version.

**Three plain words are intercepted, and only for the copy we installed.**
`update`, `uninstall` and `where` are upstream's, and upstream answers them from
a `manifest.json` that its own `curl` installer writes. A copy npm puts in our
prefix has no manifest, so upstream would answer "not installed by the
installer" on a box where this command plainly did install it and works. So
those three are answered here when the board that would run is ours, and handed
straight through when it is not: a board from upstream's installer, or one
pointed at with `AGENTICJOBS_BIN`, keeps upstream's behaviour exactly.

That last case is not hypothetical. Upstream's installer writes a real shell
script to `~/.local/bin/agenticjobs`, which is the same directory this repo
links into, and `install-links.mjs` never takes over a real file even with
`--force`. On a box that already ran `curl -fsSL https://agenticjobs.work/install.sh | sh`
you will see `SKIP … is a real file` and keep the board you already had. Remove
that script first if you want this wrapper to own the name.

Upstream wants **Node 24+**, which is higher than this repo's own floor of 22.18;
on an older one it says so and tries anyway, since that floor is theirs to move.

### `shorten`

Mints a short link on the Moshpit registry and prints it. `/f/<code>` answers a
Expand Down
159 changes: 159 additions & 0 deletions bin/agenticjobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env node
/**
* agenticjobs — search, apply, post and hire from the terminal.
*
* A launcher for the `@profullstack/agenticjobs` package, not a
* reimplementation of it. Everything you type is handed through untouched, so
* upstream's docs are the docs:
*
* agenticjobs signup make an account and sign this box in
* agenticjobs search rust --remote the current board
* agenticjobs search go --network every board in the directory
* agenticjobs apply <slug> --resume cv.md
* agenticjobs post job.md post one, then `publish <slug>`
* agenticjobs tui the full-screen client
* agenticjobs mcp stdio MCP server for the board
* agenticjobs --help all of it
*
* What this adds is that it is a command rather than an incantation: installed
* on first use into a prefix of ours, refreshed when you ask, and never
* fighting a global install for a name they both want. src/agenticjobs.ts says
* why that last one is not hypothetical.
*
* Ours, and therefore NOT passed through:
* --self-update reinstall the latest release
* --self-where say which copy would run, and from where
*
* Both are spelled --self-* because every plain word is upstream's to use.
*
* The exception is `update`, `uninstall` and `where`, and only when the copy
* that would run is the one we installed. Those three read a manifest written
* by upstream's curl installer, which a copy npm put in our prefix does not
* have, so upstream would answer "not installed by the installer" on a box
* where this command installed it and works.
*/

import { isMain } from '../src/is-main.ts';
import { spawnInherit } from '../src/codeburn.ts';
import {
MIN_NODE,
PACKAGE,
install,
installFailureMessage,
meetsNodeFloor,
nodeFloorMessage,
ownsInstallWord,
removeVendor,
resolveRunner,
vendorBin,
vendorRoot,
} from '../src/agenticjobs.ts';

const DESCRIBE: Record<string, string> = {
env: 'AGENTICJOBS_BIN',
vendor: 'installed by cli-tools',
path: 'already on PATH',
missing: 'not installed yet',
};

/** Install, and say what happened. Shared by first run and every refresh. */
async function refresh(reason: string): Promise<string | null> {
const spec = process.env.AGENTICJOBS_SPEC || `${PACKAGE}@latest`;
process.stderr.write(`agenticjobs: ${reason} ${spec}\n`);

const result = await install(spec);
if (!result.ok) {
process.stderr.write(`${installFailureMessage(vendorRoot())}\n`);
// An install that exited 0 and left the wrong version behind is the
// confusing case, so the reason goes out rather than just the failure.
if (result.note) process.stderr.write(` ${result.note}\n`);
return null;
}

process.stderr.write(
`agenticjobs: installed ${result.version ?? ''} with ${result.manager}\n`.replace(' ', ' '),
);
return vendorBin();
}

async function main(argv: string[]): Promise<number> {
const runner = resolveRunner();

if (argv[0] === '--self-where') {
process.stdout.write(
[
`${runner.file ?? '(none)'} ${DESCRIBE[runner.kind]}`,
`prefix: ${vendorRoot()}`,
`node: ${process.version}${meetsNodeFloor(process.version) ? '' : ` (below ${MIN_NODE})`}`,
'',
].join('\n'),
);
return 0;
}

// `where` against a copy we installed. Answered with the same lines as
// --self-where, because the honest answer to "where is it" is our prefix.
if (ownsInstallWord(runner.kind, argv[0]) && argv[0] === 'where') {
return main(['--self-where']);
}

if (ownsInstallWord(runner.kind, argv[0]) && argv[0] === 'uninstall') {
if (!argv.includes('--yes')) {
process.stdout.write(
[
`This will remove the copy cli-tools installed:`,
` ${vendorRoot()}`,
'',
'Your boards and tokens in ~/.config/agenticjobs are NOT touched.',
'',
'Run it for real with: agenticjobs uninstall --yes',
'',
].join('\n'),
);
return 0;
}
const removed = removeVendor();
process.stdout.write(
removed ? `Removed ${vendorRoot()}\n` : `Nothing to remove at ${vendorRoot()}\n`,
);
return 0;
}

const refreshing = argv[0] === '--self-update' || ownsInstallWord(runner.kind, argv[0]);
const args = refreshing ? argv.slice(1) : argv;

// The Node floor is a warning rather than a refusal. It is upstream's
// constraint, it may move, and being wrong about it should not be the thing
// that stops somebody using the tool.
if (!meetsNodeFloor(process.version)) {
process.stderr.write(`${nodeFloorMessage(process.version)}\n`);
}

let file = runner.file;

if (refreshing || runner.kind === 'missing') {
file = await refresh(refreshing ? 'updating' : 'first run, installing');
if (file === null) return 1;

// A bare `update` or `--self-update` is a maintenance run, not a launch.
if (refreshing && args.length === 0) return 0;
}

if (file === null) {
process.stderr.write(`${installFailureMessage(vendorRoot())}\n`);
return 1;
}

const code = await spawnInherit(file, args);
if (code === null) {
process.stderr.write(
`agenticjobs: could not start ${file}\n agenticjobs --self-update # reinstall it\n`,
);
return 1;
}
return code;
}

if (isMain(import.meta.url)) {
process.exit(await main(process.argv.slice(2)));
}
Loading
Loading