thumbgate 1.2.0 β†’ 1.4.0

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.
Files changed (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +133 -23
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +168 -0
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +85 -2
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bin/cli.js +215 -19
  14. package/bin/postinstall.js +8 -2
  15. package/config/budget.json +18 -0
  16. package/config/gates/code-edit.json +61 -0
  17. package/config/gates/db-write.json +61 -0
  18. package/config/gates/default.json +154 -3
  19. package/config/gates/deploy.json +61 -0
  20. package/config/github-about.json +2 -1
  21. package/config/merge-quality-checks.json +23 -0
  22. package/config/model-tiers.json +11 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -13
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/cursor-marketplace/README.md +2 -2
  34. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  35. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  36. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  37. package/plugins/opencode-profile/INSTALL.md +1 -1
  38. package/public/blog.html +73 -0
  39. package/public/compare/mem0.html +189 -0
  40. package/public/compare/speclock.html +180 -0
  41. package/public/compare.html +12 -4
  42. package/public/guide.html +5 -5
  43. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  44. package/public/guides/codex-cli-guardrails.html +158 -0
  45. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/pre-action-gates.html +162 -0
  47. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  48. package/public/index.html +169 -70
  49. package/public/learn/ai-agent-persistent-memory.html +1 -0
  50. package/public/lessons.html +334 -17
  51. package/public/llm-context.md +140 -0
  52. package/public/pro.html +24 -22
  53. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  54. package/scripts/access-anomaly-detector.js +1 -1
  55. package/scripts/adk-consolidator.js +1 -5
  56. package/scripts/agent-security-hardening.js +4 -6
  57. package/scripts/agentic-data-pipeline.js +1 -3
  58. package/scripts/async-job-runner.js +1 -5
  59. package/scripts/audit-trail.js +7 -5
  60. package/scripts/background-agent-governance.js +2 -10
  61. package/scripts/billing.js +2 -16
  62. package/scripts/budget-enforcer.js +173 -0
  63. package/scripts/build-codex-plugin.js +152 -0
  64. package/scripts/capture-railway-diagnostics.sh +97 -0
  65. package/scripts/check-congruence.js +133 -15
  66. package/scripts/claude-feedback-sync.js +320 -0
  67. package/scripts/cli-telemetry.js +4 -1
  68. package/scripts/commercial-offer.js +5 -7
  69. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  70. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  71. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  72. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  73. package/scripts/context-engine.js +21 -6
  74. package/scripts/contextfs.js +33 -44
  75. package/scripts/dashboard.js +104 -0
  76. package/scripts/decision-journal.js +341 -0
  77. package/scripts/delegation-runtime.js +1 -5
  78. package/scripts/distribution-surfaces.js +26 -0
  79. package/scripts/document-intake.js +927 -0
  80. package/scripts/ephemeral-agent-store.js +1 -8
  81. package/scripts/evolution-state.js +1 -5
  82. package/scripts/experiment-tracker.js +1 -5
  83. package/scripts/export-databricks-bundle.js +1 -5
  84. package/scripts/export-hf-dataset.js +1 -5
  85. package/scripts/export-training.js +1 -5
  86. package/scripts/feedback-attribution.js +1 -16
  87. package/scripts/feedback-history-distiller.js +1 -16
  88. package/scripts/feedback-loop.js +17 -5
  89. package/scripts/feedback-root-consolidator.js +2 -21
  90. package/scripts/feedback-session.js +49 -0
  91. package/scripts/feedback-to-rules.js +188 -28
  92. package/scripts/filesystem-search.js +1 -9
  93. package/scripts/fs-utils.js +104 -0
  94. package/scripts/gates-engine.js +149 -4
  95. package/scripts/github-about.js +32 -8
  96. package/scripts/gtm-revenue-loop.js +1 -5
  97. package/scripts/harness-selector.js +148 -0
  98. package/scripts/hosted-job-launcher.js +1 -5
  99. package/scripts/hybrid-feedback-context.js +7 -33
  100. package/scripts/intervention-policy.js +753 -0
  101. package/scripts/lesson-db.js +3 -18
  102. package/scripts/lesson-inference.js +194 -16
  103. package/scripts/lesson-retrieval.js +60 -24
  104. package/scripts/llm-client.js +59 -0
  105. package/scripts/local-model-profile.js +18 -2
  106. package/scripts/managed-lesson-agent.js +183 -0
  107. package/scripts/marketing-experiment.js +8 -22
  108. package/scripts/meta-agent-loop.js +624 -0
  109. package/scripts/metered-billing.js +1 -1
  110. package/scripts/model-tier-router.js +10 -1
  111. package/scripts/money-watcher.js +1 -4
  112. package/scripts/obsidian-export.js +1 -5
  113. package/scripts/operational-integrity.js +369 -34
  114. package/scripts/org-dashboard.js +6 -1
  115. package/scripts/per-step-scoring.js +2 -4
  116. package/scripts/pr-manager.js +201 -19
  117. package/scripts/pro-features.js +3 -2
  118. package/scripts/prompt-dlp.js +3 -3
  119. package/scripts/prove-adapters.js +2 -5
  120. package/scripts/prove-attribution.js +1 -5
  121. package/scripts/prove-automation.js +3 -5
  122. package/scripts/prove-cloudflare-sandbox.js +1 -3
  123. package/scripts/prove-data-pipeline.js +1 -3
  124. package/scripts/prove-intelligence.js +1 -3
  125. package/scripts/prove-lancedb.js +1 -5
  126. package/scripts/prove-local-intelligence.js +1 -3
  127. package/scripts/prove-packaged-runtime.js +326 -0
  128. package/scripts/prove-predictive-insights.js +1 -3
  129. package/scripts/prove-runtime.js +13 -0
  130. package/scripts/prove-training-export.js +1 -3
  131. package/scripts/prove-workflow-contract.js +1 -5
  132. package/scripts/rate-limiter.js +6 -4
  133. package/scripts/reddit-dm-outreach.js +14 -4
  134. package/scripts/schedule-manager.js +3 -5
  135. package/scripts/security-scanner.js +448 -0
  136. package/scripts/self-distill-agent.js +579 -0
  137. package/scripts/semantic-dedup.js +115 -0
  138. package/scripts/skill-exporter.js +1 -3
  139. package/scripts/skill-generator.js +1 -5
  140. package/scripts/social-analytics/engagement-audit.js +1 -18
  141. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  142. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  143. package/scripts/social-analytics/publishers/zernio.js +51 -0
  144. package/scripts/social-pipeline.js +1 -3
  145. package/scripts/social-post-hourly.js +47 -4
  146. package/scripts/statusline-links.js +6 -5
  147. package/scripts/statusline-local-stats.js +2 -0
  148. package/scripts/statusline.sh +38 -7
  149. package/scripts/sync-branch-protection.js +340 -0
  150. package/scripts/tessl-export.js +1 -3
  151. package/scripts/thumbgate-search.js +32 -1
  152. package/scripts/tool-kpi-tracker.js +1 -1
  153. package/scripts/tool-registry.js +108 -4
  154. package/scripts/vector-store.js +1 -5
  155. package/scripts/weekly-auto-post.js +1 -1
  156. package/scripts/workflow-sentinel.js +205 -4
  157. package/skills/thumbgate/SKILL.md +2 -2
  158. package/src/api/server.js +273 -4
  159. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  160. /package/scripts/social-analytics/db/{social-analytics.db-wal β†’ analytics.sqlite} +0 -0
@@ -4,6 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
+ const { ensureDir } = require('./fs-utils');
7
8
 
8
9
  const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
9
10
  const POS = new Set(['positive', 'positive_strong', 'up', 'thumbs_up']);
@@ -38,11 +39,6 @@ function discoverFeedbackDir() {
38
39
  * Ensure a directory exists, creating it recursively if needed.
39
40
  * @param {string} dirPath
40
41
  */
41
- function ensureDir(dirPath) {
42
- if (!fs.existsSync(dirPath)) {
43
- fs.mkdirSync(dirPath, { recursive: true });
44
- }
45
- }
46
42
 
47
43
  /**
48
44
  * Append a JSON record as a single line to a JSONL file.
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
+ const { readJsonl } = require('../fs-utils');
6
7
 
7
8
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
8
9
  const DEFAULT_REPLY_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'reply-monitor-state.json');
@@ -59,24 +60,6 @@ function readJson(filePath, fallback) {
59
60
  }
60
61
  }
61
62
 
62
- function readJsonl(filePath) {
63
- if (!fs.existsSync(filePath)) {
64
- return [];
65
- }
66
- return fs.readFileSync(filePath, 'utf8')
67
- .split('\n')
68
- .map((line) => line.trim())
69
- .filter(Boolean)
70
- .map((line) => {
71
- try {
72
- return JSON.parse(line);
73
- } catch {
74
- return null;
75
- }
76
- })
77
- .filter(Boolean);
78
- }
79
-
80
63
  function formatDateInTimezone(date, timezone = DEFAULT_TIMEZONE) {
81
64
  const formatter = new Intl.DateTimeFormat('en-CA', {
82
65
  timeZone: timezone,
@@ -22,7 +22,7 @@ const LI_V2_BASE = 'https://api.linkedin.com/v2';
22
22
  function buildRestHeaders(token) {
23
23
  return {
24
24
  Authorization: `Bearer ${token}`,
25
- 'LinkedIn-Version': '202401',
25
+ 'LinkedIn-Version': '202601',
26
26
  'X-Restli-Protocol-Version': '2.0.0',
27
27
  };
28
28
  }
@@ -155,20 +155,22 @@ async function fetchPostAnalytics(token, postUrn) {
155
155
  async function fetchLinkedInProfile(token) {
156
156
  if (!token) throw new Error('fetchLinkedInProfile: token is required');
157
157
 
158
- const url = `${LI_V2_BASE}/me`;
159
-
160
- const res = await fetch(url, { headers: buildV2Headers(token) });
161
- if (!res.ok) {
162
- const body = await res.text().catch(() => '');
163
- throw new Error(`fetchLinkedInProfile HTTP ${res.status}: ${body}`);
164
- }
165
-
166
- const json = await res.json();
167
- if (json.serviceErrorCode) {
168
- throw new Error(`fetchLinkedInProfile API error: ${JSON.stringify(json)}`);
158
+ // Try /v2/userinfo first (works with openid+profile scopes), fall back to /v2/me.
159
+ for (const url of [`${LI_V2_BASE}/userinfo`, `${LI_V2_BASE}/me`]) {
160
+ const res = await fetch(url, { headers: buildV2Headers(token) });
161
+ if (!res.ok) {
162
+ const body = await res.text().catch(() => '');
163
+ console.warn(`[linkedin] ${url} HTTP ${res.status}: ${body.slice(0, 120)}`);
164
+ continue;
165
+ }
166
+ const json = await res.json();
167
+ if (json.serviceErrorCode) {
168
+ console.warn(`[linkedin] ${url} API error: ${JSON.stringify(json).slice(0, 120)}`);
169
+ continue;
170
+ }
171
+ return json;
169
172
  }
170
-
171
- return json;
173
+ throw new Error('fetchLinkedInProfile: all endpoints failed');
172
174
  }
173
175
 
174
176
  /**
@@ -237,8 +239,16 @@ async function pollLinkedIn(db) {
237
239
 
238
240
  console.log(`[linkedin] Starting poll for ${personUrn}`);
239
241
 
240
- const posts = await fetchLinkedInPosts(token, personUrn);
241
- console.log(`[linkedin] Got ${posts.length} posts`);
242
+ let posts = [];
243
+ try {
244
+ posts = await fetchLinkedInPosts(token, personUrn);
245
+ console.log(`[linkedin] Got ${posts.length} posts`);
246
+ } catch (err) {
247
+ // 403 = token lacks r_member_social scope; 426 = version expired.
248
+ // Either way, skip posts but still collect follower count.
249
+ console.warn(`[linkedin] Posts fetch failed (non-fatal): ${err.message}`);
250
+ console.warn('[linkedin] To read posts, re-authorize with r_member_social scope.');
251
+ }
242
252
 
243
253
  for (const post of posts) {
244
254
  // Post URN is at post.id for the Posts API (urn:li:share:... or urn:li:ugcPost:...).
@@ -22,7 +22,7 @@ const LI_REST_BASE = 'https://api.linkedin.com/rest';
22
22
  function buildHeaders(token) {
23
23
  return {
24
24
  Authorization: `Bearer ${token}`,
25
- 'LinkedIn-Version': '202401',
25
+ 'LinkedIn-Version': '202601',
26
26
  'X-Restli-Protocol-Version': '2.0.0',
27
27
  'Content-Type': 'application/json',
28
28
  };
@@ -80,6 +80,43 @@ function requireApiKey() {
80
80
  return key;
81
81
  }
82
82
 
83
+ class ZernioQuotaError extends Error {
84
+ constructor(message, details = {}) {
85
+ super(message);
86
+ this.name = 'ZernioQuotaError';
87
+ this.code = 'ZERNIO_POST_LIMIT_REACHED';
88
+ this.billingPeriod = details.billingPeriod || null;
89
+ this.current = Number.isFinite(details.current) ? details.current : null;
90
+ this.endpoint = details.endpoint || null;
91
+ this.limit = Number.isFinite(details.limit) ? details.limit : null;
92
+ this.method = details.method || null;
93
+ this.planName = details.planName || null;
94
+ this.status = details.status || null;
95
+ }
96
+ }
97
+
98
+ function parseZernioErrorText(errorText) {
99
+ if (!errorText || typeof errorText !== 'string') return null;
100
+ try {
101
+ const parsed = JSON.parse(errorText);
102
+ return parsed && typeof parsed === 'object' ? parsed : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ function isZernioQuotaError(error) {
109
+ return Boolean(
110
+ error &&
111
+ (error instanceof ZernioQuotaError || error.code === 'ZERNIO_POST_LIMIT_REACHED')
112
+ );
113
+ }
114
+
115
+ function isZernioQuotaPayload(status, payload, errorText) {
116
+ const message = String(payload?.error || payload?.message || errorText || '');
117
+ return status === 403 && /post limit reached/i.test(message);
118
+ }
119
+
83
120
  function resolveAccountId(account) {
84
121
  if (!account || typeof account !== 'object') {
85
122
  return '';
@@ -176,6 +213,18 @@ async function zernioFetch(method, endpoint, body = null) {
176
213
 
177
214
  if (!res.ok) {
178
215
  const errorText = await res.text().catch(() => '');
216
+ const payload = parseZernioErrorText(errorText);
217
+ if (isZernioQuotaPayload(res.status, payload, errorText)) {
218
+ throw new ZernioQuotaError(payload?.error || 'Zernio post limit reached', {
219
+ billingPeriod: payload?.billingPeriod,
220
+ current: Number(payload?.current),
221
+ endpoint,
222
+ limit: Number(payload?.limit),
223
+ method,
224
+ planName: payload?.planName,
225
+ status: res.status,
226
+ });
227
+ }
179
228
  throw new Error(`Zernio API ${res.status} for ${method} ${endpoint}: ${errorText}`);
180
229
  }
181
230
 
@@ -434,7 +483,9 @@ module.exports = {
434
483
  buildDedupKey,
435
484
  deletePost,
436
485
  isDuplicate,
486
+ isZernioQuotaError,
437
487
  listPosts,
488
+ ZernioQuotaError,
438
489
  publishPost,
439
490
  recordPost,
440
491
  schedulePost,
@@ -8,6 +8,7 @@ const net = require('net');
8
8
  const os = require('os');
9
9
  const path = require('path');
10
10
  const { pathToFileURL } = require('url');
11
+ const { ensureDir } = require('./fs-utils');
11
12
 
12
13
  const REPO_ROOT = path.resolve(__dirname, '..');
13
14
  const DEFAULT_ASSET_HTML = path.join(
@@ -77,9 +78,6 @@ function parseArgs(argv) {
77
78
  return args;
78
79
  }
79
80
 
80
- function ensureDir(dirPath) {
81
- fs.mkdirSync(dirPath, { recursive: true });
82
- }
83
81
 
84
82
  function readText(filePath) {
85
83
  return fs.readFileSync(filePath, 'utf8');
@@ -25,7 +25,11 @@
25
25
  require('dotenv').config();
26
26
 
27
27
  const { generateWeeklyStatsPost } = require('./daily-digest');
28
- const { publishPost, getConnectedAccounts } = require('./social-analytics/publishers/zernio');
28
+ const {
29
+ getConnectedAccounts,
30
+ isZernioQuotaError,
31
+ publishPost,
32
+ } = require('./social-analytics/publishers/zernio');
29
33
 
30
34
  // Platforms that support text-only posts.
31
35
  // Reddit EXCLUDED β€” engagement only via reply-monitor, not auto-posting.
@@ -179,7 +183,46 @@ async function main() {
179
183
  }
180
184
  }
181
185
 
182
- main().catch(err => {
186
+ function isNonFatalPostFailure(err) {
187
+ return isZernioQuotaError(err);
188
+ }
189
+
190
+ function handlePostFailure(err) {
191
+ if (isNonFatalPostFailure(err)) {
192
+ console.warn(`[daily-post] Skipped: ${err.message}`);
193
+ console.warn('[daily-post] Zernio monthly post quota reached; treating as a controlled skip.');
194
+ return 0;
195
+ }
196
+
183
197
  console.error('[daily-post] Fatal:', err.message);
184
- process.exit(1);
185
- });
198
+ return 1;
199
+ }
200
+
201
+ function runCli({ run = main, exit = process.exit } = {}) {
202
+ return run().catch(err => {
203
+ const exitCode = handlePostFailure(err);
204
+ if (exitCode !== 0) {
205
+ exit(exitCode);
206
+ }
207
+ });
208
+ }
209
+
210
+ function isCliEntrypoint(entryModule = require.main) {
211
+ return Boolean(entryModule && entryModule.filename === __filename);
212
+ }
213
+
214
+ if (isCliEntrypoint()) {
215
+ void runCli();
216
+ }
217
+
218
+ module.exports = {
219
+ DAILY_ANGLES,
220
+ TEXT_PLATFORMS,
221
+ generatePost,
222
+ getTodayAngle,
223
+ handlePostFailure,
224
+ isCliEntrypoint,
225
+ isNonFatalPostFailure,
226
+ main,
227
+ runCli,
228
+ };
@@ -11,6 +11,7 @@ const { getHomeDir, getRuntimeDir, resolveProjectDir } = require('./feedback-pat
11
11
  const { resolveProKey } = require('./pro-local-dashboard');
12
12
 
13
13
  const DEFAULT_ORIGIN = 'http://localhost:3456';
14
+ const PROD_ORIGIN = 'https://thumbgate-production.up.railway.app';
14
15
  const DEFAULT_TIMEOUT_MS = 150;
15
16
  const DEFAULT_BOOT_GRACE_MS = 5000;
16
17
  const PKG_ROOT = path.join(__dirname, '..');
@@ -153,8 +154,8 @@ function buildLinkState({
153
154
  lessonsLabel: 'Lessons…',
154
155
  upLabel: 'πŸ‘',
155
156
  downLabel: 'πŸ‘Ž',
156
- dashboardUrl: '',
157
- lessonsUrl: '',
157
+ dashboardUrl: `${PROD_ORIGIN}/dashboard`,
158
+ lessonsUrl: `${PROD_ORIGIN}/lessons`,
158
159
  upUrl: '',
159
160
  downUrl: '',
160
161
  };
@@ -163,11 +164,11 @@ function buildLinkState({
163
164
  return {
164
165
  state: canBootstrap ? 'offline' : 'unavailable',
165
166
  dashboardLabel: canBootstrap ? 'Dash: thumbgate pro' : 'Dashboard',
166
- lessonsLabel: 'Learn: thumbgate lessons',
167
+ lessonsLabel: 'Lessons',
167
168
  upLabel: 'πŸ‘',
168
169
  downLabel: 'πŸ‘Ž',
169
- dashboardUrl: '',
170
- lessonsUrl: '',
170
+ dashboardUrl: `${PROD_ORIGIN}/dashboard`,
171
+ lessonsUrl: `${PROD_ORIGIN}/lessons`,
171
172
  upUrl: '',
172
173
  downUrl: '',
173
174
  };
@@ -3,8 +3,10 @@
3
3
 
4
4
  const { analyzeFeedback } = require('./feedback-loop');
5
5
  const { normalizeStatsPayload } = require('./hook-thumbgate-cache-updater');
6
+ const { syncClaudeHistoryFeedback } = require('./claude-feedback-sync');
6
7
 
7
8
  try {
9
+ syncClaudeHistoryFeedback();
8
10
  const stats = analyzeFeedback();
9
11
  const payload = {
10
12
  ...normalizeStatsPayload(stats),
@@ -11,9 +11,18 @@ LOCAL_API_ORIGIN="${THUMBGATE_LOCAL_API_ORIGIN:-http://localhost:3456}"
11
11
  # ── Parse Claude Code session JSON from stdin ─────────────────────
12
12
  eval "$(cat | jq -r '
13
13
  def n(f): f // 0;
14
- @sh "CTX_PCT=\(n(.context_window.used_percentage) | floor)"
14
+ @sh "CTX_PCT=\(n(.context_window.used_percentage) | floor)",
15
+ @sh "PROJECT_CWD=\(.cwd // .working_directory // "")"
15
16
  ' 2>/dev/null)"
16
17
  CTX_PCT="${CTX_PCT:-0}"
18
+ PROJECT_CWD="${PROJECT_CWD:-}"
19
+
20
+ if [ -n "$PROJECT_CWD" ] && [ -d "$PROJECT_CWD" ]; then
21
+ export THUMBGATE_PROJECT_DIR="$PROJECT_CWD"
22
+ if [ -z "${THUMBGATE_FEEDBACK_DIR:-}" ]; then
23
+ export THUMBGATE_FEEDBACK_DIR="${PROJECT_CWD}/.claude/memory/feedback"
24
+ fi
25
+ fi
17
26
 
18
27
  # ── ThumbGate stats from cache ────────────────────────────────────────
19
28
  THUMBGATE_CACHE=""
@@ -117,6 +126,18 @@ if [ -n "$_TOWER_JSON" ]; then
117
126
  ' 2>/dev/null)"
118
127
  fi
119
128
 
129
+ # ── Latest lesson (data available for extensions; not rendered in statusbar) ──
130
+ LESSON_TEXT=""; LESSON_ID=""; LESSON_LABEL=""; LESSON_LINK=""
131
+ _LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
132
+ if [ -n "$_LESSON_JSON" ]; then
133
+ eval "$(echo "$_LESSON_JSON" | jq -r '
134
+ @sh "LESSON_TEXT=\(.text // "")",
135
+ @sh "LESSON_ID=\(.lessonId // "")",
136
+ @sh "LESSON_LABEL=\(.label // "")",
137
+ @sh "LESSON_LINK=\(.link // "")"
138
+ ' 2>/dev/null)"
139
+ fi
140
+
120
141
  # ── Colors ────────────────────────────────────────────────────────
121
142
  G='\033[32m'; R='\033[31m'; M='\033[35m'; C='\033[36m'; D='\033[90m'; BD='\033[1m'; RST='\033[0m'
122
143
 
@@ -125,25 +146,34 @@ case "${TREND}" in
125
146
  improving) ARROW="β†—" ;; degrading) ARROW="β†˜" ;; stable) ARROW="β†’" ;; *) ARROW="?" ;;
126
147
  esac
127
148
 
128
- osc8_link() {
149
+ inline_link() {
129
150
  local url="$1"
130
151
  local label="$2"
131
152
  if [ -n "$url" ]; then
132
- printf '\033]8;;%s\a%s\033]8;;\a' "$url" "$label"
153
+ printf '%s (%s)' "$label" "$url"
133
154
  else
134
155
  printf '%s' "$label"
135
156
  fi
136
157
  }
137
158
 
138
- UP_ICON="$(osc8_link "$UP_URL" "πŸ‘")"
139
- DOWN_ICON="$(osc8_link "$DOWN_URL" "πŸ‘Ž")"
140
- DASHBOARD_LINK="$(osc8_link "$DASHBOARD_URL" "$DASHBOARD_LABEL")"
141
- LESSONS_LINK="$(osc8_link "$LESSONS_URL" "$LESSONS_LABEL")"
159
+ UP_ICON="πŸ‘"
160
+ DOWN_ICON="πŸ‘Ž"
161
+ DASHBOARD_LINK="$DASHBOARD_LABEL"
162
+ LESSONS_LINK="$LESSONS_LABEL"
163
+ LATEST_LESSON_LINK=""
164
+ if [ -n "$LESSON_LABEL" ]; then
165
+ if [ -n "$LESSON_TEXT" ]; then
166
+ LATEST_LESSON_LINK="$(inline_link "$LESSON_LINK" "${LESSON_LABEL}: ${LESSON_TEXT}")"
167
+ else
168
+ LATEST_LESSON_LINK="$(inline_link "$LESSON_LINK" "$LESSON_LABEL")"
169
+ fi
170
+ fi
142
171
 
143
172
  # ── Output (single line) ─────────────────────────────────────────
144
173
  LINE="ThumbGate v${TG_VERSION} Β· ${TG_TIER}"
145
174
  if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
146
175
  LINE="${D}${LINE} Β· no feedback yet${RST} Β· ${C}${DASHBOARD_LINK}${RST} Β· ${M}${LESSONS_LINK}${RST}"
176
+ [ -n "$LATEST_LESSON_LINK" ] && LINE="${LINE} Β· ${D}${LATEST_LESSON_LINK}${RST}"
147
177
  printf '%b\n' "$LINE"
148
178
  else
149
179
  LINE="${LINE} Β· ${G}${BD}${UP}${RST}${UP_ICON} ${R}${BD}${DOWN}${RST}${DOWN_ICON} ${ARROW}"
@@ -153,6 +183,7 @@ else
153
183
  [ "${AT_RISK:-0}" -gt 0 ] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
154
184
  [ "${ANOMALIES:-0}" -gt 0 ] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
155
185
  LINE="${LINE} Β· ${C}${DASHBOARD_LINK}${RST} Β· ${M}${LESSONS_LINK}${RST}"
186
+ [ -n "$LATEST_LESSON_LINK" ] && LINE="${LINE} Β· ${D}${LATEST_LESSON_LINK}${RST}"
156
187
 
157
188
  printf '%b\n' "$LINE"
158
189
  fi