thumbgate 1.2.0 → 1.3.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +35 -14
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +2 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +20 -11
- package/config/github-about.json +1 -1
- package/config/model-tiers.json +11 -0
- package/package.json +8 -6
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- 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/compare.html +4 -4
- package/public/guide.html +4 -4
- package/public/index.html +51 -38
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +325 -17
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/audit-trail.js +6 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/contextfs.js +32 -23
- package/scripts/dashboard.js +84 -0
- package/scripts/feedback-loop.js +16 -0
- package/scripts/intervention-policy.js +696 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/model-tier-router.js +10 -1
- package/scripts/operational-integrity.js +354 -31
- package/scripts/prove-adapters.js +1 -0
- package/scripts/prove-automation.js +2 -2
- package/scripts/prove-packaged-runtime.js +260 -0
- package/scripts/prove-runtime.js +13 -0
- package/scripts/rate-limiter.js +3 -3
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +166 -11
- package/scripts/tool-registry.js +2 -2
- package/scripts/workflow-sentinel.js +114 -4
- package/skills/thumbgate/SKILL.md +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.join(__dirname, '..');
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
13
|
+
const STATUSLINE_INPUT = JSON.stringify({ context_window: { used_percentage: 12 } });
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
16
|
+
const parsed = {};
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const token = argv[index];
|
|
19
|
+
if (!token.startsWith('--')) continue;
|
|
20
|
+
const [rawKey, inlineValue] = token.slice(2).split('=');
|
|
21
|
+
const key = rawKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
22
|
+
const value = inlineValue !== undefined ? inlineValue : argv[index + 1];
|
|
23
|
+
parsed[key] = value;
|
|
24
|
+
if (inlineValue === undefined) index += 1;
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pkgVersion() {
|
|
30
|
+
return require(path.join(ROOT, 'package.json')).version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function packCurrentRepo(packDir) {
|
|
34
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
35
|
+
const output = execFileSync('npm', ['pack', '--json', '--pack-destination', packDir], {
|
|
36
|
+
cwd: ROOT,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
const parsed = JSON.parse(output);
|
|
41
|
+
const fileName = parsed && parsed[0] && parsed[0].filename;
|
|
42
|
+
if (!fileName) {
|
|
43
|
+
throw new Error('npm pack did not return a tarball filename');
|
|
44
|
+
}
|
|
45
|
+
return path.join(packDir, fileName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function installPackage(prefixDir, packageSpec) {
|
|
49
|
+
fs.mkdirSync(prefixDir, { recursive: true });
|
|
50
|
+
execFileSync('npm', ['install', '--prefix', prefixDir, '--no-fund', '--no-audit', packageSpec], {
|
|
51
|
+
cwd: ROOT,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
return path.join(prefixDir, 'node_modules', '.bin', 'thumbgate');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function request(url, timeoutMs = 2000) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const req = http.get(url, (res) => {
|
|
61
|
+
let body = '';
|
|
62
|
+
res.setEncoding('utf8');
|
|
63
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
64
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
|
|
65
|
+
});
|
|
66
|
+
req.on('error', reject);
|
|
67
|
+
req.setTimeout(timeoutMs, () => {
|
|
68
|
+
req.destroy(new Error(`Timed out requesting ${url}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sleep(ms) {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getAvailablePort() {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const server = net.createServer();
|
|
80
|
+
server.listen(0, '127.0.0.1', () => {
|
|
81
|
+
const address = server.address();
|
|
82
|
+
const port = address && address.port;
|
|
83
|
+
server.close((error) => {
|
|
84
|
+
if (error) {
|
|
85
|
+
reject(error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
resolve(port);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
server.on('error', reject);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function waitForHealthy(origin, expectedVersion, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
98
|
+
try {
|
|
99
|
+
const response = await request(`${origin}/health`);
|
|
100
|
+
if (response.statusCode === 200) {
|
|
101
|
+
const body = JSON.parse(response.body);
|
|
102
|
+
if (body.version === expectedVersion) {
|
|
103
|
+
return body;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Keep polling until the detached API server comes online.
|
|
108
|
+
}
|
|
109
|
+
await sleep(250);
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Timed out waiting for packaged runtime health at ${origin}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderStatusline(runtimeBin, projectDir, env) {
|
|
115
|
+
return execFileSync(runtimeBin, ['statusline-render'], {
|
|
116
|
+
cwd: projectDir,
|
|
117
|
+
env,
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
input: STATUSLINE_INPUT,
|
|
120
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
121
|
+
timeout: 10000,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runtimeStatePath(homeDir) {
|
|
126
|
+
return path.join(homeDir, '.thumbgate', 'runtime', 'statusline-api.json');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function stopDetachedRuntime(homeDir) {
|
|
130
|
+
try {
|
|
131
|
+
const state = JSON.parse(fs.readFileSync(runtimeStatePath(homeDir), 'utf8'));
|
|
132
|
+
const pid = Number(state && state.pid);
|
|
133
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
134
|
+
try {
|
|
135
|
+
process.kill(pid, 'SIGTERM');
|
|
136
|
+
} catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
140
|
+
await sleep(100);
|
|
141
|
+
try {
|
|
142
|
+
process.kill(pid, 0);
|
|
143
|
+
} catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, 'SIGKILL');
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore cleanup races.
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// No runtime state means nothing to clean up.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runPackagedRuntimeSmoke(options = {}) {
|
|
158
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-packaged-runtime-'));
|
|
159
|
+
const homeDir = path.join(tempRoot, 'home');
|
|
160
|
+
const projectDir = path.join(tempRoot, 'project');
|
|
161
|
+
const packDir = path.join(tempRoot, 'pack');
|
|
162
|
+
const runtimeDir = path.join(tempRoot, 'runtime');
|
|
163
|
+
const expectedVersion = options.expectedVersion || pkgVersion();
|
|
164
|
+
const feedbackDir = path.join(projectDir, '.thumbgate');
|
|
165
|
+
fs.mkdirSync(homeDir, { recursive: true });
|
|
166
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
167
|
+
fs.mkdirSync(feedbackDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(
|
|
169
|
+
path.join(feedbackDir, 'feedback-log.jsonl'),
|
|
170
|
+
[
|
|
171
|
+
JSON.stringify({ signal: 'positive', timestamp: '2026-04-08T20:00:00.000Z', context: 'packaged runtime smoke pass' }),
|
|
172
|
+
JSON.stringify({ signal: 'negative', timestamp: '2026-04-08T20:01:00.000Z', context: 'packaged runtime smoke fail path' }),
|
|
173
|
+
].join('\n') + '\n'
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const packageSpec = options.packageSpec || packCurrentRepo(packDir);
|
|
178
|
+
const runtimeBin = installPackage(runtimeDir, packageSpec);
|
|
179
|
+
if (!fs.existsSync(runtimeBin)) {
|
|
180
|
+
throw new Error(`Installed runtime binary is missing: ${runtimeBin}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const port = await getAvailablePort();
|
|
184
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
185
|
+
const env = {
|
|
186
|
+
...process.env,
|
|
187
|
+
HOME: homeDir,
|
|
188
|
+
THUMBGATE_PROJECT_DIR: projectDir,
|
|
189
|
+
THUMBGATE_LOCAL_API_ORIGIN: origin,
|
|
190
|
+
THUMBGATE_API_KEY: 'tg_packaged_runtime_smoke',
|
|
191
|
+
NO_COLOR: '1',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const initialStatusline = renderStatusline(runtimeBin, projectDir, env);
|
|
195
|
+
if (!initialStatusline.includes(`ThumbGate v${expectedVersion}`)) {
|
|
196
|
+
throw new Error(`Statusline version mismatch before boot: ${initialStatusline.trim()}`);
|
|
197
|
+
}
|
|
198
|
+
if (!/(Dashboard|Dashboard…)/.test(initialStatusline) || !/(Lessons|Lessons…)/.test(initialStatusline)) {
|
|
199
|
+
throw new Error(`Statusline missing dashboard affordances before boot: ${initialStatusline.trim()}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const health = await waitForHealthy(origin, expectedVersion, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
203
|
+
const dashboard = await request(`${origin}/dashboard`);
|
|
204
|
+
const lessons = await request(`${origin}/lessons`);
|
|
205
|
+
if (dashboard.statusCode !== 200) {
|
|
206
|
+
throw new Error(`Packaged dashboard returned ${dashboard.statusCode}`);
|
|
207
|
+
}
|
|
208
|
+
if (lessons.statusCode !== 200) {
|
|
209
|
+
throw new Error(`Packaged lessons returned ${lessons.statusCode}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const readyStatusline = renderStatusline(runtimeBin, projectDir, env);
|
|
213
|
+
if (!readyStatusline.includes(`${origin}/dashboard`)) {
|
|
214
|
+
throw new Error(`Ready statusline missing dashboard URL: ${readyStatusline.trim()}`);
|
|
215
|
+
}
|
|
216
|
+
if (!readyStatusline.includes(`${origin}/lessons`)) {
|
|
217
|
+
throw new Error(`Ready statusline missing lessons URL: ${readyStatusline.trim()}`);
|
|
218
|
+
}
|
|
219
|
+
if (!readyStatusline.includes(`${origin}/feedback/quick?signal=up`)) {
|
|
220
|
+
throw new Error(`Ready statusline missing thumbs-up URL: ${readyStatusline.trim()}`);
|
|
221
|
+
}
|
|
222
|
+
if (!readyStatusline.includes(`${origin}/feedback/quick?signal=down`)) {
|
|
223
|
+
throw new Error(`Ready statusline missing thumbs-down URL: ${readyStatusline.trim()}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
packageSpec,
|
|
228
|
+
expectedVersion,
|
|
229
|
+
origin,
|
|
230
|
+
health,
|
|
231
|
+
};
|
|
232
|
+
} finally {
|
|
233
|
+
await stopDetachedRuntime(homeDir);
|
|
234
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function main() {
|
|
239
|
+
const args = parseArgs();
|
|
240
|
+
const result = await runPackagedRuntimeSmoke({
|
|
241
|
+
packageSpec: args.packageSpec,
|
|
242
|
+
expectedVersion: args.expectedVersion,
|
|
243
|
+
timeoutMs: args.timeoutMs,
|
|
244
|
+
});
|
|
245
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (require.main === module) {
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
process.stderr.write(`${error.message || String(error)}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
getAvailablePort,
|
|
257
|
+
packCurrentRepo,
|
|
258
|
+
runPackagedRuntimeSmoke,
|
|
259
|
+
waitForHealthy,
|
|
260
|
+
};
|
package/scripts/prove-runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const { buildManagedScheduleCommand } = require('./schedule-manager');
|
|
8
|
+
const { runPackagedRuntimeSmoke } = require('./prove-packaged-runtime');
|
|
8
9
|
|
|
9
10
|
const ROOT = path.join(__dirname, '..');
|
|
10
11
|
const RUNNER_PATH = require.resolve('./async-job-runner');
|
|
@@ -275,6 +276,18 @@ async function run() {
|
|
|
275
276
|
}
|
|
276
277
|
},
|
|
277
278
|
},
|
|
279
|
+
{
|
|
280
|
+
id: 'RUNTIME-07',
|
|
281
|
+
desc: 'packaged thumbgate runtime boots local API and serves dashboard affordances',
|
|
282
|
+
fn: async () => {
|
|
283
|
+
const result = await runPackagedRuntimeSmoke({
|
|
284
|
+
expectedVersion: require(path.join(ROOT, 'package.json')).version,
|
|
285
|
+
});
|
|
286
|
+
if (!result.health || result.health.version !== require(path.join(ROOT, 'package.json')).version) {
|
|
287
|
+
throw new Error('Packaged runtime health version did not match package.json');
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
},
|
|
278
291
|
];
|
|
279
292
|
|
|
280
293
|
console.log('Interruptible Runtime - Proof Gate\n');
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -10,9 +10,9 @@ const {
|
|
|
10
10
|
const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
|
|
11
11
|
|
|
12
12
|
const FREE_TIER_LIMITS = {
|
|
13
|
-
capture_feedback: { daily:
|
|
13
|
+
capture_feedback: { daily: 3, label: 'feedback captures' },
|
|
14
14
|
search_lessons: { daily: 5, label: 'lesson searches' },
|
|
15
|
-
search_thumbgate: { daily:
|
|
15
|
+
search_thumbgate: { daily: 5, label: 'ThumbGate searches' },
|
|
16
16
|
commerce_recall: { daily: 5, label: 'commerce recalls' },
|
|
17
17
|
export_dpo: { daily: 0, label: 'DPO exports (Pro only)' },
|
|
18
18
|
export_databricks: { daily: 0, label: 'Databricks exports (Pro only)' },
|
|
@@ -83,7 +83,7 @@ function checkLimit(action, authContext) {
|
|
|
83
83
|
const current = usage.counts[action] || 0;
|
|
84
84
|
|
|
85
85
|
if (current >= dailyLimit) {
|
|
86
|
-
return { allowed: false, message: UPGRADE_MESSAGE
|
|
86
|
+
return { allowed: false, message: `Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate-production.up.railway.app/pro\n${UPGRADE_MESSAGE}`, used: current, limit: dailyLimit };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Increment
|
|
@@ -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,16 @@ if [ -n "$_TOWER_JSON" ]; then
|
|
|
117
126
|
' 2>/dev/null)"
|
|
118
127
|
fi
|
|
119
128
|
|
|
129
|
+
# ── Latest lesson ──────────────────────────────────────────────────
|
|
130
|
+
LESSON_TEXT=""; LESSON_ID=""
|
|
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
|
+
' 2>/dev/null)"
|
|
137
|
+
fi
|
|
138
|
+
|
|
120
139
|
# ── Colors ────────────────────────────────────────────────────────
|
|
121
140
|
G='\033[32m'; R='\033[31m'; M='\033[35m'; C='\033[36m'; D='\033[90m'; BD='\033[1m'; RST='\033[0m'
|
|
122
141
|
|
|
@@ -140,19 +159,155 @@ DOWN_ICON="$(osc8_link "$DOWN_URL" "👎")"
|
|
|
140
159
|
DASHBOARD_LINK="$(osc8_link "$DASHBOARD_URL" "$DASHBOARD_LABEL")"
|
|
141
160
|
LESSONS_LINK="$(osc8_link "$LESSONS_URL" "$LESSONS_LABEL")"
|
|
142
161
|
|
|
162
|
+
is_numeric() {
|
|
163
|
+
case "$1" in
|
|
164
|
+
''|*[!0-9]*) return 1 ;;
|
|
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"
|
|
234
|
+
else
|
|
235
|
+
printf '%s...' "${text:0:$((max_chars - 3))}"
|
|
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
|
|
252
|
+
fi
|
|
253
|
+
truncated=$(truncate_plain_text "$plain" "$remaining")
|
|
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
|
+
}
|
|
269
|
+
|
|
143
270
|
# ── Output (single line) ─────────────────────────────────────────
|
|
144
|
-
LINE="ThumbGate v${TG_VERSION} · ${TG_TIER}"
|
|
145
271
|
if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
|
|
146
|
-
|
|
147
|
-
|
|
272
|
+
push_segment "ThumbGate v${TG_VERSION}" "${D}ThumbGate v${TG_VERSION}${RST}"
|
|
273
|
+
push_segment "${TG_TIER}" "${D}${TG_TIER}${RST}"
|
|
274
|
+
push_segment "no feedback yet" "${D}no feedback yet${RST}"
|
|
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
|
|
148
278
|
else
|
|
149
|
-
|
|
279
|
+
STATS_PLAIN="${UP}👍 ${DOWN}👎 ${ARROW}"
|
|
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
|
|
150
296
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
297
|
+
push_segment "ThumbGate v${TG_VERSION}" "ThumbGate v${TG_VERSION}"
|
|
298
|
+
push_segment "${TG_TIER}" "${TG_TIER}"
|
|
299
|
+
push_segment "${STATS_PLAIN}" "${STATS_RENDERED}"
|
|
300
|
+
add_segment_if_fit "${DASHBOARD_LABEL}" "${C}${DASHBOARD_LINK}${RST}"
|
|
301
|
+
add_segment_if_fit "${LESSONS_LABEL}" "${M}${LESSONS_LINK}${RST}"
|
|
302
|
+
if [ "${LESSONS:-0}" -gt 0 ]; then
|
|
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
|
|
156
311
|
|
|
157
|
-
|
|
312
|
+
render_segments
|
|
158
313
|
fi
|
package/scripts/tool-registry.js
CHANGED
|
@@ -36,7 +36,7 @@ const TOOLS = [
|
|
|
36
36
|
whatWorked: { type: 'string' },
|
|
37
37
|
chatHistory: {
|
|
38
38
|
type: 'array',
|
|
39
|
-
description: 'Optional recent conversation window used for history-aware lesson distillation.',
|
|
39
|
+
description: 'Optional caller-supplied recent conversation window used for history-aware lesson distillation. The current Claude auto-capture path sends up to 8 prior recorded entries for vague negative inline signals.',
|
|
40
40
|
items: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
@@ -59,7 +59,7 @@ const TOOLS = [
|
|
|
59
59
|
timestamp: { type: 'string' },
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
description: '
|
|
62
|
+
description: 'Recent conversation turns before the feedback signal. Raw messages, not summaries.',
|
|
63
63
|
},
|
|
64
64
|
rubricScores: {
|
|
65
65
|
type: 'array',
|