social-autoposter 1.6.35 → 1.6.36

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/bin/cli.js CHANGED
@@ -214,6 +214,30 @@ function isAppMakerVm() {
214
214
  return probe.status === 0;
215
215
  }
216
216
 
217
+ // VM / AppMaker support is strictly opt-in. A normal `init`/`update` (the
218
+ // macOS user path) installs none of it — no apt-get, no :9222 CDP env file, no
219
+ // AppMaker MCP port overrides. It activates only when explicitly requested:
220
+ // - env SA_VM=1 (or SOCIAL_AUTOPOSTER_VM=1)
221
+ // - flag --vm on the command line
222
+ // - a persisted marker written by `bootstrap-vm` (so later `update`s on the
223
+ // same VM stay in VM mode without re-passing the flag)
224
+ // - a genuine AppMaker VM (linux + /opt/startup.sh + live :9222) — kept as a
225
+ // fallback so the existing mk0r bootstrap keeps working untouched. This can
226
+ // never be true on a user's Mac.
227
+ const VM_MARKER = path.join(HOME, '.social-autoposter', 'vm-mode');
228
+ function vmModeEnabled() {
229
+ if (process.env.SA_VM === '1' || process.env.SOCIAL_AUTOPOSTER_VM === '1') return true;
230
+ if (process.argv.includes('--vm')) return true;
231
+ try { if (fs.existsSync(VM_MARKER)) return true; } catch { /* ignore */ }
232
+ return isAppMakerVm();
233
+ }
234
+ function enableVmMode() {
235
+ try {
236
+ fs.mkdirSync(path.dirname(VM_MARKER), { recursive: true });
237
+ fs.writeFileSync(VM_MARKER, 'enabled\n');
238
+ } catch { /* best-effort; vmModeEnabled() still honors env/flag/probe */ }
239
+ }
240
+
217
241
  // Write ~/.social-autoposter-env so skill/lib/twitter-backend.sh picks up the
218
242
  // AppMaker-specific TWITTER_CDP_URL before its `${VAR:-default}` fallback hits.
219
243
  // Idempotent: rewrites the file every invocation so a config edit on the VM
@@ -375,6 +399,9 @@ function bootstrapVm() {
375
399
  console.error('bootstrap-vm: not an AppMaker VM (no /opt/startup.sh + CDP :9222). Use `init` or `update` on dev boxes.');
376
400
  process.exit(2);
377
401
  }
402
+ // Persist VM mode so subsequent `update` runs on this sandbox keep the
403
+ // AppMaker tweaks without re-passing --vm/SA_VM.
404
+ enableVmMode();
378
405
  console.log(' AppMaker VM bootstrap: resolving identity from DB by sessionKey...');
379
406
 
380
407
  let sessionKey;
@@ -471,7 +498,7 @@ function bootstrapVm() {
471
498
  }
472
499
 
