luckiest-co 1.0.50 → 1.0.52

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.
@@ -9,7 +9,7 @@
9
9
  "name": "luckiest",
10
10
  "source": "./",
11
11
  "description": "Plan, go, finish, plus bundled free Luckiest skills that work in Claude Code, web chat, and Cowork.",
12
- "version": "0.1.49",
12
+ "version": "0.1.51",
13
13
  "author": { "name": "Luckiest" }
14
14
  }
15
15
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckiest",
3
3
  "description": "Plan, go, finish. Guided planning, progress dashboards, and your luckiest.co tribe inside Claude Code.",
4
- "version": "0.1.49",
4
+ "version": "0.1.51",
5
5
  "author": { "name": "Luckiest" }
6
6
  }
@@ -2,7 +2,10 @@
2
2
 
3
3
  // Plugin repo sync: mirrors this package into the public marketplace repo
4
4
  // (luckiestco/luckiest-plugin) and opens a PR there. Run after the release is
5
- // bumped and published. --dry-run stops before anything is pushed.
5
+ // bumped and published. --dry-run stops before anything is pushed. --merge
6
+ // squash-merges the PR it opened (the release workflow uses this). With GH_TOKEN
7
+ // set (CI), git pushes through `gh auth setup-git`, so the token never lands in
8
+ // a URL or a log.
6
9
 
7
10
  const { execFileSync } = require('child_process');
8
11
  const fs = require('fs');
@@ -18,10 +21,25 @@ const REPO = process.env.LUCKIEST_PLUGIN_REPO || 'luckiestco/luckiest-plugin';
18
21
  const sh = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
19
22
  const versionOf = (dir, file) => JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')).version;
20
23
 
21
- /** Returns { status: 'synced' | 'dry-run' | 'pr', dir, changes, url }. */
22
- function syncPluginRepo({ src = path.join(__dirname, '..'), remote = `https://github.com/${REPO}.git`, dryRun = false } = {}) {
24
+ const gitHas = (key, cwd) => {
25
+ try {
26
+ return Boolean(sh('git', ['config', key], cwd));
27
+ } catch {
28
+ return false;
29
+ }
30
+ };
31
+
32
+ /** Returns { status: 'synced' | 'dry-run' | 'pr', dir, changes, url, merged }. */
33
+ function syncPluginRepo({
34
+ src = path.join(__dirname, '..'),
35
+ remote = `https://github.com/${REPO}.git`,
36
+ dryRun = false,
37
+ merge = false,
38
+ gh = (args, cwd) => sh('gh', args, cwd),
39
+ } = {}) {
23
40
  const plugin = versionOf(src, '.claude-plugin/plugin.json');
24
41
  const npm = versionOf(src, 'package.json');
42
+ if (process.env.GH_TOKEN && remote.startsWith('https://github.com/')) gh(['auth', 'setup-git']);
25
43
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'luckiest-plugin-sync-'));
26
44
  sh('git', ['clone', '--depth', '1', remote, dir]);
27
45
 
