@mkterswingman/5mghost-twinkler 0.1.1 → 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,10 +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 setup
7
+ twinkler ensure --auto-update --json
8
8
  twinkler call GET /api/v1/channel/ibai/summary --query days=30
9
9
  ```
10
10
 
11
+ `npm install -g` installs bundled AI skills automatically for detected AI
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`.
15
+
11
16
  Auth uses the shared mkterswingman PAT. Do not commit `.env`, `auth.json`,
12
17
  PATs, bearer tokens, or any generated secret file to a remote repository. Enter
13
- 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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, realpathSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
3
4
  import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { getAuthStatus, logout, PAT_LOGIN_URL, savePat } from "./auth.js";
@@ -10,18 +11,25 @@ const HELP = [
10
11
  "twinkler",
11
12
  "",
12
13
  "Usage:",
13
- " twinkler setup",
14
- " twinkler install-skills",
15
14
  " twinkler doctor",
15
+ " twinkler login",
16
+ " twinkler ensure [--auto-update] [--json]",
16
17
  " twinkler auth status",
17
- " twinkler auth login --pat <TOKEN>",
18
18
  " twinkler auth login --pat-stdin",
19
19
  " twinkler call <GET|POST|DELETE> /api/v1/... [--query k=v] [--json '{...}']",
20
+ " twinkler jobs active|recent|show <job_id>",
21
+ " twinkler update",
20
22
  " twinkler version",
21
23
  "",
24
+ "Repair / advanced:",
25
+ " twinkler setup",
26
+ " twinkler install-skills",
27
+ " twinkler auth login --pat <TOKEN>",
28
+ "",
22
29
  "mkterswingman PAT:",
23
30
  ` ${PAT_LOGIN_URL}`
24
31
  ].join("\n");
