dirpush 0.1.1 → 0.1.2

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.
@@ -61,7 +61,7 @@ Workers read `dirpush capabilities` at the start of a session: email verificatio
61
61
  - **"Set up dirpush"** / **"set up this product"** / **"add a product"**: `dirpush init <product-id>`, run the Setup check above if `dirpush capabilities` says it hasn't been done, then help the user fill `workspace/products/<id>/product.yaml` (and the shared `workspace/identity.yaml`, `permissions.yaml` the first time). Validate with `dirpush product show --product <id>`.
62
62
  - **"Find N free directories I haven't submitted to and submit"**: `dirpush directories recommend --free --unsubmitted --limit N --product <id>`, show the list, `dirpush submissions enqueue <dir-id…> --product <id>`, then run the loop.
63
63
  - **"Continue my launch"**: the whole routine, no other instructions needed (nothing from a previous conversation is either):
64
- 1. `dirpush actions` — tell the human what's waiting on them. CAPTCHAs and bot checks: tell them to run `dirpush checks` in their terminal (it opens each one in the agents' Chrome and hands the job back); never solve them yourself.
64
+ 1. `dirpush actions` — tell the human what's waiting on them. Google sign-in, CAPTCHAs and bot checks: tell them to run `dirpush checks` in their terminal (it walks them through each one and hands the jobs back); never solve them yourself.
65
65
  2. `dirpush submissions check` — marks pending listings live (skip if run in the last day).
66
66
  3. Run the loop until `work next` is empty. Jobs still showing another worker with an expired lease are abandoned: `work next` hands them to you; continue from their last step.
67
67
  4. Queue empty → `dirpush directories recommend --free --unsubmitted --limit 10 --product <id>`, show the list, enqueue only what the human approves.
package/README.md CHANGED
@@ -25,7 +25,7 @@ dirpush init acme # sets up ~/.dirpush and installs the agent ski
25
25
  | `dirpush submissions list --product <id>` | Every submission and its outcome |
26
26
  | `dirpush submissions check` | Re-check pending listings and mark live ones |
27
27
  | `dirpush actions` | What needs you: CAPTCHAs, verification emails, questions |
28
- | `dirpush checks` | Clear CAPTCHAs and bot checks one by one in the agents' Chrome; agents pick each job back up |
28
+ | `dirpush checks` | Your part, guided: sign into Google, then clear CAPTCHAs and bot checks one by one; agents pick each job back up |
29
29
  | `dirpush registry updates [--apply]` | Update your registry from the published one on dirpush.com |
30
30
  | `dirpush registry audit` | Measure link type, noindex and bot checks on real listing pages |
31
31
  | `dirpush submissions add <id or URL>` | Record a listing you made yourself |
package/package.json CHANGED
@@ -37,7 +37,7 @@
37
37
  "url": "https://github.com/baronunread/dirpush.git"
38
38
  },
39
39
  "homepage": "https://dirpush.com",