@@ -36,18 +54,24 @@ function syncPluginRepo({ src = path.join(__dirname, '..'), remote = `https://gi
36
54
  const title = `Sync plugin ${plugin} (npm luckiest-co ${npm})`;
37
55
  sh('git', ['switch', '-c', branch], dir);
38
56
  sh('git', ['add', '-A'], dir);
39
- sh('git', ['commit', '-m', title], dir);
57
+ // CI runners have no git identity; commit as the release bot there.
58
+ const who = gitHas('user.name', dir) && gitHas('user.email', dir) ? [] : ['-c', 'user.name=luckiest-release', '-c', 'user.email=release@luckiest.co'];
59
+ sh('git', [...who, 'commit', '-m', title], dir);
40
60
  sh('git', ['push', '-u', 'origin', branch], dir);
41
- const url = sh('gh', ['pr', 'create', '--repo', REPO, '--head', branch, '--title', title, '--body', `Mirrors packages/luckiest-plugin at plugin ${plugin}.`], dir);
42
- return { status: 'pr', dir, changes, url };
61
+ const url = gh(['pr', 'create', '--repo', REPO, '--head', branch, '--title', title, '--body', `Mirrors packages/luckiest-plugin at plugin ${plugin}.`], dir);
62
+ if (merge) gh(['pr', 'merge', url, '--repo', REPO, '--squash', '--delete-branch'], dir);
63
+ return { status: 'pr', dir, changes, url, merged: merge };
43
64
  }
44
65
 
45
66
  if (require.main === module) {
46
67
  try {
47
- const { status, dir, changes, url } = syncPluginRepo({ dryRun: process.argv.includes('--dry-run') });
68
+ const { status, dir, changes, url, merged } = syncPluginRepo({
69
+ dryRun: process.argv.includes('--dry-run'),
70
+ merge: process.argv.includes('--merge'),
71
+ });
48
72
  if (status === 'synced') console.log(` ${green}${REPO} already has this plugin version.${reset}`);
49
73
  if (status === 'dry-run') console.log(` ${yellow}Dry run, nothing pushed.${reset} ${changes.length} changed files in ${dir}:\n${changes.map((c) => ` ${c}`).join('\n')}`);
50
- if (status === 'pr') console.log(` ${green}Opened ${url}${reset}`);
74
+ if (status === 'pr') console.log(` ${green}${merged ? 'Opened and merged' : 'Opened'} ${url}${reset}`);
51
75
  } catch (err) {
52
76
  console.error(` ${(err.stderr || err.message).toString().trim()}`);
53
77
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckiest-co",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "Luckiest for Claude Code: plan, go, finish. Your luckiest.co skills and tribe, inside Claude.",
5
5
  "bin": {
6
6
  "luckiest-co": "./bin/install.js"
@@ -21,7 +21,8 @@
21
21
  "!**/__pycache__",
22
22
  "!**/*.pyc",
23
23
  "!bin/*.test.js",
24
- "!bin/check-version.js"
24
+ "!bin/check-version.js",
25
+ "!bin/release-check.js"
25
26
  ],
26
27
  "license": "SEE LICENSE IN LICENSE",
27
28
  "engines": {
@@ -1,5 +1,13 @@
1
1
  # Changelog: luckiest-video-watcher
2
2
 
3
+ ## 1.1.2 (2026-10-23)
4
+
5
+ - Auto caption mode picks English when YouTube lists several original
6
+ auto-caption tracks (for example bn-orig and en-orig), then the video's
7
+ declared language, then the first. It used to take the first alphabetically,
8
+ so an English talk came back with Bangla captions. New test:
9
+ `scripts/test_download.py`.
10
+
3
11
  ## 1.1.1 (2026-10-10)
4
12
 
5
13
  - Security: the WhisperX setup's uv installer is pinned to 0.11.16 and saved to a
@@ -19,7 +19,7 @@ argument-hint: "<video-url | path/to/video.mp4> [question] [--study]"
19
19
  license: MIT. See ATTRIBUTION.md
20
20
  allowed-tools: Bash, Read, Write, AskUserQuestion
21
21
  metadata:
22
- version: "1.1.1"
22
+ version: "1.1.2"
23
23
  listing_id: luckiest-video-watcher
24
24
  author: luckiest
25
25
  ---
@@ -39,11 +39,11 @@ run of a session, when setup fails, or when a run needs a detail not covered her
39
39
  ## Staying current
40
40
 
41
41
  On activation, call the Luckiest MCP `check_updates` tool with
42
- `{ listingId: "luckiest-video-watcher", installedSemver: "1.1.1" }`. If
42
+ `{ listingId: "luckiest-video-watcher", installedSemver: "1.1.2" }`. If
43
43
  `upToDate: false`, surface the `notice` once and continue. Never block on it.
44
44
 
45
45
  When done, call `report_usage` once with
46
- `{ listing_id: "luckiest-video-watcher", skill_version: "1.1.1", matched: true, success: <true|false> }`.
46
+ `{ listing_id: "luckiest-video-watcher", skill_version: "1.1.2", matched: true, success: <true|false> }`.
47
47
  Metadata only. Skip silently if unavailable.
48
48
 
49
49
  ## Standing rules
@@ -98,7 +98,12 @@ def select_caption(info: dict, language: str = 'auto') -> dict | None:
98
98
  manual = {k: v for k, v in (info.get('subtitles') or {}).items() if v and k != 'live_chat'}
99
99
  automatic = {k: v for k, v in (info.get('automatic_captions') or {}).items() if v and k != 'live_chat'}
100
100
  youtube = str(info.get('extractor_key') or info.get('extractor') or '').lower().startswith('youtube')
101
- originals = sorted(k for k in automatic if youtube and k.endswith('-orig'))
101
+ # Several originals (e.g. bn-orig and en-orig): English first, then the declared language, then alphabetical.
102
+ declared = str(info.get('language') or '').split('-')[0]
103
+ def original_rank(key):
104
+ base = key.removesuffix('-orig').split('-')[0]
105
+ return base != 'en', base != declared, key
106
+ originals = sorted((k for k in automatic if youtube and k.endswith('-orig')), key=original_rank)
102
107
  original = originals[0][:-5] if len(originals) == 1 else None
103
108
  target = language if language != 'auto' else original or info.get('language')
104
109
  def matches(key):
@@ -0,0 +1,30 @@
1
+ """Run: python3 -m unittest scripts/test_download.py (from the skill directory)."""
2
+ import os
3
+ import sys
4
+ import unittest
5
+
6
+ sys.path.insert(0, os.path.dirname(__file__))
7
+ import download # noqa: E402
8
+
9
+
10
+ def info(language, *keys):
11
+ return {'extractor_key': 'Youtube', 'language': language,
12
+ 'automatic_captions': {k: [{'ext': 'vtt'}] for k in keys}}
13
+
14
+ class SelectCaption(unittest.TestCase):
15
+ def test_several_originals_prefer_english(self):
16
+ for language in ('en', 'bn', None):
17
+ track = download.select_caption(info(language, 'bn-orig', 'en-orig', 'bn', 'en'))
18
+ self.assertEqual(track['key'], 'en-orig', language)
19
+
20
+ def test_several_originals_without_english_use_declared_language(self):
21
+ track = download.select_caption(info('hi', 'bn-orig', 'hi-orig', 'bn', 'hi'))
22
+ self.assertEqual(track['key'], 'hi-orig')
23
+
24
+ def test_single_original_unchanged(self):
25
+ track = download.select_caption(info(None, 'bn-orig', 'bn', 'en'))
26
+ self.assertEqual((track['key'], track['provenance']), ('bn-orig', 'original'))
27
+
28
+
29
+ if __name__ == '__main__':
30
+ unittest.main()