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.
- package/.claude-plugin/README.md +4 -4
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +133 -23
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +85 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +215 -19
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/config/model-tiers.json +11 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -13
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +12 -4
- package/public/guide.html +5 -5
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +169 -70
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +334 -17
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +7 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +133 -15
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +33 -44
- package/scripts/dashboard.js +104 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +17 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +753 -0
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/model-tier-router.js +10 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +369 -34
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +2 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +3 -5
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +326 -0
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-runtime.js +13 -0
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +6 -4
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +38 -7
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +108 -4
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +205 -4
- package/skills/thumbgate/SKILL.md +2 -2
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /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': '
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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': '
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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),
|
package/scripts/statusline.sh
CHANGED
|
@@ -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
|
-
|
|
149
|
+
inline_link() {
|
|
129
150
|
local url="$1"
|
|
130
151
|
local label="$2"
|
|
131
152
|
if [ -n "$url" ]; then
|
|
132
|
-
printf '
|
|
153
|
+
printf '%s (%s)' "$label" "$url"
|
|
133
154
|
else
|
|
134
155
|
printf '%s' "$label"
|
|
135
156
|
fi
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
UP_ICON="
|
|
139
|
-
DOWN_ICON="
|
|
140
|
-
DASHBOARD_LINK="$
|
|
141
|
-
LESSONS_LINK="$
|
|
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
|