40
- "version": "0.1.1",
40
+ "version": "0.1.2",
41
41
  "keywords": [
42
42
  "directories",
43
43
  "startup",
@@ -7,7 +7,11 @@ PKG="$(cd "$(dirname "$0")/.." && pwd)"
7
7
  if [ -n "$LAUNCH_HOME" ]; then H="$LAUNCH_HOME"; elif [ -d "$PKG/.git" ]; then H="$PKG"; else H="$HOME/.dirpush"; fi
8
8
  mkdir -p "$H/data" && cd "$H"
9
9
  agent-browser close >/dev/null 2>&1
10
- # The agents' Chrome (`launch browser start`) holds the same profile; close it so this window owns it.
10
+ # The agents' Chrome (`dirpush browser start`) holds the same profile; close it so this window owns it.
11
11
  bun "$PKG/src/cli.ts" browser stop >/dev/null 2>&1
12
- open -na "Google Chrome" --args --user-data-dir="$PWD/data/browser-profile" --use-mock-keychain --password-store=basic \
13
- --no-first-run "https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.google.com%2F"
12
+ # Wait for the profile lock to go, then run Chrome in the foreground: this returns when the human quits it (⌘Q),
13
+ # so `dirpush checks` can carry on. A separate --user-data-dir is its own Chrome process; the human's Chrome is untouched.
14
+ for i in 1 2 3 4 5 6 7 8 9 10; do [ -L data/browser-profile/SingletonLock ] || break; sleep 0.5; done
15
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --user-data-dir="$PWD/data/browser-profile" \
16
+ --use-mock-keychain --password-store=basic --no-first-run --no-default-browser-check \
17
+ "https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.google.com%2F" >/dev/null 2>&1
package/src/checks.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `dirpush checks`: the human's part of a launch, in the terminal. Google sign-in first, in plain Chrome with no
3
+ * automation attached (Google refuses sign-in in a window software is driving); then each CAPTCHA or bot check,
4
+ * opened in the agents' Chrome. Agents never solve these: this hands each job back once the human has.
5
+ */
6
+ import { createInterface } from "node:readline/promises";
7
+ import { join } from "node:path";
8
+ import { and, eq, gt, inArray } from "drizzle-orm";
9
+ import { HOME, PKG, type Db } from "./db";
10
+ import { jobs } from "./db/schema";
11
+ import * as J from "./jobs";
12
+ import { loadDirectory } from "./registry";
13
+
14
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
15
+ const paint = (code: string) => (s: string) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
16
+ const bold = paint("1"), dim = paint("2"), green = paint("32"), yellow = paint("33"), cyan = paint("36");
17
+ const env = { ...process.env, LAUNCH_HOME: HOME };
18
+ const script = (name: string, ...args: string[]) =>
19
+ Bun.spawnSync([join(PKG, "scripts", name), ...args], { env, stdin: "ignore", stdout: "ignore", stderr: "ignore" }).exitCode;
20
+
21
+ /** One keypress, no Enter needed. Enter comes back as "\r"; Ctrl-C quits. */
22
+ async function key(prompt: string, keys: string): Promise<string> {
23
+ process.stdout.write(prompt);
24
+ process.stdin.setRawMode(true);
25
+ process.stdin.resume();
26
+ try {
27
+ for (;;) {
28
+ const k = await new Promise<string>((r) => process.stdin.once("data", (b: Buffer) => r(b.toString())));
29
+ if (k === "\x03") { process.stdout.write("\n"); process.exit(130); }
30
+ const ch = k === "\r" || k === "\n" ? "\r" : k.toLowerCase();
31
+ if (keys.includes(ch)) { process.stdout.write(ch === "\r" ? "\n" : `${ch}\n`); return ch; }
32
+ }
33
+ } finally {
34
+ process.stdin.setRawMode(false);
35
+ process.stdin.pause();
36
+ }
37
+ }
38
+
39
+ async function ask(prompt: string) {
40
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
41
+ try { return (await rl.question(prompt)).trim(); } finally { rl.close(); }
42
+ }
43
+
44
+ const WHAT: Record<string, string> = {
45
+ captcha: "Tick the CAPTCHA, then do whatever the note above asks.",
46
+ bot_protection: "Wait for the “checking your browser” page to pass, or tick its box if it shows one.",
47
+ };
48
+
49
+ export async function checks(db: Db, opts: { product?: string; types: string[] }) {
50
+ if (!process.stdin.isTTY) throw new Error("checks is interactive: run `dirpush checks` yourself in a terminal");
51
+ const blocked = (types: string[]) => db.select().from(jobs)
52
+ .where(and(eq(jobs.kind, "submission"), eq(jobs.status, "blocked"), inArray(jobs.blockType, types))).all()
53
+ .filter((j) => !opts.product || j.productId === opts.product);
54
+ const google = blocked(["google_login"]);
55
+ const queue = blocked(opts.types);
56
+ const handedBack: string[] = [], byYou: string[] = [], skipped: string[] = [];
57
+ let signedIn = false;
58
+
59
+ const summary = () => {
60
+ console.log(`\n${bold("Done for now.")}`);
61
+ if (handedBack.length) console.log(`${green("✓")} Back to the agents: ${handedBack.join(", ")}`);
62
+ if (byYou.length) console.log(`${green("✓")} Submitted by you: ${byYou.join(", ")}`);
63
+ if (skipped.length) console.log(`${dim("·")} Skipped, still waiting: ${skipped.join(", ")}`);
64
+ if (handedBack.length) console.log(`\nNext: ask your agent to ${bold("“continue my launch”")}.`);
65
+ };
66
+
67
+ const googleSignIn = async () => {
68
+ const busy = db.select().from(jobs).where(and(eq(jobs.status, "running"), gt(jobs.leaseExpiresAt, Date.now()))).all();
69
+ if (busy.length) {
70
+ console.log(yellow(`An agent is using the browser right now (${busy.map((j) => j.subject).join(", ")}). Signing in closes that window.`));
71
+ if ((await key(`${cyan("[enter]")} close it and sign in ${cyan("[n]")} not now `, "\rn")) === "n") return false;
72
+ }
73
+ console.log(`A normal Chrome window opens on Google's sign-in page (the agents' own profile, nothing automated).`);
74
+ console.log(`Sign in, then ${bold("quit that window with ⌘Q")} to come back here.`);
75
+ script("google-login"); // returns when that Chrome quits
76
+ const ok = (await key(`Signed in? ${cyan("[y]")} yes ${cyan("[n]")} no `, "yn")) === "y";
77
+ if (ok) signedIn = true;
78
+ else console.log(dim("No problem: run `dirpush checks` again whenever you want to retry."));
79
+ return ok;
80
+ };
81
+
82
+ console.log(`${bold("dirpush checks")} ${dim(`${google.length} need Google sign-in · ${queue.length} CAPTCHAs or bot checks`)}`);
83
+ if (!google.length && !queue.length) return console.log("Nothing is waiting on you.");
84
+ console.log(dim("Agents never solve these; you do, and each job goes back to them."));
85
+
86
+ if (google.length) {
87
+ console.log(`\n${bold("1. Sign into Google")} ${dim(google.map((j) => j.subject).join(", "))}`);
88
+ console.log("These directories use “Sign in with Google”. Sign in once and the agents can use it from then on.");
89
+ const k = await key(`${cyan("[enter]")} sign in now ${cyan("[n]")} later ${cyan("[q]")} quit `, "\rnq");
90
+ if (k === "q") return summary();
91
+ if (k === "\r" && (await googleSignIn())) {
92
+ for (const j of google) { J.unblock(db, j.id, "human signed into Google in the agents' Chrome profile"); handedBack.push(j.subject); }
93
+ } else skipped.push(...google.map((j) => j.subject));
94
+ }
95
+
96
+ for (const [i, job] of queue.entries()) {
97
+ const dir = await loadDirectory(job.subject).then((d) => d.directory).catch(() => null);
98
+ const url = job.blockMessage?.match(/https?:\/\/[^\s)'"”]+/)?.[0]?.replace(/[.,;:]$/, "") ?? dir?.submission_url ?? dir?.website;
99
+ const open = () => {
100
+ if (!url) return console.log(yellow("No page saved for this one; open the directory yourself."));
101
+ if (script("ab", "open", url) === 0) script("show-browser");
102
+ else console.log(yellow(`Couldn't open the agents' Chrome. Open it yourself: ${url}`));
103
+ };
104
+ console.log(`\n${bold(`${google.length ? i + 2 : i + 1}. ${dir?.name ?? job.subject}`)} ${dim(`${i + 1} of ${queue.length}`)}`);
105
+ if (job.blockMessage) console.log(dim(job.blockMessage));
106
+ if (url) console.log(`${dim("Page:")} ${url}`);
107
+ console.log(WHAT[job.blockType ?? ""] ?? "Do what the note above asks.");
108
+ console.log(dim("Asked to sign in with Google? Press g: Google refuses sign-in in the agents' window."));
109
+ console.log(dim("Check never passes there? Submit it from your own browser instead, then press s."));
110
+ open();
111
+ for (;;) {
112
+ const k = await key(`${cyan("enter")} done ${cyan("s")} I submitted it ${cyan("o")} reopen ${cyan("g")} Google sign-in ${cyan("n")} skip ${cyan("q")} quit › `, "\rsognq");
113
+ if (k === "o") { open(); continue; }
114
+ if (k === "g") { if (!signedIn) await googleSignIn(); open(); continue; }
115
+ if (k === "q") return summary();
116
+ if (k === "n") skipped.push(job.subject);
117
+ else if (k === "\r") {
118
+ J.unblock(db, job.id, "human cleared the check in the agents' Chrome; continue in the same browser session");
119
+ handedBack.push(job.subject);
120
+ } else {
121
+ const link = await ask(`Listing URL, if there is one ${dim("(enter to skip)")}: `);
122
+ J.recordManual(db, job.id, "pending", link || undefined);
123
+ byYou.push(job.subject);
124
+ }
125
+ break;
126
+ }
127
+ }
128
+ summary();
129
+ }
130
+
package/src/cli.ts CHANGED
@@ -19,6 +19,7 @@ import { auditDirectory, auditDirectoryBrowser } from "./audit";
19
19
  import { browserStatus, newPassword, startBrowser, stopBrowser, vaultLogin } from "./browser";
20
20
  import { accountsFor, backfillAccounts, listAccounts } from "./accounts";
21
21
  import { detect, loadCapabilities, saveCapabilities } from "./capabilities";
22
+ import { checks } from "./checks";
22
23
  import { ratingDomain, ratingHistory, refreshRatings } from "./ratings";
23
24
  import { fetchRegistryFile, registrySource, registryUpdates } from "./registry-updates";
24
25
  import { loadIdentity, loadPermissions, loadProduct, permission, productIds, resolveProductId, PRODUCTS, utm, WORKSPACE } from "./workspace";
@@ -523,43 +524,8 @@ const commands: Record<string, (() => Promise<unknown> | unknown) | undefined> =
523
524
  .orderBy(desc(productApprovals.id)).get() ?? null;
524
525
  },
525
526
  "assets capture-complete": () => consumeProductApproval(db, Number(arg(0, "approval-id"))),
526
- /**
527
- * The human's side of bot checks, one job at a time in the terminal: opens the page in the agents' Chrome, waits,
528
- * then hands the job back to the agents (or records that the human submitted it). Agents never solve these.
529
- */
530
- async "checks"() {
531
- const types = (flags.type ?? "captcha,bot_protection").split(",").map((t) => t.trim());
532
- const queue = db.select().from(jobs)
533
- .where(and(eq(jobs.kind, "submission"), eq(jobs.status, "blocked"), inArray(jobs.blockType, types))).all()
534
- .filter((j) => !flags.product || j.productId === flags.product);
535
- if (!queue.length) return { done: 0, note: `nothing blocked on ${types.join(" or ")}` };
536
- if (!process.stdin.isTTY) throw new Error("checks is interactive: run it yourself in a terminal (it's the human's part)");
537
- const ab = (...a: string[]) => Bun.spawnSync([join(PKG, "scripts/ab"), ...a], { env: { ...process.env, LAUNCH_HOME: HOME }, stdin: "ignore", stdout: "ignore", stderr: "ignore" }).exitCode;
538
- const lines = console[Symbol.asyncIterator]();
539
- const ask = async (q: string) => { process.stdout.write(q); return ((await lines.next()).value ?? "q").trim().toLowerCase(); };
540
- const tally = { handedBack: [] as string[], submittedByYou: [] as string[], skipped: [] as string[] };
541
- for (const [i, job] of queue.entries()) {
542
- const dir = await loadDirectory(job.subject).then((d) => d.directory).catch(() => null);
543
- const url = job.blockMessage?.match(/https?:\/\/[^\s)'"]+/)?.[0]?.replace(/[.,;]$/, "") ?? dir?.submission_url ?? dir?.website;
544
- console.log(`\n[${i + 1}/${queue.length}] ${dir?.name ?? job.subject} (${job.blockType})\n${job.blockMessage ?? ""}\n`);
545
- if (url && ab("open", url) === 0) Bun.spawnSync([join(PKG, "scripts/show-browser")], { env: { ...process.env, LAUNCH_HOME: HOME }, stdin: "ignore", stdout: "ignore", stderr: "ignore" });
546
- else if (url) console.log(`couldn't open the agents' Chrome; open it yourself: ${url}`);
547
- console.log("Clear the check in that window (and anything the message asks for).");
548
- let a = "";
549
- while (!["d", "s", "n", "q"].includes(a)) a = await ask("[d] cleared, agents take it from here [s] I submitted it myself [n] skip [q] quit: ");
550
- if (a === "q") break;
551
- if (a === "n") { tally.skipped.push(job.subject); continue; }
552
- if (a === "d") {
553
- J.unblock(db, job.id, "human cleared the bot check in the agents' Chrome; continue in the same browser session");
554
- tally.handedBack.push(job.subject);
555
- } else {
556
- const link = await ask("listing URL, if there is one (Enter to skip): ");
557
- J.recordManual(db, job.id, "pending", link && link !== "q" ? link : undefined);
558
- tally.submittedByYou.push(job.subject);
559
- }
560
- }
561
- return { ...tally, next: tally.handedBack.length ? `ask your agent to "continue my launch": ${tally.handedBack.length} job(s) are queued again` : undefined };
562
- },
527
+ /** The human's part, interactive: Google sign-in, then CAPTCHAs and bot checks one by one (src/checks.ts). */
528
+ "checks": async () => { await checks(db, { product: flags.product, types: (flags.type ?? "captcha,bot_protection").split(",").map((t) => t.trim()) }); process.exit(0); },
563
529
 
564
530
  /** Everything waiting on the human, grouped. --product narrows it. */
565
531
  "actions": () => {
@@ -699,7 +665,7 @@ const HELP: [string, [string, string][]][] = [
699
665
  ["products list", "all products in this workspace"],
700
666
  ["permission <group.action>", "automatic | confirm | never for an action, e.g. accounts.oauth_google"],
701
667
  ["actions [--product id]", "everything waiting on the human, grouped"],
702
- ["checks [--type captcha,bot_protection] [--product id]", "for the human: clear CAPTCHAs and bot checks one by one in the agents' Chrome, then hand each job back"],
668
+ ["checks [--type captcha,bot_protection] [--product id]", "for the human: sign into Google, then clear CAPTCHAs and bot checks one by one; each job goes back to the agents"],
703
669
  ]],
704
670
  ["Work loop", [
705
671
  ["work next [--claim]", "next submission job (unknown ones first); --claim leases it to you"],