32
+ const PACKAGE_NAME = "@mkterswingman/5mghost-twinkler";
25
33
  export async function runCli(context = {}) {
26
34
  const argv = context.argv ?? process.argv.slice(2);
27
35
  const env = context.env ?? process.env;
@@ -44,6 +52,21 @@ export async function runCli(context = {}) {
44
52
  return 0;
45
53
  case "auth":
46
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
+ });
47
70
  case "call": {
48
71
  const parsed = parseCallArgs(args);
49
72
  const result = await callTwinkler({
@@ -55,6 +78,13 @@ export async function runCli(context = {}) {
55
78
  out(JSON.stringify(result, null, 2));
56
79
  return 0;
57
80
  }
81
+ case "jobs":
82
+ return await runJobsCommand(args, {
83
+ paths,
84
+ env,
85
+ out,
86
+ fetchImpl: context.fetchImpl
87
+ });
58
88
  case "install-skills": {
59
89
  const results = installBundledSkills({ homeDir: paths.homeDir });
60
90
  out(renderSkillInstallResults(results));
@@ -72,6 +102,9 @@ export async function runCli(context = {}) {
72
102
  ].join("\n"));
73
103
  return results.some((result) => result.status === "error") ? 1 : 0;
74
104
  }
105
+ case "update":
106
+ case "upgrade":
107
+ return runUpdateCommand(args, { env, out, err, spawnSyncImpl: context.spawnSyncImpl ?? spawnSync });
75
108
  case "doctor": {
76
109
  const auth = getAuthStatus({ authJsonPath: paths.authJsonPath, env });
77
110
  out([
@@ -101,10 +134,12 @@ async function runAuthCommand(args, context) {
101
134
  context.out([
102
135
  "Usage:",
103
136
  " twinkler auth status",
104
- " twinkler auth login --pat <TOKEN>",
105
137
  " twinkler auth login --pat-stdin",
106
138
  " twinkler auth logout",
107
139
  "",
140
+ "Advanced compatibility:",
141
+ " twinkler auth login --pat <TOKEN>",
142
+ "",
108
143
  `PAT page: ${PAT_LOGIN_URL}`
109
144
  ].join("\n"));
110
145
  return 0;
@@ -149,6 +184,141 @@ async function runAuthCommand(args, context) {
149
184
  context.out(`Unknown auth subcommand: ${subcommand}`);
150
185
  return 1;
151
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
+ }
152
322
  function parseCallArgs(args) {
153
323
  const [method, path, ...rest] = args;
154
324
  if (!method || !path) {
@@ -188,6 +358,46 @@ function renderSkillInstallResults(results) {
188
358
  })
189
359
  ].join("\n");
190
360
  }
361
+ function runUpdateCommand(args, context) {
362
+ const dryRun = args.includes("--dry-run") || context.env.TWINKLER_UPDATE_DRY_RUN === "1";
363
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
364
+ const installArgs = ["install", "-g", `${PACKAGE_NAME}@latest`];
365
+ if (dryRun) {
366
+ context.out(`Update command: ${npmCommand} ${installArgs.join(" ")}`);
367
+ return 0;
368
+ }
369
+ context.out("Updating Twinkler helper...");
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
385
+ });
386
+ const command = `${npmCommand} ${installArgs.join(" ")}`;
387
+ if (result.error) {
388
+ return { ok: false, command, exitCode: null, error: `Update failed: ${result.error.message}` };
389
+ }
390
+ if (result.status !== 0) {
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
+ };
398
+ }
399
+ return { ok: true, command, exitCode: 0 };
400
+ }
191
401
  async function readAllStdin(stdin) {
192
402
  const chunks = [];
193
403
  for await (const chunk of stdin) {
@@ -1,5 +1,7 @@
1
1
  export interface InstallSkillsOptions {
2
- homeDir: string;
2
+ homeDir?: string;
3
+ detectedAgents?: string[];
4
+ repairTargets?: boolean;
3
5
  }
4
6
  export interface SkillInstallResult {
5
7
  agent: string;
@@ -7,5 +9,6 @@ export interface SkillInstallResult {
7
9
  status: "installed" | "skipped" | "error";
8
10
  targetDir: string;
9
11
  reason?: string;
12
+ contentHash?: string;
10
13
  }
11
- export declare function installBundledSkills(options: InstallSkillsOptions): SkillInstallResult[];
14
+ export declare function installBundledSkills(options?: InstallSkillsOptions): SkillInstallResult[];
@@ -1,58 +1,86 @@
1
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
1
+ import { applyAgentOverrides, getTargetDir, installSkills, listDetectedAgents, loadBuiltInRegistry, RECEIPT_FILENAME } from "@mkterswingman/5mghost-agent-skills";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
3
4
  import { resolveBundledAssetPath } from "./paths.js";
4
- const TARGETS = [
5
- { agent: "codex", skillsDir: ".codex/skills" },
6
- { agent: "agents", skillsDir: ".agents/skills" }
5
+ const AGENT_OVERRIDES = {
6
+ agents: {
7
+ label: "Agents legacy skills",
8
+ detect: { kind: "path", path: "~/.agents" },
9
+ skillsDir: "~/.agents/skills",
10
+ installMethod: "copy"
11
+ }
12
+ };
13
+ const TWINKLER_SKILLS = [
14
+ "setup-5mghost-twinkler",
15
+ "use-5mghost-twinkler",
16
+ "update-5mghost-twinkler"
7
17
  ];
8
- export function installBundledSkills(options) {
18
+ const LEGACY_RECEIPT_FILENAME = ".install-receipt.json";
19
+ export function installBundledSkills(options = {}) {
9
20
  const manifestPath = resolveBundledAssetPath("skills.manifest.json");
10
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
11
- const packageRoot = dirname(manifestPath);
12
- const results = [];
13
- for (const skill of manifest.skills) {
14
- const sourceDir = join(packageRoot, skill.source);
15
- for (const target of TARGETS) {
16
- const rootDir = join(options.homeDir, target.skillsDir);
17
- const targetDir = join(rootDir, skill.name);
21
+ const detectedAgents = options.detectedAgents ??
22
+ listDetectedAgents({
23
+ homeDir: options.homeDir,
24
+ agentOverrides: AGENT_OVERRIDES
25
+ });
26
+ const setupTargets = options.repairTargets === false
27
+ ? detectedAgents
28
+ : Array.from(new Set([...detectedAgents, "codex", "agents"]));
29
+ removeLegacyOwnedTargets(options.homeDir, setupTargets);
30
+ return normalizeSummary(installSkills({
31
+ manifestPath,
32
+ homeDir: options.homeDir,
33
+ agentOverrides: AGENT_OVERRIDES,
34
+ detectedAgents: setupTargets
35
+ }));
36
+ }
37
+ function removeLegacyOwnedTargets(homeDir, agentNames) {
38
+ const registry = applyAgentOverrides(loadBuiltInRegistry(), AGENT_OVERRIDES);
39
+ for (const agentName of agentNames) {
40
+ const agent = registry.agents[agentName];
41
+ if (!agent)
42
+ continue;
43
+ for (const skillName of TWINKLER_SKILLS) {
44
+ const targetDir = getTargetDir(homeDir, agent.skillsDir, skillName);
45
+ const receiptPath = findReceiptPath(targetDir);
46
+ if (!existsSync(receiptPath))
47
+ continue;
48
+ let receipt;
18
49
  try {
19
- if (!existsSync(sourceDir)) {
20
- results.push({
21
- agent: target.agent,
22
- skill: skill.name,
23
- status: "error",
24
- targetDir,
25
- reason: `missing bundled skill source ${sourceDir}`
26
- });
27
- continue;
28
- }
29
- mkdirSync(rootDir, { recursive: true });
30
- rmSync(targetDir, { recursive: true, force: true });
31
- cpSync(sourceDir, targetDir, { recursive: true });
32
- writeFileSync(join(targetDir, ".install-receipt.json"), JSON.stringify({
33
- product: manifest.product,
34
- skill: skill.name,
35
- agent: target.agent,
36
- source: basename(sourceDir),
37
- installed_at: new Date().toISOString()
38
- }, null, 2));
39
- results.push({
40
- agent: target.agent,
41
- skill: skill.name,
42
- status: "installed",
43
- targetDir
44
- });
50
+ receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
45
51
  }
46
- catch (error) {
47
- results.push({
48
- agent: target.agent,
49
- skill: skill.name,
50
- status: "error",
51
- targetDir,
52
- reason: error instanceof Error ? error.message : String(error)
53
- });
52
+ catch {
53
+ continue;
54
+ }
55
+ if (receipt.installedBy === undefined &&
56
+ receipt.product === "5mghost-twinkler" &&
57
+ receipt.skill === skillName &&
58
+ receipt.agent === agentName) {
59
+ rmSync(targetDir, { recursive: true, force: true });
54
60
  }
55
61
  }
56
62
  }
57
- return results;
63
+ }
64
+ function findReceiptPath(targetDir) {
65
+ const current = join(targetDir, RECEIPT_FILENAME);
66
+ if (existsSync(current))
67
+ return current;
68
+ return join(targetDir, LEGACY_RECEIPT_FILENAME);
69
+ }
70
+ function normalizeSummary(summary) {
71
+ return summary.results.map((result) => {
72
+ const status = result.status === "installed" || result.status === "updated"
73
+ ? "installed"
74
+ : result.status === "error"
75
+ ? "error"
76
+ : "skipped";
77
+ return {
78
+ agent: result.agent,
79
+ skill: result.skill,
80
+ status,
81
+ targetDir: result.targetDir,
82
+ reason: result.reason,
83
+ contentHash: result.contentHash
84
+ };
85
+ });
58
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-twinkler",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Lightweight AI helper for the 5mghost Twinkler API",
5
5
  "type": "module",
6
6
  "engines": {
@@ -9,6 +9,11 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/5mghost/mcp_projects.git",
15
+ "directory": "5mghost-twinkler/client"
16
+ },
12
17
  "bin": {
13
18
  "twinkler": "dist/cli.js"
14
19
  },
@@ -23,13 +28,15 @@
23
28
  "skills",
24
29
  "skills.manifest.json",
25
30
  "install",
31
+ "scripts",
26
32
  "README.md"
27
33
  ],
28
34
  "scripts": {
29
35
  "build": "tsc -p tsconfig.json",
30
36
  "test": "npm run build && node --test tests/*.test.mjs",
31
37
  "prepack": "npm run build",
32
- "pack:dry": "npm pack --dry-run"
38
+ "pack:dry": "npm pack --dry-run",
39
+ "postinstall": "node scripts/postinstall.mjs"
33
40
  },
34
41
  "keywords": [
35
42
  "twinkler",
@@ -37,6 +44,9 @@
37
44
  "ai",
38
45
  "mkterswingman"
39
46
  ],
47
+ "dependencies": {
48
+ "@mkterswingman/5mghost-agent-skills": "^0.0.1"
49
+ },
40
50
  "devDependencies": {
41
51
  "@types/node": "^25.3.2",
42
52
  "typescript": "^5.9.3"
@@ -0,0 +1,51 @@
1
+ // Runs after `npm install -g` to install Twinkler skills into detected AI clients.
2
+ // This script is intentionally non-fatal: npm install must still succeed when
3
+ // an AI client is not installed or a skill target is locked down.
4
+
5
+ const agentOverrides = {
6
+ agents: {
7
+ label: "Agents legacy skills",
8
+ detect: { kind: "path", path: "~/.agents" },
9
+ skillsDir: "~/.agents/skills",
10
+ installMethod: "copy"
11
+ }
12
+ };
13
+
14
+ let listDetectedAgents;
15
+ let installBundledSkills;
16
+
17
+ try {
18
+ ({ listDetectedAgents } = await import("@mkterswingman/5mghost-agent-skills"));
19
+ ({ installBundledSkills } = await import("../dist/skillInstall.js"));
20
+ } catch (error) {
21
+ console.log("[twinkler] agent-skills not available; skipping skill install.");
22
+ process.exit(0);
23
+ }
24
+
25
+ let agents;
26
+ try {
27
+ agents = listDetectedAgents({ agentOverrides });
28
+ } catch (error) {
29
+ console.log("[twinkler] Could not detect AI clients; skipping skill install.");
30
+ process.exit(0);
31
+ }
32
+
33
+ if (!agents || agents.length === 0) {
34
+ console.log("[twinkler] No AI clients detected; skipping skill install.");
35
+ process.exit(0);
36
+ }
37
+
38
+ try {
39
+ const results = installBundledSkills({ detectedAgents: agents, repairTargets: false });
40
+ const installed = results
41
+ .filter((entry) => entry.status === "installed")
42
+ .map((entry) => `${entry.agent}:${entry.skill}`);
43
+ if (installed.length > 0) {
44
+ console.log(`[twinkler] Installed skills: ${installed.join(", ")}`);
45
+ console.log("[twinkler] Restart your AI session before invoking the new skill text.");
46
+ } else {
47
+ console.log("[twinkler] No skill targets updated.");
48
+ }
49
+ } catch (error) {
50
+ console.log(`[twinkler] Skill install failed (non-fatal): ${String(error)}`);
51
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: setup-5mghost-twinkler
3
+ preamble-tier: 3
4
+ version: 0.2.0
5
+ description: |
6
+ Set up or repair the local Twinkler helper, bundled skills, and mkterswingman
7
+ auth for AI-driven Twitch data workflows.
8
+ ---
9
+
10
+ # Setup 5mghost Twinkler
11
+
12
+ Use when Twinkler is missing, auth is missing or expired, skills may be stale,
13
+ or the user asks to prepare Twinkler for an AI workflow.
14
+
15
+ ## Default Path
16
+
17
+ 1. Check the helper:
18
+
19
+ ```bash
20
+ twinkler doctor
21
+ ```
22
+
23
+ 2. If the helper is missing, install it:
24
+
25
+ ```bash
26
+ npm install -g @mkterswingman/5mghost-twinkler
27
+ ```
28
+
29
+ The npm install runs the bundled skill installer automatically. Use `twinkler
30
+ setup` only as a repair command when postinstall could not update the local AI
31
+ skill directories.
32
+
33
+ 3. Repair skills when needed:
34
+
35
+ ```bash
36
+ twinkler setup
37
+ ```
38
+
39
+ 4. If auth is missing, send the user to:
40
+
41
+ ```text
42
+ https://mkterswingman.com/pat/login
43
+ ```
44
+
45
+ Ask the user to copy the mkterswingman PAT, then start:
46
+
47
+ ```bash
48
+ twinkler auth login --pat-stdin
49
+ ```
50
+
51
+ Have the user paste the PAT into that local input. Do not ask them to paste the
52
+ PAT into chat, screenshots, docs, shell history, or source files.
53
+
54
+ ## Success
55
+
56
+ `twinkler doctor` should report auth ready. Then the AI can use the
57
+ `use-5mghost-twinkler` skill and `twinkler call`.
58
+
59
+ ## Recovery
60
+
61
+ - Missing command: run `npm install -g @mkterswingman/5mghost-twinkler`.
62
+ - Missing skills after install: run `twinkler setup`, then restart the AI session.
63
+ - Missing auth: use the PAT page and `twinkler auth login --pat-stdin`.
64
+ - HTTP 401/403 from API calls: run `twinkler auth status`; if stale, log in again.
65
+ - Permission or npm global install failure: use the same package manager and
66
+ Node installation method that originally installed global npm packages.
67
+
68
+ ## Secret Policy
69
+
70
+ Never print, echo, log, commit, or store PATs in project files. The only normal
71
+ local storage is `~/.mkterswingman/auth.json`, written by the helper.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: update-5mghost-twinkler
3
+ preamble-tier: 3
4
+ version: 0.2.0
5
+ description: |
6
+ Update the Twinkler helper package and refresh bundled AI skills.
7
+ ---
8
+
9
+ # Update 5mghost Twinkler
10
+
11
+ Use when the user asks to update, upgrade, repair stale skills, or pick up a new
12
+ Twinkler helper release.
13
+
14
+ ## Default Path
15
+
16
+ Run:
17
+
18
+ ```bash
19
+ twinkler update
20
+ ```
21
+
22
+ This runs the npm global update for `@mkterswingman/5mghost-twinkler@latest`.
23
+ The package postinstall refreshes bundled skills automatically.
24
+
25
+ If `twinkler update` is unavailable because the installed version is old, run:
26
+
27
+ ```bash
28
+ npm install -g @mkterswingman/5mghost-twinkler@latest
29
+ ```
30
+
31
+ Then ask the user to restart the AI session so updated skill text is loaded.
32
+
33
+ ## Verify
34
+
35
+ ```bash
36
+ twinkler version
37
+ twinkler ensure --json
38
+ ```
39
+
40
+ Do not ask the user to edit skill directories or hidden config files manually.
@@ -1,134 +1,159 @@
1
1
  ---
2
2
  name: use-5mghost-twinkler
3
3
  preamble-tier: 3
4
- version: 0.1.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 adds 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, ask the user to install it:
27
+ Read the JSON.
28
+
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.
38
+
39
+ Do not use time-based latest-version caches. The preflight checks npm latest on
40
+ every invocation.
41
+
42
+ ## Data Request Layer
43
+
44
+ Use `twinkler call` for one-off hosted API requests:
26
45
 
27
46
  ```bash
28
- npm install -g @mkterswingman/5mghost-twinkler
29
- twinkler setup
47
+ twinkler call GET /api/v1/channel/ibai/summary --query days=30
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
30
51
  ```
31
52
 
32
- ## Auth
53
+ Pick the smallest API that answers the question:
33
54
 
34
- Twinkler uses the shared mkterswingman PAT. Do not call it a Twinkler PAT.
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.
35
64
 
36
- If auth is missing:
65
+ ## Watch Job Layer
37
66
 
38
- 1. Give the user this URL: <https://mkterswingman.com/pat/login>
39
- 2. Ask the user to copy the mkterswingman PAT.
40
- 3. Start the helper login command and have the user paste the PAT into the
41
- helper input, not into the AI chat.
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.
42
69
 
43
- Preferred save path when stdin is available:
70
+ Before creating a new watch job in a new session, recover existing work:
44
71
 
45
72
  ```bash
46
- twinkler auth login --pat-stdin
73
+ twinkler jobs active
74
+ twinkler jobs recent
47
75
  ```
48
76
 
49
- Then pass the PAT through stdin. Do not put the PAT in command-line arguments
50
- unless no stdin-capable tool is available.
51
-
52
- Fallback:
77
+ Create a job:
53
78
 
54
79
  ```bash
55
- twinkler auth login --pat <TOKEN>
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"}}'
56
81
  ```
57
82
 
58
- Never echo the PAT back to the user. Never put the PAT in source files, docs,
59
- logs, URLs, screenshots, PR text, or committed `.env` files. Do not commit
60
- `~/.mkterswingman/auth.json` or any generated secret/config file to a remote
61
- repository. Do not ask non-technical users to paste PATs into chat; use the
62
- helper's stdin path or a secure secret-entry UI.
63
-
64
- For AI-managed cloud/server environments, store the PAT as a secret named
65
- `MKTERSWINGMAN_PAT`. Do not explain environment variables to non-technical
66
- users unless they ask; configure the target environment yourself when you have
67
- tool access.
83
+ Then inspect it with:
68
84
 
69
- ## Preferred Calls
85
+ ```bash
86
+ twinkler jobs show <job_id>
87
+ twinkler call GET /api/v1/twitch/watch/<job_id>/ccv
88
+ twinkler call GET /api/v1/twitch/watch/<job_id>/chat
89
+ ```
70
90
 
71
- Use:
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:
72
93
 
73
94
  ```bash
74
- twinkler call GET /api/v1/channel/ibai/summary --query days=30
75
- twinkler call GET /api/v1/rankings/channels --query sort_by=mostfollowers --query days=30 --query page_size=5
76
- 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"}}'
95
+ node scripts/create-watch-monitor.mjs --job-id <job_id> --cadence-min 10 --stop "stream ended or no longer matches requested game"
77
96
  ```
78
97
 
79
- `twinkler call` only allows Twinkler `/api/v1/*` paths. It returns JSON on
80
- stdout.
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`.
81
102
 
82
- ## Calling The Helper From Scripts
103
+ Do not make the user remember or run the recovery commands. Use them yourself in
104
+ new sessions.
83
105
 
84
- When writing a local automation script for a non-technical user, prefer calling
85
- the helper instead of re-implementing auth:
106
+ ## Data Processing Layer
86
107
 
87
- ```js
88
- import { execFile } from "node:child_process";
89
- 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:
90
110
 
91
- 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.
92
118
 
93
- const { stdout } = await execFileAsync("twinkler", [
94
- "call",
95
- "GET",
96
- "/api/v1/channel/ibai/summary",
97
- "--query",
98
- "days=30",
99
- ]);
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.
100
122
 
101
- const payload = JSON.parse(stdout);
102
- console.log(payload);
103
- ```
123
+ ## Analysis Layer
104
124
 
105
- Do not pass PATs as script arguments. Let `twinkler auth login --pat-stdin`
106
- 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.
107
127
 
108
- ## Direct REST Fallback
128
+ Example: "近 3 年播过 Delta Force、CCV/avg viewers > 1000、stream hours > 30、英语区主播".
109
129
 
110
- Use direct REST only when the helper cannot be installed or the runtime is
111
- 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.
112
134
 
113
- Python example:
135
+ Stop conditions:
114
136
 
115
- ```python
116
- import os
117
- import requests
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.
118
144
 
119
- token = os.environ["MKTERSWINGMAN_PAT"]
145
+ ## Error And Recovery States
120
146
 
121
- response = requests.get(
122
- "https://mkterswingman.com/5mghost/twinkler/api/v1/channel/ibai/summary",
123
- params={"days": "30"},
124
- headers={"Authorization": f"Bearer {token}"},
125
- timeout=30,
126
- )
127
- response.raise_for_status()
128
- print(response.json())
129
- ```
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
130
157
 
131
- If `MKTERSWINGMAN_PAT` is missing in a non-helper environment, send the user to
132
- <https://mkterswingman.com/pat/login>, then store the PAT in the target
133
- runtime's secret store yourself when tool access provides a safe secret-entry
134
- 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
+ }
@@ -1,10 +1,38 @@
1
1
  {
2
+ "schemaVersion": 1,
2
3
  "product": "5mghost-twinkler",
3
4
  "skills": [
4
5
  {
5
6
  "name": "use-5mghost-twinkler",
6
- "source": "skills/use-5mghost-twinkler",
7
- "description": "Use the Twinkler helper to call the mkterswingman Twinkler API from AI workflows."
7
+ "source": { "type": "local", "path": "./skills/use-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
+ {
22
+ "name": "update-5mghost-twinkler",
23
+ "source": { "type": "local", "path": "./skills/update-5mghost-twinkler" },
24
+ "targets": [
25
+ "claude",
26
+ "claude-internal",
27
+ "codex",
28
+ "codex-internal",
29
+ "gemini",
30
+ "gemini-internal",
31
+ "openclaw",
32
+ "workbuddy",
33
+ "codebuddy",
34
+ "agents"
35
+ ]
8
36
  }
9
37
  ]
10
38
  }