atris 3.55.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.
@@ -6,13 +6,16 @@ const {
6
6
  folderName,
7
7
  freshMinuteJson,
8
8
  isCertifiedReview,
9
+ listUserVisibleWork,
9
10
  isFreshWorkspace,
10
11
  personName,
11
12
  pickNext,
12
13
  renderWorkspace,
13
14
  speakFirstMinute,
14
15
  taskCommand,
16
+ visibleWorkTitle,
15
17
  } = require('../lib/first-minute');
18
+ const { startFirstTalk } = require('../lib/context-gatherer');
16
19
  const { isNonInteractive } = require('../lib/noninteractive');
17
20
  const { loadContext } = require('../lib/state-detection');
18
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
@@ -64,7 +67,8 @@ function reviewSoftTitle(title, maxWords = 5) {
64
67
  }
65
68
 
66
69
  function isHumanDeskNext(command) {
67
- return /^atris task (?:claim|ready|accept)\b/.test(String(command || ''));
70
+ const text = String(command || '');
71
+ return /^atris do\b/.test(text) || /^atris task (?:claim|show|ready|accept)\b/.test(text);
68
72
  }
69
73
 
70
74
  function loadReviewTasks(root = process.cwd()) {
@@ -506,7 +510,7 @@ async function planAtris(userInput = null) {
506
510
  // init --minimal is optional context, not a factory bounce.
507
511
  if (!fs.existsSync(targetDir)) {
508
512
  if (args.includes('--json')) {
509
- console.log(JSON.stringify(freshMinuteJson(), null, 2));
513
+ console.log(JSON.stringify(freshMinuteJson(folderName(cwd), listUserVisibleWork(cwd), { root: cwd }), null, 2));
510
514
  process.exit(2);
511
515
  }
512
516
  const screen = buildFirstMinute({ root: cwd, fresh: true });
@@ -877,11 +881,20 @@ async function doAtris() {
877
881
  const cwd = process.cwd();
878
882
  const targetDir = path.join(cwd, 'atris');
879
883
 
880
- // Empty folder talks like bare atris. Missing executor.md after
881
- // init --minimal is optional context, not a factory bounce.
884
+ // Empty folder talks like bare atris. Files already here start
885
+ // first-talk, then next is atris do. A second do stays two first-minute
886
+ // lines. Missing executor.md after init --minimal is optional
887
+ // context, not a factory bounce.
882
888
  if (!fs.existsSync(targetDir)) {
889
+ const visible = listUserVisibleWork(cwd);
890
+ if (visible.length) {
891
+ const title = visibleWorkTitle(visible, folderName(cwd));
892
+ const code = startFirstTalk(cwd, title, { asJson: args.includes('--json') });
893
+ if (code !== 0) process.exit(code);
894
+ return;
895
+ }
883
896
  if (args.includes('--json')) {
884
- console.log(JSON.stringify(freshMinuteJson(), null, 2));
897
+ console.log(JSON.stringify(freshMinuteJson(folderName(cwd), visible, { root: cwd }), null, 2));
885
898
  process.exit(2);
886
899
  }
887
900
  const screen = buildFirstMinute({ root: cwd, fresh: true });
@@ -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
  };