thumbgate 1.3.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/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 +109 -20
- 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 +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +200 -13
- 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/openapi/openapi.yaml +168 -0
- package/package.json +42 -10
- 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/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 +10 -2
- package/public/guide.html +2 -2
- 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 +136 -50
- package/public/lessons.html +33 -24
- 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 +1 -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/check-congruence.js +132 -14
- 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 +1 -21
- package/scripts/dashboard.js +20 -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 +1 -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 +58 -1
- 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/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/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- 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 +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- 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 +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +3 -1
- 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.sh +29 -153
- 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 +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- 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
|
};
|
package/scripts/statusline.sh
CHANGED
|
@@ -126,13 +126,15 @@ if [ -n "$_TOWER_JSON" ]; then
|
|
|
126
126
|
' 2>/dev/null)"
|
|
127
127
|
fi
|
|
128
128
|
|
|
129
|
-
# โโ Latest lesson
|
|
130
|
-
LESSON_TEXT=""; LESSON_ID=""
|
|
129
|
+
# โโ Latest lesson (data available for extensions; not rendered in statusbar) โโ
|
|
130
|
+
LESSON_TEXT=""; LESSON_ID=""; LESSON_LABEL=""; LESSON_LINK=""
|
|
131
131
|
_LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
|
|
132
132
|
if [ -n "$_LESSON_JSON" ]; then
|
|
133
133
|
eval "$(echo "$_LESSON_JSON" | jq -r '
|
|
134
134
|
@sh "LESSON_TEXT=\(.text // "")",
|
|
135
|
-
@sh "LESSON_ID=\(.lessonId // "")"
|
|
135
|
+
@sh "LESSON_ID=\(.lessonId // "")",
|
|
136
|
+
@sh "LESSON_LABEL=\(.label // "")",
|
|
137
|
+
@sh "LESSON_LINK=\(.link // "")"
|
|
136
138
|
' 2>/dev/null)"
|
|
137
139
|
fi
|
|
138
140
|
|
|
@@ -144,170 +146,44 @@ case "${TREND}" in
|
|
|
144
146
|
improving) ARROW="โ" ;; degrading) ARROW="โ" ;; stable) ARROW="โ" ;; *) ARROW="?" ;;
|
|
145
147
|
esac
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
inline_link() {
|
|
148
150
|
local url="$1"
|
|
149
151
|
local label="$2"
|
|
150
152
|
if [ -n "$url" ]; then
|
|
151
|
-
printf '
|
|
153
|
+
printf '%s (%s)' "$label" "$url"
|
|
152
154
|
else
|
|
153
155
|
printf '%s' "$label"
|
|
154
156
|
fi
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
UP_ICON="
|
|
158
|
-
DOWN_ICON="
|
|
159
|
-
DASHBOARD_LINK="$
|
|
160
|
-
LESSONS_LINK="$
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
*) return 0 ;;
|
|
166
|
-
esac
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Keep ThumbGate within a conservative left-side budget so Claude's own
|
|
170
|
-
# right-side notices do not visually collide with our line.
|
|
171
|
-
STATUSLINE_DEFAULT_MAX_CHARS="${THUMBGATE_STATUSLINE_DEFAULT_MAX_CHARS:-96}"
|
|
172
|
-
STATUSLINE_RIGHT_RESERVE="${THUMBGATE_STATUSLINE_RIGHT_RESERVE:-28}"
|
|
173
|
-
if ! is_numeric "$STATUSLINE_DEFAULT_MAX_CHARS"; then STATUSLINE_DEFAULT_MAX_CHARS=96; fi
|
|
174
|
-
if ! is_numeric "$STATUSLINE_RIGHT_RESERVE"; then STATUSLINE_RIGHT_RESERVE=28; fi
|
|
175
|
-
|
|
176
|
-
if is_numeric "${THUMBGATE_STATUSLINE_MAX_CHARS:-}"; then
|
|
177
|
-
STATUSLINE_MAX_CHARS="$THUMBGATE_STATUSLINE_MAX_CHARS"
|
|
178
|
-
else
|
|
179
|
-
STATUSLINE_MAX_CHARS="$STATUSLINE_DEFAULT_MAX_CHARS"
|
|
180
|
-
if is_numeric "${COLUMNS:-}"; then
|
|
181
|
-
_AVAILABLE_CHARS=$(( COLUMNS - STATUSLINE_RIGHT_RESERVE ))
|
|
182
|
-
if [ "$_AVAILABLE_CHARS" -gt 0 ] && [ "$_AVAILABLE_CHARS" -lt "$STATUSLINE_MAX_CHARS" ]; then
|
|
183
|
-
STATUSLINE_MAX_CHARS="$_AVAILABLE_CHARS"
|
|
184
|
-
fi
|
|
185
|
-
fi
|
|
186
|
-
fi
|
|
187
|
-
if [ "$STATUSLINE_MAX_CHARS" -lt 48 ]; then STATUSLINE_MAX_CHARS=48; fi
|
|
188
|
-
|
|
189
|
-
PLAIN_SEGMENTS=()
|
|
190
|
-
RENDERED_SEGMENTS=()
|
|
191
|
-
|
|
192
|
-
current_plain_length() {
|
|
193
|
-
local total=0
|
|
194
|
-
local i
|
|
195
|
-
for ((i = 0; i < ${#PLAIN_SEGMENTS[@]}; i++)); do
|
|
196
|
-
if [ "$i" -gt 0 ]; then
|
|
197
|
-
total=$((total + 3))
|
|
198
|
-
fi
|
|
199
|
-
total=$((total + ${#PLAIN_SEGMENTS[$i]}))
|
|
200
|
-
done
|
|
201
|
-
printf '%s' "$total"
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
push_segment() {
|
|
205
|
-
PLAIN_SEGMENTS+=("$1")
|
|
206
|
-
RENDERED_SEGMENTS+=("$2")
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
add_segment_if_fit() {
|
|
210
|
-
local plain="$1"
|
|
211
|
-
local rendered="$2"
|
|
212
|
-
local current extra
|
|
213
|
-
current=$(current_plain_length)
|
|
214
|
-
extra=${#plain}
|
|
215
|
-
if [ "${#PLAIN_SEGMENTS[@]}" -gt 0 ]; then
|
|
216
|
-
extra=$((extra + 3))
|
|
217
|
-
fi
|
|
218
|
-
if [ $((current + extra)) -le "$STATUSLINE_MAX_CHARS" ]; then
|
|
219
|
-
push_segment "$plain" "$rendered"
|
|
220
|
-
return 0
|
|
221
|
-
fi
|
|
222
|
-
return 1
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
truncate_plain_text() {
|
|
226
|
-
local text="$1"
|
|
227
|
-
local max_chars="$2"
|
|
228
|
-
if [ "$max_chars" -le 0 ]; then
|
|
229
|
-
printf ''
|
|
230
|
-
elif [ "${#text}" -le "$max_chars" ]; then
|
|
231
|
-
printf '%s' "$text"
|
|
232
|
-
elif [ "$max_chars" -le 3 ]; then
|
|
233
|
-
printf '%.*s' "$max_chars" "$text"
|
|
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}")"
|
|
234
167
|
else
|
|
235
|
-
|
|
236
|
-
fi
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
add_truncated_segment_if_fit() {
|
|
240
|
-
local plain="$1"
|
|
241
|
-
local color="$2"
|
|
242
|
-
local min_chars="${3:-14}"
|
|
243
|
-
local current sep remaining truncated
|
|
244
|
-
current=$(current_plain_length)
|
|
245
|
-
sep=0
|
|
246
|
-
if [ "${#PLAIN_SEGMENTS[@]}" -gt 0 ]; then
|
|
247
|
-
sep=3
|
|
248
|
-
fi
|
|
249
|
-
remaining=$((STATUSLINE_MAX_CHARS - current - sep))
|
|
250
|
-
if [ "$remaining" -lt "$min_chars" ]; then
|
|
251
|
-
return 1
|
|
168
|
+
LATEST_LESSON_LINK="$(inline_link "$LESSON_LINK" "$LESSON_LABEL")"
|
|
252
169
|
fi
|
|
253
|
-
|
|
254
|
-
push_segment "$truncated" "${color}${truncated}${RST}"
|
|
255
|
-
return 0
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
render_segments() {
|
|
259
|
-
local line=''
|
|
260
|
-
local i
|
|
261
|
-
for ((i = 0; i < ${#RENDERED_SEGMENTS[@]}; i++)); do
|
|
262
|
-
if [ "$i" -gt 0 ]; then
|
|
263
|
-
line="${line} ยท "
|
|
264
|
-
fi
|
|
265
|
-
line="${line}${RENDERED_SEGMENTS[$i]}"
|
|
266
|
-
done
|
|
267
|
-
printf '%b\n' "$line"
|
|
268
|
-
}
|
|
170
|
+
fi
|
|
269
171
|
|
|
270
172
|
# โโ Output (single line) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
173
|
+
LINE="ThumbGate v${TG_VERSION} ยท ${TG_TIER}"
|
|
271
174
|
if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
add_segment_if_fit "${DASHBOARD_LABEL}" "${C}${DASHBOARD_LINK}${RST}"
|
|
276
|
-
add_segment_if_fit "${LESSONS_LABEL}" "${M}${LESSONS_LINK}${RST}"
|
|
277
|
-
render_segments
|
|
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}"
|
|
177
|
+
printf '%b\n' "$LINE"
|
|
278
178
|
else
|
|
279
|
-
|
|
280
|
-
STATS_RENDERED="${G}${BD}${UP}${RST}${UP_ICON} ${R}${BD}${DOWN}${RST}${DOWN_ICON} ${ARROW}"
|
|
281
|
-
ALERTS_PLAIN=''
|
|
282
|
-
ALERTS_RENDERED=''
|
|
283
|
-
|
|
284
|
-
if [ "${SLO_V:-0}" -gt 0 ]; then
|
|
285
|
-
ALERTS_PLAIN="${ALERTS_PLAIN}${ALERTS_PLAIN:+ }${SLO_V} SLO"
|
|
286
|
-
ALERTS_RENDERED="${ALERTS_RENDERED}${ALERTS_RENDERED:+ }${R}${SLO_V} SLO${RST}"
|
|
287
|
-
fi
|
|
288
|
-
if [ "${AT_RISK:-0}" -gt 0 ]; then
|
|
289
|
-
ALERTS_PLAIN="${ALERTS_PLAIN}${ALERTS_PLAIN:+ }${AT_RISK}โ "
|
|
290
|
-
ALERTS_RENDERED="${ALERTS_RENDERED}${ALERTS_RENDERED:+ }${R}${AT_RISK}โ ${RST}"
|
|
291
|
-
fi
|
|
292
|
-
if [ "${ANOMALIES:-0}" -gt 0 ]; then
|
|
293
|
-
ALERTS_PLAIN="${ALERTS_PLAIN}${ALERTS_PLAIN:+ }${ANOMALIES}โ "
|
|
294
|
-
ALERTS_RENDERED="${ALERTS_RENDERED}${ALERTS_RENDERED:+ }${R}${ANOMALIES}โ ${RST}"
|
|
295
|
-
fi
|
|
179
|
+
LINE="${LINE} ยท ${G}${BD}${UP}${RST}${UP_ICON} ${R}${BD}${DOWN}${RST}${DOWN_ICON} ${ARROW}"
|
|
296
180
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
add_segment_if_fit "${LESSONS} lessons" "${M}${BD}${LESSONS}${RST} lessons"
|
|
304
|
-
fi
|
|
305
|
-
if [ -n "${ALERTS_PLAIN}" ]; then
|
|
306
|
-
add_segment_if_fit "${ALERTS_PLAIN}" "${ALERTS_RENDERED}"
|
|
307
|
-
fi
|
|
308
|
-
if [ -n "${LESSON_TEXT}" ]; then
|
|
309
|
-
add_truncated_segment_if_fit "${LESSON_TEXT}" "${D}" 14
|
|
310
|
-
fi
|
|
181
|
+
# Control Tower alerts (if any)
|
|
182
|
+
[ "${SLO_V:-0}" -gt 0 ] && LINE="${LINE} ${R}${SLO_V} SLO${RST}"
|
|
183
|
+
[ "${AT_RISK:-0}" -gt 0 ] && LINE="${LINE} ${R}${AT_RISK}โ ${RST}"
|
|
184
|
+
[ "${ANOMALIES:-0}" -gt 0 ] && LINE="${LINE} ${R}${ANOMALIES}โ ${RST}"
|
|
185
|
+
LINE="${LINE} ยท ${C}${DASHBOARD_LINK}${RST} ยท ${M}${LESSONS_LINK}${RST}"
|
|
186
|
+
[ -n "$LATEST_LESSON_LINK" ] && LINE="${LINE} ยท ${D}${LATEST_LESSON_LINK}${RST}"
|
|
311
187
|
|
|
312
|
-
|
|
188
|
+
printf '%b\n' "$LINE"
|
|
313
189
|
fi
|