thumbgate 1.3.0 → 1.4.1
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 +25 -0
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +204 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +54 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/operational-summary.js +41 -5
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* billing-setup.js — Wire up hosted billing for the CFO dashboard
|
|
6
|
+
*
|
|
7
|
+
* Generates a THUMBGATE_OPERATOR_KEY and stores it locally so that
|
|
8
|
+
* `node bin/cli.js cfo --today` pulls live revenue from the production server.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/billing-setup.js
|
|
12
|
+
*
|
|
13
|
+
* After running, set the printed key on Railway:
|
|
14
|
+
* railway variables set THUMBGATE_OPERATOR_KEY=<key>
|
|
15
|
+
* Then redeploy (or let Railway auto-deploy).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const crypto = require('node:crypto');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
|
|
23
|
+
const LOCAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
|
|
24
|
+
const PROD_URL = 'https://thumbgate-production.up.railway.app';
|
|
25
|
+
|
|
26
|
+
function generateOperatorKey() {
|
|
27
|
+
return `tg_op_${crypto.randomBytes(20).toString('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadExistingConfig() {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(LOCAL_CONFIG_PATH, 'utf8');
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveConfig(config) {
|
|
40
|
+
const dir = path.dirname(LOCAL_CONFIG_PATH);
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
fs.writeFileSync(LOCAL_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function verifyEndpoint(baseUrl, key) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL('/v1/billing/summary?window=today', baseUrl);
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
headers: { authorization: `Bearer ${key}`, accept: 'application/json' },
|
|
51
|
+
});
|
|
52
|
+
return { status: res.status, ok: res.ok };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return { status: null, ok: false, error: err.message };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const existing = loadExistingConfig();
|
|
60
|
+
|
|
61
|
+
if (existing && existing.operatorKey && existing.baseUrl) {
|
|
62
|
+
console.log('\n✓ Operator config already exists at', LOCAL_CONFIG_PATH);
|
|
63
|
+
console.log(' Base URL :', existing.baseUrl);
|
|
64
|
+
console.log(' Operator key:', existing.operatorKey);
|
|
65
|
+
console.log('\nTo regenerate, delete the file and re-run this script.');
|
|
66
|
+
|
|
67
|
+
// Set env for this process so the verify check works
|
|
68
|
+
process.env.THUMBGATE_OPERATOR_KEY = existing.operatorKey;
|
|
69
|
+
process.env.THUMBGATE_BILLING_API_BASE_URL = existing.baseUrl;
|
|
70
|
+
|
|
71
|
+
const check = await verifyEndpoint(existing.baseUrl, existing.operatorKey);
|
|
72
|
+
if (check.ok) {
|
|
73
|
+
console.log('\n✓ Production endpoint responds OK — hosted billing is active.');
|
|
74
|
+
} else if (check.status === 403) {
|
|
75
|
+
console.log('\n⚠ Production endpoint returned 403 — the operator key is not yet set on Railway.');
|
|
76
|
+
console.log('\nSet it now:\n');
|
|
77
|
+
console.log(` railway variables set THUMBGATE_OPERATOR_KEY=${existing.operatorKey}`);
|
|
78
|
+
console.log('\nThen redeploy (Railway will pick it up automatically).');
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`\n⚠ Endpoint check returned status ${check.status || 'error'}: ${check.error || ''}`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = generateOperatorKey();
|
|
86
|
+
const config = {
|
|
87
|
+
operatorKey: key,
|
|
88
|
+
baseUrl: process.env.THUMBGATE_BILLING_API_BASE_URL || PROD_URL,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
saveConfig(config);
|
|
93
|
+
|
|
94
|
+
console.log('\n✓ Operator key generated and saved to', LOCAL_CONFIG_PATH);
|
|
95
|
+
console.log('\n──────────────────────────────────────────────────────');
|
|
96
|
+
console.log(' THUMBGATE_OPERATOR_KEY =', key);
|
|
97
|
+
console.log('──────────────────────────────────────────────────────');
|
|
98
|
+
console.log('\nSet this key on Railway (one-time):');
|
|
99
|
+
console.log('\n railway variables set THUMBGATE_OPERATOR_KEY=' + key);
|
|
100
|
+
console.log('\nOr paste it into the Railway dashboard under Variables.');
|
|
101
|
+
console.log('\nAfter Railway redeploys, run:\n');
|
|
102
|
+
console.log(' node bin/cli.js cfo --today\n');
|
|
103
|
+
console.log('The CFO dashboard will use live production billing data.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main().catch((err) => {
|
|
107
|
+
console.error('billing-setup error:', err.message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
package/scripts/billing.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
resolveAnalyticsWindow,
|
|
44
44
|
serializeAnalyticsWindow,
|
|
45
45
|
} = require('./analytics-window');
|
|
46
|
+
const { ensureParentDir } = require('./fs-utils');
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
49
|
// Config
|
|
@@ -73,15 +74,7 @@ const CONFIG = {
|
|
|
73
74
|
get NEWSLETTER_SUBSCRIBERS_PATH() {
|
|
74
75
|
return process.env._TEST_NEWSLETTER_SUBSCRIBERS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
|
|
75
76
|
},
|
|
76
|
-
CREDIT_PACKS: {
|
|
77
|
-
'mistake-free-starter': {
|
|
78
|
-
id: 'mistake-free-starter',
|
|
79
|
-
name: 'Mistake-Free Starter Pack',
|
|
80
|
-
amountCents: 4900,
|
|
81
|
-
credits: 500,
|
|
82
|
-
currency: 'USD',
|
|
83
|
-
}
|
|
84
|
-
}
|
|
77
|
+
CREDIT_PACKS: {}
|
|
85
78
|
};
|
|
86
79
|
|
|
87
80
|
function resolveLegacyBillingPath(fileName) {
|
|
@@ -148,13 +141,6 @@ function safeCompareHex(expectedHex, actualHex) {
|
|
|
148
141
|
// Internal helpers
|
|
149
142
|
// ---------------------------------------------------------------------------
|
|
150
143
|
|
|
151
|
-
function ensureParentDir(filePath) {
|
|
152
|
-
const dir = path.dirname(filePath);
|
|
153
|
-
if (!fs.existsSync(dir)) {
|
|
154
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
144
|
function sanitizeMetadata(metadata) {
|
|
159
145
|
if (!metadata || typeof metadata !== 'object') return {};
|
|
160
146
|
try {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Budget Enforcer — action count, token, and time limits for agent sessions.
|
|
6
|
+
*
|
|
7
|
+
* Competitive parity with LaneKeep's budget system. Tracks:
|
|
8
|
+
* - max_actions: total tool calls allowed per session
|
|
9
|
+
* - max_time_minutes: wall-clock session duration cap
|
|
10
|
+
* - action_count: running count of tool calls in current session
|
|
11
|
+
*
|
|
12
|
+
* Budget state persists to ~/.thumbgate/budget-state.json.
|
|
13
|
+
* Config lives in config/budget.json or can be set via env vars.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const BUDGET_STATE_PATH = process.env.THUMBGATE_BUDGET_STATE_PATH || path.join(
|
|
20
|
+
process.env.HOME || '/tmp',
|
|
21
|
+
'.thumbgate',
|
|
22
|
+
'budget-state.json'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BUDGET_CONFIG_PATH = process.env.THUMBGATE_BUDGET_CONFIG_PATH || path.join(
|
|
26
|
+
__dirname, '..', 'config', 'budget.json'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const DEFAULT_BUDGET = {
|
|
30
|
+
max_actions: 2000,
|
|
31
|
+
max_time_minutes: 600, // 10 hours
|
|
32
|
+
profiles: {
|
|
33
|
+
strict: { max_actions: 500, max_time_minutes: 150 },
|
|
34
|
+
guided: { max_actions: 2000, max_time_minutes: 600 },
|
|
35
|
+
autonomous: { max_actions: 5000, max_time_minutes: 1200 },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function loadBudgetConfig() {
|
|
40
|
+
// 1. Environment overrides
|
|
41
|
+
const envProfile = process.env.THUMBGATE_BUDGET_PROFILE;
|
|
42
|
+
const envMaxActions = process.env.THUMBGATE_MAX_ACTIONS;
|
|
43
|
+
const envMaxTime = process.env.THUMBGATE_MAX_TIME_MINUTES;
|
|
44
|
+
|
|
45
|
+
// 2. Config file
|
|
46
|
+
let fileConfig = {};
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(DEFAULT_BUDGET_CONFIG_PATH)) {
|
|
49
|
+
fileConfig = JSON.parse(fs.readFileSync(DEFAULT_BUDGET_CONFIG_PATH, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
} catch { /* use defaults */ }
|
|
52
|
+
|
|
53
|
+
const merged = { ...DEFAULT_BUDGET, ...fileConfig };
|
|
54
|
+
|
|
55
|
+
// Apply profile if set
|
|
56
|
+
if (envProfile && merged.profiles && merged.profiles[envProfile]) {
|
|
57
|
+
Object.assign(merged, merged.profiles[envProfile]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Env overrides take final precedence
|
|
61
|
+
if (envMaxActions) {
|
|
62
|
+
const parsedMaxActions = parseInt(envMaxActions, 10);
|
|
63
|
+
if (Number.isFinite(parsedMaxActions) && parsedMaxActions > 0) {
|
|
64
|
+
merged.max_actions = parsedMaxActions;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (envMaxTime) {
|
|
68
|
+
const parsedMaxTime = parseInt(envMaxTime, 10);
|
|
69
|
+
if (Number.isFinite(parsedMaxTime) && parsedMaxTime > 0) {
|
|
70
|
+
merged.max_time_minutes = parsedMaxTime;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadBudgetState() {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(BUDGET_STATE_PATH)) {
|
|
80
|
+
return JSON.parse(fs.readFileSync(BUDGET_STATE_PATH, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
} catch { /* corrupted state — reset */ }
|
|
83
|
+
return { action_count: 0, session_start: new Date().toISOString() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function saveBudgetState(state) {
|
|
87
|
+
const dir = path.dirname(BUDGET_STATE_PATH);
|
|
88
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(BUDGET_STATE_PATH, JSON.stringify(state, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resetBudget() {
|
|
93
|
+
const state = { action_count: 0, session_start: new Date().toISOString() };
|
|
94
|
+
saveBudgetState(state);
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Evaluate budget limits. Called before every gate evaluation.
|
|
100
|
+
* Returns null if within budget, or a deny result if budget exceeded.
|
|
101
|
+
*/
|
|
102
|
+
function evaluateBudget(toolName, toolInput) {
|
|
103
|
+
const config = loadBudgetConfig();
|
|
104
|
+
const state = loadBudgetState();
|
|
105
|
+
|
|
106
|
+
// Increment action count
|
|
107
|
+
state.action_count = (state.action_count || 0) + 1;
|
|
108
|
+
saveBudgetState(state);
|
|
109
|
+
|
|
110
|
+
// Check action limit
|
|
111
|
+
if (config.max_actions && state.action_count > config.max_actions) {
|
|
112
|
+
return {
|
|
113
|
+
decision: 'deny',
|
|
114
|
+
gate: 'budget-action-limit',
|
|
115
|
+
message: `Budget exceeded: ${state.action_count}/${config.max_actions} actions used. Session budget is exhausted.`,
|
|
116
|
+
severity: 'critical',
|
|
117
|
+
reasoning: `Tool call #${state.action_count} exceeds the configured max_actions limit of ${config.max_actions}.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check time limit
|
|
122
|
+
if (config.max_time_minutes && state.session_start) {
|
|
123
|
+
const elapsedMs = Date.now() - new Date(state.session_start).getTime();
|
|
124
|
+
const elapsedMinutes = elapsedMs / (60 * 1000);
|
|
125
|
+
if (elapsedMinutes > config.max_time_minutes) {
|
|
126
|
+
return {
|
|
127
|
+
decision: 'deny',
|
|
128
|
+
gate: 'budget-time-limit',
|
|
129
|
+
message: `Budget exceeded: session has run ${Math.round(elapsedMinutes)}min, limit is ${config.max_time_minutes}min.`,
|
|
130
|
+
severity: 'critical',
|
|
131
|
+
reasoning: `Session duration (${Math.round(elapsedMinutes)}min) exceeds max_time_minutes (${config.max_time_minutes}).`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null; // Within budget
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get current budget status for dashboard/reporting.
|
|
141
|
+
*/
|
|
142
|
+
function getBudgetStatus() {
|
|
143
|
+
const config = loadBudgetConfig();
|
|
144
|
+
const state = loadBudgetState();
|
|
145
|
+
const elapsedMs = state.session_start
|
|
146
|
+
? Date.now() - new Date(state.session_start).getTime()
|
|
147
|
+
: 0;
|
|
148
|
+
const elapsedMinutes = Math.round(elapsedMs / (60 * 1000));
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
action_count: state.action_count || 0,
|
|
152
|
+
max_actions: config.max_actions,
|
|
153
|
+
actions_remaining: Math.max(0, (config.max_actions || Infinity) - (state.action_count || 0)),
|
|
154
|
+
actions_pct: config.max_actions ? Math.round(((state.action_count || 0) / config.max_actions) * 100) : 0,
|
|
155
|
+
elapsed_minutes: elapsedMinutes,
|
|
156
|
+
max_time_minutes: config.max_time_minutes,
|
|
157
|
+
time_remaining_minutes: Math.max(0, (config.max_time_minutes || Infinity) - elapsedMinutes),
|
|
158
|
+
time_pct: config.max_time_minutes ? Math.round((elapsedMinutes / config.max_time_minutes) * 100) : 0,
|
|
159
|
+
session_start: state.session_start,
|
|
160
|
+
profile: process.env.THUMBGATE_BUDGET_PROFILE || 'guided',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
evaluateBudget,
|
|
166
|
+
getBudgetStatus,
|
|
167
|
+
loadBudgetConfig,
|
|
168
|
+
loadBudgetState,
|
|
169
|
+
saveBudgetState,
|
|
170
|
+
resetBudget,
|
|
171
|
+
BUDGET_STATE_PATH,
|
|
172
|
+
DEFAULT_BUDGET_CONFIG_PATH,
|
|
173
|
+
};
|
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
7
|
const {
|
|
8
|
+
getClaudePluginReviewVersionedAssetName,
|
|
8
9
|
getClaudePluginVersionedAssetName,
|
|
9
10
|
} = require('./distribution-surfaces');
|
|
10
11
|
|
|
@@ -27,6 +28,14 @@ const RUNTIME_COPY_PATHS = [
|
|
|
27
28
|
'SECURITY.md',
|
|
28
29
|
'server.json',
|
|
29
30
|
];
|
|
31
|
+
const REVIEW_PACKET_COPY_PATHS = [
|
|
32
|
+
'.claude-plugin',
|
|
33
|
+
'docs/CLAUDE_DESKTOP_EXTENSION.md',
|
|
34
|
+
'README.md',
|
|
35
|
+
'LICENSE',
|
|
36
|
+
'SECURITY.md',
|
|
37
|
+
'server.json',
|
|
38
|
+
];
|
|
30
39
|
|
|
31
40
|
function readJson(relativePath) {
|
|
32
41
|
return JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, relativePath), 'utf8'));
|
|
@@ -173,17 +182,74 @@ function buildClaudeMcpb(outputDir = DEFAULT_OUTPUT_DIR) {
|
|
|
173
182
|
};
|
|
174
183
|
}
|
|
175
184
|
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
185
|
+
function buildClaudeReviewZip(outputDir = DEFAULT_OUTPUT_DIR) {
|
|
186
|
+
const packageJson = readJson('package.json');
|
|
187
|
+
const reviewRoot = path.join(outputDir, 'review');
|
|
188
|
+
const reviewDirName = 'thumbgate-claude-plugin-review';
|
|
189
|
+
const stageDir = path.join(reviewRoot, reviewDirName);
|
|
190
|
+
const outputFile = path.join(outputDir, getClaudePluginReviewVersionedAssetName(packageJson.version));
|
|
191
|
+
|
|
192
|
+
fs.rmSync(reviewRoot, { recursive: true, force: true });
|
|
193
|
+
fs.mkdirSync(stageDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
for (const relativePath of REVIEW_PACKET_COPY_PATHS) {
|
|
196
|
+
copyEntry(relativePath, stageDir);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fs.rmSync(outputFile, { force: true });
|
|
200
|
+
exec('zip', ['-qr', outputFile, reviewDirName], { cwd: reviewRoot });
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
stageDir,
|
|
204
|
+
outputFile,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveBuildRequest(args = [], cwd = process.cwd()) {
|
|
209
|
+
const mode = args.includes('--review-zip')
|
|
210
|
+
? 'review-zip'
|
|
211
|
+
: args.includes('--all')
|
|
212
|
+
? 'all'
|
|
213
|
+
: 'mcpb';
|
|
214
|
+
const outputArg = args.find((arg) => !arg.startsWith('--'));
|
|
215
|
+
const outputDir = outputArg
|
|
216
|
+
? path.resolve(cwd, outputArg)
|
|
179
217
|
: DEFAULT_OUTPUT_DIR;
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
|
|
219
|
+
return { mode, outputDir };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function runBuildRequest({ mode, outputDir }, deps = { buildClaudeMcpb, buildClaudeReviewZip }) {
|
|
223
|
+
if (mode === 'review-zip') {
|
|
224
|
+
const { outputFile } = deps.buildClaudeReviewZip(outputDir);
|
|
225
|
+
return [`Built Claude plugin review zip: ${outputFile}`];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (mode === 'all') {
|
|
229
|
+
const { outputFile } = deps.buildClaudeMcpb(outputDir);
|
|
230
|
+
const { outputFile: reviewOutputFile } = deps.buildClaudeReviewZip(outputDir);
|
|
231
|
+
return [
|
|
232
|
+
`Built Claude Desktop bundle: ${outputFile}`,
|
|
233
|
+
`Built Claude plugin review zip: ${reviewOutputFile}`,
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { outputFile } = deps.buildClaudeMcpb(outputDir);
|
|
238
|
+
return [`Built Claude Desktop bundle: ${outputFile}`];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (require.main === module) {
|
|
242
|
+
for (const line of runBuildRequest(resolveBuildRequest(process.argv.slice(2)))) {
|
|
243
|
+
console.log(line);
|
|
244
|
+
}
|
|
182
245
|
}
|
|
183
246
|
|
|
184
247
|
module.exports = {
|
|
185
248
|
DEFAULT_OUTPUT_DIR,
|
|
186
249
|
buildClaudeMcpbManifest,
|
|
250
|
+
buildClaudeReviewZip,
|
|
187
251
|
stageClaudeMcpbBundle,
|
|
188
252
|
buildClaudeMcpb,
|
|
253
|
+
resolveBuildRequest,
|
|
254
|
+
runBuildRequest,
|
|
189
255
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
const {
|
|
8
|
+
getCodexPluginVersionedAssetName,
|
|
9
|
+
} = require('./distribution-surfaces');
|
|
10
|
+
|
|
11
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
12
|
+
const DEFAULT_OUTPUT_DIR = path.join(PROJECT_ROOT, '.artifacts', 'codex-plugin');
|
|
13
|
+
const BUNDLE_ROOT_NAME = 'thumbgate-codex-plugin';
|
|
14
|
+
const FIXED_BINARY_DIRS = ['/usr/bin', '/bin'];
|
|
15
|
+
|
|
16
|
+
function readJson(relativePath) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, relativePath), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readText(relativePath) {
|
|
21
|
+
return fs.readFileSync(path.join(PROJECT_ROOT, relativePath), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyEntry(sourceRelativePath, targetRelativePath, stageDir) {
|
|
25
|
+
const sourcePath = path.join(PROJECT_ROOT, sourceRelativePath);
|
|
26
|
+
if (!fs.existsSync(sourcePath)) return;
|
|
27
|
+
|
|
28
|
+
const targetPath = path.join(stageDir, targetRelativePath || sourceRelativePath);
|
|
29
|
+
const stat = fs.statSync(sourcePath);
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
32
|
+
if (stat.isDirectory()) {
|
|
33
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildStandaloneMarketplace() {
|
|
41
|
+
const marketplace = readJson('.agents/plugins/marketplace.json');
|
|
42
|
+
return {
|
|
43
|
+
...marketplace,
|
|
44
|
+
plugins: (marketplace.plugins || []).map((plugin) => ({
|
|
45
|
+
...plugin,
|
|
46
|
+
source: {
|
|
47
|
+
...plugin.source,
|
|
48
|
+
path: './',
|
|
49
|
+
},
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveFixedBinary(binaryName, options = {}) {
|
|
55
|
+
const accessSync = options.accessSync || fs.accessSync;
|
|
56
|
+
const dirs = options.dirs || FIXED_BINARY_DIRS;
|
|
57
|
+
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
const binaryPath = path.join(dir, binaryName);
|
|
60
|
+
try {
|
|
61
|
+
accessSync(binaryPath, fs.constants.X_OK);
|
|
62
|
+
return binaryPath;
|
|
63
|
+
} catch {
|
|
64
|
+
// Try the next fixed, system-owned directory.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error(`Unable to find executable ${binaryName} in fixed system paths: ${dirs.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stageCodexPluginBundle(outputDir = DEFAULT_OUTPUT_DIR) {
|
|
72
|
+
const packageJson = readJson('package.json');
|
|
73
|
+
const bundleDir = path.join(outputDir, 'bundle');
|
|
74
|
+
const stageDir = path.join(bundleDir, BUNDLE_ROOT_NAME);
|
|
75
|
+
const outputFile = path.join(outputDir, getCodexPluginVersionedAssetName(packageJson.version));
|
|
76
|
+
|
|
77
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
78
|
+
fs.mkdirSync(stageDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
copyEntry('plugins/codex-profile/.codex-plugin', '.codex-plugin', stageDir);
|
|
81
|
+
copyEntry('plugins/codex-profile/.mcp.json', '.mcp.json', stageDir);
|
|
82
|
+
copyEntry('plugins/codex-profile/README.md', 'README.md', stageDir);
|
|
83
|
+
copyEntry('plugins/codex-profile/INSTALL.md', 'INSTALL.md', stageDir);
|
|
84
|
+
copyEntry('plugins/codex-profile/AGENTS.md', 'AGENTS.md', stageDir);
|
|
85
|
+
copyEntry('LICENSE', 'LICENSE', stageDir);
|
|
86
|
+
copyEntry('adapters/codex/config.toml', 'config.toml', stageDir);
|
|
87
|
+
|
|
88
|
+
fs.mkdirSync(path.join(stageDir, '.agents', 'plugins'), { recursive: true });
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(stageDir, '.agents', 'plugins', 'marketplace.json'),
|
|
91
|
+
JSON.stringify(buildStandaloneMarketplace(), null, 2) + '\n'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
bundleDir,
|
|
96
|
+
stageDir,
|
|
97
|
+
outputFile,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildCodexPlugin(outputDir = DEFAULT_OUTPUT_DIR) {
|
|
102
|
+
const { bundleDir, stageDir, outputFile } = stageCodexPluginBundle(outputDir);
|
|
103
|
+
const zipBinary = resolveFixedBinary('zip');
|
|
104
|
+
const unzipBinary = resolveFixedBinary('unzip');
|
|
105
|
+
|
|
106
|
+
execFileSync(zipBinary, ['-qr', outputFile, BUNDLE_ROOT_NAME], {
|
|
107
|
+
cwd: bundleDir,
|
|
108
|
+
stdio: 'inherit',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const contents = execFileSync(unzipBinary, ['-l', outputFile], {
|
|
112
|
+
cwd: PROJECT_ROOT,
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
process.stdout.write(contents);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
stageDir,
|
|
120
|
+
outputFile,
|
|
121
|
+
contents,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isCliEntrypoint(argv = process.argv) {
|
|
126
|
+
return path.resolve(argv[1] || '') === __filename;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function runCli(argv = process.argv, cwd = process.cwd(), log = console.log) {
|
|
130
|
+
const outputDir = argv[2]
|
|
131
|
+
? path.resolve(cwd, argv[2])
|
|
132
|
+
: DEFAULT_OUTPUT_DIR;
|
|
133
|
+
const { outputFile } = buildCodexPlugin(outputDir);
|
|
134
|
+
log(`Built Codex plugin bundle: ${outputFile}`);
|
|
135
|
+
return outputFile;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (isCliEntrypoint()) {
|
|
139
|
+
runCli();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
BUNDLE_ROOT_NAME,
|
|
144
|
+
DEFAULT_OUTPUT_DIR,
|
|
145
|
+
FIXED_BINARY_DIRS,
|
|
146
|
+
buildCodexPlugin,
|
|
147
|
+
buildStandaloneMarketplace,
|
|
148
|
+
isCliEntrypoint,
|
|
149
|
+
resolveFixedBinary,
|
|
150
|
+
runCli,
|
|
151
|
+
stageCodexPluginBundle,
|
|
152
|
+
};
|