@sprid/scout 0.1.0 → 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 - 2026-10-23
4
+
5
+ - `job_results` refuses a run id that is not one scout issued. A crafted id could read any JSON file on the machine.
6
+
7
+ ## 0.1.1 - 2026-10-19
8
+
9
+ - Remove automatic likes from TikTok discovery. Research no longer clicks like controls while collecting accounts.
10
+ - Clarify local storage, requests to research platforms and optional uploads to Sprid.
11
+ - Report the installed package version in the MCP server handshake.
package/README.md CHANGED
@@ -5,9 +5,10 @@ driving your own Chrome. It finds accounts posting photo slideshows about your
5
5
  keywords, measures each one, and files what it learns into a library on your
6
6
  machine.
7
7
 
8
- Nothing leaves this computer unless you ask for it. There is no account and no
9
- server behind it; the Sprid plugin's `scout` skill teaches your agent how to use
10
- it well.
8
+ Research results are saved locally. Searches and page visits go to the platforms
9
+ you research through Chrome. Sending findings to Sprid is optional and requires
10
+ your instruction. Discovery does not like posts, follow accounts or send messages.
11
+ The Sprid plugin's `scout` skill guides the research.
11
12
 
12
13
  ## What it does
13
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprid/scout",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local MCP server that researches a niche over your own Chrome: the accounts winning it, and the ads running in it.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://sprid.studio/agents",
@@ -16,6 +16,7 @@
16
16
  "files": [
17
17
  "src",
18
18
  "README.md",
19
+ "CHANGELOG.md",
19
20
  "LICENSE"
20
21
  ],
21
22
  "engines": {
package/src/discover.js CHANGED
@@ -17,7 +17,6 @@ const FIRST_ROUND_CANDIDATES_PER_ACCOUNT = 4;
17
17
  const MIN_CANDIDATE_POOL = 10;
18
18
  const MAX_CANDIDATE_POOL = 500;
19
19
  const BOT_THRESHOLD = 3;
20
- const LIKE_EVERY = 6;
21
20
 
22
21
  function clamp(value, lo, hi) {
23
22
  return Math.max(lo, Math.min(hi, value));
@@ -33,47 +32,16 @@ function nextPool(missing, passRate) {
33
32
  return clamp(needed, MIN_CANDIDATE_POOL, MAX_CANDIDATE_POOL);
34
33
  }
35
34
 
36
- async function likePost(context, username, postId, log) {
37
- log(" Liking a post to train the algorithm...");
38
- let page = null;
39
- try {
40
- page = await context.newPage();
41
- await page.goto(`https://www.tiktok.com/@${username}/photo/${postId}`, {
42
- waitUntil: "domcontentloaded",
43
- timeout: 15_000,
44
- });
45
- await page.waitForTimeout(2000);
46
- for (const selector of [
47
- '[data-e2e="like-icon"]',
48
- '[data-e2e="browse-like-icon"]',
49
- 'button[aria-label*="ike"]',
50
- ]) {
51
- try {
52
- const button = page.locator(selector).first();
53
- if (await button.isVisible({ timeout: 2000 })) {
54
- await button.click();
55
- await page.waitForTimeout(1000);
56
- break;
57
- }
58
- } catch {}
59
- }
60
- } catch {
61
- } finally {
62
- await page?.close().catch(() => {});
63
- }
64
- }
65
-
66
35
  /**
67
36
  * Phase 1. Types each keyword into TikTok's own search and reads the results
68
37
  * the page fetches for itself, Top tab first and Photo behind it when Top runs
69
38
  * thin. Keywords are walked in order and spent only as far as needed.
70
39
  */
71
40
  async function searchViaFeed({ keywords, cursor, maxPool, alreadyProcessed, session, log, gate, challenge }) {
72
- const { context, newPage } = session;
41
+ const { newPage } = session;
73
42
  const searchPage = await newPage();
74
43
  const candidates = {};
75
44
  const hashtagFreq = {};
76
- const liked = new Set();
77
45
  let consumed = 0;
78
46
 
79
47
  async function sweep(keyword, tab) {
@@ -157,14 +125,6 @@ async function searchViaFeed({ keywords, cursor, maxPool, alreadyProcessed, sess
157
125
  }
158
126
  }
159
127
 
160
- // Tell the algorithm what this session is into, occasionally.
161
- const fresh = slideshowPosts.filter((p) => !liked.has(p.post_id));
162
- if (fresh.length && liked.size < Math.ceil(scroll / LIKE_EVERY) + 1) {
163
- const pick = fresh[0];
164
- liked.add(pick.post_id);
165
- await likePost(context, pick.username, pick.post_id, log);
166
- }
167
-
168
128
  if (Object.keys(candidates).length >= maxPool) break;
169
129
  silentScrolls = Object.keys(candidates).length === prev ? silentScrolls + 1 : 0;
170
130
  if (silentScrolls >= 3) break;
package/src/server.js CHANGED
@@ -156,7 +156,7 @@ function persistDiscovery(job) {
156
156
 
157
157
  // ---------------------------------------------------------------------------
158
158
 
159
- const mcp = new McpServer({ name: NAME, version: "0.1.0" }, { instructions: INSTRUCTIONS });
159
+ const mcp = new McpServer({ name: NAME, version: require("../package.json").version }, { instructions: INSTRUCTIONS });
160
160
 
161
161
  mcp.registerTool(
162
162
  "status",
package/src/store.js CHANGED
@@ -23,16 +23,25 @@ function readJson(file, fallback) {
23
23
  }
24
24
  }
25
25
 
26
+ // A run id is `${tool}-${8 hex}` (jobs.js). It arrives from a tool argument,
27
+ // so anything else is refused before it reaches a path: `../` in it read any
28
+ // JSON file on the machine.
29
+ const RUN_ID = /^[a-z]+-[0-9a-f]{8}$/;
30
+
26
31
  function runFile(runId) {
32
+ if (!RUN_ID.test(String(runId))) return null;
27
33
  return path.join(RUNS_DIR, `${runId}.json`);
28
34
  }
29
35
 
30
36
  function saveRun(run) {
31
- writeAtomic(runFile(run.id), run);
37
+ const file = runFile(run.id);
38
+ if (!file) throw new Error(`Invalid run id: ${run.id}`);
39
+ writeAtomic(file, run);
32
40
  }
33
41
 
34
42
  function loadRun(runId) {
35
- return readJson(runFile(runId), null);
43
+ const file = runFile(runId);
44
+ return file ? readJson(file, null) : null;
36
45
  }
37
46
 
38
47
  function listRuns() {