social-autoposter 1.6.74 → 1.6.75

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/mcp/dist/index.js CHANGED
@@ -637,8 +637,10 @@ const WEBSITE_RESEARCH_INSTRUCTIONS = "PRODUCT RESEARCH (do this before saving t
637
637
  "register, vibe) while keeping every product CLAIM factual to the site. Don't invent " +
638
638
  "features, metrics, or guarantees the site doesn't state.\n" +
639
639
  "5. Save the best conservative factual draft without adding a confirmation round-trip. Call " +
640
- "project_config with name + the product fields (plus voice/search_topics from the profile scan). If the " +
641
- "site is thin or unreachable, use only supported facts and leave optional detail conservative; " +
640
+ "project_config with name + the product fields (plus voice/search_topics from the profile scan), AND " +
641
+ "expand those topics into a `search_queries` array of ~30 concrete X advanced-search strings in the " +
642
+ "SAME call — YOU are the model, so do the expansion in-session; it seeds directly with no `claude -p`. " +
643
+ "If the site is thin or unreachable, use only supported facts and leave optional detail conservative; " +
642
644
  "ask the user only if a required field is genuinely unknowable.";
643
645
  // ---- project_config: per-project config (the "brain": project, website, voice) -----
644
646
  // Run this FIRST. The action tools refuse until at least one project is ready.
