seekter 0.0.0-stage → 0.11.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 +18 -2
  2. package/bin/seekter.mjs +151 -0
  3. package/package.json +30 -4
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # seekter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sets up [Seekter](https://seekter.dev), an open-source job-search kit for Claude Code. It searches job sources, filters postings against your own rules, fills application forms in your own Chrome, and keeps the tracker as markdown files on your machine.
4
+
5
+ ```bash
6
+ npx seekter init
7
+ cd seekter
8
+ claude
9
+ ```
10
+
11
+ Then, inside Claude Code, run `/seekter-init`.
12
+
13
+ `init` downloads the newest release from [GitHub](https://github.com/selfishprimate/seekter/releases/latest) and unpacks it into `./seekter`, or into the folder you name (`npx seekter init my-search`). The folder must be new or empty. Nothing else is installed: the kit has no dependencies, and this package has none either.
14
+
15
+ You need Claude Code, the Claude in Chrome extension and Node.js 20 or newer. Seekter is free; running it is not, because it runs on your own paid Claude plan or API account.
16
+
17
+ Applications go out in your name. Read [DISCLAIMER.md](https://github.com/selfishprimate/seekter/blob/main/DISCLAIMER.md) before the first run.
18
+
19
+ MIT licensed.
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ // npx seekter init [folder]: puts the newest Seekter release in a folder, ready for Claude Code.
3
+ // Node 20+, no packages. It reads one release from GitHub and unpacks it with the `tar` every
4
+ // supported system ships (macOS, Linux, Windows 10 and later). Nothing else is downloaded, nothing
5
+ // is sent anywhere, and nothing outside the target folder is written apart from one temp file.
6
+ //
7
+ // npx seekter init # into ./seekter
8
+ // npx seekter init my-search # into ./my-search
9
+ // npx seekter init . --from seekter.tar.gz # unpack a release archive you already have
10
+ //
11
+ // The kit lives at https://github.com/selfishprimate/seekter. This package is only the way in:
12
+ // the skills, scripts and references come from the release itself, so they are the same files a
13
+ // clone or the seekter.zip download would give.
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { spawnSync } from "node:child_process";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const REPO = "selfishprimate/seekter";
21
+ const PRIVATE = ["profile", "applications", "runs"];
22
+ const VERSION = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
23
+
24
+ const USAGE = `usage: npx seekter init [folder] [--from <release.tar.gz>]
25
+
26
+ init [folder] Put the newest Seekter release in ./seekter, or in the folder you name.
27
+ The folder must be new or empty.
28
+ --from <file> Unpack this release archive instead of downloading one.
29
+ --version Print this package's version.`;
30
+
31
+ class Exit extends Error {
32
+ constructor(message, code = 1) { super(message); this.code = code; }
33
+ }
34
+ const die = (message, code = 1) => { throw new Exit(message, code); };
35
+
36
+ function parse(argv) {
37
+ const out = { cmd: null, folder: null, from: null };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (a === "--from") { out.from = argv[++i] ?? die(`--from needs a file\n\n${USAGE}`, 2); continue; }
41
+ if (a === "-h" || a === "--help" || a === "help") { out.cmd = "help"; continue; }
42
+ if (a === "-v" || a === "--version") { out.cmd = "version"; continue; }
43
+ if (a.startsWith("-")) die(`unknown option ${a}\n\n${USAGE}`, 2);
44
+ if (!out.cmd) out.cmd = a;
45
+ else if (!out.folder) out.folder = a;
46
+ else die(`too many arguments\n\n${USAGE}`, 2);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // The folder may not exist yet, or may exist and be empty. Anything in it is somebody's.
52
+ function prepare(folder) {
53
+ if (fs.existsSync(folder)) {
54
+ if (!fs.statSync(folder).isDirectory()) die(`${folder} exists and is not a folder`);
55
+ if (fs.readdirSync(folder).length) die(`${folder} is not empty. Pick a new folder, or empty this one first.`);
56
+ } else fs.mkdirSync(folder, { recursive: true });
57
+ }
58
+
59
+ async function latestRelease() {
60
+ const headers = { "User-Agent": `seekter-npm/${VERSION}`, Accept: "application/vnd.github+json" };
61
+ const r = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { headers, signal: AbortSignal.timeout(30000) });
62
+ if (!r.ok) die(`GitHub answered ${r.status} for the latest release. Try again later, or download seekter.zip from https://github.com/${REPO}/releases/latest`);
63
+ const rel = await r.json();
64
+ return { tag: rel.tag_name, tarball: rel.tarball_url };
65
+ }
66
+
67
+ async function download(url, file) {
68
+ const r = await fetch(url, { headers: { "User-Agent": `seekter-npm/${VERSION}` }, redirect: "follow", signal: AbortSignal.timeout(120000) });
69
+ if (!r.ok) die(`the release archive answered ${r.status}`);
70
+ fs.writeFileSync(file, Buffer.from(await r.arrayBuffer()));
71
+ }
72
+
73
+ // GitHub's archive has one top folder (owner-repo-sha); the kit goes straight into the target.
74
+ // The archive goes in on stdin and the folder is the working directory: given a path such as
75
+ // C:\Users\..., the GNU tar that Git for Windows puts on the PATH reads "C:" as a remote host.
76
+ function unpack(archive, folder) {
77
+ const p = spawnSync("tar", ["-xzf", "-", "--strip-components=1"], { cwd: folder, input: fs.readFileSync(archive), maxBuffer: 1 << 26 });
78
+ if (p.error) die(`could not run tar (${p.error.code}). It ships with macOS, Linux and Windows 10 or later.`);
79
+ if (p.status !== 0) die(`tar could not unpack the release: ${String(p.stderr || "").trim()}`);
80
+ }
81
+
82
+ // A release never carries anything of a candidate's, but this is the folder they will put
83
+ // their CV in, so check rather than assume, the way the release workflow does.
84
+ function check(folder) {
85
+ if (!fs.existsSync(path.join(folder, ".claude", "skills"))) die("the archive does not look like a Seekter release (no .claude/skills)");
86
+ for (const p of PRIVATE) {
87
+ const dir = path.join(folder, p);
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ const stray = fs.readdirSync(dir).filter((f) => f !== ".gitkeep");
90
+ if (stray.length) die(`the release carried files under ${p}/ (${stray.join(", ")}). Not using it; please report this at https://github.com/${REPO}/issues`);
91
+ }
92
+ }
93
+
94
+ async function init(a) {
95
+ const folder = path.resolve(a.folder || "seekter");
96
+ prepare(folder);
97
+ let tag = null;
98
+ try {
99
+ if (a.from) {
100
+ if (!fs.existsSync(a.from)) die(`no such file: ${a.from}`);
101
+ unpack(path.resolve(a.from), folder);
102
+ } else {
103
+ const rel = await latestRelease();
104
+ tag = rel.tag;
105
+ process.stdout.write(`Downloading Seekter ${tag} ...\n`);
106
+ const tmp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "seekter-")), "release.tar.gz");
107
+ try {
108
+ await download(rel.tarball, tmp);
109
+ unpack(tmp, folder);
110
+ } finally {
111
+ fs.rmSync(path.dirname(tmp), { recursive: true, force: true });
112
+ }
113
+ }
114
+ check(folder);
115
+ } catch (e) {
116
+ // Leave nothing half-unpacked behind in a folder this command created or found empty.
117
+ fs.rmSync(folder, { recursive: true, force: true });
118
+ throw e;
119
+ }
120
+ const rel = path.relative(process.cwd(), folder) || ".";
121
+ console.log(`Seekter${tag ? ` ${tag}` : ""} is in ${rel}
122
+
123
+ Before you start:
124
+ - Applications go out in your name, from your browser. You are responsible for what they say.
125
+ - Job sites and application systems have their own terms; following them is yours to do.
126
+ - Seekter is free, but it runs on your own paid Claude plan or API account.
127
+ Read DISCLAIMER.md in the folder for the whole of it.
128
+
129
+ Next:
130
+ cd ${rel}
131
+ claude
132
+ /seekter-init
133
+
134
+ You need Claude Code, the Claude in Chrome extension and Node.js 20 or newer.`);
135
+ return 0;
136
+ }
137
+
138
+ async function main() {
139
+ const [major] = process.versions.node.split(".").map(Number);
140
+ if (major < 20) die(`Seekter needs Node.js 20 or newer; this is ${process.versions.node}.`);
141
+ const a = parse(process.argv.slice(2));
142
+ if (a.cmd === "version") { console.log(VERSION); return 0; }
143
+ if (!a.cmd || a.cmd === "help") { console.log(USAGE); return a.cmd ? 0 : 2; }
144
+ if (a.cmd === "init") return init(a);
145
+ die(`unknown command ${a.cmd}\n\n${USAGE}`, 2);
146
+ }
147
+
148
+ main().then((code) => { process.exitCode = code; }, (e) => {
149
+ if (e instanceof Exit) { process.stderr.write(`seekter: ${e.message}\n`); process.exitCode = e.code; }
150
+ else { process.stderr.write(`seekter: ${e.stack || e}\n`); process.exitCode = 1; }
151
+ });
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "seekter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.11.0",
4
+ "description": "Sets up Seekter, the open-source job-search kit for Claude Code: npx seekter init",
5
+ "type": "module",
6
+ "bin": {
7
+ "seekter": "bin/seekter.mjs"
8
+ },
9
+ "files": [
10
+ "bin/seekter.mjs",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Halil İbrahim Çakıroğlu",
18
+ "homepage": "https://seekter.dev",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/selfishprimate/seekter.git",
22
+ "directory": "npm"
23
+ },
24
+ "bugs": "https://github.com/selfishprimate/seekter/issues",
25
+ "keywords": [
26
+ "claude-code",
27
+ "job-search",
28
+ "agent",
29
+ "job-application",
30
+ "applicant-tracking"
31
+ ]
32
+ }