@mkterswingman/5mghost-twinkler 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -4,13 +4,15 @@ Lightweight AI runtime for the hosted Twinkler API.
4
4
 
5
5
  ```bash
6
6
  npm install -g @mkterswingman/5mghost-twinkler
7
+ twinkler ensure --auto-update --json
7
8
  twinkler call GET /api/v1/channel/ibai/summary --query days=30
8
9
  ```
9
10
 
10
11
  `npm install -g` installs bundled AI skills automatically for detected AI
11
- clients. `twinkler setup` remains an idempotent repair command when a skill
12
- target was unavailable during install.
12
+ clients. `twinkler setup` remains an advanced repair command when a skill target
13
+ was unavailable during install; normal AI workflows should start with
14
+ `twinkler ensure --auto-update --json`.
13
15
 
14
16
  Auth uses the shared mkterswingman PAT. Do not commit `.env`, `auth.json`,
15
17
  PATs, bearer tokens, or any generated secret file to a remote repository. Enter
16
- PATs through `twinkler auth login --pat-stdin`; do not paste PATs into AI chat.
18
+ PATs through `twinkler login`; do not paste PATs into AI chat.
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  export interface CliContext {
3
4
  argv?: string[];
4
5
  env?: NodeJS.ProcessEnv;
@@ -7,5 +8,6 @@ export interface CliContext {
7
8
  stderr?: (message: string) => void;
8
9
  stdin?: AsyncIterable<Buffer | string>;
9
10
  fetchImpl?: typeof fetch;
11
+ spawnSyncImpl?: typeof spawnSync;
10
12
  }
11
13
  export declare function runCli(context?: CliContext): Promise<number>;
package/dist/cli.js CHANGED
@@ -11,19 +11,25 @@ const HELP = [
11
11
  "twinkler",
12
12
  "",
13
13
  "Usage:",
14
- " twinkler setup",
15
- " twinkler install-skills",
16
14
  " twinkler doctor",
15
+ " twinkler login",
16
+ " twinkler ensure [--auto-update] [--json]",
17
17
  " twinkler auth status",
18
- " twinkler auth login --pat <TOKEN>",
19
18
  " twinkler auth login --pat-stdin",
20
19
  " twinkler call <GET|POST|DELETE> /api/v1/... [--query k=v] [--json '{...}']",
20
+ " twinkler jobs active|recent|show <job_id>",
21
21
  " twinkler update",
22
22
  " twinkler version",
23
23
  "",
24
+ "Repair / advanced:",
25
+ " twinkler setup",
26
+ " twinkler install-skills",
27
+ " twinkler auth login --pat <TOKEN>",
28
+ "",
24
29
  "mkterswingman PAT:",
25
30
  ` ${PAT_LOGIN_URL}`
26
31
  ].join("\n");
32
+ const PACKAGE_NAME = "@mkterswingman/5mghost-twinkler";
27
33
  export async function runCli(context = {}) {
28
34
  const argv = context.argv ?? process.argv.slice(2);
29
35
  const env = context.env ?? process.env;
@@ -46,6 +52,21 @@ export async function runCli(context = {}) {
46
52
  return 0;
47
53
  case "auth":
48
54
  return await runAuthCommand(args, { paths, env, out, stdin: context.stdin ?? process.stdin });
55
+ case "login":
56
+ return await runAuthCommand(["login", "--pat-stdin", ...args.filter((arg) => arg !== "--pat-stdin")], {
57
+ paths,
58
+ env,
59
+ out,
60
+ stdin: context.stdin ?? process.stdin
61
+ });
62
+ case "ensure":
63
+ return runEnsureCommand(args, {
64
+ paths,
65
+ env,
66
+ out,
67
+ err,
68
+ spawnSyncImpl: context.spawnSyncImpl ?? spawnSync
69
+ });
49
70
  case "call": {
50
71
  const parsed = parseCallArgs(args);
51
72
  const result = await callTwinkler({
@@ -57,6 +78,13 @@ export async function runCli(context = {}) {
57
78
  out(JSON.stringify(result, null, 2));
58
79
  return 0;
59
80
  }
81
+ case "jobs":
82
+ return await runJobsCommand(args, {
83
+ paths,
84
+ env,
85
+ out,
86
+ fetchImpl: context.fetchImpl
87
+ });
60
88
  case "install-skills": {
61
89
  const results = installBundledSkills({ homeDir: paths.homeDir });
62
90
  out(renderSkillInstallResults(results));
@@ -76,7 +104,7 @@ export async function runCli(context = {}) {
76
104
  }
77
105
  case "update":
78
106
  case "upgrade":
79
- return runUpdateCommand(args, { env, out, err });
107
+ return runUpdateCommand(args, { env, out, err, spawnSyncImpl: context.spawnSyncImpl ?? spawnSync });
80
108
  case "doctor": {
81
109
  const auth = getAuthStatus({ authJsonPath: paths.authJsonPath, env });
82
110
  out([
@@ -106,10 +134,12 @@ async function runAuthCommand(args, context) {
106
134
  context.out([
107
135
  "Usage:",
108
136
  " twinkler auth status",
109
- " twinkler auth login --pat <TOKEN>",
110
137
  " twinkler auth login --pat-stdin",
111
138
  " twinkler auth logout",
112
139
  "",
140
+ "Advanced compatibility:",
141
+ " twinkler auth login --pat <TOKEN>",
142
+ "",
113
143
  `PAT page: ${PAT_LOGIN_URL}`
114
144
  ].join("\n"));
115
145
  return 0;
@@ -154,6 +184,141 @@ async function runAuthCommand(args, context) {
154
184
  context.out(`Unknown auth subcommand: ${subcommand}`);
155
185
  return 1;
156
186
  }
187
+ function runEnsureCommand(args, context) {
188
+ const json = args.includes("--json");
189
+ const autoUpdate = args.includes("--auto-update");
190
+ const localVersion = readPackageVersion();
191
+ const latest = readNpmLatestVersion(context.spawnSyncImpl, context.env);
192
+ const auth = getAuthStatus({ authJsonPath: context.paths.authJsonPath, env: context.env });
193
+ const updateNeeded = latest.version !== null && compareSemver(latest.version, localVersion) > 0;
194
+ let update = {
195
+ needed: updateNeeded,
196
+ attempted: false,
197
+ status: updateNeeded ? "skipped" : "not_needed"
198
+ };
199
+ let exitCode = 0;
200
+ if (updateNeeded && autoUpdate) {
201
+ const result = runGlobalNpmInstall({ env: context.env, spawnSyncImpl: context.spawnSyncImpl });
202
+ update = {
203
+ needed: true,
204
+ attempted: true,
205
+ status: result.ok ? "updated" : "failed",
206
+ command: result.command,
207
+ exit_code: result.exitCode,
208
+ error: result.error
209
+ };
210
+ exitCode = result.ok ? 0 : 1;
211
+ }
212
+ else if (latest.error) {
213
+ update = {
214
+ needed: false,
215
+ attempted: false,
216
+ status: "unknown",
217
+ error: latest.error
218
+ };
219
+ }
220
+ const result = {
221
+ status: exitCode === 0 ? "ok" : "error",
222
+ package: PACKAGE_NAME,
223
+ local_version: localVersion,
224
+ latest_version: latest.version,
225
+ latest_check: latest.error ? { ok: false, error: latest.error } : { ok: true },
226
+ update,
227
+ auth: {
228
+ authenticated: auth.authenticated,
229
+ type: auth.type,
230
+ source: auth.source,
231
+ auth_file: auth.authJsonPath
232
+ },
233
+ ai_session_restart_required: update.status === "updated"
234
+ };
235
+ if (json) {
236
+ context.out(JSON.stringify(result, null, 2));
237
+ }
238
+ else {
239
+ context.out(renderEnsureResult(result));
240
+ }
241
+ if (update.status === "failed" && update.error)
242
+ context.err(update.error);
243
+ return exitCode;
244
+ }
245
+ function renderEnsureResult(result) {
246
+ const latest = result.latest_version ?? `unknown (${result.latest_check.ok ? "not checked" : result.latest_check.error})`;
247
+ const lines = [
248
+ "Twinkler ensure",
249
+ `Package: ${result.package}`,
250
+ `Local version: ${result.local_version}`,
251
+ `Latest version: ${latest}`,
252
+ `Update: ${result.update.status}`,
253
+ `Auth: ${result.auth.authenticated ? `ready (${result.auth.type} via ${result.auth.source})` : "missing"}`
254
+ ];
255
+ if (result.ai_session_restart_required) {
256
+ lines.push("AI session: restart recommended so updated skill text is loaded.");
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ function readNpmLatestVersion(spawnSyncImpl, env) {
261
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
262
+ const result = spawnSyncImpl(npmCommand, ["view", `${PACKAGE_NAME}@latest`, "version", "--silent"], {
263
+ encoding: "utf8",
264
+ env
265
+ });
266
+ if (result.error)
267
+ return { version: null, error: result.error.message };
268
+ if (result.status !== 0) {
269
+ return { version: null, error: (result.stderr || `npm exited with ${result.status ?? "unknown status"}`).trim() };
270
+ }
271
+ const version = result.stdout.trim();
272
+ return version ? { version } : { version: null, error: "npm returned an empty latest version" };
273
+ }
274
+ function compareSemver(a, b) {
275
+ const left = parseSemver(a);
276
+ const right = parseSemver(b);
277
+ for (let index = 0; index < 3; index += 1) {
278
+ if (left[index] !== right[index])
279
+ return left[index] > right[index] ? 1 : -1;
280
+ }
281
+ return 0;
282
+ }
283
+ function parseSemver(version) {
284
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
285
+ if (!match)
286
+ return [0, 0, 0];
287
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
288
+ }
289
+ async function runJobsCommand(args, context) {
290
+ const [subcommand, value] = args.filter((arg) => arg !== "--json");
291
+ let path;
292
+ if (subcommand === "active") {
293
+ path = "/api/v1/twitch/watch";
294
+ }
295
+ else if (subcommand === "recent") {
296
+ path = "/api/v1/twitch/watch";
297
+ }
298
+ else if (subcommand === "show" && value) {
299
+ path = `/api/v1/twitch/watch/${encodeURIComponent(value)}`;
300
+ }
301
+ else {
302
+ context.out([
303
+ "Usage:",
304
+ " twinkler jobs active",
305
+ " twinkler jobs recent",
306
+ " twinkler jobs show <job_id>"
307
+ ].join("\n"));
308
+ return 1;
309
+ }
310
+ const query = subcommand === "active" ? { status: ["active"] } : subcommand === "recent" ? { status: ["recent"] } : {};
311
+ const result = await callTwinkler({
312
+ method: "GET",
313
+ path,
314
+ query,
315
+ authJsonPath: context.paths.authJsonPath,
316
+ env: context.env,
317
+ fetchImpl: context.fetchImpl
318
+ });
319
+ context.out(JSON.stringify(result, null, 2));
320
+ return 0;
321
+ }
157
322
  function parseCallArgs(args) {
158
323
  const [method, path, ...rest] = args;
159
324
  if (!method || !path) {
@@ -196,26 +361,42 @@ function renderSkillInstallResults(results) {
196
361
  function runUpdateCommand(args, context) {
197
362
  const dryRun = args.includes("--dry-run") || context.env.TWINKLER_UPDATE_DRY_RUN === "1";
198
363
  const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
199
- const installArgs = ["install", "-g", "@mkterswingman/5mghost-twinkler@latest"];
364
+ const installArgs = ["install", "-g", `${PACKAGE_NAME}@latest`];
200
365
  if (dryRun) {
201
366
  context.out(`Update command: ${npmCommand} ${installArgs.join(" ")}`);
202
367
  return 0;
203
368
  }
204
369
  context.out("Updating Twinkler helper...");
205
- const result = spawnSync(npmCommand, installArgs, {
206
- stdio: "inherit",
207
- env: context.env
370
+ const result = runGlobalNpmInstall({ env: context.env, spawnSyncImpl: context.spawnSyncImpl, inheritStdio: true });
371
+ if (!result.ok) {
372
+ context.err(result.error ?? `Update failed: npm exited with ${result.exitCode ?? "unknown status"}`);
373
+ return result.exitCode ?? 1;
374
+ }
375
+ context.out("Twinkler updated. Restart your AI session so newly installed skill text is loaded.");
376
+ return 0;
377
+ }
378
+ function runGlobalNpmInstall(options) {
379
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
380
+ const installArgs = ["install", "-g", `${PACKAGE_NAME}@latest`];
381
+ const result = options.spawnSyncImpl(npmCommand, installArgs, {
382
+ stdio: options.inheritStdio ? "inherit" : "pipe",
383
+ encoding: options.inheritStdio ? undefined : "utf8",
384
+ env: options.env
208
385
  });
386
+ const command = `${npmCommand} ${installArgs.join(" ")}`;
209
387
  if (result.error) {
210
- context.err(`Update failed: ${result.error.message}`);
211
- return 1;
388
+ return { ok: false, command, exitCode: null, error: `Update failed: ${result.error.message}` };
212
389
  }
213
390
  if (result.status !== 0) {
214
- context.err(`Update failed: npm exited with ${result.status ?? "unknown status"}`);
215
- return result.status ?? 1;
391
+ const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
392
+ return {
393
+ ok: false,
394
+ command,
395
+ exitCode: result.status,
396
+ error: stderr || `Update failed: npm exited with ${result.status ?? "unknown status"}`
397
+ };
216
398
  }
217
- context.out("Twinkler updated. Restart your AI session so newly installed skill text is loaded.");
218
- return 0;
399
+ return { ok: true, command, exitCode: 0 };
219
400
  }
220
401
  async function readAllStdin(stdin) {
221
402
  const chunks = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-twinkler",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Lightweight AI helper for the 5mghost Twinkler API",
5
5
  "type": "module",
6
6
  "engines": {
@@ -26,7 +26,6 @@ If `twinkler update` is unavailable because the installed version is old, run:
26
26
 
27
27
  ```bash
28
28
  npm install -g @mkterswingman/5mghost-twinkler@latest
29
- twinkler setup
30
29
  ```
31
30
 
32
31
  Then ask the user to restart the AI session so updated skill text is loaded.
@@ -35,7 +34,7 @@ Then ask the user to restart the AI session so updated skill text is loaded.
35
34
 
36
35
  ```bash
37
36
  twinkler version
38
- twinkler doctor
37
+ twinkler ensure --json
39
38
  ```
40
39
 
41
40
  Do not ask the user to edit skill directories or hidden config files manually.
@@ -1,137 +1,159 @@
1
1
  ---
2
2
  name: use-5mghost-twinkler
3
3
  preamble-tier: 3
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  description: |
6
6
  Use when the user wants Twitch streamer, game, ranking, stream-session, CCV,
7
- or chat-watch data from the mkterswingman Twinkler API.
8
- Keywords: Twinkler, Twitch, streamer, CCV, chat, watch job, SullyGnome, Arc Raiders.
7
+ chat, live-watch, or SullyGnome data through the mkterswingman Twinkler API.
8
+ Also use for vague requests such as "查这个主播", "盯这个直播", "拉 Twitch 数据",
9
+ "看看 CCV/chat", or "找符合条件的主播".
9
10
  ---
10
11
 
11
12
  # Use 5mghost Twinkler
12
13
 
13
- Use the local `twinkler` helper. It handles auth, refreshes short-lived
14
- mkterswingman access tokens when available, and calls the hosted Twinkler REST
15
- API. Prefer the helper over hand-written `curl`.
14
+ Operate Twinkler for the user. Do not ask non-technical users to run CLI
15
+ commands, remember job IDs, choose API routes, or design pagination strategy.
16
+ Use the local `twinkler` helper instead of hand-written `curl`; it handles
17
+ mkterswingman auth and only allows hosted Twinkler `/api/v1/*` paths.
16
18
 
17
- ## First Check
19
+ ## Required Preflight
18
20
 
19
- Run:
21
+ Before any data work, run:
20
22
 
21
23
  ```bash
22
- twinkler doctor
24
+ twinkler ensure --auto-update --json
23
25
  ```
24
26
 
25
- If the helper is missing or auth is missing, use `setup-5mghost-twinkler`.
26
- Do not ask non-technical users to understand CLI concepts; operate the helper
27
- for them and explain only the action they need to take.
27
+ Read the JSON.
28
28
 
29
- ## Auth
29
+ - If `update.status` is `updated`, continue the current task. Tell the user only
30
+ that the helper is updated and a new AI session is needed to load changed skill
31
+ text.
32
+ - If auth is missing or expired, run `twinkler login`, give the user
33
+ <https://mkterswingman.com/pat/login>, and have them paste the PAT into local
34
+ stdin. Never ask for the PAT in chat.
35
+ - If update failed but the current helper can still complete the task, continue
36
+ and mention the update failure briefly. Stop only when the helper is too old
37
+ for the requested command.
30
38
 
31
- Twinkler uses the shared mkterswingman login/PAT. Do not call it a Twinkler PAT.
39
+ Do not use time-based latest-version caches. The preflight checks npm latest on
40
+ every invocation.
32
41
 
33
- If auth is missing:
42
+ ## Data Request Layer
34
43
 
35
- 1. Give the user this URL: <https://mkterswingman.com/pat/login>
36
- 2. Ask the user to copy the mkterswingman PAT.
37
- 3. Start `twinkler auth login --pat-stdin`.
38
- 4. Have the user paste the PAT into that local input, not into AI chat.
39
-
40
- Never echo the PAT back to the user. Never put the PAT in source files, docs,
41
- logs, URLs, screenshots, PR text, or committed `.env` files. Do not commit
42
- `~/.mkterswingman/auth.json` or any generated secret/config file.
43
-
44
- ## Common Calls
45
-
46
- Use:
44
+ Use `twinkler call` for one-off hosted API requests:
47
45
 
48
46
  ```bash
49
47
  twinkler call GET /api/v1/channel/ibai/summary --query days=30
50
- twinkler call GET /api/v1/rankings/channels --query sort_by=mostfollowers --query days=30 --query page_size=5
48
+ twinkler call GET /api/v1/channel/ibai/streams --query days=30 --query sort_by=avgviewers --query page=1 --query page_size=25
49
+ twinkler call GET /api/v1/game/Delta%20Force/channels --query days=365 --query sort_by=watched --query page=1 --query page_size=50
50
+ twinkler call GET /api/v1/rankings/channels --query sort_by=mostfollowers --query days=30 --query page=1 --query page_size=25
51
51
  ```
52
52
 
53
- `twinkler call` only allows Twinkler `/api/v1/*` paths. It returns JSON on
54
- stdout.
53
+ Pick the smallest API that answers the question:
54
+
55
+ - Channel profile/current history: `/channel/{name}/summary`.
56
+ - A channel's recent broadcasts: `/channel/{name}/streams`.
57
+ - A channel's games: `/channel/{name}/games`.
58
+ - Stream-session chart or game splits: `/channel/{name}/stream/{stream_id}/chart`
59
+ and `/games`.
60
+ - Game creator discovery: `/game/{game_identifier}/channels`.
61
+ - Broad Twitch leaderboards: `/rankings/channels`, `/rankings/games`,
62
+ `/rankings/teams`.
63
+ - Live CCV/chat sampling: create a watch job instead of polling summary pages.
64
+
65
+ ## Watch Job Layer
66
+
67
+ Use watch jobs when the user asks to monitor a live channel, collect CCV points,
68
+ collect chat messages, or stop when the stream ends or changes game.
55
69
 
56
- ## Watch Jobs
70
+ Before creating a new watch job in a new session, recover existing work:
57
71
 
58
- Use watch jobs when the user asks to monitor a currently live Twitch channel,
59
- collect CCV points, collect chat messages, or stop when the stream ends or
60
- changes game.
72
+ ```bash
73
+ twinkler jobs active
74
+ twinkler jobs recent
75
+ ```
61
76
 
62
- Create:
77
+ Create a job:
63
78
 
64
79
  ```bash
65
80
  twinkler call POST /api/v1/twitch/watch --json '{"logins":["theburntpeanut"],"collect":["ccv","chat"],"start":{"type":"time","at":"now"},"stop":{"type":"game","game_name":"ARC Raiders"}}'
66
81
  ```
67
82
 
68
- The response contains a `job_id`. Save it in your working notes. Then poll:
83
+ Then inspect it with:
69
84
 
70
85
  ```bash
71
- twinkler call GET /api/v1/twitch/watch/<job_id>
86
+ twinkler jobs show <job_id>
72
87
  twinkler call GET /api/v1/twitch/watch/<job_id>/ccv
73
88
  twinkler call GET /api/v1/twitch/watch/<job_id>/chat
74
89
  ```
75
90
 
76
- Stop/cleanup when the user is done:
91
+ For long-running jobs, create a monitor in the AI environment. Generate the
92
+ portable monitor payload with the bundled script in this skill directory:
77
93
 
78
94
  ```bash
79
- twinkler call DELETE /api/v1/twitch/watch/<job_id>
95
+ node scripts/create-watch-monitor.mjs --job-id <job_id> --cadence-min 10 --stop "stream ended or no longer matches requested game"
80
96
  ```
81
97
 
82
- Normal logged-in users can create and delete their own watch jobs. If the API
83
- returns `WATCH_DISABLED`, explain that the hosted watch worker is not enabled
84
- right now. If it returns `AUTH_*`, repair mkterswingman auth. If it returns a
85
- limit error, tell the user which limit was hit and stop creating more jobs.
98
+ If the current runtime has an automation, heartbeat, reminder, or monitor tool,
99
+ submit the generated prompt there. If not, state that proactive notification is
100
+ not available in this environment, but the job can be recovered later with
101
+ `twinkler jobs active` and `twinkler jobs recent`.
86
102
 
87
- ## Calling The Helper From Scripts
103
+ Do not make the user remember or run the recovery commands. Use them yourself in
104
+ new sessions.
88
105
 
89
- When writing a local automation script for a non-technical user, prefer calling
90
- the helper instead of re-implementing auth:
106
+ ## Data Processing Layer
91
107
 
92
- ```js
93
- import { execFile } from "node:child_process";
94
- import { promisify } from "node:util";
108
+ Short responses can be parsed directly from JSON. For multi-page or
109
+ more-than-100-row tasks, write raw rows before analysis:
95
110
 
96
- const execFileAsync = promisify(execFile);
111
+ - Prefer JSONL for append-only pulls and quick inspection.
112
+ - Prefer SQLite when sampling over time, joining channel/game rows, deduping,
113
+ or applying repeated filters.
114
+ - Keep raw records plus derived columns such as `avg_viewers`, `stream_hours`,
115
+ `watch_hours`, `language`, `game_name`, and source route.
116
+ - Preserve timestamps and route/query parameters so metric definitions remain
117
+ auditable.
97
118
 
98
- const { stdout } = await execFileAsync("twinkler", [
99
- "call",
100
- "GET",
101
- "/api/v1/channel/ibai/summary",
102
- "--query",
103
- "days=30",
104
- ]);
119
+ For "take a point every 1 minute" live tasks, prefer a hosted watch job. If a
120
+ local ad hoc sampler is unavoidable, store one raw row per poll in JSONL/SQLite;
121
+ do not rely on chat context as the data store.
105
122
 
106
- const payload = JSON.parse(stdout);
107
- console.log(payload);
108
- ```
123
+ ## Analysis Layer
109
124
 
110
- Do not pass PATs as script arguments. Let `twinkler auth login --pat-stdin`
111
- store the mkterswingman PAT once, then scripts can call `twinkler call`.
125
+ Plan the upstream sort order before paginating. For compound filters, sort by a
126
+ proxy that satisfies multiple constraints.
112
127
 
113
- ## Direct REST Fallback
128
+ Example: "近 3 年播过 Delta Force、CCV/avg viewers > 1000、stream hours > 30、英语区主播".
114
129
 
115
- Use direct REST only when the helper cannot be installed or the runtime is
116
- non-Node, such as a Python-only cloud job. Direct REST still requires auth.
130
+ Use game channels or relevant rankings sorted by watch hours/watched first, not
131
+ stream hours. Watch hours approximates viewers times duration, so high rows are
132
+ more likely to satisfy both viewer and duration thresholds. Sorting by stream
133
+ hours alone pushes many low-viewer marathon channels ahead and wastes pages.
117
134
 
118
- ```python
119
- import os
120
- import requests
135
+ Stop conditions:
121
136
 
122
- token = os.environ["MKTERSWINGMAN_PAT"]
137
+ - Compute the minimum possible watched threshold when definitions are aligned:
138
+ `min_watch_hours = min_avg_viewers * min_stream_hours`.
139
+ - Once sorted watched/watch-hours values fall materially below that threshold
140
+ and several pages have produced no qualifying candidates, stop or ask before
141
+ continuing.
142
+ - If the API's metric names or time windows are ambiguous, say which field is
143
+ being used and verify with a small sample before scaling.
123
144
 
124
- response = requests.get(
125
- "https://mkterswingman.com/5mghost/twinkler/api/v1/channel/ibai/summary",
126
- params={"days": "30"},
127
- headers={"Authorization": f"Bearer {token}"},
128
- timeout=30,
129
- )
130
- response.raise_for_status()
131
- print(response.json())
132
- ```
145
+ ## Error And Recovery States
146
+
147
+ - Missing helper: install `@mkterswingman/5mghost-twinkler` globally with npm,
148
+ then rerun preflight.
149
+ - Auth missing/expired: run `twinkler login`.
150
+ - Update needed: preflight auto-updates; continue after it succeeds.
151
+ - Watch disabled: explain that the hosted watch worker is disabled right now.
152
+ - Upstream blocked/rate limited: report retry timing and do not loop blindly.
153
+ - Empty result: distinguish real empty from wrong ID, wrong sort, wrong locale,
154
+ or unsupported time window.
155
+
156
+ ## Secret Policy
133
157
 
134
- If `MKTERSWINGMAN_PAT` is missing in a non-helper environment, send the user to
135
- <https://mkterswingman.com/pat/login>, then store the PAT in the target
136
- runtime's secret store yourself when tool access provides a safe secret-entry
137
- mechanism. Do not ask the user to paste the PAT into chat.
158
+ Never print, echo, log, commit, screenshot, or store PATs in project files. The
159
+ normal local storage is `~/.mkterswingman/auth.json`, written by the helper.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = parseArgs(process.argv.slice(2));
4
+ if (!args.jobId) {
5
+ console.error("Usage: create-watch-monitor.mjs --job-id <job_id> [--cadence-min 10] [--stop text] [--fields ccv_points,chat_messages,latest_ccv]");
6
+ process.exit(1);
7
+ }
8
+
9
+ const cadenceMin = Number.parseInt(args.cadenceMin ?? "10", 10);
10
+ if (!Number.isFinite(cadenceMin) || cadenceMin < 1) {
11
+ console.error("--cadence-min must be a positive integer");
12
+ process.exit(1);
13
+ }
14
+
15
+ const fields = (args.fields ?? "ccv_points,chat_messages,latest_ccv")
16
+ .split(",")
17
+ .map((field) => field.trim())
18
+ .filter(Boolean);
19
+ const stop = args.stop ?? "the watch job completes, expires, is cancelled, the stream ends, or the requested game no longer matches";
20
+ const jobId = args.jobId;
21
+
22
+ const prompt = [
23
+ `Check the production 5mghost-twinkler watch job ${jobId}.`,
24
+ "Do not print secrets.",
25
+ "Run `twinkler ensure --auto-update --json` first; if auth is missing, ask the user to complete `twinkler login`.",
26
+ `Fetch \`twinkler jobs show ${jobId}\` and report status plus ${fields.join(", ")}.`,
27
+ "If the job is active, include latest online/game/title/viewer count when available.",
28
+ `Stop monitoring when ${stop}.`,
29
+ "If the job has completed, tell the user clearly that the monitor can be disabled."
30
+ ].join(" ");
31
+
32
+ const payload = {
33
+ kind: "twinkler_watch_monitor",
34
+ job_id: jobId,
35
+ cadence_minutes: cadenceMin,
36
+ stop_condition: stop,
37
+ fields,
38
+ prompt
39
+ };
40
+
41
+ console.log(JSON.stringify(payload, null, 2));
42
+
43
+ function parseArgs(argv) {
44
+ const parsed = {};
45
+ for (let index = 0; index < argv.length; index += 1) {
46
+ const arg = argv[index];
47
+ if (arg === "--job-id") parsed.jobId = argv[++index];
48
+ else if (arg === "--cadence-min") parsed.cadenceMin = argv[++index];
49
+ else if (arg === "--stop") parsed.stop = argv[++index];
50
+ else if (arg === "--fields") parsed.fields = argv[++index];
51
+ else {
52
+ console.error(`Unknown option: ${arg}`);
53
+ process.exit(1);
54
+ }
55
+ }
56
+ return parsed;
57
+ }
@@ -2,22 +2,6 @@
2
2
  "schemaVersion": 1,
3
3
  "product": "5mghost-twinkler",
4
4
  "skills": [
5
- {
6
- "name": "setup-5mghost-twinkler",
7
- "source": { "type": "local", "path": "./skills/setup-5mghost-twinkler" },
8
- "targets": [
9
- "claude",
10
- "claude-internal",
11
- "codex",
12
- "codex-internal",
13
- "gemini",
14
- "gemini-internal",
15
- "openclaw",
16
- "workbuddy",
17
- "codebuddy",
18
- "agents"
19
- ]
20
- },
21
5
  {
22
6
  "name": "use-5mghost-twinkler",
23
7
  "source": { "type": "local", "path": "./skills/use-5mghost-twinkler" },