dirpush 0.1.3 → 0.1.5
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.
- package/package.json +1 -1
- package/src/browser.ts +31 -4
- package/src/checks.ts +31 -8
package/package.json
CHANGED
package/src/browser.ts
CHANGED
|
@@ -78,8 +78,22 @@ export async function stopBrowser() {
|
|
|
78
78
|
return { stopped: true };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The CDP port of the agents' Chrome: the one `browser start` runs on BROWSER_PORT, else the one `dirpush ab` launched
|
|
83
|
+
* through agent-browser (its port is in the profile's DevToolsActivePort file). Only a live port counts.
|
|
84
|
+
*/
|
|
85
|
+
async function agentsChromePort() {
|
|
86
|
+
const live = async (port: number) => fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(1500) }).then((r) => r.ok, () => false);
|
|
87
|
+
if (await live(BROWSER_PORT)) return BROWSER_PORT;
|
|
88
|
+
const file = Bun.file(join(PROFILE, "DevToolsActivePort"));
|
|
89
|
+
const port = (await file.exists()) ? Number((await file.text()).split("\n")[0]) : NaN;
|
|
90
|
+
if (port && (await live(port))) return port;
|
|
91
|
+
throw new Error("the agents' browser is not open: open the page with 'dirpush ab open <url>' first");
|
|
92
|
+
}
|
|
93
|
+
let cdpPort = BROWSER_PORT;
|
|
94
|
+
|
|
81
95
|
function ab(args: string[], stdin?: string) {
|
|
82
|
-
const p = Bun.spawnSync(["agent-browser", "--session", SECRETS_SESSION, "--cdp", String(
|
|
96
|
+
const p = Bun.spawnSync(["agent-browser", "--session", SECRETS_SESSION, "--cdp", String(cdpPort), ...args], { stdin: stdin === undefined ? "ignore" : Buffer.from(stdin), stdout: "pipe", stderr: "pipe", timeout: 60_000 });
|
|
83
97
|
const out = p.stdout.toString().trim();
|
|
84
98
|
if (p.exitCode !== 0) throw new Error(`agent-browser ${args[0]} failed: ${(p.stderr.toString() || out).trim().slice(0, 300)}`);
|
|
85
99
|
return out;
|
|
@@ -88,8 +102,9 @@ function ab(args: string[], stdin?: string) {
|
|
|
88
102
|
/** The open tab on `site`'s host (the one the agent is working in); the most recently active one if several. */
|
|
89
103
|
async function pageOn(site: string) {
|
|
90
104
|
const host = new URL(site.includes("://") ? site : `https://${site}`).hostname.replace(/^www\./, "");
|
|
91
|
-
|
|
92
|
-
const
|
|
105
|
+
cdpPort = await agentsChromePort();
|
|
106
|
+
const list = (await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`, { signal: AbortSignal.timeout(1500) })).json()) as Target[];
|
|
107
|
+
const page = list.find((t) => t.type === "page" && /^https?:/.test(t.url) && new URL(t.url).hostname.replace(/^www\./, "").endsWith(host));
|
|
93
108
|
if (!page) throw new Error(`no open tab on ${host}; open the login/sign-up page first`);
|
|
94
109
|
ab(["tab", page.id]);
|
|
95
110
|
return page;
|
|
@@ -97,6 +112,7 @@ async function pageOn(site: string) {
|
|
|
97
112
|
|
|
98
113
|
/** Fills a saved login (agent-browser vault) into the open tab on that site. The password never leaves the vault. */
|
|
99
114
|
export async function vaultLogin(name: string) {
|
|
115
|
+
cdpPort = await agentsChromePort();
|
|
100
116
|
const meta = JSON.parse(ab(["auth", "show", name, "--json"])) as { data?: { profile?: { url?: string } } };
|
|
101
117
|
const url = meta.data?.profile?.url;
|
|
102
118
|
if (!url) throw new Error(`vault entry ${name} has no login URL`);
|
|
@@ -105,6 +121,17 @@ export async function vaultLogin(name: string) {
|
|
|
105
121
|
return { vault: name, page: page.url.slice(0, 200), filled: true };
|
|
106
122
|
}
|
|
107
123
|
|
|
124
|
+
/** 24 random characters with every class sites ask for (upper, lower, digit, symbol), symbols that forms accept. */
|
|
125
|
+
export function strongPassword() {
|
|
126
|
+
const sets = ["ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnopqrstuvwxyz", "23456789", "!@#%^*-_=+?"];
|
|
127
|
+
const all = sets.join("");
|
|
128
|
+
const pick = (set: string, b: number) => set[b % set.length]!;
|
|
129
|
+
const bytes = randomBytes(32);
|
|
130
|
+
const chars = [...sets.map((set, i) => pick(set, bytes[i]!)), ...Array.from({ length: 20 }, (_, i) => pick(all, bytes[i + 4]!))];
|
|
131
|
+
for (let i = chars.length - 1; i > 0; i--) { const j = bytes[(i + 8) % 32]! % (i + 1); [chars[i], chars[j]] = [chars[j]!, chars[i]!]; }
|
|
132
|
+
return chars.join("");
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
/**
|
|
109
136
|
* New account password: generated here, typed into every given field of the open tab on `url`'s site, checked,
|
|
110
137
|
* then saved to the vault as `name`. Returns no secret.
|
|
@@ -112,7 +139,7 @@ export async function vaultLogin(name: string) {
|
|
|
112
139
|
export async function newPassword(name: string, url: string, username: string, selectors: string[]) {
|
|
113
140
|
if (!selectors.length) throw new Error("pass the password field selector(s), e.g. 'input[type=password]'");
|
|
114
141
|
const page = await pageOn(url);
|
|
115
|
-
const pw =
|
|
142
|
+
const pw = strongPassword();
|
|
116
143
|
for (const s of selectors) ab(["fill", s, pw]);
|
|
117
144
|
// A fill can "succeed" on a selector that matched nothing; never save a password the site didn't get.
|
|
118
145
|
const lengths = JSON.parse(ab(["eval", `JSON.stringify(${JSON.stringify(selectors)}.map((s) => document.querySelector(s)?.value.length ?? -1))`]));
|
package/src/checks.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { and, eq, gt, inArray } from "drizzle-orm";
|
|
|
9
9
|
import { HOME, PKG, type Db } from "./db";
|
|
10
10
|
import { jobs } from "./db/schema";
|
|
11
11
|
import * as J from "./jobs";
|
|
12
|
+
import { loadCapabilities } from "./capabilities";
|
|
12
13
|
import { loadDirectory } from "./registry";
|
|
13
14
|
|
|
14
15
|
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -56,12 +57,12 @@ export async function checks(db: Db, opts: { product?: string; types: string[] }
|
|
|
56
57
|
const handedBack: string[] = [], byYou: string[] = [], skipped: string[] = [];
|
|
57
58
|
let signedIn = false;
|
|
58
59
|
|
|
59
|
-
const summary = () => {
|
|
60
|
+
const summary = async () => {
|
|
60
61
|
console.log(`\n${bold("Done for now.")}`);
|
|
61
|
-
if (handedBack.length) console.log(`${green("✓")}
|
|
62
|
+
if (handedBack.length) console.log(`${green("✓")} Ready for the agents to finish: ${handedBack.join(", ")}`);
|
|
62
63
|
if (byYou.length) console.log(`${green("✓")} Submitted by you: ${byYou.join(", ")}`);
|
|
63
64
|
if (skipped.length) console.log(`${dim("·")} Skipped, still waiting: ${skipped.join(", ")}`);
|
|
64
|
-
if (handedBack.length)
|
|
65
|
+
if (handedBack.length) await startAgent(handedBack.length);
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
const googleSignIn = async () => {
|
|
@@ -81,13 +82,13 @@ export async function checks(db: Db, opts: { product?: string; types: string[] }
|
|
|
81
82
|
|
|
82
83
|
console.log(`${bold("dirpush checks")} ${dim(`${google.length} need Google sign-in · ${queue.length} CAPTCHAs or bot checks`)}`);
|
|
83
84
|
if (!google.length && !queue.length) return console.log("Nothing is waiting on you.");
|
|
84
|
-
console.log(dim("Agents never solve these
|
|
85
|
+
console.log(dim("Agents never solve these. You clear each one, then an agent finishes the submission (at the end I can start yours)."));
|
|
85
86
|
|
|
86
87
|
if (google.length) {
|
|
87
88
|
console.log(`\n${bold("1. Sign into Google")} ${dim(google.map((j) => j.subject).join(", "))}`);
|
|
88
89
|
console.log("These directories use “Sign in with Google”. Sign in once and the agents can use it from then on.");
|
|
89
90
|
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 === "q") return await summary();
|
|
91
92
|
if (k === "\r" && (await googleSignIn())) {
|
|
92
93
|
for (const j of google) { J.unblock(db, j.id, "human signed into Google in the agents' Chrome profile"); handedBack.push(j.subject); }
|
|
93
94
|
} else skipped.push(...google.map((j) => j.subject));
|
|
@@ -109,10 +110,10 @@ export async function checks(db: Db, opts: { product?: string; types: string[] }
|
|
|
109
110
|
console.log(dim("Check never passes there? Submit it from your own browser instead, then press s."));
|
|
110
111
|
open();
|
|
111
112
|
for (;;) {
|
|
112
|
-
const k = await key(`${cyan("enter")}
|
|
113
|
+
const k = await key(`${cyan("enter")} cleared, agent finishes it ${cyan("s")} I submitted it ${cyan("o")} reopen ${cyan("g")} Google sign-in ${cyan("n")} skip ${cyan("q")} quit › `, "\rsognq");
|
|
113
114
|
if (k === "o") { open(); continue; }
|
|
114
115
|
if (k === "g") { if (!signedIn) await googleSignIn(); open(); continue; }
|
|
115
|
-
if (k === "q") return summary();
|
|
116
|
+
if (k === "q") return await summary();
|
|
116
117
|
if (k === "n") skipped.push(job.subject);
|
|
117
118
|
else if (k === "\r") {
|
|
118
119
|
J.unblock(db, job.id, "human cleared the check in the agents' Chrome; continue in the same browser session");
|
|
@@ -125,6 +126,28 @@ export async function checks(db: Db, opts: { product?: string; types: string[] }
|
|
|
125
126
|
break;
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
|
-
summary();
|
|
129
|
+
await summary();
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
|
|
133
|
+
/** The agent CLIs we can open on "continue my launch", most likely first: the one that recorded capabilities, then any on PATH. */
|
|
134
|
+
async function startAgent(n: number) {
|
|
135
|
+
const caps = await loadCapabilities().catch(() => null);
|
|
136
|
+
const known = [
|
|
137
|
+
{ id: "claude-code", name: "Claude Code", bin: "claude" },
|
|
138
|
+
{ id: "codex", name: "Codex", bin: "codex" },
|
|
139
|
+
].filter((a) => Bun.which(a.bin));
|
|
140
|
+
const agent = known.find((a) => caps?.checked_by?.startsWith(a.id)) ?? known[0];
|
|
141
|
+
const prompt = "continue my launch";
|
|
142
|
+
if (!agent || process.platform !== "darwin") {
|
|
143
|
+
console.log(`\nNext: open your agent and say ${bold(`“${prompt}”`)}; it finishes the ${n} job(s) above.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log(`\nThe ${n} job(s) above are waiting for an agent to finish them.`);
|
|
147
|
+
const k = await key(`${cyan("[enter]")} open ${agent.name} on “${prompt}” now ${cyan("[n]")} I'll do it later `, "\rn");
|
|
148
|
+
if (k === "n") return console.log(dim(`Whenever you're ready: open your agent and say “${prompt}”.`));
|
|
149
|
+
// A new Terminal window, so the human watches it work and approves what it asks for. Nothing runs unattended.
|
|
150
|
+
const cmd = `cd ${JSON.stringify(process.cwd())} && ${agent.bin} ${JSON.stringify(prompt)}`;
|
|
151
|
+
const ok = Bun.spawnSync(["osascript", "-e", `tell application "Terminal" to do script ${JSON.stringify(cmd)}`, "-e", `tell application "Terminal" to activate`], { stdin: "ignore", stdout: "ignore", stderr: "ignore" }).exitCode === 0;
|
|
152
|
+
console.log(ok ? `${green("✓")} Opened ${agent.name} in a new Terminal window.` : `Couldn't open Terminal. Run this yourself: ${cmd}`);
|
|
153
|
+
}
|