From 710bd1c09cb5f885be0b36db7e16bd4bbcb73c99 Mon Sep 17 00:00:00 2001 From: Laurance Walden <6620582+lwalden@users.noreply.github.com> Date: Mon, 25 May 2026 19:35:36 -0700 Subject: [PATCH] feat(scripts): post to Bluesky via solo-ops-agents ops CLI (B-002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First consumer integration of solo-ops-agents. scripts/post-to-bluesky.ts shells out to the ops CLI (Phase 1 contract — programmatic entrypoint deferred) with a draft -> review(dry-run) -> post flow. Live posting only runs with --confirm; locates the CLI at ../solo-ops-agents/dist/cli.js (override via OPS_CLI). Validated end-to-end: real post made and deleted. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/post-to-bluesky.ts | 105 +++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts/post-to-bluesky.ts diff --git a/scripts/post-to-bluesky.ts b/scripts/post-to-bluesky.ts new file mode 100644 index 0000000..845edda --- /dev/null +++ b/scripts/post-to-bluesky.ts @@ -0,0 +1,105 @@ +/** + * Post to Bluesky via the solo-ops-agents `ops` CLI. + * + * This is lwalden.dev's first consumer integration of solo-ops-agents + * (portfolio social automation). It shells out to the `ops` CLI rather than + * importing it — the Phase 1 consumption contract (see solo-ops-agents + * DECISIONS.md ADR-001; a programmatic entrypoint is deferred to Phase 3). + * + * Flow: draft -> review (dry-run) -> post. The live post step only runs when + * --confirm is passed, so an accidental invocation can never publish. + * + * Usage: + * # Draft + dry-run review only (safe, never posts): + * npx tsx scripts/post-to-bluesky.ts --body "Hello from lwalden.dev" + * + * # Draft, review, then actually post: + * npx tsx scripts/post-to-bluesky.ts --body "Hello from lwalden.dev" --confirm + * + * Requirements: + * - BW_SESSION set in the environment (the ops CLI reads Bluesky creds from + * the ClimbOn Co Bitwarden vault at runtime). + * - solo-ops-agents built (`npm run build` there) so dist/cli.js exists. + * Override its location with OPS_CLI=/abs/path/to/dist/cli.js if the repos + * are not siblings under the same parent directory. + */ +import { spawnSync } from 'node:child_process'; +import { dirname, resolve } from 'node:path'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const OPS_CLI = + process.env.OPS_CLI ?? + resolve(repoRoot, '..', 'solo-ops-agents', 'dist', 'cli.js'); + +interface Args { + body: string; + confirm: boolean; +} + +function parseArgs(argv: string[]): Args { + let body = ''; + let confirm = false; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--body') body = argv[++i] ?? ''; + else if (argv[i] === '--confirm') confirm = true; + } + if (!body.trim()) { + console.error('error: --body "" is required'); + process.exit(2); + } + return { body, confirm }; +} + +/** Invoke `node `, streaming output, returning captured stdout. */ +function ops(args: string[]): string { + const result = spawnSync(process.execPath, [OPS_CLI, ...args], { + encoding: 'utf8', + env: process.env, + }); + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + if (result.status !== 0) { + console.error(`error: ops ${args[0]} exited with code ${result.status}`); + process.exit(result.status ?? 1); + } + return result.stdout ?? ''; +} + +function main(): void { + const { body, confirm } = parseArgs(process.argv.slice(2)); + + if (!existsSync(OPS_CLI)) { + console.error( + `error: ops CLI not found at ${OPS_CLI}\n` + + 'Build solo-ops-agents (npm run build) or set OPS_CLI to dist/cli.js.' + ); + process.exit(1); + } + + // 1. Draft + const draftOut = ops(['draft', '--platform', 'bluesky', '--body', body]); + const idMatch = /Draft (\d+) created/.exec(draftOut); + if (!idMatch) { + console.error('error: could not parse draft id from ops output'); + process.exit(1); + } + const draftId = idMatch[1]; + + // 2. Review — dry-run rehearsal (validates, never posts) + console.log('\n--- review (dry-run) ---'); + ops(['post', '--id', draftId, '--dry-run']); + + // 3. Post — only with explicit confirmation + if (!confirm) { + console.log( + `\nReview only. Re-run with --confirm to post draft ${draftId} live.` + ); + return; + } + console.log('\n--- posting live ---'); + ops(['post', '--id', draftId]); +} + +main();