atris 3.15.37 → 3.15.39

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/ax CHANGED
@@ -107,6 +107,54 @@ function formatUsage() {
107
107
  ].join('\n');
108
108
  }
109
109
 
110
+ function timestampForFile(date = new Date()) {
111
+ return date.toISOString().replace(/[:.]/g, '-');
112
+ }
113
+
114
+ function stripAnsi(value) {
115
+ return String(value || '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
116
+ }
117
+
118
+ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', output = process.stdout } = {}) {
119
+ if (process.env.AX_AUTO_LOG === '0') return null;
120
+ if (output !== process.stdout && output !== process.stderr && !output.isTTY) return null;
121
+
122
+ const baseDir = fs.existsSync(path.join(cwd, 'atris'))
123
+ ? path.join(cwd, 'atris', 'runs')
124
+ : path.join(os.homedir(), '.atris', 'runs');
125
+ fs.mkdirSync(baseDir, { recursive: true });
126
+ const logPath = path.join(baseDir, `ax-${kind}-${timestampForFile()}.log`);
127
+ fs.appendFileSync(logPath, [
128
+ `command: ${process.argv.join(' ')}`,
129
+ `cwd: ${cwd}`,
130
+ `mode: ${mode}`,
131
+ `started_at: ${new Date().toISOString()}`,
132
+ '',
133
+ ].join('\n'));
134
+
135
+ const writeLog = (chunk) => {
136
+ fs.appendFileSync(logPath, stripAnsi(chunk));
137
+ };
138
+ const teeOutput = {
139
+ isTTY: Boolean(output && output.isTTY),
140
+ write(chunk) {
141
+ const text = String(chunk || '');
142
+ if (output && typeof output.write === 'function') output.write(text);
143
+ writeLog(text);
144
+ return true;
145
+ }
146
+ };
147
+
148
+ return {
149
+ path: logPath,
150
+ output: teeOutput,
151
+ write: writeLog,
152
+ close(exitCode = 0) {
153
+ writeLog(`\nexit_code: ${exitCode}\nfinished_at: ${new Date().toISOString()}\n`);
154
+ }
155
+ };
156
+ }
157
+
110
158
  function backendBaseUrl() {
111
159
  return (process.env.AX_BACKEND_URL
112
160
  || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
@@ -339,9 +387,16 @@ function workspaceIntent(message) {
339
387
  return /\b(files?|folders?|repo|workspace|project|directory|tree|read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|tests?|build|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map)\b/i.test(message || '');
340
388
  }
341
389
 
390
+ function githubWorkspaceIntent(message) {
391
+ const text = String(message || '');
392
+ if (!/\bgithub\b/i.test(text)) return false;
393
+ return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
394
+ }
395
+
342
396
  function resolveRoute(message, options = {}) {
343
397
  if (options.route === 'local' || options.forceLocal) return 'local';
344
398
  if (options.route === 'cloud' || options.forceCloud) return 'cloud';
399
+ if (githubWorkspaceIntent(message)) return 'local';
345
400
  if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
346
401
  return 'local';
347
402
  }
@@ -370,6 +425,112 @@ function paint(text, codes, options = {}) {
370
425
  return `${codes.join('')}${text}${ANSI.reset}`;
371
426
  }
372
427
 
428
+ function renderTerminalMarkdown(text, options = {}) {
429
+ let rendered = String(text || '');
430
+ const codeSpans = [];
431
+ rendered = rendered.replace(/`([^`\n]+)`/g, (_, code) => {
432
+ const token = `\u0000CODE${codeSpans.length}\u0000`;
433
+ codeSpans.push(code);
434
+ return token;
435
+ });
436
+ rendered = rendered.replace(/^#{1,6}\s+(.+)$/gm, (_, title) => paint(title, [ANSI.bold], options));
437
+ rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, (_, value) => paint(value, [ANSI.bold], options));
438
+ rendered = rendered.replace(/__([^_\n]+)__/g, (_, value) => paint(value, [ANSI.bold], options));
439
+ rendered = rendered.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => paint(codeSpans[Number(index)] || '', [ANSI.accent], options));
440
+ return rendered;
441
+ }
442
+
443
+ function resetMarkdownState(state) {
444
+ state.markdownMode = 'normal';
445
+ state.markdownBuffer = '';
446
+ state.markdownCarry = '';
447
+ }
448
+
449
+ function ensureMarkdownState(state) {
450
+ if (!state.markdownMode) state.markdownMode = 'normal';
451
+ if (typeof state.markdownBuffer !== 'string') state.markdownBuffer = '';
452
+ if (typeof state.markdownCarry !== 'string') state.markdownCarry = '';
453
+ }
454
+
455
+ function renderStreamingMarkdown(state, text, options = {}) {
456
+ ensureMarkdownState(state);
457
+ const input = `${state.markdownCarry || ''}${String(text || '')}`;
458
+ state.markdownCarry = '';
459
+ let out = '';
460
+
461
+ for (let i = 0; i < input.length;) {
462
+ const char = input[i];
463
+ const next = input[i + 1];
464
+
465
+ if (state.markdownMode === 'normal') {
466
+ if (char === '*' && next === undefined) {
467
+ state.markdownCarry = '*';
468
+ break;
469
+ }
470
+ if (char === '*' && next === '*') {
471
+ state.markdownMode = 'bold';
472
+ state.markdownBuffer = '';
473
+ i += 2;
474
+ continue;
475
+ }
476
+ if (char === '`') {
477
+ state.markdownMode = 'code';
478
+ state.markdownBuffer = '';
479
+ i += 1;
480
+ continue;
481
+ }
482
+ out += char;
483
+ i += 1;
484
+ continue;
485
+ }
486
+
487
+ if (state.markdownMode === 'bold') {
488
+ if (char === '*' && next === undefined) {
489
+ state.markdownCarry = '*';
490
+ break;
491
+ }
492
+ if (char === '*' && next === '*') {
493
+ out += paint(state.markdownBuffer, [ANSI.bold], options);
494
+ state.markdownMode = 'normal';
495
+ state.markdownBuffer = '';
496
+ i += 2;
497
+ continue;
498
+ }
499
+ state.markdownBuffer += char;
500
+ i += 1;
501
+ continue;
502
+ }
503
+
504
+ if (state.markdownMode === 'code') {
505
+ if (char === '`') {
506
+ out += paint(state.markdownBuffer, [ANSI.accent], options);
507
+ state.markdownMode = 'normal';
508
+ state.markdownBuffer = '';
509
+ i += 1;
510
+ continue;
511
+ }
512
+ state.markdownBuffer += char;
513
+ i += 1;
514
+ continue;
515
+ }
516
+ }
517
+
518
+ return out;
519
+ }
520
+
521
+ function flushStreamingMarkdown(state, output) {
522
+ ensureMarkdownState(state);
523
+ let out = state.markdownCarry || '';
524
+ if (state.markdownMode === 'bold' && state.markdownBuffer) {
525
+ out += paint(state.markdownBuffer, [ANSI.bold], output);
526
+ } else if (state.markdownMode === 'code' && state.markdownBuffer) {
527
+ out += paint(state.markdownBuffer, [ANSI.accent], output);
528
+ }
529
+ resetMarkdownState(state);
530
+ if (out) output.write(out);
531
+ return out;
532
+ }
533
+
373
534
  function truncateMiddle(value, limit = 120) {
374
535
  const text = String(value || '');
375
536
  if (text.length <= limit) return text;
@@ -523,6 +684,7 @@ function clearRetriedText(state) {
523
684
  state.wroteText = false;
524
685
  state.lastChar = '\n';
525
686
  state.inAuxBlock = false;
687
+ resetMarkdownState(state);
526
688
  }
527
689
 
528
690
  function stopProgress(state) {
@@ -532,13 +694,20 @@ function stopProgress(state) {
532
694
  }
533
695
 
534
696
  function flushPendingText(state, output) {
535
- if (!state.pendingText) return;
697
+ const hasMarkdownRemainder = output && output.isTTY && (
698
+ state.markdownCarry
699
+ || state.markdownBuffer
700
+ || (state.markdownMode && state.markdownMode !== 'normal')
701
+ );
702
+ if (!state.pendingText && !hasMarkdownRemainder) return;
536
703
  stopProgress(state);
537
704
  if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
538
- output.write(state.pendingText);
705
+ if (state.pendingText) output.write(output && output.isTTY ? renderTerminalMarkdown(state.pendingText, output) : state.pendingText);
706
+ const flushedMarkdown = hasMarkdownRemainder ? flushStreamingMarkdown(state, output) : '';
539
707
  state.wroteText = true;
540
708
  state.wroteActivity = true;
541
- state.lastChar = state.pendingText.slice(-1);
709
+ const written = `${state.pendingText || ''}${flushedMarkdown || ''}`;
710
+ state.lastChar = written.slice(-1);
542
711
  state.pendingText = '';
543
712
  state.inAuxBlock = false;
544
713
  }
@@ -547,10 +716,11 @@ function writeStreamingText(state, output, content) {
547
716
  if (!content) return;
548
717
  stopProgress(state);
549
718
  if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
550
- output.write(content);
719
+ const rendered = renderStreamingMarkdown(state, content, output);
720
+ if (rendered) output.write(rendered);
551
721
  state.wroteText = true;
552
722
  state.wroteActivity = true;
553
- state.lastChar = String(content).slice(-1);
723
+ state.lastChar = String(rendered || content).slice(-1);
554
724
  state.inAuxBlock = false;
555
725
  }
556
726
 
@@ -691,7 +861,10 @@ async function postTurn(message, options = {}) {
691
861
  durationMs: 0,
692
862
  lastChar: '\n',
693
863
  progress: null,
694
- inAuxBlock: false
864
+ inAuxBlock: false,
865
+ markdownMode: 'normal',
866
+ markdownBuffer: '',
867
+ markdownCarry: ''
695
868
  };
696
869
 
697
870
  return new Promise((resolve, reject) => {
@@ -789,16 +962,20 @@ async function chat(options = {}) {
789
962
  const mode = options.mode === 'fast' ? 'fast' : 'pro';
790
963
  const cwd = options.cwd || process.cwd();
791
964
  const input = options.input || process.stdin;
792
- const output = options.output || process.stdout;
965
+ const baseOutput = options.output || process.stdout;
966
+ const logger = createRunLogger({ cwd, mode, kind: 'play', output: baseOutput });
967
+ const output = logger ? logger.output : baseOutput;
793
968
  const history = [];
794
969
 
795
970
  output.write(`${formatHeader({ mode, cwd, chat: true })}\n\n`);
971
+ if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
796
972
 
797
973
  const runLine = async (line) => {
798
974
  const trimmed = String(line || '').trim();
799
975
  if (!trimmed) return false;
800
976
  if (EXIT_WORDS.has(trimmed.toLowerCase())) return true;
801
977
 
978
+ if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
802
979
  output.write('\n');
803
980
  const result = await postTurn(trimmed, { mode, cwd, history, output });
804
981
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
@@ -813,10 +990,11 @@ async function chat(options = {}) {
813
990
  for await (const line of rl) {
814
991
  if (await runLine(line)) break;
815
992
  }
993
+ if (logger) logger.close(0);
816
994
  return;
817
995
  }
818
996
 
819
- const rl = readline.createInterface({ input, output });
997
+ const rl = readline.createInterface({ input, output: baseOutput });
820
998
  const ask = () => new Promise(resolve => rl.question(formatPrompt(mode), resolve));
821
999
  while (true) {
822
1000
  const line = await ask();
@@ -825,6 +1003,7 @@ async function chat(options = {}) {
825
1003
  break;
826
1004
  }
827
1005
  }
1006
+ if (logger) logger.close(0);
828
1007
  }
829
1008
 
830
1009
  function printBackendHint() {
@@ -1061,6 +1240,7 @@ module.exports = {
1061
1240
  buildConnectionContext,
1062
1241
  buildRunProfile,
1063
1242
  chat,
1243
+ createRunLogger,
1064
1244
  createProgressReporter,
1065
1245
  formatDoneLine,
1066
1246
  formatDuration,
@@ -1076,6 +1256,8 @@ module.exports = {
1076
1256
  modelForMode,
1077
1257
  parseSseBlock,
1078
1258
  postTurn,
1259
+ renderStreamingMarkdown,
1260
+ renderTerminalMarkdown,
1079
1261
  resolveRoute,
1080
1262
  runBenchmark,
1081
1263
  summarizeToolInput,
@@ -747,27 +747,36 @@ function secondsUntilMissionDue(mission, now = new Date()) {
747
747
  return Math.max(0, Math.ceil((dueAt - now.getTime()) / 1000));
748
748
  }
749
749
 
750
+ function missionIsRunnable(mission) {
751
+ return mission && !TERMINAL_STATUSES.has(mission.status) && mission.status !== 'paused';
752
+ }
753
+
754
+ function missionSortTime(mission) {
755
+ return Date.parse(mission?.updated_at || mission?.created_at || '') || 0;
756
+ }
757
+
750
758
  function selectDueMission(root = process.cwd(), now = new Date()) {
751
759
  const candidates = listMissions(root)
752
- .filter((mission) => !TERMINAL_STATUSES.has(mission.status))
760
+ .filter(missionIsRunnable)
753
761
  .filter((mission) => mission.verifier)
754
762
  .filter((mission) => mission.always_on || !missionVerifierPassed(mission))
755
763
  .filter((mission) => missionDueAt(mission, now));
756
764
 
757
765
  candidates.sort((a, b) => {
758
- const aTime = Date.parse(a.last_tick_at || a.created_at || '') || 0;
759
- const bTime = Date.parse(b.last_tick_at || b.created_at || '') || 0;
760
- return aTime - bTime;
766
+ const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
767
+ const bCaller = runnerUsesCallerSession(b.runner) ? 1 : 0;
768
+ if (aCaller !== bCaller) return bCaller - aCaller;
769
+
770
+ const aTime = missionSortTime(a);
771
+ const bTime = missionSortTime(b);
772
+ return bTime - aTime;
761
773
  });
762
774
  return candidates[0] || null;
763
775
  }
764
776
 
765
777
  function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
766
- const due = selectDueMission(root, now);
767
- if (due) return { mission: due, reason: 'due' };
768
-
769
778
  const candidates = listMissions(root)
770
- .filter((mission) => !TERMINAL_STATUSES.has(mission.status));
779
+ .filter(missionIsRunnable);
771
780
 
772
781
  candidates.sort((a, b) => {
773
782
  const aCaller = runnerUsesCallerSession(a.runner) ? 1 : 0;
@@ -778,14 +787,15 @@ function selectCodexGoalMission(root = process.cwd(), now = new Date()) {
778
787
  const bVerifier = b.verifier ? 1 : 0;
779
788
  if (aVerifier !== bVerifier) return bVerifier - aVerifier;
780
789
 
781
- const aTime = Date.parse(a.updated_at || a.created_at || '') || 0;
782
- const bTime = Date.parse(b.updated_at || b.created_at || '') || 0;
790
+ const aTime = missionSortTime(a);
791
+ const bTime = missionSortTime(b);
783
792
  return bTime - aTime;
784
793
  });
785
794
 
786
795
  const mission = candidates[0] || null;
787
796
  if (!mission) return null;
788
- return { mission, reason: 'active' };
797
+ const due = mission.verifier && missionDueAt(mission, now);
798
+ return { mission, reason: due ? 'due' : 'active' };
789
799
  }
790
800
 
791
801
  function codexGoalObjective(mission) {
package/commands/task.js CHANGED
@@ -795,6 +795,15 @@ function taskStatusSummary(projection, { history = false } = {}) {
795
795
  done: tasks.filter(task => taskColumn(task) === 'done'),
796
796
  };
797
797
  const active = [...columns.do, ...columns.review, ...columns.plan];
798
+ const reviewNeedingAgentAction = columns.review.filter(task => {
799
+ const handoff = reviewHandoffForTask(task);
800
+ return handoff && handoff.next_action === 'agent_review_again';
801
+ });
802
+ const reviewAgentCertified = columns.review.filter(task => {
803
+ const handoff = reviewHandoffForTask(task);
804
+ return handoff && handoff.next_action === 'continue_work';
805
+ }).length;
806
+ const blocked = columns.review.filter(task => taskColumn(task) === 'blocked').length;
798
807
  const lastUpdated = tasks.reduce((max, task) => Math.max(max, Number(task.updated_at || 0)), 0);
799
808
  const swarloFeed = history ? tasks
800
809
  .flatMap(task => (task.events || []).map(event => ({
@@ -830,14 +839,17 @@ function taskStatusSummary(projection, { history = false } = {}) {
830
839
  goals: projection.goals || { source_path: null, items: [] },
831
840
  counts: {
832
841
  total: fullTaskCount,
833
- active: columns.plan.length + columns.do.length + columns.review.length,
842
+ active: columns.plan.length + columns.do.length + reviewNeedingAgentAction.length,
834
843
  backlog: columns.backlog.length,
835
844
  plan: columns.plan.length,
836
845
  do: columns.do.length,
837
846
  review: columns.review.length,
847
+ review_blocking: reviewNeedingAgentAction.length,
848
+ review_certified: reviewAgentCertified,
849
+ blocked,
838
850
  done: tasks.filter(task => task.status === 'done' || (task.status === 'failed' && taskHasReview(task))).length + hiddenDoneCount,
839
851
  },
840
- current: compactTaskForStatus(columns.do[0] || columns.review[0] || null),
852
+ current: compactTaskForStatus(columns.do[0] || reviewNeedingAgentAction[0] || null),
841
853
  next: compactTaskForStatus(columns.plan[0] || null),
842
854
  needs_review: columns.review.slice(0, 5).map(compactTaskForStatus),
843
855
  streams: (projection.streams || []).slice(0, 8).map(stream => ({
@@ -1299,25 +1311,6 @@ function cmdNext(args) {
1299
1311
  const reviewTasks = (reviewProjection.projection.tasks || [])
1300
1312
  .map(compactTaskForStatus)
1301
1313
  .filter(task => task && task.review && task.review.handoff);
1302
- const secondReviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again');
1303
- if (secondReviewTask) {
1304
- const handoff = secondReviewTask.review.handoff;
1305
- if (wantsJson(args)) {
1306
- printJson({
1307
- ok: true,
1308
- action: handoff.next_action,
1309
- task_id: secondReviewTask.id,
1310
- owner: String(owner),
1311
- projection_path: reviewProjection.outPath,
1312
- handoff,
1313
- review_task: secondReviewTask,
1314
- });
1315
- return;
1316
- }
1317
- console.log(`${taskRef(secondReviewTask)} needs one more agent review before continuation.`);
1318
- console.log('Review this task again before claiming new work.');
1319
- return;
1320
- }
1321
1314
  const open = taskDb.listTasks(db, {
1322
1315
  workspaceRoot: taskDb.workspaceRoot(),
1323
1316
  status: 'open',
@@ -1325,7 +1318,8 @@ function cmdNext(args) {
1325
1318
  });
1326
1319
  if (!open.length) {
1327
1320
  const { projection, outPath } = reviewProjection;
1328
- const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'continue_work');
1321
+ const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
1322
+ || reviewTasks.find(task => task.review.handoff.next_action === 'continue_work');
1329
1323
  if (reviewTask) {
1330
1324
  const handoff = reviewTask.review.handoff;
1331
1325
  if (wantsJson(args)) {
@@ -2013,11 +2007,34 @@ function extractTodoSectionMarkdown(content, sectionName) {
2013
2007
  return match ? match[1].trimEnd() : null;
2014
2008
  }
2015
2009
 
2010
+ function normalizeRenderedTaskRef(value) {
2011
+ return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
2012
+ }
2013
+
2014
+ function renderedTaskRefSet(taskDb, rows, refRows) {
2015
+ const byId = new Map();
2016
+ for (const row of [...(Array.isArray(rows) ? rows : []), ...(Array.isArray(refRows) ? refRows : [])]) {
2017
+ if (row && row.id && !byId.has(row.id)) byId.set(row.id, row);
2018
+ }
2019
+ const displayRows = taskDb.withTaskDisplayRefs([...byId.values()]);
2020
+ const refs = new Set();
2021
+ for (const row of displayRows) {
2022
+ for (const value of [row.id, row.display_id, row.legacy_ref]) {
2023
+ const ref = normalizeRenderedTaskRef(value);
2024
+ if (ref) refs.add(ref);
2025
+ }
2026
+ }
2027
+ return refs;
2028
+ }
2029
+
2016
2030
  function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
2017
2031
  if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
2018
2032
  const { parseTodoFile } = require('../lib/todo-fallback');
2033
+ const existingTodo = fs.readFileSync(existingTodoPath, 'utf8');
2034
+ const generatedTodo = existingTodo.includes('Regenerated from durable Atris task state');
2019
2035
  const parsed = parseTodoFile(existingTodoPath);
2020
2036
  const ws = taskDb.workspaceRoot();
2037
+ const existingRefs = renderedTaskRefSet(taskDb, rows, refRows);
2021
2038
  const existingSourceKeys = new Set(
2022
2039
  (Array.isArray(refRows) ? refRows : [])
2023
2040
  .map(row => row && row.source_key)
@@ -2041,7 +2058,13 @@ function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
2041
2058
  if (!task.title) continue;
2042
2059
  const sk = taskDb.sourceKey(existingTodoPath, task.title);
2043
2060
  const normalizedTitle = taskDb.normalizeTitle(task.title);
2044
- if ((sk && existingSourceKeys.has(sk)) || existingTitles.has(normalizedTitle)) continue;
2061
+ const renderedRef = normalizeRenderedTaskRef(task.id);
2062
+ if (
2063
+ (renderedRef && existingRefs.has(renderedRef)) ||
2064
+ (sk && existingSourceKeys.has(sk)) ||
2065
+ existingTitles.has(normalizedTitle) ||
2066
+ generatedTodo
2067
+ ) continue;
2045
2068
  out.push({
2046
2069
  id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
2047
2070
  title: task.title,
@@ -21,7 +21,7 @@ function parseTodoFile(todoPath) {
21
21
  }
22
22
 
23
23
  function tagsFromText(text) {
24
- const allTags = [...String(text || '').matchAll(/\[(\w+)\]/g)].map(m => m[1]);
24
+ const allTags = [...String(text || '').matchAll(/\[([\w-]+)\]/g)].map(m => m[1]);
25
25
  return {
26
26
  allTags,
27
27
  tag: allTags.includes('endgame') ? 'endgame' : (allTags[0] || null),
@@ -30,7 +30,7 @@ function tagsFromText(text) {
30
30
 
31
31
  function cleanTaskTitle(text) {
32
32
  const raw = String(text || '').trim();
33
- const withoutTags = raw.replace(/\s*\[\w+\]/g, '').trim();
33
+ const withoutTags = raw.replace(/\s*\[[\w-]+\]/g, '').trim();
34
34
  const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[—-]\s*)?(.*)$/);
35
35
  if (!bold) return withoutTags;
36
36
  return [bold[1], bold[2]].filter(Boolean).join(' — ').trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.15.37",
3
+ "version": "3.15.39",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",