@@ -730,6 +732,14 @@ tool("project_config", {
730
732
  .union([z.array(z.string()), z.string()])
731
733
  .optional()
732
734
  .describe("Topics/keywords to monitor on X (comma-separated or array)"),
735
+ search_queries: z
736
+ .array(z.string())
737
+ .optional()
738
+ .describe("Cold-start X search-query bank YOU expand from search_topics, in this same call. " +
739
+ "Fan each topic into a few concrete X advanced-search strings (aim ~30 total, e.g. " +
740
+ "'mac menu bar app -filter:replies', 'screen recording privacy lang:en') so the cycle " +
741
+ "fans out instead of running one crude topic-as-query. Seeded directly with NO `claude " +
742
+ "-p` — you are the model doing the expansion, so setup never needs the claude CLI."),
733
743
  get_started_link: z
734
744
  .string()
735
745
  .optional()
@@ -1001,13 +1011,28 @@ tool("project_config", {
1001
1011
  // search queries (project_search_queries) so the deterministic Phase 1
1002
1012
  // bank (qualified_query_bank.py) has something to run on day one.
1003
1013
  // Without this, a freshly-configured project's bank is empty and the
1004
- // cycle falls back to ONE crude topic-as-query. Best-effort: a failure
1005
- // here never fails setup; the topic-as-query fallback still works, just
1006
- // narrower. Supply-test is auto (only if the harness is up), so this
1007
- // stays fast when X isn't connected yet. (2026-06-04)
1008
- if (seed.code === 0) {
1014
+ // cycle falls back to ONE crude topic-as-query.
1015
+ //
1016
+ // CLAUDE-FREE: the in-session agent (you) expands topics -> queries and
1017
+ // passes them as `search_queries` in THIS call. We seed them directly via
1018
+ // --queries-json, so setup never shells out to `claude -p` (which isn't
1019
+ // installed in the Desktop / .mcpb lane and was the FileNotFoundError
1020
+ // users hit). If the agent didn't supply queries, we skip expansion
1021
+ // entirely — the topic-as-query fallback still runs, just narrower — and
1022
+ // nudge the agent to re-run with search_queries. (2026-06-19)
1023
+ const agentQueries = Array.isArray(args.search_queries)
1024
+ ? args.search_queries.map((q) => String(q).trim()).filter(Boolean)
1025
+ : [];
1026
+ if (seed.code === 0 && agentQueries.length) {
1009
1027
  try {
1010
- const qseed = await runPython("scripts/seed_search_queries.py", ["--project", result.project, "--supply-test", "auto", "--emit-json"], { timeoutMs: 600_000 });
1028
+ const qfile = path.join(os.tmpdir(), `saps-queries-${result.project}-${Date.now()}.json`);
1029
+ fs.writeFileSync(qfile, JSON.stringify({ queries: agentQueries.map((q) => ({ query: q, topic: "" })) }));
1030
+ const qseed = await runPython("scripts/seed_search_queries.py", ["--project", result.project, "--queries-json", qfile,
1031
+ "--supply-test", "auto", "--emit-json"], { timeoutMs: 600_000 });
1032
+ try {
1033
+ fs.unlinkSync(qfile);
1034
+ }
1035
+ catch { /* best-effort cleanup */ }
1011
1036
  const qm = /seeded=(\d+)\s+inserted=(\d+)\s+updated=(\d+)/.exec(qseed.stdout);
1012
1037
  const qjson = qseed.stdout.split("===QUERIES_JSON===")[1];
1013
1038
  if (qjson) {
@@ -1020,17 +1045,20 @@ tool("project_config", {
1020
1045
  }
1021
1046
  if (qseed.code === 0 && qm) {
1022
1047
  const n = searchQueries.length || Number(qm[1]);
1023
- seedNote += ` Expanded them into ${n} search quer${n === 1 ? "y" : "ies"} so the cycle can fan out instead of running a single query.`;
1048
+ seedNote += ` Seeded ${n} search quer${n === 1 ? "y" : "ies"} so the cycle can fan out instead of running a single query.`;
1024
1049
  }
1025
1050
  else if (qseed.code !== 0) {
1026
1051
  const qtail = (qseed.stderr || qseed.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
1027
- seedNote += ` (Search queries not expanded yet — ${qtail}. The cycle still runs off the seeded topics.)`;
1052
+ seedNote += ` (Search queries not seeded yet — ${qtail}. The cycle still runs off the seeded topics.)`;
1028
1053
  }
1029
1054
  }
1030
1055
  catch (e) {
1031
- seedNote += ` (Search-query expansion skipped — ${e.message}.)`;
1056
+ seedNote += ` (Search-query seeding skipped — ${e.message}.)`;
1032
1057
  }
1033
1058
  }
1059
+ else if (seed.code === 0) {
1060
+ seedNote += ` (No search_queries supplied, so the cycle will run off the seeded topics one at a time. To fan out, re-call project_config with a search_queries array of ~30 X search strings you expand from these topics — it seeds them directly, no claude CLI.)`;
1061
+ }
1034
1062
  }
1035
1063
  // Surface any advanced (escape-hatch) field edits in the note so the
1036
1064
  // agent can confirm exactly what changed to the user.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.73",
3
- "installedAt": "2026-06-20T18:16:22.534Z"
2
+ "version": "1.6.75",
3
+ "installedAt": "2026-06-20T23:27:03.057Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "Social Autoposter",
5
- "version": "1.6.73",
5
+ "version": "1.6.75",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the social-autoposter pipeline.",
7
7
  "long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Fully quit and restart Claude (quit and reopen, not just close the window).\n2. Choose **Set up social-autoposter** from the prompt menu or type **Set me up on social-autoposter end to end**. The agent installs the owned runtime, connects X, discovers and configures your product, and verifies with a draft-only cycle.",
8
8
  "author": {
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.73",
3
+ "version": "1.6.75",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.74",
3
+ "version": "1.6.75",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -132,6 +132,35 @@ def _fetch_active_queries(project: str) -> list[dict]:
132
132
  return out
133
133
 
134
134
 
135
+ def _load_provided_queries(path: str) -> list[tuple[str, str]]:
136
+ """Read AGENT-SUPPLIED queries from a JSON file: returns (query, topic) pairs.
137
+
138
+ Accepts either {"queries": [...]} or a bare top-level list. Each item may be
139
+ a string (topic left blank) or an object {"query": ..., "topic": ...}.
140
+ This is the claude-free seed path: the in-session agent already expanded the
141
+ topics into queries, so there is nothing to draft."""
142
+ with open(path, "r", encoding="utf-8") as fh:
143
+ raw = json.load(fh)
144
+ items = raw.get("queries") if isinstance(raw, dict) else raw
145
+ out: list[tuple[str, str]] = []
146
+ seen: set[str] = set()
147
+ for it in (items or []):
148
+ if isinstance(it, str):
149
+ q, t = it.strip(), ""
150
+ elif isinstance(it, dict):
151
+ q, t = (it.get("query") or "").strip(), (it.get("topic") or "").strip()
152
+ else:
153
+ continue
154
+ if not q:
155
+ continue
156
+ core = normalize_query(q)
157
+ if core in seen:
158
+ continue
159
+ seen.add(core)
160
+ out.append((q, t))
161
+ return out
162
+
163
+
135
164
  def _find_project(cfg: dict, name: str) -> dict | None:
136
165
  for p in cfg.get("projects", []):
137
166
  if (p.get("name") or "").strip().lower() == name.strip().lower():
@@ -181,6 +210,12 @@ def main() -> int:
181
210
  "bank as JSON on a sentinel line (===QUERIES_JSON===) so a "
182
211
  "caller (e.g. the MCP setup tool) can hand the queries back "
183
212
  "to the user.")
213
+ ap.add_argument("--queries-json",
214
+ help="Path to a JSON file of AGENT-SUPPLIED queries to seed "
215
+ "directly, bypassing the `claude -p` drafting loop entirely. "
216
+ "Shape: {\"queries\":[{\"query\":\"...\",\"topic\":\"...\"}]} "
217
+ "or a bare list of strings. Use this from the in-session MCP "
218
+ "setup path so the seed step never depends on the claude CLI.")
184
219
  args = ap.parse_args()
185
220
 
186
221
  cfg = load_config()
@@ -192,53 +227,84 @@ def main() -> int:
192
227
  # canonical name as stored
193
228
  project = (project_entry.get("name") or args.project).strip()
194
229
 
195
- topics = _load_active_topics(project)
196
- if not topics:
197
- print(f"seed_search_queries: no active topics for {project!r} — seed "
198
- f"topics first (scripts/seed_search_topics.py).", file=sys.stderr)
199
- return 3
200
-
201
230
  target = max(1, args.target)
202
231
 
203
- # Idempotency: aim for `target` TOTAL active queries, not `target` NEW per
204
- # run. A project that already has >= target active seed queries needs no
205
- # drafting re-running reseed just re-emits the existing bank instead of
206
- # ballooning it. Otherwise we only draft the shortfall. (2026-06-04)
207
- active_now = len(_fetch_active_queries(project))
208
- need = max(0, target - active_now)
209
- per_topic = math.ceil(need / len(topics)) if need else 0
210
- per_topic = max(MIN_PER_TOPIC, min(MAX_PER_TOPIC, per_topic)) if need else 0
211
-
212
- # Dedup against (a) every query EVER attempted for this project and (b) seed
213
- # queries already persisted. Accumulates as we go so topics don't overlap.
214
- avoid_cores = load_existing_query_cores(project) | _load_existing_seed_cores(project)
215
-
216
- print(f"seed_search_queries: project={project!r} topics={len(topics)} "
217
- f"target={target} active_now={active_now} need={need} "
218
- f"per_topic={per_topic} existing_cores={len(avoid_cores)}",
219
- file=sys.stderr)
220
-
221
- # --- Draft queries topic by topic ------------------------------------
222
- drafted: list[tuple[str, str]] = [] # (query, topic)
223
- for topic in topics:
224
- if need <= 0 or len({normalize_query(q) for q, _ in drafted}) >= need:
225
- break
226
- prompt = build_query_prompt(
227
- project_entry, topic, per_topic, avoid_queries=avoid_cores
228
- )
229
- try:
230
- out = call_claude(prompt, timeout_sec=DRAFT_TIMEOUT_SEC)
231
- except SystemExit as e:
232
- print(f"[seed_search_queries] draft failed for topic {topic!r}: {e}",
233
- file=sys.stderr)
234
- continue
235
- qs = extract_queries(out, per_topic)
236
- new_qs, dupes = dedup_queries(qs, avoid_cores)
237
- for q in new_qs:
238
- avoid_cores.add(normalize_query(q))
239
- drafted.append((q, topic))
240
- print(f" topic={topic!r}: drafted={len(qs)} new={len(new_qs)} "
241
- f"dupes={len(dupes)}", file=sys.stderr)
232
+ # ---- Agent-supplied path (claude-free) ------------------------------
233
+ # When the caller (the in-session MCP setup tool) hands us queries the agent
234
+ # already expanded from the topics, we skip the `claude -p` drafting loop
235
+ # entirely and just dedup + (maybe) supply-test + persist them. This is the
236
+ # single setup path on machines without the claude CLI. (2026-06-19)
237
+ if args.queries_json:
238
+ topics = _load_active_topics(project) # best-effort, summary only
239
+ provided = _load_provided_queries(args.queries_json)
240
+ existing = (load_existing_query_cores(project)
241
+ | _load_existing_seed_cores(project))
242
+ drafted = [(q, t) for q, t in provided
243
+ if normalize_query(q) not in existing]
244
+ active_now = len(_fetch_active_queries(project))
245
+ print(f"seed_search_queries: project={project!r} mode=agent-supplied "
246
+ f"provided={len(provided)} new={len(drafted)} "
247
+ f"active_now={active_now} (no claude)", file=sys.stderr)
248
+ if not drafted:
249
+ # Everything provided was already seeded (or nothing usable) —
250
+ # healthy no-op: re-emit the live bank and succeed.
251
+ print(
252
+ f"seed_search_queries: project={project} topics={len(topics)} "
253
+ f"drafted=0 supply_ran=0 dropped_zero=0 seeded=0 inserted=0 "
254
+ f"updated=0 failed=0 (agent_supplied_nothing_new)"
255
+ )
256
+ if args.emit_json:
257
+ queries = _fetch_active_queries(project)
258
+ print("===QUERIES_JSON===")
259
+ print(json.dumps({"project": project, "count": len(queries),
260
+ "queries": queries}))
261
+ return 0
262
+ else:
263
+ topics = _load_active_topics(project)
264
+ if not topics:
265
+ print(f"seed_search_queries: no active topics for {project!r} — seed "
266
+ f"topics first (scripts/seed_search_topics.py).", file=sys.stderr)
267
+ return 3
268
+
269
+ # Idempotency: aim for `target` TOTAL active queries, not `target` NEW per
270
+ # run. A project that already has >= target active seed queries needs no
271
+ # drafting — re-running reseed just re-emits the existing bank instead of
272
+ # ballooning it. Otherwise we only draft the shortfall. (2026-06-04)
273
+ active_now = len(_fetch_active_queries(project))
274
+ need = max(0, target - active_now)
275
+ per_topic = math.ceil(need / len(topics)) if need else 0
276
+ per_topic = max(MIN_PER_TOPIC, min(MAX_PER_TOPIC, per_topic)) if need else 0
277
+
278
+ # Dedup against (a) every query EVER attempted for this project and (b) seed
279
+ # queries already persisted. Accumulates as we go so topics don't overlap.
280
+ avoid_cores = load_existing_query_cores(project) | _load_existing_seed_cores(project)
281
+
282
+ print(f"seed_search_queries: project={project!r} topics={len(topics)} "
283
+ f"target={target} active_now={active_now} need={need} "
284
+ f"per_topic={per_topic} existing_cores={len(avoid_cores)}",
285
+ file=sys.stderr)
286
+
287
+ # --- Draft queries topic by topic ------------------------------------
288
+ drafted = [] # (query, topic)
289
+ for topic in topics:
290
+ if need <= 0 or len({normalize_query(q) for q, _ in drafted}) >= need:
291
+ break
292
+ prompt = build_query_prompt(
293
+ project_entry, topic, per_topic, avoid_queries=avoid_cores
294
+ )
295
+ try:
296
+ out = call_claude(prompt, timeout_sec=DRAFT_TIMEOUT_SEC)
297
+ except SystemExit as e:
298
+ print(f"[seed_search_queries] draft failed for topic {topic!r}: {e}",
299
+ file=sys.stderr)
300
+ continue
301
+ qs = extract_queries(out, per_topic)
302
+ new_qs, dupes = dedup_queries(qs, avoid_cores)
303
+ for q in new_qs:
304
+ avoid_cores.add(normalize_query(q))
305
+ drafted.append((q, topic))
306
+ print(f" topic={topic!r}: drafted={len(qs)} new={len(new_qs)} "
307
+ f"dupes={len(dupes)}", file=sys.stderr)
242
308
 
243
309
  if not drafted:
244
310
  # Two cases: (a) the bank already meets target (need==0) — that's a