thumbgate 0.9.11 → 0.9.13
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- 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 +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/package.json +2 -2
- 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/opencode-profile/INSTALL.md +1 -1
- package/public/index.html +1 -1
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/hook-runtime.js +4 -12
- package/scripts/install-mcp.js +0 -3
- package/scripts/mcp-config.js +11 -18
- package/scripts/post-everywhere.js +29 -1
- package/scripts/published-cli.js +30 -3
- package/scripts/social-analytics/poll-all.js +20 -5
- package/scripts/social-analytics/pollers/plausible.js +2 -4
- package/scripts/social-analytics/publish-thumbgate-launch.js +6 -0
- package/scripts/social-analytics/publishers/zernio.js +95 -4
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +1 -1
- package/scripts/social-reply-monitor.js +39 -8
- package/scripts/statusline-local-stats.js +16 -0
- package/scripts/statusline.sh +16 -1
- package/scripts/sync-version.js +13 -0
- package/scripts/test-coverage.js +1 -0
- package/src/api/server.js +11 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"description": "Pre-action gates that block AI coding agents from repeating known mistakes. Captures feedback, auto-promotes failures into prevention rules, and enforces them via PreToolUse hooks.",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.13",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
|
|
5
5
|
"homepage": "https://github.com/IgorGanapolsky/thumbgate",
|
|
6
6
|
"transport": "stdio",
|
package/adapters/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- `chatgpt/openapi.yaml`: import into GPT Actions.
|
|
4
4
|
- `gemini/function-declarations.json`: Gemini function-calling definitions.
|
|
5
5
|
- `mcp/server-stdio.js`: underlying local MCP stdio server implementation.
|
|
6
|
-
- `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@0.9.
|
|
6
|
+
- `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@0.9.13 thumbgate serve`.
|
|
7
7
|
- `codex/config.toml`: example Codex MCP profile section using the same version-pinned portable launcher.
|
|
8
8
|
- `amp/skills/thumbgate-feedback/SKILL.md`: Amp skill template.
|
|
9
9
|
- `opencode/opencode.json`: portable OpenCode MCP profile using the same version-pinned portable launcher.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@0.9.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@0.9.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Codex MCP profile (copy into ~/.codex/config.toml or merge section)
|
|
2
2
|
[mcp_servers.thumbgate]
|
|
3
3
|
command = "npx"
|
|
4
|
-
args = ["--yes", "--package", "thumbgate@0.9.
|
|
4
|
+
args = ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "serve"]
|
|
5
5
|
|
|
6
6
|
# Hard PreToolUse hook for Codex
|
|
7
7
|
[hooks.pre_tool_use]
|
|
8
8
|
command = "npx"
|
|
9
|
-
args = ["--yes", "--package", "thumbgate@0.9.
|
|
9
|
+
args = ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "gate-check"]
|
|
@@ -111,7 +111,7 @@ const {
|
|
|
111
111
|
finalizeSession: finalizeFeedbackSession,
|
|
112
112
|
} = require('../../scripts/feedback-session');
|
|
113
113
|
|
|
114
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '0.9.
|
|
114
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '0.9.13' };
|
|
115
115
|
const COMMERCE_CATEGORIES = [
|
|
116
116
|
'product_recommendation',
|
|
117
117
|
'brand_compliance',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "ThumbGate
|
|
3
|
+
"version": "0.9.13",
|
|
4
|
+
"description": "ThumbGate — Make your AI coding agent self-improving. Every mistake becomes a prevention rule that physically blocks the agent from repeating it. Feedback-driven enforcement via PreToolUse hooks, Thompson Sampling for adaptive gates, SQLite+FTS5 lesson DB, and LanceDB vector search. Your agent gets smarter with every session.",
|
|
5
5
|
"homepage": "https://thumbgate-production.up.railway.app",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-bridge",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "Run Codex review, adversarial review, and second-pass handoffs from Claude Code while keeping ThumbGate reliability memory in the loop.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-profile",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "ThumbGate for Codex: pre-action gates, skill packs, hallucination detection, PII scanning, progressive disclosure (82% token savings), and MCP-backed reliability memory.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
@@ -31,7 +31,7 @@ The following block is appended to `~/.codex/config.toml`:
|
|
|
31
31
|
```toml
|
|
32
32
|
[mcp_servers.thumbgate]
|
|
33
33
|
command = "npx"
|
|
34
|
-
args = ["--yes", "--package", "thumbgate@0.9.
|
|
34
|
+
args = ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "serve"]
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
The repo-local Codex app plugin ships the same runtime path through `plugins/codex-profile/.mcp.json`, so the manual config and plugin metadata stay aligned.
|
|
@@ -29,7 +29,7 @@ That profile launches:
|
|
|
29
29
|
```toml
|
|
30
30
|
[mcp_servers.thumbgate]
|
|
31
31
|
command = "npx"
|
|
32
|
-
args = ["--yes", "--package", "thumbgate@0.9.
|
|
32
|
+
args = ["--yes", "--package", "thumbgate@0.9.13", "thumbgate", "serve"]
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Why this exists
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"displayName": "ThumbGate",
|
|
4
4
|
"description": "👍👎 Thumbs down a mistake — your AI agent won't repeat it. Thumbs up good work — it remembers the pattern.",
|
|
5
|
-
"version": "0.9.
|
|
5
|
+
"version": "0.9.13",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Igor Ganapolsky"
|
|
8
8
|
},
|
|
@@ -25,7 +25,7 @@ The portable profile adds this MCP server entry:
|
|
|
25
25
|
"mcp": {
|
|
26
26
|
"thumbgate": {
|
|
27
27
|
"type": "local",
|
|
28
|
-
"command": ["npx", "--yes", "--package", "thumbgate@0.9.
|
|
28
|
+
"command": ["npx", "--yes", "--package", "thumbgate@0.9.13", "thumbgate", "serve"],
|
|
29
29
|
"enabled": true
|
|
30
30
|
}
|
|
31
31
|
}
|
package/public/index.html
CHANGED
|
@@ -554,7 +554,7 @@ __GA_BOOTSTRAP__
|
|
|
554
554
|
<!-- HOW IT WORKS -->
|
|
555
555
|
<section class="how-it-works" id="how-it-works">
|
|
556
556
|
<div class="container">
|
|
557
|
-
<div class="section-label">New in v0.9.
|
|
557
|
+
<div class="section-label">New in v0.9.13</div>
|
|
558
558
|
<h2 class="section-title">Three steps to stop repeated AI failures</h2>
|
|
559
559
|
<div class="steps">
|
|
560
560
|
<div class="step">
|
|
Binary file
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -6,7 +6,7 @@ const {
|
|
|
6
6
|
isSourceCheckout,
|
|
7
7
|
publishedCliAvailable,
|
|
8
8
|
} = require('./mcp-config');
|
|
9
|
-
const {
|
|
9
|
+
const { publishedCliShellCommand } = require('./published-cli');
|
|
10
10
|
|
|
11
11
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
12
12
|
const featureSupportCache = new Map();
|
|
@@ -28,15 +28,7 @@ function publishedHookCommandsAvailable(version) {
|
|
|
28
28
|
return featureSupportCache.get(version);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const helpText = runPublishedCliHelp(version, { timeout: 8000 });
|
|
34
|
-
available = ['gate-check', 'cache-update', 'statusline-render', 'hook-auto-capture', 'session-start']
|
|
35
|
-
.every((command) => helpText.includes(command));
|
|
36
|
-
} catch {
|
|
37
|
-
available = false;
|
|
38
|
-
}
|
|
39
|
-
|
|
31
|
+
const available = true;
|
|
40
32
|
featureSupportCache.set(version, available);
|
|
41
33
|
return available;
|
|
42
34
|
}
|
|
@@ -44,12 +36,12 @@ function publishedHookCommandsAvailable(version) {
|
|
|
44
36
|
function resolveCliBaseCommand() {
|
|
45
37
|
const version = packageVersion();
|
|
46
38
|
if (publishedHookCommandsAvailable(version)) {
|
|
47
|
-
return
|
|
39
|
+
return publishedCliShellCommand(version);
|
|
48
40
|
}
|
|
49
41
|
if (isSourceCheckout(PKG_ROOT)) {
|
|
50
42
|
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
|
|
51
43
|
}
|
|
52
|
-
return
|
|
44
|
+
return publishedCliShellCommand(version);
|
|
53
45
|
}
|
|
54
46
|
|
|
55
47
|
function buildPortableHookCommand(subcommand) {
|
package/scripts/install-mcp.js
CHANGED
|
@@ -30,8 +30,6 @@ function resolveMcpServerConfig(flags = {}) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const MCP_SERVER_CONFIG = resolveMcpServerConfig();
|
|
34
|
-
|
|
35
33
|
function parseFlags(argv) {
|
|
36
34
|
const flags = {};
|
|
37
35
|
for (const arg of argv) {
|
|
@@ -155,7 +153,6 @@ function installMcp(flags) {
|
|
|
155
153
|
module.exports = {
|
|
156
154
|
MCP_SERVER_KEY,
|
|
157
155
|
LEGACY_MCP_SERVER_KEYS,
|
|
158
|
-
MCP_SERVER_CONFIG,
|
|
159
156
|
resolveMcpServerConfig,
|
|
160
157
|
resolveSettingsPath,
|
|
161
158
|
loadSettings,
|
package/scripts/mcp-config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFileSync } = require('child_process');
|
|
6
|
-
const {
|
|
6
|
+
const { publishedCliShellCommand } = require('./published-cli');
|
|
7
7
|
const DEFAULT_PKG_ROOT = path.join(__dirname, '..');
|
|
8
8
|
const cliAvailabilityCache = new Map();
|
|
9
9
|
|
|
@@ -101,8 +101,8 @@ function resolveLocalServerPath(pkgRoot, scope = 'project') {
|
|
|
101
101
|
|
|
102
102
|
function portableMcpEntry(pkgVersion) {
|
|
103
103
|
return {
|
|
104
|
-
command: '
|
|
105
|
-
args:
|
|
104
|
+
command: 'sh',
|
|
105
|
+
args: ['-lc', publishedCliShellCommand(pkgVersion, ['serve'])],
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -163,33 +163,26 @@ function publishedCliOverride() {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function publishedCliAvailable(pkgVersion) {
|
|
166
|
-
if (!isVersionPublished(pkgVersion)) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
166
|
const override = publishedCliOverride();
|
|
170
167
|
if (override !== null) {
|
|
171
168
|
return override;
|
|
172
169
|
}
|
|
173
|
-
if (
|
|
174
|
-
return
|
|
170
|
+
if (!isVersionPublished(pkgVersion)) {
|
|
171
|
+
return false;
|
|
175
172
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
runPublishedCliHelp(pkgVersion, { timeout: 8000 });
|
|
180
|
-
available = true;
|
|
181
|
-
} catch (_) {
|
|
182
|
-
available = false;
|
|
173
|
+
if (!cliAvailabilityCache.has(pkgVersion)) {
|
|
174
|
+
cliAvailabilityCache.set(pkgVersion, true);
|
|
183
175
|
}
|
|
184
|
-
|
|
185
|
-
cliAvailabilityCache.set(pkgVersion, available);
|
|
186
|
-
return available;
|
|
176
|
+
return cliAvailabilityCache.get(pkgVersion);
|
|
187
177
|
}
|
|
188
178
|
|
|
189
179
|
function resolveMcpEntry({ pkgRoot, pkgVersion, scope = 'project', targetDir = pkgRoot }) {
|
|
190
180
|
if (!isSourceCheckout(pkgRoot)) {
|
|
191
181
|
return portableMcpEntry(pkgVersion);
|
|
192
182
|
}
|
|
183
|
+
if (scope === 'home' && publishedCliAvailable(pkgVersion)) {
|
|
184
|
+
return portableMcpEntry(pkgVersion);
|
|
185
|
+
}
|
|
193
186
|
if (scope === 'project' && !isSameCheckoutFamily(pkgRoot, targetDir) && publishedCliAvailable(pkgVersion)) {
|
|
194
187
|
return portableMcpEntry(pkgVersion);
|
|
195
188
|
}
|
|
@@ -183,6 +183,32 @@ async function postToDevTo(parsed, dryRun) {
|
|
|
183
183
|
return devto.publishArticle({ title, body_markdown: body, tags: parsed.tags });
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
async function postToTikTok(parsed, dryRun) {
|
|
187
|
+
const text = parsed.body || '';
|
|
188
|
+
if (!text) throw new Error('TikTok post requires body');
|
|
189
|
+
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
console.log(`[dry-run] TikTok: "${text.slice(0, 100)}..." (${text.length} chars)`);
|
|
192
|
+
return { dryRun: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const tiktok = getPublisher('tiktok');
|
|
196
|
+
return tiktok.publishPost({ text });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function postToYouTube(parsed, dryRun) {
|
|
200
|
+
const { title, body } = parsed;
|
|
201
|
+
if (!title || !body) throw new Error('YouTube post requires title and body');
|
|
202
|
+
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
console.log(`[dry-run] YouTube: "${title}" (${body.length} chars)`);
|
|
205
|
+
return { dryRun: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const youtube = getPublisher('youtube');
|
|
209
|
+
return youtube.publishPost({ title, description: body });
|
|
210
|
+
}
|
|
211
|
+
|
|
186
212
|
// ---------------------------------------------------------------------------
|
|
187
213
|
// Main orchestrator
|
|
188
214
|
// ---------------------------------------------------------------------------
|
|
@@ -192,6 +218,8 @@ const DISPATCHERS = {
|
|
|
192
218
|
x: postToX,
|
|
193
219
|
linkedin: postToLinkedIn,
|
|
194
220
|
devto: postToDevTo,
|
|
221
|
+
tiktok: postToTikTok,
|
|
222
|
+
youtube: postToYouTube,
|
|
195
223
|
};
|
|
196
224
|
|
|
197
225
|
async function postEverywhere(filePath, { platforms, dryRun } = {}) {
|
|
@@ -211,7 +239,7 @@ async function postEverywhere(filePath, { platforms, dryRun } = {}) {
|
|
|
211
239
|
// Determine which platforms to post to.
|
|
212
240
|
// Default excludes devto — high-volume Dev.to posting is counterproductive (0 engagement on 427 posts).
|
|
213
241
|
// Use --platforms=devto explicitly for monthly cross-posts only.
|
|
214
|
-
const DEFAULT_PLATFORMS = ['reddit', 'x', 'linkedin'];
|
|
242
|
+
const DEFAULT_PLATFORMS = ['reddit', 'x', 'linkedin', 'tiktok', 'youtube'];
|
|
215
243
|
const targetPlatforms = platforms || (parsed.platform ? [parsed.platform] : DEFAULT_PLATFORMS);
|
|
216
244
|
|
|
217
245
|
// Preserve original body/comment so each platform gets a fresh UTM tag
|
package/scripts/published-cli.js
CHANGED
|
@@ -5,14 +5,39 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
7
|
|
|
8
|
-
function
|
|
9
|
-
return
|
|
8
|
+
function shellQuote(value) {
|
|
9
|
+
return JSON.stringify(String(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runtimePrefixDir(prefixDir) {
|
|
13
|
+
return prefixDir || path.join(os.homedir(), '.thumbgate', 'runtime');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function publishedCliArgs(pkgVersion, commandArgs = [], options = {}) {
|
|
17
|
+
return [
|
|
18
|
+
'exec',
|
|
19
|
+
'--prefix',
|
|
20
|
+
runtimePrefixDir(options.prefixDir),
|
|
21
|
+
'--yes',
|
|
22
|
+
'--package',
|
|
23
|
+
`thumbgate@${pkgVersion}`,
|
|
24
|
+
'--',
|
|
25
|
+
'thumbgate',
|
|
26
|
+
...commandArgs,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
|
|
31
|
+
const prefixDir = runtimePrefixDir(options.prefixDir);
|
|
32
|
+
return `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
|
|
10
33
|
}
|
|
11
34
|
|
|
12
35
|
function runPublishedCli(pkgVersion, commandArgs = [], options = {}) {
|
|
13
36
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-published-cli-'));
|
|
37
|
+
const prefixDir = path.join(tmpDir, 'runtime');
|
|
14
38
|
try {
|
|
15
|
-
|
|
39
|
+
fs.mkdirSync(prefixDir, { recursive: true });
|
|
40
|
+
return execFileSync('npm', publishedCliArgs(pkgVersion, commandArgs, { prefixDir }), {
|
|
16
41
|
encoding: 'utf8',
|
|
17
42
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
43
|
timeout: options.timeout || 8000,
|
|
@@ -29,6 +54,8 @@ function runPublishedCliHelp(pkgVersion, options = {}) {
|
|
|
29
54
|
|
|
30
55
|
module.exports = {
|
|
31
56
|
publishedCliArgs,
|
|
57
|
+
publishedCliShellCommand,
|
|
58
|
+
runtimePrefixDir,
|
|
32
59
|
runPublishedCli,
|
|
33
60
|
runPublishedCliHelp,
|
|
34
61
|
};
|
|
@@ -9,6 +9,9 @@ const { initDb } = require('./store');
|
|
|
9
9
|
|
|
10
10
|
const POLLERS = [
|
|
11
11
|
{ name: 'github', module: './pollers/github', envRequired: ['GITHUB_TOKEN'] },
|
|
12
|
+
// Direct Instagram Graph API poller. Requires INSTAGRAM_ACCESS_TOKEN + INSTAGRAM_USER_ID.
|
|
13
|
+
// When those are absent, Instagram engagement data is still captured via the Zernio poller
|
|
14
|
+
// below (getConnectedAccounts returns Instagram accounts when Zernio is connected to IG).
|
|
12
15
|
{ name: 'instagram', module: './pollers/instagram', envRequired: ['INSTAGRAM_ACCESS_TOKEN', 'INSTAGRAM_USER_ID'] },
|
|
13
16
|
{ name: 'tiktok', module: './pollers/tiktok', envRequired: ['TIKTOK_ACCESS_TOKEN'] },
|
|
14
17
|
{ name: 'linkedin', module: './pollers/linkedin', envRequired: ['LINKEDIN_ACCESS_TOKEN', 'LINKEDIN_PERSON_URN'] },
|
|
@@ -16,7 +19,10 @@ const POLLERS = [
|
|
|
16
19
|
{ name: 'reddit', module: './pollers/reddit', envRequired: ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_USERNAME', 'REDDIT_PASSWORD'] },
|
|
17
20
|
{ name: 'threads', module: './pollers/threads', envRequired: ['THREADS_ACCESS_TOKEN', 'THREADS_USER_ID'] },
|
|
18
21
|
{ name: 'youtube', module: './pollers/youtube', envRequired: ['YOUTUBE_API_KEY', 'YOUTUBE_CHANNEL_ID'] },
|
|
19
|
-
|
|
22
|
+
// PLAUSIBLE_SITE_ID defaults to thumbgate-production.up.railway.app if not set.
|
|
23
|
+
{ name: 'plausible', module: './pollers/plausible', envRequired: ['PLAUSIBLE_API_KEY'] },
|
|
24
|
+
// Zernio covers all connected social accounts (including Instagram) via its unified API.
|
|
25
|
+
// Instagram posts published via Zernio will have their engagement metrics captured here.
|
|
20
26
|
{ name: 'zernio', module: './pollers/zernio', envRequired: ['ZERNIO_API_KEY'] },
|
|
21
27
|
];
|
|
22
28
|
|
|
@@ -37,10 +43,19 @@ async function pollAll(options = {}) {
|
|
|
37
43
|
|
|
38
44
|
try {
|
|
39
45
|
const mod = require(poller.module);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// Resolve the poll function by trying the simple title-case name first, then
|
|
47
|
+
// known capitalization variants (pollGitHub, pollTikTok, pollLinkedIn, pollYouTube),
|
|
48
|
+
// and finally any exported function whose name starts with "poll" as a last resort.
|
|
49
|
+
const baseName = poller.name.charAt(0).toUpperCase() + poller.name.slice(1);
|
|
50
|
+
const KNOWN_VARIANTS = {
|
|
51
|
+
github: 'pollGitHub',
|
|
52
|
+
tiktok: 'pollTikTok',
|
|
53
|
+
linkedin: 'pollLinkedIn',
|
|
54
|
+
youtube: 'pollYouTube',
|
|
55
|
+
};
|
|
56
|
+
const fn = mod[`poll${baseName}`]
|
|
57
|
+
|| (KNOWN_VARIANTS[poller.name] && mod[KNOWN_VARIANTS[poller.name]])
|
|
58
|
+
|| Object.values(mod).find((v) => typeof v === 'function' && v.name && v.name.startsWith('poll'));
|
|
44
59
|
|
|
45
60
|
if (!fn) {
|
|
46
61
|
console.log(`⚠ ${poller.name}: no poll function found in module`);
|
|
@@ -23,10 +23,9 @@ const PLAUSIBLE_BASE = 'https://plausible.io/api/v1';
|
|
|
23
23
|
*/
|
|
24
24
|
async function plausibleQuery(endpoint, params = {}) {
|
|
25
25
|
const apiKey = process.env.PLAUSIBLE_API_KEY;
|
|
26
|
-
const siteId = process.env.PLAUSIBLE_SITE_ID;
|
|
26
|
+
const siteId = process.env.PLAUSIBLE_SITE_ID || 'thumbgate-production.up.railway.app';
|
|
27
27
|
|
|
28
28
|
if (!apiKey) throw new Error('PLAUSIBLE_API_KEY is not set');
|
|
29
|
-
if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
|
|
30
29
|
|
|
31
30
|
const qs = new URLSearchParams({ site_id: siteId, ...params });
|
|
32
31
|
const url = `${PLAUSIBLE_BASE}${endpoint}?${qs.toString()}`;
|
|
@@ -153,9 +152,8 @@ async function getFunnelMetrics(period = '7d') {
|
|
|
153
152
|
* @returns {Promise<object>} Summary of stored results.
|
|
154
153
|
*/
|
|
155
154
|
async function pollPlausible(db) {
|
|
156
|
-
const siteId = process.env.PLAUSIBLE_SITE_ID;
|
|
155
|
+
const siteId = process.env.PLAUSIBLE_SITE_ID || 'thumbgate-production.up.railway.app';
|
|
157
156
|
if (!process.env.PLAUSIBLE_API_KEY) throw new Error('PLAUSIBLE_API_KEY is not set');
|
|
158
|
-
if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
|
|
159
157
|
|
|
160
158
|
const period = '7d';
|
|
161
159
|
const fetchedAt = new Date().toISOString();
|
|
@@ -146,6 +146,8 @@ function buildCampaignEntries() {
|
|
|
146
146
|
buildLandingUrl('linkedin', 'campaign_proof_pack'),
|
|
147
147
|
].join(' '),
|
|
148
148
|
instagram: `${THUMBGATE_CAPTION}\n\nProof-backed workflow hardening.\n\n${buildLandingUrl('instagram', 'campaign_proof_pack')}`,
|
|
149
|
+
tiktok: `Your AI agent has amnesia. Give it memory that survives restarts.\n\nThumbGate: proof-backed workflow hardening for coding agents.\n\n#AIAgents #DeveloperTools #ClaudeCode #ThumbGate`,
|
|
150
|
+
youtube: `Your AI agent has amnesia. Give it memory that survives restarts.\n\nThumbGate turns thumbs-down feedback into prevention rules that block mistakes permanently.\n\n${buildLandingUrl('youtube', 'campaign_proof_pack')}`,
|
|
149
151
|
},
|
|
150
152
|
},
|
|
151
153
|
{
|
|
@@ -166,6 +168,8 @@ function buildCampaignEntries() {
|
|
|
166
168
|
'ThumbGate keeps the feedback loop local, durable, and enforceable.',
|
|
167
169
|
buildLandingUrl('instagram', 'campaign_free_local'),
|
|
168
170
|
].join('\n\n'),
|
|
171
|
+
tiktok: `Free and local-first. ThumbGate blocks repeated AI coding mistakes without a cloud account.\n\nnpx thumbgate init\n\n#FreeDeveloperTools #AIAgents #OpenSource`,
|
|
172
|
+
youtube: `ThumbGate runs local-first. No cloud account needed. Feedback capture, prevention rules, and blocking — all on your machine.\n\n${buildLandingUrl('youtube', 'campaign_free_local')}`,
|
|
169
173
|
},
|
|
170
174
|
},
|
|
171
175
|
{
|
|
@@ -187,6 +191,8 @@ function buildCampaignEntries() {
|
|
|
187
191
|
'Next session, the same mistake gets blocked.',
|
|
188
192
|
buildLandingUrl('instagram', 'campaign_checkout_path'),
|
|
189
193
|
].join('\n\n'),
|
|
194
|
+
tiktok: `Stop your AI agent from repeating the same mistake. One thumbs-down = permanent block.\n\nFree to start. Pro when you need the dashboard.\n\n#ThumbGate #AIAgents #DeveloperTools`,
|
|
195
|
+
youtube: `Repeated agent mistakes are a systems problem. ThumbGate blocks known-bad patterns before the next tool call executes.\n\nFree local path. Pro adds dashboard and exports.\n\n${buildLandingUrl('youtube', 'campaign_checkout_path')}`,
|
|
190
196
|
},
|
|
191
197
|
},
|
|
192
198
|
];
|
|
@@ -10,15 +10,68 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
13
14
|
const { tagUrlsInText } = require('../utm');
|
|
14
15
|
const { loadLocalEnv } = require('../load-env');
|
|
15
16
|
|
|
16
17
|
const ZERNIO_UTM = { source: 'zernio', medium: 'social', campaign: 'organic' };
|
|
17
18
|
|
|
18
19
|
const ZERNIO_BASE = 'https://zernio.com/api/v1';
|
|
20
|
+
const DEFAULT_DEDUP_LOG_PATH = path.join(__dirname, '..', '..', '..', '.thumbgate', 'zernio-dedup-log.json');
|
|
19
21
|
|
|
20
22
|
loadLocalEnv();
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Content-hash dedup: prevents the same content from being posted to the same
|
|
26
|
+
* platform twice within a 24-hour window.
|
|
27
|
+
*/
|
|
28
|
+
function getDedupLogPath() {
|
|
29
|
+
return process.env.THUMBGATE_DEDUP_LOG_PATH || DEFAULT_DEDUP_LOG_PATH;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildDedupKey(content, platform) {
|
|
33
|
+
const hash = crypto.createHash('sha256').update(content.trim()).digest('hex').slice(0, 16);
|
|
34
|
+
return `${platform}::${hash}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadDedupLog() {
|
|
38
|
+
const logPath = getDedupLogPath();
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(logPath)) {
|
|
41
|
+
return JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
42
|
+
}
|
|
43
|
+
} catch { /* ignore corrupt log */ }
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveDedupLog(log) {
|
|
48
|
+
const logPath = getDedupLogPath();
|
|
49
|
+
const dir = path.dirname(logPath);
|
|
50
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(logPath, JSON.stringify(log, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isDuplicate(content, platform) {
|
|
55
|
+
const log = loadDedupLog();
|
|
56
|
+
const key = buildDedupKey(content, platform);
|
|
57
|
+
const entry = log[key];
|
|
58
|
+
if (!entry) return false;
|
|
59
|
+
const ageMs = Date.now() - new Date(entry.postedAt).getTime();
|
|
60
|
+
return ageMs < 24 * 60 * 60 * 1000; // 24-hour dedup window
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function recordPost(content, platform) {
|
|
64
|
+
const log = loadDedupLog();
|
|
65
|
+
const key = buildDedupKey(content, platform);
|
|
66
|
+
log[key] = { platform, postedAt: new Date().toISOString() };
|
|
67
|
+
// Prune entries older than 7 days
|
|
68
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
69
|
+
for (const [k, v] of Object.entries(log)) {
|
|
70
|
+
if (new Date(v.postedAt).getTime() < cutoff) delete log[k];
|
|
71
|
+
}
|
|
72
|
+
saveDedupLog(log);
|
|
73
|
+
}
|
|
74
|
+
|
|
22
75
|
function requireApiKey() {
|
|
23
76
|
const key = process.env.ZERNIO_API_KEY;
|
|
24
77
|
if (!key) {
|
|
@@ -224,17 +277,35 @@ async function publishPost(content, platforms, options = {}) {
|
|
|
224
277
|
return { blocked: true, reasons: gateResult.findings };
|
|
225
278
|
}
|
|
226
279
|
|
|
227
|
-
|
|
280
|
+
// Dedup: filter out platforms where identical content was posted in last 24h
|
|
281
|
+
const dedupedPlatforms = normalizedPlatforms.filter((p) => {
|
|
282
|
+
if (isDuplicate(content, p.platform)) {
|
|
283
|
+
console.log(`[zernio:publisher] SKIPPED ${p.platform} — duplicate content within 24h`);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (dedupedPlatforms.length === 0) {
|
|
290
|
+
console.log('[zernio:publisher] All platforms skipped (duplicate content)');
|
|
291
|
+
return { blocked: true, reasons: [{ reason: 'duplicate_content_all_platforms' }] };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(`[zernio:publisher] Publishing to ${dedupedPlatforms.length} platform(s): ${dedupedPlatforms.map((p) => p.platform).join(', ')}`);
|
|
228
295
|
|
|
229
296
|
const json = await zernioFetch('POST', '/posts', {
|
|
230
297
|
content,
|
|
231
298
|
firstComment: options.firstComment,
|
|
232
299
|
mediaItems: options.mediaItems,
|
|
233
300
|
publishNow: true,
|
|
234
|
-
platforms:
|
|
301
|
+
platforms: dedupedPlatforms,
|
|
235
302
|
});
|
|
236
303
|
|
|
237
304
|
const data = normalizePostResult(json);
|
|
305
|
+
// Record each platform to prevent future dupes
|
|
306
|
+
for (const p of dedupedPlatforms) {
|
|
307
|
+
recordPost(content, p.platform);
|
|
308
|
+
}
|
|
238
309
|
console.log(`[zernio:publisher] Post published. id=${data.id ?? 'unknown'}`);
|
|
239
310
|
return data;
|
|
240
311
|
}
|
|
@@ -262,19 +333,36 @@ async function schedulePost(content, platforms, scheduledFor, timezone, options
|
|
|
262
333
|
|
|
263
334
|
content = tagUrlsInText(content, options.utm || ZERNIO_UTM);
|
|
264
335
|
|
|
265
|
-
|
|
336
|
+
// Dedup: filter out platforms where identical content was scheduled in last 24h
|
|
337
|
+
const dedupedPlatforms = normalizedPlatforms.filter((p) => {
|
|
338
|
+
if (isDuplicate(content, p.platform)) {
|
|
339
|
+
console.log(`[zernio:publisher] SKIPPED ${p.platform} schedule — duplicate content within 24h`);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (dedupedPlatforms.length === 0) {
|
|
346
|
+
console.log('[zernio:publisher] All platforms skipped (duplicate content)');
|
|
347
|
+
return { blocked: true, reasons: [{ reason: 'duplicate_content_all_platforms' }] };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log(`[zernio:publisher] Scheduling post for ${scheduledFor} (${timezone}) to ${dedupedPlatforms.length} platform(s)`);
|
|
266
351
|
|
|
267
352
|
const json = await zernioFetch('POST', '/posts', {
|
|
268
353
|
content,
|
|
269
354
|
firstComment: options.firstComment,
|
|
270
355
|
mediaItems: options.mediaItems,
|
|
271
356
|
publishNow: false,
|
|
272
|
-
platforms:
|
|
357
|
+
platforms: dedupedPlatforms,
|
|
273
358
|
scheduledFor,
|
|
274
359
|
timezone,
|
|
275
360
|
});
|
|
276
361
|
|
|
277
362
|
const data = normalizePostResult(json);
|
|
363
|
+
for (const p of dedupedPlatforms) {
|
|
364
|
+
recordPost(content, p.platform);
|
|
365
|
+
}
|
|
278
366
|
console.log(`[zernio:publisher] Post scheduled. id=${data.id ?? 'unknown'}`);
|
|
279
367
|
return data;
|
|
280
368
|
}
|
|
@@ -343,9 +431,12 @@ async function publishToAllPlatforms(content, options = {}) {
|
|
|
343
431
|
}
|
|
344
432
|
|
|
345
433
|
module.exports = {
|
|
434
|
+
buildDedupKey,
|
|
346
435
|
deletePost,
|
|
436
|
+
isDuplicate,
|
|
347
437
|
listPosts,
|
|
348
438
|
publishPost,
|
|
439
|
+
recordPost,
|
|
349
440
|
schedulePost,
|
|
350
441
|
publishToAllPlatforms,
|
|
351
442
|
getConnectedAccounts,
|
|
@@ -116,7 +116,7 @@ async function scheduleCampaign(options = {}, api = {}) {
|
|
|
116
116
|
: defaultCampaignSchedule();
|
|
117
117
|
const platforms = options.platforms && options.platforms.length > 0
|
|
118
118
|
? options.platforms
|
|
119
|
-
: ['twitter', 'linkedin', 'instagram'];
|
|
119
|
+
: ['twitter', 'linkedin', 'instagram', 'tiktok', 'youtube'];
|
|
120
120
|
const statePath = options.statePath || DEFAULT_STATE_PATH;
|
|
121
121
|
const state = readScheduleState(statePath);
|
|
122
122
|
const scheduledState = state.scheduled && typeof state.scheduled === 'object' ? state.scheduled : {};
|
|
@@ -209,11 +209,21 @@ async function generateReply(comment, context) {
|
|
|
209
209
|
return `\`npx thumbgate init\` auto-detects your agent and wires the hooks. Takes about 30 seconds. What agent are you using?`;
|
|
210
210
|
}
|
|
211
211
|
if (mentionsSkeptical) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
// Build a reply that mirrors the commenter's frame (memory, context docs, or general rules)
|
|
213
|
+
// so gateContextualReply's topic-overlap check passes.
|
|
214
|
+
let replyBase;
|
|
215
|
+
if (mentionsMemory) {
|
|
216
|
+
replyBase = 'The distinction from memory tools is enforcement: memory helps the agent remember a past mistake, but it can still repeat it. The gate stops the already-rejected move before it runs. Whether that extra step is worth the setup depends on how often your agent ignores its own memory.';
|
|
217
|
+
} else {
|
|
218
|
+
replyBase = 'The difference from cursorrules or instruction files is enforcement: the bad action gets stopped before execution instead of being added to context docs and then ignored anyway. Whether that tradeoff is worth it depends on how often your agent repeats the same mistake.';
|
|
219
|
+
}
|
|
220
|
+
const gate = gateContextualReply(comment, replyBase, context);
|
|
221
|
+
return gate.allowed ? replyBase : null;
|
|
215
222
|
}
|
|
216
|
-
if (mentionsHow &&
|
|
223
|
+
if (mentionsGates && (mentionsHow || (!isReddit && !mentionsThanks))) {
|
|
224
|
+
// On X, engage on gate-topic statements too (not just "how does" questions).
|
|
225
|
+
// On Reddit, keep the old conservative behavior (questions only via mentionsHow).
|
|
226
|
+
if (isReddit && !mentionsHow) return null;
|
|
217
227
|
const reply = isReddit
|
|
218
228
|
? 'The short version is: the tool call gets checked before it runs. If it matches a previously rejected pattern, it is blocked and the agent has to try a different path.'
|
|
219
229
|
: 'PreToolUse hooks intercept the tool call before it runs. Each call is checked against prevention rules promoted from past failures. If it matches, the action is blocked and the agent has to try a different approach. The rules adapt over time so false positives decrease.';
|
|
@@ -228,7 +238,9 @@ async function generateReply(comment, context) {
|
|
|
228
238
|
const gate = gateContextualReply(comment, reply, context);
|
|
229
239
|
return gate.allowed ? reply : null;
|
|
230
240
|
}
|
|
231
|
-
if (mentionsMemory
|
|
241
|
+
if (mentionsMemory) {
|
|
242
|
+
// Engage on memory/context topics whether it's a question or a statement — both are worth a reply on X
|
|
243
|
+
if (isReddit && !isQuestion) return null; // Reddit: only reply to direct questions
|
|
232
244
|
const reply = 'The useful distinction is memory versus enforcement. Memory helps the agent remember, but it can still ignore that memory. Enforcement is what stops the already-rejected move from happening again.';
|
|
233
245
|
const gate = gateContextualReply(comment, reply, context);
|
|
234
246
|
return gate.allowed ? reply : null;
|
|
@@ -246,8 +258,8 @@ async function generateReply(comment, context) {
|
|
|
246
258
|
return gate.allowed ? reply : null;
|
|
247
259
|
}
|
|
248
260
|
if (isQuestion) {
|
|
249
|
-
// They asked something specific we didn't match —
|
|
250
|
-
return
|
|
261
|
+
// They asked something specific we didn't match — signal to caller to save a draft for human review
|
|
262
|
+
return '__DRAFT__';
|
|
251
263
|
}
|
|
252
264
|
// Not a question, not hostile, not thanks — probably a statement. Don't reply.
|
|
253
265
|
return null;
|
|
@@ -327,7 +339,7 @@ async function checkRedditReplies(state, dryRun) {
|
|
|
327
339
|
isQuestion,
|
|
328
340
|
});
|
|
329
341
|
|
|
330
|
-
if (!generatedReply) {
|
|
342
|
+
if (!generatedReply || generatedReply === '__DRAFT__') {
|
|
331
343
|
console.warn(`[reply-monitor] Could not generate reply for ${commentId}`);
|
|
332
344
|
continue;
|
|
333
345
|
}
|
|
@@ -439,6 +451,25 @@ async function checkXReplies(state, dryRun) {
|
|
|
439
451
|
continue;
|
|
440
452
|
}
|
|
441
453
|
|
|
454
|
+
// Unmatched question — save a draft for human review rather than silently skipping
|
|
455
|
+
if (generatedReply === '__DRAFT__') {
|
|
456
|
+
const draft = {
|
|
457
|
+
platform: 'x',
|
|
458
|
+
tweetId,
|
|
459
|
+
author: tweet.author_id,
|
|
460
|
+
theirTweet: tweet.text.slice(0, 500),
|
|
461
|
+
suggestedReply: null,
|
|
462
|
+
draftedAt: new Date().toISOString(),
|
|
463
|
+
status: 'needs_human_reply',
|
|
464
|
+
reason: 'unmatched_question',
|
|
465
|
+
};
|
|
466
|
+
saveDraft(draft);
|
|
467
|
+
state.repliedTo[`x_${tweetId}`] = { at: new Date().toISOString(), platform: 'x', drafted: true, skipped: 'needs_human_reply' };
|
|
468
|
+
results.push({ tweetId, reply: null, posted: false, drafted: true });
|
|
469
|
+
console.log(`[reply-monitor] 📝 DRAFTED (needs human reply) for tweet ${tweetId} — saved to .thumbgate/reply-drafts.jsonl`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
442
473
|
// Truncate to 280 chars for Twitter
|
|
443
474
|
const truncated = generatedReply.slice(0, 275) + (generatedReply.length > 275 ? '...' : '');
|
|
444
475
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { analyzeFeedback } = require('./feedback-loop');
|
|
5
|
+
const { normalizeStatsPayload } = require('./hook-thumbgate-cache-updater');
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const stats = analyzeFeedback();
|
|
9
|
+
const payload = {
|
|
10
|
+
...normalizeStatsPayload(stats),
|
|
11
|
+
updated_at: String(Math.floor(Date.now() / 1000)),
|
|
12
|
+
};
|
|
13
|
+
process.stdout.write(JSON.stringify(payload));
|
|
14
|
+
} catch (_) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
package/scripts/statusline.sh
CHANGED
|
@@ -44,8 +44,23 @@ if [ -f "$THUMBGATE_CACHE" ]; then
|
|
|
44
44
|
' "$THUMBGATE_CACHE" 2>/dev/null)"
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
|
-
# Background refresh from REST API when cache is stale (>120s)
|
|
48
47
|
_NOW=$(date +%s)
|
|
48
|
+
if [ "$UP" = "0" ] && [ "$DOWN" = "0" ] || [ $(( _NOW - ${CACHE_TS:-0} )) -gt 120 ]; then
|
|
49
|
+
_LOCAL_STATS_JSON=$(node "${SCRIPT_DIR}/statusline-local-stats.js" 2>/dev/null)
|
|
50
|
+
if [ -n "$_LOCAL_STATS_JSON" ]; then
|
|
51
|
+
mkdir -p "$(dirname "$THUMBGATE_CACHE")"
|
|
52
|
+
printf '%s' "$_LOCAL_STATS_JSON" > "$THUMBGATE_CACHE"
|
|
53
|
+
eval "$(echo "$_LOCAL_STATS_JSON" | jq -r '
|
|
54
|
+
@sh "UP=\(.thumbs_up // "0")",
|
|
55
|
+
@sh "DOWN=\(.thumbs_down // "0")",
|
|
56
|
+
@sh "LESSONS=\(.lessons // "0")",
|
|
57
|
+
@sh "TREND=\(.trend // "?")",
|
|
58
|
+
@sh "CACHE_TS=\(.updated_at // "0")"
|
|
59
|
+
' 2>/dev/null)"
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Background refresh from REST API when cache is stale (>120s)
|
|
49
64
|
if [ $(( _NOW - ${CACHE_TS:-0} )) -gt 120 ]; then
|
|
50
65
|
(
|
|
51
66
|
_R=$(curl -s --max-time 3 "http://localhost:3456/v1/feedback/stats" -H "Authorization: Bearer ${THUMBGATE_API_KEY:-tg_creator_dev_enterprise}" 2>/dev/null)
|
package/scripts/sync-version.js
CHANGED
|
@@ -199,6 +199,19 @@ function syncVersion(opts) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// 8. Codex plugin manifest + MCP config
|
|
202
|
+
const codexAdapterConfigPath = 'adapters/codex/config.toml';
|
|
203
|
+
if (fs.existsSync(path.join(PROJECT_ROOT, codexAdapterConfigPath))) {
|
|
204
|
+
const content = fs.readFileSync(path.join(PROJECT_ROOT, codexAdapterConfigPath), 'utf8');
|
|
205
|
+
const updated = content.replace(/thumbgate@(\d+\.\d+\.\d+)/g, `thumbgate@${version}`);
|
|
206
|
+
if (updated !== content) {
|
|
207
|
+
drifted.push({ file: codexAdapterConfigPath, field: 'package-version-string', current: content.match(/thumbgate@\d+\.\d+\.\d+/g)?.join(', ') || null });
|
|
208
|
+
if (!checkOnly) {
|
|
209
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, codexAdapterConfigPath), updated);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
targets.push(codexAdapterConfigPath);
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
const codexPluginManifestPath = 'plugins/codex-profile/.codex-plugin/plugin.json';
|
|
203
216
|
if (fs.existsSync(path.join(PROJECT_ROOT, codexPluginManifestPath))) {
|
|
204
217
|
const codexPlugin = readJson(codexPluginManifestPath);
|
package/scripts/test-coverage.js
CHANGED
package/src/api/server.js
CHANGED
|
@@ -3811,11 +3811,21 @@ async function addContext(){
|
|
|
3811
3811
|
return;
|
|
3812
3812
|
}
|
|
3813
3813
|
const body = await parseJsonBody(req);
|
|
3814
|
+
// Auto-include conversation window when caller doesn't provide one
|
|
3815
|
+
let chatHistory = Array.isArray(body.chatHistory) ? body.chatHistory : body.messages;
|
|
3816
|
+
if (!chatHistory || chatHistory.length === 0) {
|
|
3817
|
+
try {
|
|
3818
|
+
chatHistory = readRecentConversationWindow({
|
|
3819
|
+
feedbackDir: getSafeDataDir(),
|
|
3820
|
+
limit: 10,
|
|
3821
|
+
});
|
|
3822
|
+
} catch (_) { /* best-effort — conversation window is optional */ }
|
|
3823
|
+
}
|
|
3814
3824
|
const result = captureFeedback({
|
|
3815
3825
|
signal: body.signal,
|
|
3816
3826
|
context: body.context || '',
|
|
3817
3827
|
relatedFeedbackId: body.relatedFeedbackId,
|
|
3818
|
-
chatHistory
|
|
3828
|
+
chatHistory,
|
|
3819
3829
|
whatWentWrong: body.whatWentWrong,
|
|
3820
3830
|
whatToChange: body.whatToChange,
|
|
3821
3831
|
whatWorked: body.whatWorked,
|