thumbgate 1.27.18 → 1.27.20
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/noop-detect.js
|
|
4
|
+
//
|
|
5
|
+
// No-op / redundant-action detection for ThumbGate.
|
|
6
|
+
//
|
|
7
|
+
// Given an action's pre/post state (file diff, command exit code + output),
|
|
8
|
+
// this module decides whether the action actually changed anything OR whether
|
|
9
|
+
// it is byte-identical to an attempt already seen this session. Both are strong,
|
|
10
|
+
// cheap "the agent is looping" repeat signals that plug into
|
|
11
|
+
// track_action -> prevention_rules.
|
|
12
|
+
//
|
|
13
|
+
// Design goals:
|
|
14
|
+
// * Pure / file-local. No edits to shared modules from inside here.
|
|
15
|
+
// * Normalize volatile fields (timestamps, shas, ANSI, trailing whitespace)
|
|
16
|
+
// before hashing so non-deterministic output does not defeat detection.
|
|
17
|
+
// * Guard partial writes: a truncated after-state that is a strict prefix of
|
|
18
|
+
// the before-state must NOT be reported as a no-op.
|
|
19
|
+
//
|
|
20
|
+
// Persistence lives beside session-actions.json (derived from
|
|
21
|
+
// gates-engine.SESSION_ACTIONS_PATH) so it picks up THUMBGATE_STATE_DIR
|
|
22
|
+
// overrides and shares the same TTL semantics.
|
|
23
|
+
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const gatesEngine = require('./gates-engine');
|
|
29
|
+
|
|
30
|
+
// Match the same 1h session window the engine uses for session actions.
|
|
31
|
+
const ATTEMPT_TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
// -- volatile-field normalization ------------------------------------------
|
|
34
|
+
//
|
|
35
|
+
// Each pattern strips a class of non-deterministic noise so that two outputs
|
|
36
|
+
// which differ only by a timestamp / sha / uuid / ANSI color / trailing
|
|
37
|
+
// whitespace hash-equal. Exported so the test suite can assert the contract.
|
|
38
|
+
const VOLATILE_PATTERNS = [
|
|
39
|
+
// ANSI escape / color codes (e.g. \x1b[0m). Strip first so later patterns
|
|
40
|
+
// see clean text.
|
|
41
|
+
{ name: 'ansi', re: /\x1b\[[0-9;]*[A-Za-z]/g, replacement: '' },
|
|
42
|
+
// ISO-8601 timestamps: 2026-05-31T12:34:56(.123)?(Z|+00:00)
|
|
43
|
+
{
|
|
44
|
+
name: 'iso8601',
|
|
45
|
+
re: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
|
|
46
|
+
replacement: '<TS>',
|
|
47
|
+
},
|
|
48
|
+
// UUIDs (dashed form) — must run before the hexblob pattern, otherwise the
|
|
49
|
+
// trailing 12-char hex segment gets rewritten first and the UUID never matches.
|
|
50
|
+
{
|
|
51
|
+
name: 'uuid',
|
|
52
|
+
re: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
53
|
+
replacement: '<UUID>',
|
|
54
|
+
},
|
|
55
|
+
// Epoch integers wider than 10 digits (ms/ns timestamps) — run before the
|
|
56
|
+
// hexblob pattern so all-decimal epochs are not swallowed as hex first.
|
|
57
|
+
{ name: 'epoch', re: /\b\d{11,}\b/g, replacement: '<EPOCH>' },
|
|
58
|
+
// Long hex blobs (commit shas, uuids without dashes, content hashes): 12+ hex chars.
|
|
59
|
+
{ name: 'hexblob', re: /\b[0-9a-fA-F]{12,}\b/g, replacement: '<HEX>' },
|
|
60
|
+
// Trailing whitespace is stripped in normalizeVolatile() via a bounded linear
|
|
61
|
+
// scan (not a regex) to avoid super-linear backtracking on adversarial input.
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Strip trailing spaces/tabs from a single line via a bounded reverse scan.
|
|
65
|
+
// Linear in line length; replaces /[ \t]+$/ without any regex backtracking.
|
|
66
|
+
function stripTrailingSpaceTab(line) {
|
|
67
|
+
let end = line.length;
|
|
68
|
+
while (end > 0) {
|
|
69
|
+
const c = line.charCodeAt(end - 1);
|
|
70
|
+
if (c !== 32 && c !== 9) break; // 32 = space, 9 = tab
|
|
71
|
+
end -= 1;
|
|
72
|
+
}
|
|
73
|
+
return line.slice(0, end);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Normalize a string by applying every volatile pattern, then trimming a
|
|
77
|
+
// trailing newline so "foo\n" and "foo" are equivalent.
|
|
78
|
+
function normalizeVolatile(value) {
|
|
79
|
+
if (value === null || value === undefined) return '';
|
|
80
|
+
let out = String(value);
|
|
81
|
+
for (const pattern of VOLATILE_PATTERNS) {
|
|
82
|
+
out = out.replace(pattern.re, pattern.replacement);
|
|
83
|
+
}
|
|
84
|
+
// Strip trailing space/tab per line without a backtracking-prone regex.
|
|
85
|
+
out = out.split('\n').map(stripTrailingSpaceTab).join('\n');
|
|
86
|
+
// Normalize CRLF, then strip the trailing run of newlines via a bounded
|
|
87
|
+
// linear scan (replaces /\n+$/ without regex backtracking).
|
|
88
|
+
out = out.replace(/\r\n/g, '\n');
|
|
89
|
+
let end = out.length;
|
|
90
|
+
while (end > 0 && out.charCodeAt(end - 1) === 10) end -= 1; // 10 = \n
|
|
91
|
+
return out.slice(0, end);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sha256(value) {
|
|
95
|
+
return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -- state hashing ----------------------------------------------------------
|
|
99
|
+
//
|
|
100
|
+
// computeActionStateHash produces a single stable fingerprint for an action's
|
|
101
|
+
// observable state. For file actions the fingerprint is the normalized content;
|
|
102
|
+
// for command actions it is exit code + normalized stdout/stderr.
|
|
103
|
+
function computeActionStateHash(action = {}) {
|
|
104
|
+
const kind = action.kind === 'command' ? 'command' : 'file';
|
|
105
|
+
|
|
106
|
+
if (kind === 'command') {
|
|
107
|
+
const exitCode = action.exitCode === undefined || action.exitCode === null
|
|
108
|
+
? ''
|
|
109
|
+
: String(action.exitCode);
|
|
110
|
+
const parts = [
|
|
111
|
+
'command',
|
|
112
|
+
`exit:${exitCode}`,
|
|
113
|
+
`stdout:${normalizeVolatile(action.stdout)}`,
|
|
114
|
+
`stderr:${normalizeVolatile(action.stderr)}`,
|
|
115
|
+
];
|
|
116
|
+
return sha256(parts.join('\x00'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// file kind: hash the relevant content (after-content is the canonical state
|
|
120
|
+
// for a single snapshot; for diff comparisons detectNoop compares before/after
|
|
121
|
+
// separately). Include filePath so two unrelated files never collide.
|
|
122
|
+
const content = action.afterContent !== undefined && action.afterContent !== null
|
|
123
|
+
? action.afterContent
|
|
124
|
+
: action.beforeContent;
|
|
125
|
+
const parts = [
|
|
126
|
+
'file',
|
|
127
|
+
`path:${action.filePath || ''}`,
|
|
128
|
+
`content:${normalizeVolatile(content)}`,
|
|
129
|
+
];
|
|
130
|
+
return sha256(parts.join('\x00'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Hash just a content blob for before/after comparison (no path/kind framing).
|
|
134
|
+
function contentHash(content) {
|
|
135
|
+
return sha256(normalizeVolatile(content));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// -- no-op detection --------------------------------------------------------
|
|
139
|
+
//
|
|
140
|
+
// detectNoop({ before, after }) returns { noop, reason }.
|
|
141
|
+
// before/after: { kind, filePath, beforeContent/afterContent OR content,
|
|
142
|
+
// exitCode, stdout, stderr }
|
|
143
|
+
//
|
|
144
|
+
// We accept a flexible shape: the caller may pass a single action object with
|
|
145
|
+
// beforeContent/afterContent, or split before/after sub-objects. Both forms
|
|
146
|
+
// resolve to the same decision.
|
|
147
|
+
function detectNoop(input = {}) {
|
|
148
|
+
const before = input.before || {};
|
|
149
|
+
const after = input.after || {};
|
|
150
|
+
|
|
151
|
+
// Determine kind from whichever side declares it (default file).
|
|
152
|
+
const kind = (before.kind || after.kind || input.kind) === 'command'
|
|
153
|
+
? 'command'
|
|
154
|
+
: 'file';
|
|
155
|
+
|
|
156
|
+
if (kind === 'command') {
|
|
157
|
+
const beforeExit = before.exitCode;
|
|
158
|
+
const afterExit = after.exitCode;
|
|
159
|
+
if (beforeExit !== undefined && afterExit !== undefined && beforeExit !== afterExit) {
|
|
160
|
+
return { noop: false, reason: 'exit-code-changed' };
|
|
161
|
+
}
|
|
162
|
+
const beforeOut = contentHash(`${normalizeVolatile(before.stdout)}\x00${normalizeVolatile(before.stderr)}`);
|
|
163
|
+
const afterOut = contentHash(`${normalizeVolatile(after.stdout)}\x00${normalizeVolatile(after.stderr)}`);
|
|
164
|
+
if (beforeOut === afterOut) {
|
|
165
|
+
return { noop: true, reason: 'command-output-unchanged' };
|
|
166
|
+
}
|
|
167
|
+
return { noop: false, reason: 'command-output-changed' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// file kind.
|
|
171
|
+
const beforeContent = before.content !== undefined ? before.content
|
|
172
|
+
: (before.beforeContent !== undefined ? before.beforeContent : input.beforeContent);
|
|
173
|
+
const afterContent = after.content !== undefined ? after.content
|
|
174
|
+
: (after.afterContent !== undefined ? after.afterContent : input.afterContent);
|
|
175
|
+
|
|
176
|
+
const beforeStr = beforeContent === undefined || beforeContent === null ? '' : String(beforeContent);
|
|
177
|
+
const afterStr = afterContent === undefined || afterContent === null ? '' : String(afterContent);
|
|
178
|
+
|
|
179
|
+
// Partial-write guard: an after-state that is a strict, shorter prefix of the
|
|
180
|
+
// before-state is a truncation, not a no-op — even though hashes differ, we
|
|
181
|
+
// make the intent explicit so callers never mistake it.
|
|
182
|
+
if (afterStr.length > 0 && afterStr.length < beforeStr.length && beforeStr.startsWith(afterStr)) {
|
|
183
|
+
return { noop: false, reason: 'partial-write-truncation' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (contentHash(beforeStr) === contentHash(afterStr)) {
|
|
187
|
+
return { noop: true, reason: 'file-content-unchanged' };
|
|
188
|
+
}
|
|
189
|
+
return { noop: false, reason: 'file-content-changed' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -- attempt persistence ----------------------------------------------------
|
|
193
|
+
//
|
|
194
|
+
// Stores (sessionId -> { "actionId\x00stateHash": timestamp }) so a repeated
|
|
195
|
+
// (actionId, stateHash) tuple within the TTL window is a strong repeat signal.
|
|
196
|
+
function attemptsPath() {
|
|
197
|
+
const sessionActionsPath = gatesEngine.SESSION_ACTIONS_PATH;
|
|
198
|
+
return path.join(path.dirname(sessionActionsPath), 'noop-attempts.json');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function loadAttempts() {
|
|
202
|
+
try {
|
|
203
|
+
const raw = fs.readFileSync(attemptsPath(), 'utf8');
|
|
204
|
+
const parsed = JSON.parse(raw);
|
|
205
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
206
|
+
return parsed;
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pruneExpired(store, now) {
|
|
213
|
+
let changed = false;
|
|
214
|
+
for (const sessionId of Object.keys(store)) {
|
|
215
|
+
const bucket = store[sessionId];
|
|
216
|
+
if (!bucket || typeof bucket !== 'object') {
|
|
217
|
+
delete store[sessionId];
|
|
218
|
+
changed = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const key of Object.keys(bucket)) {
|
|
222
|
+
const ts = bucket[key];
|
|
223
|
+
if (typeof ts !== 'number' || now - ts >= ATTEMPT_TTL_MS) {
|
|
224
|
+
delete bucket[key];
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (Object.keys(bucket).length === 0) {
|
|
229
|
+
delete store[sessionId];
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return changed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function saveAttempts(store) {
|
|
237
|
+
const file = attemptsPath();
|
|
238
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
239
|
+
fs.writeFileSync(file, JSON.stringify(store, null, 2) + '\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function attemptKey(actionId, stateHash) {
|
|
243
|
+
return `${String(actionId)}\x00${String(stateHash)}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// recordActionAttempt persists that (sessionId, actionId, stateHash) was seen.
|
|
247
|
+
// Returns { recorded: boolean, alreadySeen: boolean }.
|
|
248
|
+
function recordActionAttempt(sessionId, actionId, stateHash) {
|
|
249
|
+
const sid = String(sessionId || 'default');
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const store = loadAttempts();
|
|
252
|
+
pruneExpired(store, now);
|
|
253
|
+
|
|
254
|
+
if (!store[sid] || typeof store[sid] !== 'object') store[sid] = {};
|
|
255
|
+
const key = attemptKey(actionId, stateHash);
|
|
256
|
+
const alreadySeen = Object.prototype.hasOwnProperty.call(store[sid], key);
|
|
257
|
+
store[sid][key] = now;
|
|
258
|
+
saveAttempts(store);
|
|
259
|
+
return { recorded: true, alreadySeen };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// isRepeatAttempt returns true when this exact (actionId, stateHash) tuple was
|
|
263
|
+
// already recorded for this session within the TTL window.
|
|
264
|
+
function isRepeatAttempt(sessionId, actionId, stateHash) {
|
|
265
|
+
const sid = String(sessionId || 'default');
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
const store = loadAttempts();
|
|
268
|
+
const bucket = store[sid];
|
|
269
|
+
if (!bucket || typeof bucket !== 'object') return false;
|
|
270
|
+
const ts = bucket[attemptKey(actionId, stateHash)];
|
|
271
|
+
if (typeof ts !== 'number') return false;
|
|
272
|
+
return now - ts < ATTEMPT_TTL_MS;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
VOLATILE_PATTERNS,
|
|
277
|
+
ATTEMPT_TTL_MS,
|
|
278
|
+
normalizeVolatile,
|
|
279
|
+
computeActionStateHash,
|
|
280
|
+
detectNoop,
|
|
281
|
+
recordActionAttempt,
|
|
282
|
+
isRepeatAttempt,
|
|
283
|
+
// Exposed for tests / introspection.
|
|
284
|
+
attemptsPath,
|
|
285
|
+
};
|
|
@@ -580,17 +580,51 @@ function resolveGitHubRepository(env = process.env) {
|
|
|
580
580
|
function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } = {}) {
|
|
581
581
|
const normalizedBranch = String(branchName || '').trim();
|
|
582
582
|
if (!normalizedBranch) return null;
|
|
583
|
-
|
|
583
|
+
const token = env.GH_TOKEN || env.GITHUB_TOKEN;
|
|
584
|
+
if (!token) {
|
|
584
585
|
return null;
|
|
585
586
|
}
|
|
586
|
-
const args = ['pr', 'list'];
|
|
587
587
|
const repository = resolveGitHubRepository(env);
|
|
588
|
-
if (repository)
|
|
589
|
-
|
|
588
|
+
if (!repository) return null;
|
|
589
|
+
|
|
590
|
+
// Try REST API first as it is much more robust and doesn't rely on gh CLI's GraphQL auth
|
|
591
|
+
try {
|
|
592
|
+
const owner = repository.split('/')[0];
|
|
593
|
+
const url = `https://api.github.com/repos/${repository}/pulls?head=${encodeURIComponent(owner + ':' + normalizedBranch)}&state=open`;
|
|
594
|
+
const curlConfig = `header = "Authorization: token ${token}"\nheader = "User-Agent: ThumbGate-CI"`;
|
|
595
|
+
const curlResult = spawnSync('curl', [
|
|
596
|
+
'-s',
|
|
597
|
+
'-K', '-',
|
|
598
|
+
url
|
|
599
|
+
], {
|
|
600
|
+
encoding: 'utf8',
|
|
601
|
+
input: curlConfig
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (curlResult && curlResult.status === 0) {
|
|
605
|
+
const prs = JSON.parse(curlResult.stdout || '[]');
|
|
606
|
+
if (Array.isArray(prs) && prs.length > 0) {
|
|
607
|
+
return {
|
|
608
|
+
number: prs[0].number,
|
|
609
|
+
state: prs[0].state,
|
|
610
|
+
isDraft: prs[0].draft || false,
|
|
611
|
+
url: prs[0].html_url
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.warn(`[findOpenPrForBranch] REST fallback failed: ${err.message}`);
|
|
590
617
|
}
|
|
591
|
-
|
|
618
|
+
|
|
619
|
+
// Fall back to gh CLI
|
|
620
|
+
const args = ['pr', 'list', '--repo', repository, '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url'];
|
|
592
621
|
const result = runner(args);
|
|
593
622
|
if (!result || result.status !== 0) {
|
|
623
|
+
if (result) {
|
|
624
|
+
console.warn(`[findOpenPrForBranch] gh command failed with status ${result.status}. Stderr: ${result.stderr || ''}`);
|
|
625
|
+
} else {
|
|
626
|
+
console.warn(`[findOpenPrForBranch] gh command failed to spawn.`);
|
|
627
|
+
}
|
|
594
628
|
return null;
|
|
595
629
|
}
|
|
596
630
|
try {
|
|
@@ -8,6 +8,7 @@ const DEFAULT_PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
|
|
|
8
8
|
const DEFAULT_OUTPUT_DIR = path.join(__dirname, '..', 'docs', 'marketing');
|
|
9
9
|
const KNOWN_REPOS = Object.freeze({
|
|
10
10
|
'@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
|
|
11
|
+
'@modelcontextprotocol/sdk': 'modelcontextprotocol/typescript-sdk',
|
|
11
12
|
'@google/genai': 'googleapis/js-genai',
|
|
12
13
|
'@huggingface/transformers': 'huggingface/transformers.js',
|
|
13
14
|
'@lancedb/lancedb': 'lancedb/lancedb',
|
|
@@ -23,6 +24,24 @@ const KNOWN_REPOS = Object.freeze({
|
|
|
23
24
|
undici: 'nodejs/undici',
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
// Communities where ThumbGate's buyers live even though they are not npm
|
|
28
|
+
// dependencies. ThumbGate ships an MCP server, so the Model Context Protocol
|
|
29
|
+
// repos are the single highest-ROI ecosystem to contribute to — but the
|
|
30
|
+
// dependency-scan above would never surface them. These are always scouted on
|
|
31
|
+
// the default (no explicit --dependencies) path, de-duped against package.json.
|
|
32
|
+
const STRATEGIC_DEPENDENCIES = Object.freeze([
|
|
33
|
+
'@modelcontextprotocol/sdk', // MCP TypeScript SDK — the protocol ThumbGate implements
|
|
34
|
+
'modelcontextprotocol/servers', // MCP servers ecosystem — where MCP authors (our buyers) collaborate
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Honest, repo-accurate framing for the outreach draft. ThumbGate does not
|
|
38
|
+
// `import` these — it implements the protocol — so the generic "while using X"
|
|
39
|
+
// line would be a false claim. Keep drafts truthful (CEO honesty rule).
|
|
40
|
+
const RELATIONSHIP_OVERRIDES = Object.freeze({
|
|
41
|
+
'@modelcontextprotocol/sdk': 'building ThumbGate as an MCP server against the Model Context Protocol spec',
|
|
42
|
+
'modelcontextprotocol/servers': 'building and testing ThumbGate as an MCP server alongside the reference MCP servers',
|
|
43
|
+
});
|
|
44
|
+
|
|
26
45
|
const BOUNTY_KEYWORDS = [
|
|
27
46
|
'bug bounty',
|
|
28
47
|
'bounty',
|
|
@@ -70,7 +89,10 @@ function dependencyNames(pkg = {}) {
|
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
function repoFromDependency(name) {
|
|
73
|
-
|
|
92
|
+
if (KNOWN_REPOS[name]) return KNOWN_REPOS[name];
|
|
93
|
+
// A strategic identifier may itself be an "owner/repo" slug (not an npm name).
|
|
94
|
+
if (!name.startsWith('@') && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(name)) return name;
|
|
95
|
+
return '';
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
function buildIssueSearchQueries(repo) {
|
|
@@ -89,14 +111,18 @@ function scoreOpportunity(depName, repo, options = {}) {
|
|
|
89
111
|
score += 20;
|
|
90
112
|
reasons.push('known upstream repository');
|
|
91
113
|
}
|
|
92
|
-
if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
|
|
114
|
+
if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
|
|
93
115
|
score += 20;
|
|
94
116
|
reasons.push('high product adjacency for agent tooling');
|
|
95
117
|
}
|
|
96
|
-
if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
|
|
118
|
+
if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
|
|
97
119
|
score += 15;
|
|
98
120
|
reasons.push('large ecosystem visibility');
|
|
99
121
|
}
|
|
122
|
+
if (/modelcontext|mcp/i.test(depName) || /modelcontextprotocol/i.test(repo)) {
|
|
123
|
+
score += 12;
|
|
124
|
+
reasons.push("ThumbGate's own protocol surface — buyers are MCP authors");
|
|
125
|
+
}
|
|
100
126
|
if (options.includeBounties) {
|
|
101
127
|
score += 10;
|
|
102
128
|
reasons.push('bounty search enabled');
|
|
@@ -138,7 +164,7 @@ function buildOpportunity(depName, options = {}) {
|
|
|
138
164
|
'no bounty, security, or maintainer-policy claim without source link',
|
|
139
165
|
],
|
|
140
166
|
outreachDraft: repo
|
|
141
|
-
? `I found this while using ${depName} in ThumbGate. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
|
|
167
|
+
? `I found this while ${RELATIONSHIP_OVERRIDES[depName] || `using ${depName} in ThumbGate`}. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
|
|
142
168
|
: '',
|
|
143
169
|
};
|
|
144
170
|
}
|
|
@@ -149,7 +175,9 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
|
|
|
149
175
|
const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
|
|
150
176
|
const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
|
|
151
177
|
const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
|
|
152
|
-
const deps = explicitDeps.length
|
|
178
|
+
const deps = explicitDeps.length
|
|
179
|
+
? explicitDeps
|
|
180
|
+
: [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
|
|
153
181
|
const opportunities = deps
|
|
154
182
|
.map((dep) => buildOpportunity(dep, { includeBounties }))
|
|
155
183
|
.filter((opportunity) => opportunity.repo)
|
|
@@ -223,6 +251,8 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
|
|
|
223
251
|
|
|
224
252
|
module.exports = {
|
|
225
253
|
KNOWN_REPOS,
|
|
254
|
+
STRATEGIC_DEPENDENCIES,
|
|
255
|
+
RELATIONSHIP_OVERRIDES,
|
|
226
256
|
buildIssueSearchQueries,
|
|
227
257
|
buildOpportunity,
|
|
228
258
|
buildOssPrOpportunityScoutPlan,
|