atris 3.56.0 → 3.56.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.
@@ -2,9 +2,12 @@
2
2
 
3
3
  const { apiRequestJson } = require('../utils/api');
4
4
  const { ensureBilledCommandAuth } = require('./auth');
5
+ const applyGate = require('../lib/apply-gate');
5
6
 
6
7
  const DEFAULT_TIMEOUT_MS = 120000;
7
8
  const COST_HINT = '5 credits per search';
9
+ const APPLY_NEXT_MESSAGE =
10
+ 'next: write one apply (change + receipt) for this query.';
8
11
 
9
12
  function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
10
13
  output('');
@@ -13,6 +16,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
13
16
  output('');
14
17
  output(`Search X/Twitter via Atris (${COST_HINT}).`);
15
18
  output('Requires login. Same auth path as atris youtube process.');
19
+ output('Empty or failed search refunds the credits.');
16
20
  output('');
17
21
  output('Options:');
18
22
  output(' --limit <n> Max results hint (search only)');
@@ -213,6 +217,58 @@ function resultErrorText(result) {
213
217
  }
214
218
  }
215
219
 
220
+ function unwrapXSearchPayload(data) {
221
+ return data?.data && typeof data.data === 'object' ? data.data : data;
222
+ }
223
+
224
+ function xSearchContent(data) {
225
+ const payload = unwrapXSearchPayload(data);
226
+ return payload?.content != null ? String(payload.content).trim() : '';
227
+ }
228
+
229
+ function xSearchCitations(data) {
230
+ const payload = unwrapXSearchPayload(data);
231
+ if (Array.isArray(payload?.citations)) return payload.citations;
232
+ if (Array.isArray(data?.citations)) return data.citations;
233
+ return [];
234
+ }
235
+
236
+ function xSearchCredits(data) {
237
+ if (!data || typeof data !== 'object') {
238
+ return { used: undefined, remaining: undefined, refunded: undefined };
239
+ }
240
+ const payload = unwrapXSearchPayload(data) || {};
241
+ const used = data.credits_used !== undefined ? data.credits_used : payload.credits_used;
242
+ const remaining = data.credits_remaining !== undefined
243
+ ? data.credits_remaining
244
+ : payload.credits_remaining;
245
+ let refunded = data.credits_refunded !== undefined
246
+ ? data.credits_refunded
247
+ : payload.credits_refunded;
248
+ if (refunded === undefined && (data.refunded === true || payload.refunded === true)) {
249
+ refunded = true;
250
+ }
251
+ return { used, remaining, refunded };
252
+ }
253
+
254
+ function creditsWereRefunded(credits) {
255
+ if (!credits) return false;
256
+ if (credits.used === 0) return true;
257
+ if (credits.refunded === true) return true;
258
+ return typeof credits.refunded === 'number' && credits.refunded > 0;
259
+ }
260
+
261
+ function formatCreditsLines(credits) {
262
+ const lines = [];
263
+ if (credits.used !== undefined || credits.remaining !== undefined) {
264
+ lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
265
+ }
266
+ if (creditsWereRefunded(credits)) {
267
+ lines.push('credits refunded');
268
+ }
269
+ return lines;
270
+ }
271
+
216
272
  function xSearchFailureError(result) {
217
273
  const hint = result.status === 401
218
274
  ? ' Run "atris login --force".'
@@ -221,7 +277,13 @@ function xSearchFailureError(result) {
221
277
  : result.status === 502
222
278
  ? ' xAI is unavailable; retry in a few seconds.'
223
279
  : '';
224
- return new Error(`X search failed (${result.status}): ${resultErrorText(result)}.${hint}`);
280
+ const credits = xSearchCredits(result.data);
281
+ const refundHint = result.status === 502 && creditsWereRefunded(credits)
282
+ ? ' credits refunded.'
283
+ : '';
284
+ const lines = [`X search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
285
+ lines.push(...formatCreditsLines(credits));
286
+ return new Error(lines.join('\n'));
225
287
  }
226
288
 
227
289
  async function ensureToken(deps = {}) {
@@ -265,13 +327,35 @@ async function runXSearch(options, deps = {}) {
265
327
  return result.data;
266
328
  }
267
329
 
330
+ function xSearchApplySource(options) {
331
+ if (options?.mode === 'person') return options.name;
332
+ return options?.query;
333
+ }
334
+
335
+ function xSearchApplyRel(source) {
336
+ return applyGate.applySidecarRel('x-search', applyGate.applySlug(source));
337
+ }
338
+
339
+ function xSearchHasResults(data) {
340
+ return Boolean(xSearchContent(data)) || xSearchCitations(data).length > 0;
341
+ }
342
+
343
+ function ensureXSearchApply({ cwd, source, now, output } = {}) {
344
+ return applyGate.ensureApply({
345
+ cwd,
346
+ source,
347
+ rel: source ? xSearchApplyRel(source) : null,
348
+ now,
349
+ output,
350
+ incompleteMessage: APPLY_NEXT_MESSAGE,
351
+ required: false,
352
+ });
353
+ }
354
+
268
355
  function formatXSearchResult(data) {
269
356
  const lines = [];
270
- const payload = data?.data && typeof data.data === 'object' ? data.data : data;
271
- const content = payload?.content != null ? String(payload.content).trim() : '';
272
- const citations = Array.isArray(payload?.citations)
273
- ? payload.citations
274
- : (Array.isArray(data?.citations) ? data.citations : []);
357
+ const content = xSearchContent(data);
358
+ const citations = xSearchCitations(data);
275
359
 
276
360
  if (content) {
277
361
  lines.push(content);
@@ -289,18 +373,25 @@ function formatXSearchResult(data) {
289
373
  }
290
374
  }
291
375
 
292
- const used = data?.credits_used !== undefined ? data.credits_used : payload?.credits_used;
293
- const remaining = data?.credits_remaining !== undefined
294
- ? data.credits_remaining
295
- : payload?.credits_remaining;
296
- if (used !== undefined || remaining !== undefined) {
376
+ const creditLines = formatCreditsLines(xSearchCredits(data));
377
+ if (creditLines.length) {
297
378
  lines.push('');
298
- lines.push(`Credits: ${used !== undefined ? used : '?'} used, ${remaining !== undefined ? remaining : '?'} remaining`);
379
+ lines.push(...creditLines);
299
380
  }
300
381
 
301
382
  return lines.join('\n');
302
383
  }
303
384
 
385
+ function formatEmptyXSearchResult(data) {
386
+ const lines = ['no results'];
387
+ const creditLines = formatCreditsLines(xSearchCredits(data));
388
+ if (creditLines.length) {
389
+ lines.push('');
390
+ lines.push(...creditLines);
391
+ }
392
+ return lines.join('\n');
393
+ }
394
+
304
395
  async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
305
396
  const output = deps.output || ((line = '') => console.log(line));
306
397
  let options;
@@ -319,7 +410,23 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
319
410
  let status = 0;
320
411
  try {
321
412
  const data = await runXSearch(options, deps);
322
- output(options.json ? JSON.stringify(data, null, 2) : formatXSearchResult(data));
413
+ const hasResults = xSearchHasResults(data);
414
+ if (options.json) {
415
+ output(JSON.stringify(data, null, 2));
416
+ } else {
417
+ output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
418
+ }
419
+ if (hasResults) {
420
+ const ensureApply = deps.ensureApply || ensureXSearchApply;
421
+ status = ensureApply({
422
+ cwd: deps.cwd || process.cwd(),
423
+ source: xSearchApplySource(options),
424
+ now: deps.applyNow,
425
+ output,
426
+ });
427
+ } else {
428
+ status = 2;
429
+ }
323
430
  } catch (err) {
324
431
  output(err.message);
325
432
  status = 1;
@@ -332,9 +439,12 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
332
439
 
333
440
  module.exports = {
334
441
  DEFAULT_TIMEOUT_MS,
442
+ APPLY_NEXT_MESSAGE,
335
443
  parseXSearchArgs,
336
444
  buildSearchPayload,
337
445
  buildPersonPayload,
338
446
  formatXSearchResult,
447
+ xSearchHasResults,
448
+ xSearchApplyRel,
339
449
  xSearchCommand,
340
450
  };