thumbgate 1.27.17 → 1.27.18

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.
Files changed (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -221
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +99 -229
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -145
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -1,285 +0,0 @@
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
- };
@@ -1,192 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- function loadDotEnv(filePath = path.join(process.cwd(), '.env')) {
8
- if (!fs.existsSync(filePath)) return false;
9
- const text = fs.readFileSync(filePath, 'utf8');
10
- for (const line of text.split(/\r?\n/)) {
11
- const trimmed = line.trim();
12
- if (!trimmed || trimmed.startsWith('#')) continue;
13
- const eq = trimmed.indexOf('=');
14
- if (eq <= 0) continue;
15
- const key = trimmed.slice(0, eq).trim();
16
- const rawValue = trimmed.slice(eq + 1).trim();
17
- if (!key || process.env[key]) continue;
18
- process.env[key] = rawValue.replace(/^['"]|['"]$/g, '');
19
- }
20
- return true;
21
- }
22
-
23
- function parseArgs(argv = []) {
24
- const out = {
25
- json: argv.includes('--json'),
26
- requireLive: argv.includes('--require-live'),
27
- chat: argv.includes('--chat'),
28
- baseUrl: null,
29
- model: null,
30
- };
31
-
32
- for (let i = 0; i < argv.length; i += 1) {
33
- if (argv[i] === '--base-url') out.baseUrl = argv[++i];
34
- if (argv[i] === '--model') out.model = argv[++i];
35
- }
36
-
37
- return out;
38
- }
39
-
40
- function normalizeBaseUrl(baseUrl) {
41
- return String(baseUrl || '')
42
- .replace(/\/chat\/completions\/?$/i, '')
43
- .replace(/\/models\/?$/i, '')
44
- .replace(/\/+$/, '');
45
- }
46
-
47
- function extractModels(payload) {
48
- const data = Array.isArray(payload?.data) ? payload.data : [];
49
- return data
50
- .map((entry) => {
51
- if (typeof entry === 'string') return entry;
52
- if (entry && typeof entry === 'object') return entry.id || entry.name || null;
53
- return null;
54
- })
55
- .filter(Boolean);
56
- }
57
-
58
- async function fetchModels(baseUrl, fetchImpl = global.fetch) {
59
- if (typeof fetchImpl !== 'function') {
60
- return { ok: false, status: 'fetch_unavailable', models: [] };
61
- }
62
-
63
- const url = `${normalizeBaseUrl(baseUrl)}/models`;
64
- try {
65
- const response = await fetchImpl(url, { method: 'GET' });
66
- if (!response || !response.ok) {
67
- return {
68
- ok: false,
69
- status: 'models_endpoint_failed',
70
- httpStatus: response?.status || null,
71
- url,
72
- models: [],
73
- };
74
- }
75
- const payload = await response.json();
76
- return {
77
- ok: true,
78
- status: 'models_ready',
79
- httpStatus: response.status || 200,
80
- url,
81
- models: extractModels(payload),
82
- };
83
- } catch (error) {
84
- return {
85
- ok: false,
86
- status: 'models_endpoint_error',
87
- url,
88
- message: error?.message || String(error),
89
- models: [],
90
- };
91
- }
92
- }
93
-
94
- async function run(options = {}) {
95
- loadDotEnv();
96
- const {
97
- callOmlx,
98
- getOmlxBaseUrl,
99
- getOmlxModel,
100
- } = require('./llm-client');
101
-
102
- const baseUrl = normalizeBaseUrl(options.baseUrl || getOmlxBaseUrl());
103
- const modelsResult = await fetchModels(baseUrl, options.fetchImpl || global.fetch);
104
- const configuredModel = options.model || getOmlxModel();
105
- const selectedModel = options.model || modelsResult.models[0] || configuredModel;
106
- const result = {
107
- ok: modelsResult.ok,
108
- status: modelsResult.status,
109
- provider: 'omlx',
110
- baseUrl,
111
- model: selectedModel,
112
- models: modelsResult.models,
113
- modelsUrl: modelsResult.url,
114
- httpStatus: modelsResult.httpStatus || null,
115
- };
116
-
117
- if (!modelsResult.ok) {
118
- result.message = modelsResult.message || 'oMLX /v1/models is not reachable';
119
- if (options.requireLive) process.exitCode = 1;
120
- return result;
121
- }
122
-
123
- if (!options.chat) {
124
- result.status = modelsResult.models.length > 0 ? 'models_ready' : 'models_empty';
125
- result.ok = !options.requireLive || modelsResult.models.length > 0;
126
- if (!result.ok) process.exitCode = 1;
127
- return result;
128
- }
129
-
130
- process.env.THUMBGATE_OMLX_ENABLED = '1';
131
- process.env.THUMBGATE_OMLX_BASE_URL = baseUrl;
132
- process.env.THUMBGATE_OMLX_MODEL = selectedModel;
133
-
134
- const chat = await callOmlx({
135
- systemPrompt: 'You are a local inference smoke test.',
136
- userPrompt: 'Say that ThumbGate local oMLX inference is reachable.',
137
- maxTokens: 64,
138
- temperature: 0,
139
- returnMetadata: true,
140
- });
141
-
142
- if (!chat || typeof chat.text !== 'string' || chat.text.trim().length < 1) {
143
- process.exitCode = 1;
144
- return {
145
- ...result,
146
- ok: false,
147
- status: 'chat_failed',
148
- message: 'oMLX /v1/chat/completions did not return assistant text',
149
- };
150
- }
151
-
152
- return {
153
- ...result,
154
- ok: true,
155
- status: 'ready',
156
- model: chat.model || selectedModel,
157
- textPreview: chat.text.slice(0, 200),
158
- usage: chat.usage || null,
159
- };
160
- }
161
-
162
- async function main(argv = process.argv.slice(2)) {
163
- const options = parseArgs(argv);
164
- const result = await run(options);
165
- if (options.json) {
166
- console.log(JSON.stringify(result, null, 2));
167
- return;
168
- }
169
-
170
- console.log(`oMLX smoke status: ${result.status}`);
171
- console.log(`Provider: ${result.provider}`);
172
- console.log(`Base URL: ${result.baseUrl}`);
173
- console.log(`Model: ${result.model}`);
174
- console.log(`Models: ${result.models.length}`);
175
- console.log(`Result: ${result.ok ? 'ready' : result.message || 'not ready'}`);
176
- }
177
-
178
- if (require.main === module) {
179
- main().catch((error) => {
180
- console.error(error?.message || error);
181
- process.exit(1);
182
- });
183
- }
184
-
185
- module.exports = {
186
- loadDotEnv,
187
- parseArgs,
188
- normalizeBaseUrl,
189
- extractModels,
190
- fetchModels,
191
- run,
192
- };