social-autoposter 1.6.73 → 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 +59 -17
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +2 -9
- package/mcp/menubar/s4l_state.py +8 -30
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/seed_search_queries.py +111 -45
package/mcp/dist/index.js
CHANGED
|
@@ -35,6 +35,10 @@ const PANEL_URI = "ui://social-autoposter/panel.html";
|
|
|
35
35
|
// continuous autopilot instead of each run overwriting the last; post_drafts posts
|
|
36
36
|
// the approved subset and marks them posted (filtered out of the cards thereafter).
|
|
37
37
|
const REVIEW_QUEUE_ID = "review-queue";
|
|
38
|
+
// The Desktop scheduled task onboarding creates for the autopilot. Its presence on
|
|
39
|
+
// disk is the single "autopilot is set up" signal the dashboard + menu bar share
|
|
40
|
+
// (the legacy launchd autopilot is retired).
|
|
41
|
+
const AUTOPILOT_TASK_ID = "social-autoposter-autopilot";
|
|
38
42
|
const TWITTER_AUTOPILOT_LABEL = "com.m13v.social-twitter-cycle";
|
|
39
43
|
const TWITTER_AUTOPILOT_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${TWITTER_AUTOPILOT_LABEL}.plist`);
|
|
40
44
|
// Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
|
|
@@ -633,8 +637,10 @@ const WEBSITE_RESEARCH_INSTRUCTIONS = "PRODUCT RESEARCH (do this before saving t
|
|
|
633
637
|
"register, vibe) while keeping every product CLAIM factual to the site. Don't invent " +
|
|
634
638
|
"features, metrics, or guarantees the site doesn't state.\n" +
|
|
635
639
|
"5. Save the best conservative factual draft without adding a confirmation round-trip. Call " +
|
|
636
|
-
"project_config with name + the product fields (plus voice/search_topics from the profile scan)
|
|
637
|
-
"
|
|
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; " +
|
|
638
644
|
"ask the user only if a required field is genuinely unknowable.";
|
|
639
645
|
// ---- project_config: per-project config (the "brain": project, website, voice) -----
|
|
640
646
|
// Run this FIRST. The action tools refuse until at least one project is ready.
|
|
@@ -726,6 +732,14 @@ tool("project_config", {
|
|
|
726
732
|
.union([z.array(z.string()), z.string()])
|
|
727
733
|
.optional()
|
|
728
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."),
|
|
729
743
|
get_started_link: z
|
|
730
744
|
.string()
|
|
731
745
|
.optional()
|
|
@@ -997,13 +1011,28 @@ tool("project_config", {
|
|
|
997
1011
|
// search queries (project_search_queries) so the deterministic Phase 1
|
|
998
1012
|
// bank (qualified_query_bank.py) has something to run on day one.
|
|
999
1013
|
// Without this, a freshly-configured project's bank is empty and the
|
|
1000
|
-
// cycle falls back to ONE crude topic-as-query.
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
|
|
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) {
|
|
1005
1027
|
try {
|
|
1006
|
-
const
|
|
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 */ }
|
|
1007
1036
|
const qm = /seeded=(\d+)\s+inserted=(\d+)\s+updated=(\d+)/.exec(qseed.stdout);
|
|
1008
1037
|
const qjson = qseed.stdout.split("===QUERIES_JSON===")[1];
|
|
1009
1038
|
if (qjson) {
|
|
@@ -1016,17 +1045,20 @@ tool("project_config", {
|
|
|
1016
1045
|
}
|
|
1017
1046
|
if (qseed.code === 0 && qm) {
|
|
1018
1047
|
const n = searchQueries.length || Number(qm[1]);
|
|
1019
|
-
seedNote += `
|
|
1048
|
+
seedNote += ` Seeded ${n} search quer${n === 1 ? "y" : "ies"} so the cycle can fan out instead of running a single query.`;
|
|
1020
1049
|
}
|
|
1021
1050
|
else if (qseed.code !== 0) {
|
|
1022
1051
|
const qtail = (qseed.stderr || qseed.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
1023
|
-
seedNote += ` (Search queries not
|
|
1052
|
+
seedNote += ` (Search queries not seeded yet — ${qtail}. The cycle still runs off the seeded topics.)`;
|
|
1024
1053
|
}
|
|
1025
1054
|
}
|
|
1026
1055
|
catch (e) {
|
|
1027
|
-
seedNote += ` (Search-query
|
|
1056
|
+
seedNote += ` (Search-query seeding skipped — ${e.message}.)`;
|
|
1028
1057
|
}
|
|
1029
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
|
+
}
|
|
1030
1062
|
}
|
|
1031
1063
|
// Surface any advanced (escape-hatch) field edits in the note so the
|
|
1032
1064
|
// agent can confirm exactly what changed to the user.
|
|
@@ -1354,18 +1386,28 @@ function runtimeSnapshot() {
|
|
|
1354
1386
|
// and re-reads status. The tool itself returns the first-paint snapshot so the
|
|
1355
1387
|
// view has data the instant it loads.
|
|
1356
1388
|
// Is either launchd job (cycle / daily updater) currently loaded?
|
|
1389
|
+
// "Autopilot" is now the Claude Desktop scheduled task `social-autoposter-autopilot`
|
|
1390
|
+
// (created during onboarding via create_scheduled_task), NOT the legacy launchd job.
|
|
1391
|
+
// We can't read the host's enabled/paused flag, but the task's presence on disk is the
|
|
1392
|
+
// single signal the dashboard AND the menu bar key off of, so they stay aligned.
|
|
1357
1393
|
async function autopilotLoaded() {
|
|
1394
|
+
let autopilot_on = false;
|
|
1395
|
+
try {
|
|
1396
|
+
const cfg = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
1397
|
+
autopilot_on = fs.existsSync(path.join(cfg, "scheduled-tasks", AUTOPILOT_TASK_ID, "SKILL.md"));
|
|
1398
|
+
}
|
|
1399
|
+
catch {
|
|
1400
|
+
/* leave false */
|
|
1401
|
+
}
|
|
1402
|
+
let auto_update_on = false;
|
|
1358
1403
|
try {
|
|
1359
1404
|
const res = await run("launchctl", ["list"], { timeoutMs: 10_000 });
|
|
1360
|
-
|
|
1361
|
-
return {
|
|
1362
|
-
autopilot_on: lines.some((l) => l.includes(TWITTER_AUTOPILOT_LABEL)),
|
|
1363
|
-
auto_update_on: lines.some((l) => l.includes(UPDATER_LABEL)),
|
|
1364
|
-
};
|
|
1405
|
+
auto_update_on = res.stdout.split("\n").some((l) => l.includes(UPDATER_LABEL));
|
|
1365
1406
|
}
|
|
1366
1407
|
catch {
|
|
1367
|
-
|
|
1408
|
+
/* leave false */
|
|
1368
1409
|
}
|
|
1410
|
+
return { autopilot_on, auto_update_on };
|
|
1369
1411
|
}
|
|
1370
1412
|
// Assemble everything the panel needs in one shot (projects + X + autopilot +
|
|
1371
1413
|
// version). Resilient: any probe that throws degrades to a safe default rather
|
package/mcp/dist/version.json
CHANGED
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.
|
|
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": {
|
|
@@ -191,12 +191,8 @@ class S4LMenuBar(rumps.App):
|
|
|
191
191
|
except Exception:
|
|
192
192
|
pass
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if st.set_autopilot(desired):
|
|
197
|
-
sender.state = 1 if desired else 0
|
|
198
|
-
else:
|
|
199
|
-
self._notify("S4L", "Open Claude Desktop to change autopilot.")
|
|
194
|
+
# _toggle_ap removed: autopilot is the Claude Desktop scheduled task now, managed
|
|
195
|
+
# in the Scheduled tab. The menu bar mirrors the dashboard (no launchd toggle).
|
|
200
196
|
|
|
201
197
|
# ---- activity spinner -------------------------------------------------
|
|
202
198
|
# The server writes activity.json while a tool runs (scanning/drafting/
|
|
@@ -469,9 +465,6 @@ class S4LMenuBar(rumps.App):
|
|
|
469
465
|
out.append(self._label("7d stats — open dashboard"))
|
|
470
466
|
|
|
471
467
|
out.append(rumps.separator)
|
|
472
|
-
ap = rumps.MenuItem("Autopilot", callback=self._toggle_ap)
|
|
473
|
-
ap.state = 1 if snap.get("autopilot_on") else 0
|
|
474
|
-
out.append(ap)
|
|
475
468
|
out.append(
|
|
476
469
|
rumps.MenuItem("Run draft cycle in Claude", callback=self._draft)
|
|
477
470
|
)
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -103,7 +103,11 @@ def _launchctl_list() -> str:
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def autopilot_loaded() -> bool:
|
|
106
|
-
|
|
106
|
+
# Autopilot is now the Claude Desktop scheduled task, not the legacy launchd job.
|
|
107
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.join(str(Path.home()), ".claude")
|
|
108
|
+
return os.path.exists(
|
|
109
|
+
os.path.join(cfg, "scheduled-tasks", "social-autoposter-autopilot", "SKILL.md")
|
|
110
|
+
)
|
|
107
111
|
|
|
108
112
|
|
|
109
113
|
# ---- loopback panel server (live, when Claude Desktop is running) ----------
|
|
@@ -207,35 +211,9 @@ def stats_7d():
|
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
plist when Claude Desktop is closed. Returns False if it couldn't act."""
|
|
214
|
-
res = loopback_tool("autopilot", {"action": "enable" if enable else "disable"})
|
|
215
|
-
if res is not None:
|
|
216
|
-
return True
|
|
217
|
-
plist = str(
|
|
218
|
-
Path.home() / "Library" / "LaunchAgents" / (AUTOPILOT_LABEL + ".plist")
|
|
219
|
-
)
|
|
220
|
-
uid = os.getuid()
|
|
221
|
-
try:
|
|
222
|
-
if enable:
|
|
223
|
-
if not os.path.exists(plist):
|
|
224
|
-
return False # first enable needs the server to write the plist
|
|
225
|
-
subprocess.run(
|
|
226
|
-
["launchctl", "bootstrap", f"gui/{uid}", plist],
|
|
227
|
-
capture_output=True,
|
|
228
|
-
timeout=15,
|
|
229
|
-
)
|
|
230
|
-
else:
|
|
231
|
-
subprocess.run(
|
|
232
|
-
["launchctl", "bootout", f"gui/{uid}/{AUTOPILOT_LABEL}"],
|
|
233
|
-
capture_output=True,
|
|
234
|
-
timeout=15,
|
|
235
|
-
)
|
|
236
|
-
return True
|
|
237
|
-
except Exception:
|
|
238
|
-
return False
|
|
214
|
+
# set_autopilot() (the launchd toggle) was removed: the autopilot is now the Claude
|
|
215
|
+
# Desktop scheduled task `social-autoposter-autopilot`, managed in the Scheduled tab,
|
|
216
|
+
# so the menu bar no longer toggles a launchd job (it mirrors the dashboard instead).
|
|
239
217
|
|
|
240
218
|
|
|
241
219
|
def panel_url():
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/social-autoposter-mcp",
|
|
3
|
-
"version": "1.6.
|
|
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
|
@@ -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
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|