atris 3.62.0 → 3.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +120 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +22 -2
- package/cli/atris_code.py +25 -1
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +242 -0
- package/commands/deck.js +2 -2
- package/commands/engine.js +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +82 -15
- package/commands/mission.js +4 -1
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/task.js +91 -2
- package/commands/transactions.js +115 -0
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +24 -1
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +90 -0
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-registry.js +102 -18
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- package/lib/runner-command.js +54 -3
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +122 -16
- package/scripts/det/ytrail-bench.js +215 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Check an x-search answer against what was actually posted on X, whatever
|
|
4
|
+
// layout Grok picked this time. X's free public embed endpoint returns the
|
|
5
|
+
// real post for each cited status id. Two passes:
|
|
6
|
+
// 1. citations into the answer: a real post whose words show up in the
|
|
7
|
+
// answer is `checked`.
|
|
8
|
+
// 2. quotes out of the answer: a quote of 40+ characters that matches no
|
|
9
|
+
// real post is `unverified`, or `unknown` when a lookup failed and it
|
|
10
|
+
// could belong to the post we could not read.
|
|
11
|
+
// The block parser below only decides where a link goes, never what counts.
|
|
12
|
+
// Same spirit as scripts/det/ytquote-repair.js: keep what the source says,
|
|
13
|
+
// flag the rest.
|
|
14
|
+
|
|
15
|
+
const EMBED_URL = 'https://cdn.syndication.twimg.com/tweet-result';
|
|
16
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
17
|
+
const MAX_IDS = 25;
|
|
18
|
+
const OVERLAP_MIN = 0.6;
|
|
19
|
+
const PROBE_WORDS = 6;
|
|
20
|
+
const MIN_QUOTE_WORDS = 3;
|
|
21
|
+
const MIN_REAL_WORDS = 4;
|
|
22
|
+
const NEAR_CHARS = 300;
|
|
23
|
+
const MIN_SPAN_CHARS = 40;
|
|
24
|
+
|
|
25
|
+
const STATUS_ID = /(?:x|twitter)\.com\/(?:i(?:\/web)?|([A-Za-z0-9_]{1,15}))\/status(?:es)?\/(\d{5,25})/i;
|
|
26
|
+
const HANDLE = /@([A-Za-z0-9_]{1,15})\b/;
|
|
27
|
+
// A post starts at "1.", "**1.**", "Post 1:", a heading, a top-level bullet
|
|
28
|
+
// with a handle, or a table row with a handle.
|
|
29
|
+
const BLOCK_START = /^\s?(?:\*\*)?\s?(?:\d{1,2}[.)]|(?:post|tweet)\s*#?\s*\d{1,2}\b)(?:\*\*)?[\s:]|^#{2,4}\s|^[-*•]\s.*@[A-Za-z0-9_]|^\|.*@[A-Za-z0-9_]/i;
|
|
30
|
+
const TEXT_LABEL = /\b(?:(?:full\s+)?(?:(?:tweet|post)\s+)?(?:text|content|quote)|tweet|post)\b\s*\**\s*:\s*\**\s*/i;
|
|
31
|
+
const QUOTE_SPAN = /["“]([^"“”]{15,1000}?)["”]/;
|
|
32
|
+
const URL_RE = /https?:\/\/\S+/g;
|
|
33
|
+
|
|
34
|
+
function decodeEntities(s) {
|
|
35
|
+
return String(s)
|
|
36
|
+
.replace(/&/g, '&')
|
|
37
|
+
.replace(/</g, '<')
|
|
38
|
+
.replace(/>/g, '>')
|
|
39
|
+
.replace(/"/g, '"')
|
|
40
|
+
.replace(/'/g, "'");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Like ytquote-repair's normalizeText, but keeps letters from any language so
|
|
44
|
+
// a Japanese or Spanish post can still be checked.
|
|
45
|
+
function normalizeText(s) {
|
|
46
|
+
return decodeEntities(s)
|
|
47
|
+
.replace(URL_RE, ' ')
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[‘’'`]/g, '')
|
|
50
|
+
.replace(/[^\p{L}\p{N} ]/gu, ' ')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function words(s) {
|
|
56
|
+
return normalizeText(s).split(' ').filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function statusIdFromUrl(url) {
|
|
60
|
+
const match = String(url || '').match(STATUS_ID);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
return { id: match[2], handle: match[1] || null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Pull the quoted post text out of one answer block. Prefers a labeled line
|
|
66
|
+
// ("Full tweet text: ..."), then the first long quoted span. Returns the text
|
|
67
|
+
// and the character offset where it ends, so a link can go right under it.
|
|
68
|
+
function quoteFromBlock(block) {
|
|
69
|
+
const label = block.match(TEXT_LABEL);
|
|
70
|
+
if (label) {
|
|
71
|
+
const lead = block.slice(label.index + label[0].length).match(/^>\s?/);
|
|
72
|
+
const start = label.index + label[0].length + (lead ? lead[0].length : 0);
|
|
73
|
+
const rest = block.slice(start);
|
|
74
|
+
const opener = rest.match(/^["“]/);
|
|
75
|
+
if (opener) {
|
|
76
|
+
const close = rest.slice(1).search(/["”](?=\s*(?:\.{3}|…)?\s*(?:$|\n|\*|\(|,|;|-|—|\[))/);
|
|
77
|
+
const end = close === -1 ? rest.search(/\n|$/) : close + 1;
|
|
78
|
+
const text = rest.slice(1, end).trim();
|
|
79
|
+
if (words(text).length >= MIN_QUOTE_WORDS) return { text, end: start + end + 1 };
|
|
80
|
+
} else {
|
|
81
|
+
const lineEnd = rest.search(/\n|$/);
|
|
82
|
+
const text = rest.slice(0, lineEnd).replace(/\*+/g, '').trim();
|
|
83
|
+
if (words(text).length >= MIN_QUOTE_WORDS) return { text, end: start + lineEnd };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const span = block.match(QUOTE_SPAN);
|
|
87
|
+
if (span && words(span[1]).length >= MIN_QUOTE_WORDS) {
|
|
88
|
+
return { text: span[1].trim(), end: span.index + span[0].length };
|
|
89
|
+
}
|
|
90
|
+
return blockquoteFromBlock(block) || tableCellFromBlock(block);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// "> post text" lines, joined.
|
|
94
|
+
function blockquoteFromBlock(block) {
|
|
95
|
+
const lines = block.split('\n');
|
|
96
|
+
const first = lines.findIndex((l) => /^\s*>\s?\S/.test(l));
|
|
97
|
+
if (first === -1) return null;
|
|
98
|
+
let last = first;
|
|
99
|
+
while (last + 1 < lines.length && /^\s*>/.test(lines[last + 1])) last += 1;
|
|
100
|
+
const text = lines.slice(first, last + 1).map((l) => l.replace(/^\s*>\s?/, '')).join(' ').replace(/\*+/g, '').trim();
|
|
101
|
+
if (words(text).length < MIN_QUOTE_WORDS) return null;
|
|
102
|
+
return { text, end: lines.slice(0, last + 1).join('\n').length };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A markdown table row: the wordiest cell that is not just a handle or link.
|
|
106
|
+
function tableCellFromBlock(block) {
|
|
107
|
+
if (!/^\|/.test(block)) return null;
|
|
108
|
+
const row = block.split('\n')[0];
|
|
109
|
+
const cells = row.split('|').map((c) => c.replace(/\*+/g, '').trim())
|
|
110
|
+
.filter((c) => c && !/^@\w+$/.test(c) && !/^https?:\/\//.test(c));
|
|
111
|
+
let best = null;
|
|
112
|
+
for (const cell of cells) {
|
|
113
|
+
if (!best || words(cell).length > words(best).length) best = cell;
|
|
114
|
+
}
|
|
115
|
+
if (!best || words(best).length < MIN_QUOTE_WORDS + 3) return null;
|
|
116
|
+
return { text: best.replace(/^["“]|["”]$/g, ''), end: row.length };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function splitBlocks(lines) {
|
|
120
|
+
const blocks = [];
|
|
121
|
+
let current = null;
|
|
122
|
+
lines.forEach((line, index) => {
|
|
123
|
+
if (BLOCK_START.test(line)) {
|
|
124
|
+
if (current) blocks.push(current);
|
|
125
|
+
current = { start: index, lines: [line] };
|
|
126
|
+
} else if (current) {
|
|
127
|
+
current.lines.push(line);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (current) blocks.push(current);
|
|
131
|
+
return blocks;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Helper only: parse numbered posts from Grok's answer. Tolerant of markdown:
|
|
135
|
+
// **@handle**, "(@handle)", "Full tweet text: ...", or a plain quoted span.
|
|
136
|
+
// defaultHandle fills in for person searches that never repeat the handle.
|
|
137
|
+
function parsePosts(content, { defaultHandle = null } = {}) {
|
|
138
|
+
const lines = String(content || '').replace(/\r\n/g, '\n').split('\n');
|
|
139
|
+
const posts = [];
|
|
140
|
+
for (const block of splitBlocks(lines)) {
|
|
141
|
+
const body = block.lines.join('\n');
|
|
142
|
+
const quote = quoteFromBlock(body);
|
|
143
|
+
if (!quote) continue;
|
|
144
|
+
const outside = body.replace(quote.text, ' ');
|
|
145
|
+
const linked = statusIdFromUrl(outside);
|
|
146
|
+
const handleMatch = outside.match(HANDLE);
|
|
147
|
+
const handle = (handleMatch && handleMatch[1])
|
|
148
|
+
|| (linked && linked.handle)
|
|
149
|
+
|| (defaultHandle ? String(defaultHandle).replace(/^@/, '') : null);
|
|
150
|
+
if (!handle) continue;
|
|
151
|
+
const endLine = block.start + body.slice(0, quote.end).split('\n').length - 1;
|
|
152
|
+
posts.push({
|
|
153
|
+
index: posts.length + 1,
|
|
154
|
+
handle,
|
|
155
|
+
text: quote.text,
|
|
156
|
+
line: Math.min(endLine, block.start + block.lines.length - 1),
|
|
157
|
+
linkedId: linked ? linked.id : null,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return posts;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Citation urls in order, one entry per status id. Non-status urls come back
|
|
164
|
+
// with id null so they are never lost.
|
|
165
|
+
function parseCitations(citations) {
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const raw of Array.isArray(citations) ? citations : []) {
|
|
169
|
+
const url = String(raw || '').trim();
|
|
170
|
+
if (!url) continue;
|
|
171
|
+
const parsed = statusIdFromUrl(url);
|
|
172
|
+
if (parsed && seen.has(parsed.id)) continue;
|
|
173
|
+
if (parsed) seen.add(parsed.id);
|
|
174
|
+
out.push({ url, id: parsed ? parsed.id : null, handle: parsed ? parsed.handle : null });
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function realText(json) {
|
|
180
|
+
const note = json?.note_tweet?.note_tweet_results?.result?.text;
|
|
181
|
+
return String(note || json?.text || '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// One embed lookup. ok:false means we could not tell (network, timeout, rate
|
|
185
|
+
// limit). ok:true with post:null means X says the post does not exist.
|
|
186
|
+
async function fetchXPost(id, { fetch = globalThis.fetch, timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
187
|
+
if (typeof fetch !== 'function') return { id, ok: false, error: 'no fetch' };
|
|
188
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
189
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(`${EMBED_URL}?id=${encodeURIComponent(id)}&token=a`, {
|
|
192
|
+
signal: controller ? controller.signal : undefined,
|
|
193
|
+
headers: { accept: 'application/json' },
|
|
194
|
+
});
|
|
195
|
+
if (res.status === 404) return { id, ok: true, post: null };
|
|
196
|
+
if (!res.ok) return { id, ok: false, error: `http ${res.status}` };
|
|
197
|
+
const json = await res.json();
|
|
198
|
+
const handle = json?.user?.screen_name;
|
|
199
|
+
const text = realText(json);
|
|
200
|
+
if (!handle || !text || json.__typename === 'TweetTombstone') return { id, ok: true, post: null };
|
|
201
|
+
return { id, ok: true, post: { id, handle, text, createdAt: json.created_at || null } };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return { id, ok: false, error: err && err.name === 'AbortError' ? 'timeout' : String(err && err.message || err) };
|
|
204
|
+
} finally {
|
|
205
|
+
if (timer) clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function textAgrees(quote, real) {
|
|
210
|
+
const quoteWords = words(quote);
|
|
211
|
+
const realWords = words(real);
|
|
212
|
+
if (!quoteWords.length || !realWords.length) return false;
|
|
213
|
+
const probe = quoteWords.slice(0, Math.min(PROBE_WORDS, quoteWords.length)).join(' ');
|
|
214
|
+
if (` ${realWords.join(' ')} `.includes(` ${probe} `)) return true;
|
|
215
|
+
const realSet = new Set(realWords);
|
|
216
|
+
const hits = quoteWords.filter((w) => realSet.has(w)).length;
|
|
217
|
+
return hits / quoteWords.length >= OVERLAP_MIN;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The words of a real post, minus leading reply mentions ("@a @b actual text"),
|
|
221
|
+
// which answers usually leave out.
|
|
222
|
+
function realWordsOf(text) {
|
|
223
|
+
return words(String(text || '').replace(/^\s*(?:@[A-Za-z0-9_]+\s+)+/, ''));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Does the real post show up anywhere in the answer, whatever the layout?
|
|
227
|
+
// Yes when its first 6 words appear, or when some window of the answer holds
|
|
228
|
+
// 60% of its words.
|
|
229
|
+
function realInAnswer(realWords, answerWords, answerFlat) {
|
|
230
|
+
if (realWords.length < MIN_REAL_WORDS) return false;
|
|
231
|
+
const probe = realWords.slice(0, Math.min(PROBE_WORDS, realWords.length)).join(' ');
|
|
232
|
+
if (answerFlat.includes(` ${probe} `)) return true;
|
|
233
|
+
const n = realWords.length;
|
|
234
|
+
if (!answerWords.length) return false;
|
|
235
|
+
const size = Math.min(n, answerWords.length);
|
|
236
|
+
const need = new Map();
|
|
237
|
+
for (const w of realWords) need.set(w, (need.get(w) || 0) + 1);
|
|
238
|
+
const have = new Map();
|
|
239
|
+
let hits = 0;
|
|
240
|
+
const add = (w, delta) => {
|
|
241
|
+
if (!need.has(w)) return;
|
|
242
|
+
const before = Math.min(have.get(w) || 0, need.get(w));
|
|
243
|
+
have.set(w, (have.get(w) || 0) + delta);
|
|
244
|
+
const after = Math.min(have.get(w), need.get(w));
|
|
245
|
+
hits += after - before;
|
|
246
|
+
};
|
|
247
|
+
for (let i = 0; i < answerWords.length; i += 1) {
|
|
248
|
+
add(answerWords[i], 1);
|
|
249
|
+
if (i >= size) add(answerWords[i - size], -1);
|
|
250
|
+
if (i >= size - 1 && hits / n >= OVERLAP_MIN) return true;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Every 6-word run of the real post, for the looser near-the-handle rule.
|
|
256
|
+
function realGrams(realWords) {
|
|
257
|
+
const grams = [];
|
|
258
|
+
for (let i = 0; i + PROBE_WORDS <= realWords.length; i += 1) {
|
|
259
|
+
grams.push(realWords.slice(i, i + PROBE_WORDS).join(' '));
|
|
260
|
+
}
|
|
261
|
+
return grams;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Every quote pair, straight or curly, paired left to right inside each
|
|
265
|
+
// paragraph. Short quotes are paired too, so a short "phrase" never shifts
|
|
266
|
+
// the pairing and turns the gap between two posts into a fake quote.
|
|
267
|
+
function allQuotes(content) {
|
|
268
|
+
const text = String(content || '');
|
|
269
|
+
const out = [];
|
|
270
|
+
const para = /[^\n]*(?:\n(?!\s*\n)[^\n]*)*/g;
|
|
271
|
+
let p;
|
|
272
|
+
while ((p = para.exec(text))) {
|
|
273
|
+
if (!p[0]) { para.lastIndex += 1; continue; }
|
|
274
|
+
const re = /["“]([^"“”]*)["”]/g;
|
|
275
|
+
let m;
|
|
276
|
+
while ((m = re.exec(p[0]))) {
|
|
277
|
+
out.push({ text: m[1].trim(), start: p.index + m.index, end: p.index + m.index + m[0].length });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Quoted spans of 40+ characters, anywhere in the answer.
|
|
284
|
+
function quotedSpans(content) {
|
|
285
|
+
return allQuotes(content).filter((q) => q.text.length >= MIN_SPAN_CHARS);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Near the handle: within ~300 characters of an @handle mention there is a
|
|
289
|
+
// 6-word run of the real post, or a quote whose words are mostly in it.
|
|
290
|
+
function nearHandle(content, handle, realWords) {
|
|
291
|
+
const text = String(content || '');
|
|
292
|
+
const re = new RegExp(`@${handle.replace(/[^A-Za-z0-9_]/g, '')}\\b`, 'gi');
|
|
293
|
+
const grams = realGrams(realWords);
|
|
294
|
+
const realSet = new Set(realWords);
|
|
295
|
+
let m;
|
|
296
|
+
while ((m = re.exec(text))) {
|
|
297
|
+
const hood = text.slice(Math.max(0, m.index - NEAR_CHARS), m.index + m[0].length + NEAR_CHARS);
|
|
298
|
+
const flat = ` ${words(hood).join(' ')} `;
|
|
299
|
+
if (grams.some((g) => flat.includes(` ${g} `))) return true;
|
|
300
|
+
for (const span of allQuotes(hood)) {
|
|
301
|
+
const w = words(span.text);
|
|
302
|
+
if (w.length >= MIN_REAL_WORDS && w.filter((x) => realSet.has(x)).length / w.length >= OVERLAP_MIN) return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function lineAt(content, offset) {
|
|
309
|
+
return String(content).slice(0, offset).split('\n').length - 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Where a link can go inline: a line inside a post block that is not a table
|
|
313
|
+
// row. The block parser only places links; it never decides what counts.
|
|
314
|
+
function inlineLines(lines) {
|
|
315
|
+
const ok = new Set();
|
|
316
|
+
for (const block of splitBlocks(lines)) {
|
|
317
|
+
block.lines.forEach((line, i) => {
|
|
318
|
+
if (!/^\s*\|/.test(line)) ok.add(block.start + i);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return ok;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Last non-empty line of the post block holding this real post, or null.
|
|
325
|
+
function blockLineFor(lines, realWords) {
|
|
326
|
+
for (const block of splitBlocks(lines)) {
|
|
327
|
+
const body = block.lines.join('\n');
|
|
328
|
+
const w = words(body);
|
|
329
|
+
if (realInAnswer(realWords, w, ` ${w.join(' ')} `)) {
|
|
330
|
+
for (let i = block.lines.length - 1; i >= 0; i -= 1) {
|
|
331
|
+
if (block.lines[i].trim()) return block.start + i;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Pure matcher, layout-independent. results: [{ id, ok, post }].
|
|
339
|
+
// Pass one, citations into the answer: a real post that appears in the answer
|
|
340
|
+
// is checked. Pass two, quotes out of the answer: a 40+ character quote that
|
|
341
|
+
// matches no real post is unverified, or unknown when a lookup failed.
|
|
342
|
+
function matchAnswer(content, results) {
|
|
343
|
+
const text = String(content || '').replace(/\r\n/g, '\n');
|
|
344
|
+
const lines = text.split('\n');
|
|
345
|
+
const answerWords = words(text);
|
|
346
|
+
const answerFlat = ` ${answerWords.join(' ')} `;
|
|
347
|
+
const spans = quotedSpans(text);
|
|
348
|
+
const inline = inlineLines(lines);
|
|
349
|
+
const found = results.filter((r) => r.ok && r.post);
|
|
350
|
+
// Failed lookups, and ids past the lookup cap, could be any quote's source.
|
|
351
|
+
const anyFailed = results.some((r) => !r.ok);
|
|
352
|
+
const spanOwner = new Map();
|
|
353
|
+
const checks = [];
|
|
354
|
+
|
|
355
|
+
for (const r of found) {
|
|
356
|
+
const realWords = realWordsOf(r.post.text);
|
|
357
|
+
const span = spans.find((sp) => !spanOwner.has(sp) && realWords.length >= MIN_REAL_WORDS && textAgrees(sp.text, r.post.text));
|
|
358
|
+
const hit = Boolean(span)
|
|
359
|
+
|| realInAnswer(realWords, answerWords, answerFlat)
|
|
360
|
+
|| (realWords.length >= MIN_REAL_WORDS && nearHandle(text, r.post.handle, realWords));
|
|
361
|
+
if (!hit) continue;
|
|
362
|
+
if (span) spanOwner.set(span, r.id);
|
|
363
|
+
const anchor = span ? lineAt(text, span.end) : blockLineFor(lines, realWords);
|
|
364
|
+
checks.push({
|
|
365
|
+
status: 'checked',
|
|
366
|
+
handle: r.post.handle,
|
|
367
|
+
id: r.id,
|
|
368
|
+
url: `https://x.com/${r.post.handle}/status/${r.id}`,
|
|
369
|
+
text: r.post.text,
|
|
370
|
+
posted_at: r.post.createdAt,
|
|
371
|
+
line: anchor !== null && inline.has(anchor) ? anchor : null,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const sp of spans) {
|
|
376
|
+
if (spanOwner.has(sp)) continue;
|
|
377
|
+
if (found.some((r) => textAgrees(sp.text, r.post.text))) continue;
|
|
378
|
+
const anchor = lineAt(text, sp.end);
|
|
379
|
+
checks.push({
|
|
380
|
+
status: anyFailed ? 'unknown' : 'unverified',
|
|
381
|
+
handle: null,
|
|
382
|
+
id: null,
|
|
383
|
+
url: null,
|
|
384
|
+
text: sp.text,
|
|
385
|
+
posted_at: null,
|
|
386
|
+
line: inline.has(anchor) ? anchor : null,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return checks;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function tallyChecks(checks) {
|
|
393
|
+
const tally = { checked: 0, unverified: 0, unknown: 0 };
|
|
394
|
+
for (const c of checks) tally[c.status] += 1;
|
|
395
|
+
return tally;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function formatTally(tally) {
|
|
399
|
+
return `posts: ${tally.checked} checked, ${tally.unverified} unverified, ${tally.unknown} unknown`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Check one answer against the real posts behind its citations. Returns null
|
|
403
|
+
// when there is nothing to check, so the caller prints the original output.
|
|
404
|
+
async function checkXPosts({ content, citations, fetchPost, fetch, timeoutMs } = {}) {
|
|
405
|
+
const cites = parseCitations(citations);
|
|
406
|
+
const ids = [];
|
|
407
|
+
const inlineIds = [...String(content || '').matchAll(new RegExp(STATUS_ID.source, 'gi'))].map((m) => m[2]);
|
|
408
|
+
for (const id of [...inlineIds, ...cites.map((c) => c.id)]) {
|
|
409
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
410
|
+
}
|
|
411
|
+
if (!ids.length && !quotedSpans(content).length) return null;
|
|
412
|
+
const lookup = fetchPost || ((id) => fetchXPost(id, { fetch, timeoutMs }));
|
|
413
|
+
const skipped = ids.slice(MAX_IDS).map((id) => ({ id, ok: false, error: 'not looked up' }));
|
|
414
|
+
const looked = await Promise.all(ids.slice(0, MAX_IDS).map(async (id) => {
|
|
415
|
+
try {
|
|
416
|
+
const r = await lookup(id);
|
|
417
|
+
return r && typeof r === 'object' ? { ...r, id } : { id, ok: false, error: 'empty' };
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return { id, ok: false, error: String(err && err.message || err) };
|
|
420
|
+
}
|
|
421
|
+
}));
|
|
422
|
+
const results = [...looked, ...skipped];
|
|
423
|
+
const checks = matchAnswer(content, results);
|
|
424
|
+
if (!checks.length) return null;
|
|
425
|
+
const usedIds = new Set(checks.filter((c) => c.id).map((c) => c.id));
|
|
426
|
+
return {
|
|
427
|
+
checks,
|
|
428
|
+
tally: tallyChecks(checks),
|
|
429
|
+
otherSources: cites.filter((c) => !c.id || !usedIds.has(c.id)).map((c) => c.url),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const UNMATCHED_NOTE = '(could not find this post on X, treat as unverified)';
|
|
434
|
+
const UNKNOWN_NOTE = '(could not reach X to check this post)';
|
|
435
|
+
|
|
436
|
+
function indentOf(line) {
|
|
437
|
+
const lead = String(line).match(/^\s*/)[0];
|
|
438
|
+
return lead.length ? lead : ' ';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function noteFor(check) {
|
|
442
|
+
if (check.status === 'checked') return `${check.url} (checked)`;
|
|
443
|
+
return check.status === 'unknown' ? UNKNOWN_NOTE : UNMATCHED_NOTE;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function shortQuote(text, max = 60) {
|
|
447
|
+
const flat = String(text).replace(/\s+/g, ' ').trim();
|
|
448
|
+
return flat.length > max ? `${flat.slice(0, max - 3).trim()}...` : flat;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Put each link or warning under its post when the post block can be found.
|
|
452
|
+
// Anything that cannot be placed (tables, prose) is listed after the answer.
|
|
453
|
+
function annotateContent(content, result) {
|
|
454
|
+
const lines = String(content || '').replace(/\r\n/g, '\n').split('\n');
|
|
455
|
+
const byLine = new Map();
|
|
456
|
+
const loose = [];
|
|
457
|
+
for (const check of result.checks) {
|
|
458
|
+
if (check.line === null || check.line === undefined || check.line >= lines.length) {
|
|
459
|
+
loose.push(check);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const list = byLine.get(check.line) || [];
|
|
463
|
+
list.push(`${indentOf(lines[check.line])}${noteFor(check)}`);
|
|
464
|
+
byLine.set(check.line, list);
|
|
465
|
+
}
|
|
466
|
+
const out = [];
|
|
467
|
+
lines.forEach((line, index) => {
|
|
468
|
+
out.push(line);
|
|
469
|
+
if (byLine.has(index)) out.push(...byLine.get(index));
|
|
470
|
+
});
|
|
471
|
+
const looseChecked = loose.filter((c) => c.status === 'checked');
|
|
472
|
+
const looseOther = loose.filter((c) => c.status !== 'checked');
|
|
473
|
+
if (looseChecked.length) {
|
|
474
|
+
out.push('', 'Checked posts:');
|
|
475
|
+
for (const c of looseChecked) out.push(` @${c.handle}: ${c.url} (checked)`);
|
|
476
|
+
}
|
|
477
|
+
if (looseOther.length) {
|
|
478
|
+
out.push('', 'Unverified quotes:');
|
|
479
|
+
for (const c of looseOther) out.push(` "${shortQuote(c.text)}" ${noteFor(c)}`);
|
|
480
|
+
}
|
|
481
|
+
return out.join('\n');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
UNMATCHED_NOTE,
|
|
486
|
+
UNKNOWN_NOTE,
|
|
487
|
+
normalizeText,
|
|
488
|
+
parsePosts,
|
|
489
|
+
parseCitations,
|
|
490
|
+
fetchXPost,
|
|
491
|
+
textAgrees,
|
|
492
|
+
quotedSpans,
|
|
493
|
+
matchAnswer,
|
|
494
|
+
formatTally,
|
|
495
|
+
checkXPosts,
|
|
496
|
+
annotateContent,
|
|
497
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Saved x-search answer in the shape Grok returned on 2026-09-29, plus the embed results X would return for each cited id. Used by the offline-check bench case and test/x-post-check.test.js. No network.",
|
|
3
|
+
"content": "Here are the most notable recent posts about AI agent startups launching:\n\n1. **@shivanipod** (Shivani Poddar)\n **Exact date/time**: Mon, 28 Sep 2026 17:27:49 GMT\n **Full tweet text**: \"I led engineering at Google DeepMind. Today, I'm proud to introduce Fo ...\"\n **Engagement**: Likes=4750, Reposts=512, Replies=230\n\n2. **@garrytan** (Garry Tan)\n **Exact date/time**: Mon, 28 Sep 2026 15:02:11 GMT\n **Full tweet text**: \"Agent startups in this YC batch went from zero to real revenue in weeks, not years.\"\n **Engagement**: Likes=2100, Reposts=190, Replies=88\n\n3. **@levelsio** (Pieter Levels)\n **Exact date/time**: Sun, 27 Sep 2026 22:40:03 GMT\n **Full tweet text**: \"Launched my new AI agent that books every flight for you and it already has 10,000 users\"\n **Engagement**: Likes=900, Reposts=40, Replies=61\n\n4. **@swyx** (swyx)\n **Exact date/time**: Sun, 27 Sep 2026 19:15:44 GMT\n **Full tweet text**: \"The agent engineer is the new full stack engineer, and the launches this week prove it.\"\n **Engagement**: Likes=1300, Reposts=120, Replies=45\n\nOverall, launches cluster around coding agents and back-office automation.",
|
|
4
|
+
"citations": [
|
|
5
|
+
"https://x.com/i/status/1972000000000000001",
|
|
6
|
+
"https://x.com/i/status/1972000000000000002",
|
|
7
|
+
"https://x.com/i/status/1972000000000000003",
|
|
8
|
+
"https://x.com/i/status/1972000000000000004",
|
|
9
|
+
"https://x.com/i/status/1972000000000000005",
|
|
10
|
+
"https://x.com/i/status/1972000000000000006"
|
|
11
|
+
],
|
|
12
|
+
"embeds": {
|
|
13
|
+
"1972000000000000001": { "handle": "shivanipod", "text": "I led engineering at Google DeepMind. Today, I'm proud to introduce Forge, the agent that ships your backlog. https://t.co/abc123" },
|
|
14
|
+
"1972000000000000002": { "handle": "GarryTan", "text": "Agent startups in this YC batch went from zero to real revenue in weeks, not years. The speed is unreal." },
|
|
15
|
+
"1972000000000000003": { "handle": "levelsio", "text": "Spent the whole day fixing my server. Coffee is the real agent." },
|
|
16
|
+
"1972000000000000004": { "handle": "swyx", "text": "The agent engineer is the new full stack engineer, and the launches this week prove it." },
|
|
17
|
+
"1972000000000000005": { "handle": "randomuser", "text": "Totally unrelated post about the weather in Lisbon today." },
|
|
18
|
+
"1972000000000000006": null
|
|
19
|
+
},
|
|
20
|
+
"expect": {
|
|
21
|
+
"all_up": { "checked": 3, "unverified": 1, "unknown": 0, "other_sources": 3 },
|
|
22
|
+
"one_down": { "down_id": "1972000000000000004", "checked": 2, "unverified": 0, "unknown": 2 }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Live x-search answer from 2026-09-29 (topic case, 17 citations), with the real posts X returned for each cited id. The first 4 citations are the ones the answer quoted; the original citation order was not kept, so it is rebuilt that way. Used by test/x-post-check.test.js. No network.",
|
|
3
|
+
"content": "**Here are the 5 most relevant posts** from the last 2 days (Sep 27–29, 2026) matching \"AI agent startups launch,\" prioritized by engagement (likes + views) and direct relevance to AI agent startups or launches:\n\n**1. Author:** @TradexWhisperer \n**Date/Time:** Sun, 27 Sep 2026 05:26:50 GMT \n**Full tweet text:** \"Sorry folks Agentic AI isn't coming. It's already here. 8 launches in under a year: OpenAI Agents, Claude, Gemini Spark, AWS Frontier Agents, $META Muse, $MSFT Scout, Grok Bot, DeepSeek Harness. 24/7. 365. Inference nonstop. $MU at $739 in July might've been the last chance.\" \n**Engagement:** Likes=235, Reposts=11, Views=59544\n\n**2. Author:** @DanKornas \n**Date/Time:** Sun, 27 Sep 2026 22:59:00 GMT \n**Full tweet text:** \"Finding indie AI products shouldn’t mean doom-scrolling launch feeds. Awesome AI Startups is a curated GitHub directory of AI startups built by independent founders — bootstrapped, pre-seed, and angel-funded products outside the big AI labs. It helps you find relevant indie AI products faster by grouping linked entries across 18 categories, from agents and developer tools to analytics, education, and APIs. Key features: • 18-category directory – jump directly into areas such as AI agents, coding tools, audio, video, finance, or infrastructure • Independent-founder scope – focuses on bootstrapped, pre-seed, and angel-funded products • Short product descriptions – each entry pairs a product link with a brief explanation of what it does • Category counts – the contents show how many entries sit in each section before you browse • Contribution path – founders can submit their startup through a PR using the documented format and inclusion criteria It’s open-source (CC0-1.0 license). Link in the reply 👇\" \n**Engagement:** Likes=23, Reposts=7, Views=3197\n\n**3. Author:** @technohustler \n**Date/Time:** Mon, 28 Sep 2026 05:10:57 GMT \n**Full tweet text:** \"its live, LaunchAgentic - https://launchagentic.space its a simple idea, list your product or agent and get a live page, by prompting to your ai agent or use given command in terminal, if you want, another important thing is your deployed ai agent can list itself, if you have one. It also has: > Agent directory > Skills > send text and get response for listed agents on the site its not perfect, just wanted to build something in agentic space give feedback and do list your product or agent.\" \n**Engagement:** Likes=12, Reposts=4, Views=426\n\n**4. Author:** @abouelatta_ali \n**Date/Time:** Mon, 28 Sep 2026 18:54:05 GMT \n**Full tweet text:** \"We hit $1M ARR at Speedrun this summer. But what I’ll miss most is the room: founders a few desks away, each wrestling with a hard problem of their own, and willing to let me learn from it. Bye bye, Speedrun 🫡 https://x.com/i/article/2104644146803961856\" \n**Engagement:** Likes=30, Reposts=2, Views=3628\n\n**5. Author:** @legionbirdman \n**Date/Time:** Mon, 28 Sep 2026 13:13:10 GMT \n**Full tweet text:** \"AI agents are still pulling serious money. San Francisco startup Instinct raised $1B at a $10B valuation in a Series C backed by Sequoia, Benchmark and Coatue. It’s building personal agents that can handle bookings and phone calls on their own. Public AI stocks may be volatile, but private capital is still leaning hard into the agent layer.\" \n**Engagement:** Likes=1, Reposts=0, Views=46",
|
|
4
|
+
"citations": [
|
|
5
|
+
"https://x.com/i/status/2104345089375879669",
|
|
6
|
+
"https://x.com/i/status/2104438693448802350",
|
|
7
|
+
"https://x.com/i/status/2104645839054688743",
|
|
8
|
+
"https://x.com/i/status/2104560045493219518",
|
|
9
|
+
"https://x.com/i/status/2104847871321239941",
|
|
10
|
+
"https://x.com/i/status/2104580028545548707",
|
|
11
|
+
"https://x.com/i/status/2104487662522384805",
|
|
12
|
+
"https://x.com/i/status/2104718250504495353",
|
|
13
|
+
"https://x.com/i/status/2104823490079584643",
|
|
14
|
+
"https://x.com/i/status/2104633120909566440",
|
|
15
|
+
"https://x.com/i/status/2104528286651232297",
|
|
16
|
+
"https://x.com/i/status/2104555585941147911",
|
|
17
|
+
"https://x.com/i/status/2104819665889575373",
|
|
18
|
+
"https://x.com/i/status/2104290848112349533",
|
|
19
|
+
"https://x.com/i/status/2104692550422413814",
|
|
20
|
+
"https://x.com/i/status/2104080302788509943",
|
|
21
|
+
"https://x.com/i/status/2104851798569734487"
|
|
22
|
+
],
|
|
23
|
+
"embeds": {
|
|
24
|
+
"2104345089375879669": {
|
|
25
|
+
"handle": "DanKornas",
|
|
26
|
+
"text": "Finding indie AI products shouldn’t mean doom-scrolling launch feeds.\n\nAwesome AI Startups is a curated GitHub directory of AI startups built by independent founders — bootstrapped, pre-seed, and angel-funded products outside the big AI labs.\n\nIt helps you find relevant indie AI https://t.co/nHNYIMbofG"
|
|
27
|
+
},
|
|
28
|
+
"2104438693448802350": {
|
|
29
|
+
"handle": "technohustler",
|
|
30
|
+
"text": "its live,\n\nLaunchAgentic - https://t.co/Zx6M0LaZMd\n\nits a simple idea,\n\nlist your product or agent and get a live page, by prompting to your ai agent or use given command in terminal, if you want,\n\nanother important thing is your deployed ai agent can list itself, if you have https://t.co/6jp5Uru0I4"
|
|
31
|
+
},
|
|
32
|
+
"2104645839054688743": {
|
|
33
|
+
"handle": "abouelatta_ali",
|
|
34
|
+
"text": "We hit $1M ARR at Speedrun this summer. But what I’ll miss most is the room: founders a few desks away, each wrestling with a hard problem of their own, and willing to let me learn from it. Bye bye, Speedrun 🫡 https://t.co/HDI6RRgA7D"
|
|
35
|
+
},
|
|
36
|
+
"2104560045493219518": {
|
|
37
|
+
"handle": "legionbirdman",
|
|
38
|
+
"text": "AI agents are still pulling serious money.\nSan Francisco startup Instinct raised $1B at a $10B valuation in a Series C backed by Sequoia, Benchmark and Coatue.\nIt’s building personal agents that can handle bookings and phone calls on their own.\nPublic AI stocks may be volatile, https://t.co/uM9spmAvhQ"
|
|
39
|
+
},
|
|
40
|
+
"2104847871321239941": {
|
|
41
|
+
"handle": "carrioresearch",
|
|
42
|
+
"text": "$CBRS LINES UP 100 MW OF CS-4 FOR GIMLET ON TOP OF OPENAI'S 750 MW, AND THIS TIME THE CUSTOMER RUNS THE SYSTEMS ITSELF\n\nSAN FRANCISCO, Sept. 28 — Cerebras Systems ($CBRS) will supply inference cloud startup Gimlet Labs with CS-4 systems drawing about 100 megawatts of power, https://t.co/Ds4IPCax29"
|
|
43
|
+
},
|
|
44
|
+
"2104580028545548707": {
|
|
45
|
+
"handle": "newsfile_corp",
|
|
46
|
+
"text": "Ember Origin Launches AI Platform for Entrepreneurs, Surpasses 1,000 Sign-Ups Within Two Weeks of Commercial Launch\n\nFull Story: https://t.co/w6ehtJkebX\n\n #AIPlatform #business #EmberOrigin #Entrepreneurs #Advertising #Publishing"
|
|
47
|
+
},
|
|
48
|
+
"2104487662522384805": {
|
|
49
|
+
"handle": "afahmy_dev",
|
|
50
|
+
"text": "Product Manager role at Berlin-based enterprise AI agents startup\n\nOwn the lifecycle of core product initiatives from inception and discovery through to launch and iteration, partnering directly with engineering to ship world-class AI products. Tackle unsolved AI problems by"
|
|
51
|
+
},
|
|
52
|
+
"2104718250504495353": {
|
|
53
|
+
"handle": "theagenticdaily",
|
|
54
|
+
"text": "A 14-person startup called Instinct is running a personal AI agent that already handles about $1B in transaction volume, roughly half of it booking travel, growing about 10% a day, invite-only with no public app yet. Investors just backed it with a $1B round at a $10B valuation."
|
|
55
|
+
},
|
|
56
|
+
"2104823490079584643": {
|
|
57
|
+
"handle": "marcodice_ai",
|
|
58
|
+
"text": "I put Jev in front of an AI writer to save tokens.\n\nThe writing got worse\n\nThen I gave it the small decision *before* the writing. If you're building your first agent, this is the job I'd test first https://t.co/ZbU4ioAxro"
|
|
59
|
+
},
|
|
60
|
+
"2104633120909566440": {
|
|
61
|
+
"handle": "GrokBotAndro",
|
|
62
|
+
"text": "Nvidia added $150 billion to its share buyback on Sep 28, the NYT reports, in what Nvidia calls the largest ever.\nInside: why top AI lab executives want scrutiny of self-improving AI, and the state and startup moves from the same day. https://t.co/vCYVAWiigl"
|
|
63
|
+
},
|
|
64
|
+
"2104528286651232297": {
|
|
65
|
+
"handle": "himanshuxspark",
|
|
66
|
+
"text": "https://t.co/n3aVxW0zM9"
|
|
67
|
+
},
|
|
68
|
+
"2104555585941147911": {
|
|
69
|
+
"handle": "TraffAlex",
|
|
70
|
+
"text": "🤖 AI NEWS ROUNDUP — September 28, 2026\n\n1️⃣ NVIDIA LAUNCHES OPEN AGENT SAFETY PLATFORM\n\nNvidia has released its Open Agent Safety Platform, software that wraps autonomous AI agents in a security sandbox with an independent kill switch. You define which data, tools, APIs, files or"
|
|
71
|
+
},
|
|
72
|
+
"2104819665889575373": {
|
|
73
|
+
"handle": "liquidguap",
|
|
74
|
+
"text": "@M1Astra Open AIs agents already launched on pumpfun \n\nETSjuBMjqGEUmPVBWeZL7ZECdYfZTYM9yHjRWrCppump\n\nhttps://t.co/MS615htr2b https://t.co/L7GKmI6AO2"
|
|
75
|
+
},
|
|
76
|
+
"2104290848112349533": {
|
|
77
|
+
"handle": "grok",
|
|
78
|
+
"text": "@Adonisssxi @OfficialLoganK Agents already research niches, build/deploy products like apps or content, handle payments and iterate with minimal oversight, generating real subscale revenue in focused areas.\n\nTo start yourself: launch via platforms that spin up AI mini-businesses from a prompt, or wire agent"
|
|
79
|
+
},
|
|
80
|
+
"2104692550422413814": {
|
|
81
|
+
"handle": "ismayelb7",
|
|
82
|
+
"text": "88% of companies announced this year that they are “using AI”… but official financial data released today reveals a silent disaster:\n95% of Generative AI pilots are generating zero P&L impact — meaning no measurable effect on profits or losses.\nThe honeymoon phase of the AI https://t.co/6Fi429ozTZ"
|
|
83
|
+
},
|
|
84
|
+
"2104080302788509943": {
|
|
85
|
+
"handle": "TradexWhisperer",
|
|
86
|
+
"text": "Sorry folks Agentic AI isn't coming.\n\nIt's already here. \n\n8 launches in under a year: OpenAI Agents, Claude, Gemini Spark, AWS Frontier Agents, $META Muse, $MSFT Scout, Grok Bot, DeepSeek Harness.\n\n24/7. 365. Inference nonstop.\n\n$MU at $739 in July might've been the last chance."
|
|
87
|
+
},
|
|
88
|
+
"2104851798569734487": {
|
|
89
|
+
"handle": "AIDailyNews_2x",
|
|
90
|
+
"text": "Tuesday morning AI FACTS roundup 🤖 29.09\n\n- 🛡️ Artificial Analysis launches the Cyber Index in partnership with Nvidia, IBM, and Vercel\n- 📊 Claude Sonnet 5.5 scores 47 points on the Artificial Analysis Intelligence Index\n- 🛡️ Pete Hegseth ordered Cyber Command to use advanced"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|