473
500
  function installBrowserHarness() {
474
- const onAppMaker = isAppMakerVm();
501
+ const onAppMaker = vmModeEnabled();
475
502
  if (onAppMaker) {
476
503
  console.log(' AppMaker VM detected -> installing harness toolchain (deps); MCP will be pointed at port 9222');
477
504
  writeAppMakerEnvFile();
@@ -803,7 +830,7 @@ function init() {
803
830
  installBrowserAgentConfigs();
804
831
  // On AppMaker VMs, patch the twitter-harness MCP config so its server.py
805
832
  // drives port 9222 (AppMaker Chromium) instead of the default 9555.
806
- if (isAppMakerVm()) applyAppMakerMcpConfigOverrides();
833
+ if (vmModeEnabled()) applyAppMakerMcpConfigOverrides();
807
834
  // Register those MCP servers with Claude so they show up in `claude mcp list`.
808
835
  registerBrowserAgentMcpServers();
809
836
 
@@ -891,7 +918,7 @@ function update() {
891
918
  installBrowserAgentConfigs();
892
919
  // On AppMaker VMs, patch the twitter-harness MCP config so its server.py
893
920
  // drives port 9222 (AppMaker Chromium) instead of the default 9555.
894
- if (isAppMakerVm()) applyAppMakerMcpConfigOverrides();
921
+ if (vmModeEnabled()) applyAppMakerMcpConfigOverrides();
895
922
  // Register any newly added MCP servers with Claude (idempotent).
896
923
  registerBrowserAgentMcpServers();
897
924
 
package/mcp/dist/index.js CHANGED
@@ -131,6 +131,34 @@ function jsonContent(obj) {
131
131
  function textContent(text) {
132
132
  return { content: [{ type: "text", text }] };
133
133
  }
134
+ // Map a pipeline failure-reason key (from scripts/classify_run_error.py, emitted
135
+ // by run-twitter-cycle.sh as `DRAFT_ONLY_BLOCKED=<reason>`) to a clear,
136
+ // actionable message. The most common one on a fresh machine is
137
+ // claude_not_logged_in: the background `claude` CLI the pipeline shells out to
138
+ // has its OWN login, separate from Claude Desktop, so it can be logged out even
139
+ // though this MCP host is signed in. Without this, an auth failure was silently
140
+ // reported as a benign empty cycle ("all threads already engaged").
141
+ function blockedReasonMessage(reason) {
142
+ switch (reason) {
143
+ case "claude_not_logged_in":
144
+ return ("The background Claude CLI on this machine isn't logged in, so the drafting step " +
145
+ "couldn't run. (It DID find and rank threads, it just couldn't draft replies.) This " +
146
+ "CLI uses its own login, separate from Claude Desktop. To fix it, open a terminal and run:\n\n" +
147
+ " claude\n\n" +
148
+ "then `/login` inside it (or run `claude setup-token`). Once it's logged in, run draft_cycle again.");
149
+ case "monthly_limit":
150
+ case "daily_limit":
151
+ case "rate_limit_5h":
152
+ return (`The drafting step hit an Anthropic usage limit (${reason}), so no replies were drafted. ` +
153
+ "Wait for the limit to reset, then run draft_cycle again.");
154
+ case "credit_balance":
155
+ return ("The drafting step failed because the Anthropic account is out of credits. " +
156
+ "Add credits, then run draft_cycle again.");
157
+ default:
158
+ return (`The drafting step failed (${reason}) and produced no drafts. ` +
159
+ "Check skill/logs/twitter-cycle-*.log on this machine for details, then run draft_cycle again.");
160
+ }
161
+ }
134
162
  async function produceDrafts(project) {
135
163
  // Run the real pipeline in DRAFT_ONLY mode: scan -> score -> draft -> link-gen,
136
164
  // then STOP before posting. The script prints `DRAFT_ONLY_PLAN=<path>` and
@@ -150,6 +178,13 @@ async function produceDrafts(project) {
150
178
  const marker = /DRAFT_ONLY_PLAN=\/tmp\/twitter_cycle_plan_(.+)\.json/.exec(res.stdout + "\n" + res.stderr);
151
179
  if (marker && marker[1])
152
180
  return { batchId: marker[1] };
181
+ // A real prep-step failure (e.g. the background claude CLI isn't logged in)
182
+ // emits DRAFT_ONLY_BLOCKED=<reason>. Surface that instead of silently falling
183
+ // back to a stale/empty batch and mis-reporting "no fresh candidates".
184
+ const blockedMarker = /DRAFT_ONLY_BLOCKED=([a-z0-9_]+)/.exec(res.stdout + "\n" + res.stderr);
185
+ if (blockedMarker && blockedMarker[1]) {
186
+ return { batchId: null, blocked: blockedReasonMessage(blockedMarker[1]) };
187
+ }
153
188
  const existing = latestBatchId();
154
189
  if (existing)
155
190
  return { batchId: existing };