jaitus 0.3.13 → 0.3.14

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/src/index.js CHANGED
@@ -45,6 +45,7 @@ import login from './commands/login.js';
45
45
  import logout from './commands/logout.js';
46
46
  import doctor from './commands/doctor.js';
47
47
  import update, { updateNotice, autoUpdate } from './commands/update.js';
48
+ import { startupNotice } from './serviceNotice.js';
48
49
  import init from './commands/init.js';
49
50
  import research from './commands/research.js';
50
51
  import explain from './commands/explain.js';
@@ -1436,6 +1437,9 @@ async function session({ flags }) {
1436
1437
  // A global install never updates itself; say so when there is something newer.
1437
1438
  // Background and cached for a day — the app never waits on the network for this.
1438
1439
  void updateNotice().then((line) => { if (line) tui.notice(line, 'info'); }).catch(() => {});
1440
+ // An outage or maintenance on the Jaitus service, said once a day at most; the
1441
+ // status page has the rest. Background, short timeout, silent on failure.
1442
+ void startupNotice().then((line) => { if (line) tui.notice(line, 'warn'); }).catch(() => {});
1439
1443
  // A project's .mcp.json runs programs on this machine. Nothing in it starts until the
1440
1444
  // exact command is approved here — asked once, remembered per project and command.
1441
1445
  void (async () => {
@@ -2878,7 +2882,7 @@ const commands = {
2878
2882
  const unset = rest.length === 1 && /^(--unset|none|unset)$/i.test(rest[0]);
2879
2883
  const value = unset ? '' : rest.join(' ').trim();
2880
2884
  const settings = loadSettings();
2881
- const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'trimToolResults', 'mouse', 'autoUpdate'];
2885
+ const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'trimToolResults', 'mouse'];
2882
2886
  const SECRET = ['braveSearchKey', 'tavilyKey', 'serperKey'];
2883
2887
  if (!key) {
2884
2888
  const e = endpoints();
@@ -3010,7 +3014,7 @@ const commands = {
3010
3014
  update: {
3011
3015
  summary: 'check whether a newer version is published',
3012
3016
  usage: ['jaitus update [--json]'],
3013
- detail: ['Checks npm for a newer version and, for a global npm install, offers to install it (--yes installs without asking). Launching jaitus also updates itself; turn that off with `jaitus config autoUpdate off`.'],
3017
+ detail: ['Checks npm for a newer version and, for a global npm install, installs it with progress. Starting jaitus does the same every time, so this is rarely needed.'],
3014
3018
  run: (argv, { json }) => update(argv, { json }),
3015
3019
  },
3016
3020
 
@@ -3088,7 +3092,7 @@ export default async function main(argv) {
3088
3092
  }
3089
3093
 
3090
3094
  // Always on the newest version: install it and reopen before doing anything else.
3091
- const relaunched = await autoUpdate({ argv, settings: loadSettings() }).catch(() => null);
3095
+ const relaunched = await autoUpdate({ argv }).catch(() => null);
3092
3096
  if (relaunched !== null) return relaunched;
3093
3097
 
3094
3098
  const [first, ...others] = rest;
@@ -0,0 +1,151 @@
1
+ // ─── Service notices in the CLI ─────────────────────────────────────────────────
2
+ //
3
+ // The status page (www.jaitus.com/status) is the record. The CLI only says something
4
+ // when it affects using Jaitus right now, the way flyctl checks its status page:
5
+ //
6
+ // - At start-up, in the background with a short timeout: one line for a major or
7
+ // critical incident on the Jaitus service, maintenance in progress, or maintenance
8
+ // starting within 24 hours. Each notice at most once a day on this machine.
9
+ // - When a request has finally failed (after the usual retries) as unavailable, timed
10
+ // out or down for maintenance: the error says which known problem it is, or when
11
+ // Jaitus will be back.
12
+ //
13
+ // The server decides which notices qualify (GET /api/status/notices), by its own clock.
14
+ // Nothing here ever blocks, and any failure is silent. JAITUS_NO_STATUS_CHECK=1 or CI
15
+ // turns the start-up line off.
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { HOME, endpoints } from './config.js';
20
+
21
+ const TIMEOUT_MS = 2000;
22
+ const DAY_MS = 24 * 60 * 60 * 1000;
23
+ const SHOWN_FILE = path.join(HOME, 'notice-check.json');
24
+
25
+ /** Whether the start-up line may run at all. */
26
+ export function noticeCheckEnabled(env = process.env) {
27
+ return !(env.JAITUS_NO_STATUS_CHECK || env.CI);
28
+ }
29
+
30
+ /** The public notices payload, or null. Never throws. */
31
+ export async function fetchNotices({ baseUrl = endpoints().baseUrl, fetchImpl = fetch, timeoutMs = TIMEOUT_MS } = {}) {
32
+ const ctrl = new AbortController();
33
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
34
+ try {
35
+ const res = await fetchImpl(`${baseUrl}/api/status/notices`, { signal: ctrl.signal, headers: { accept: 'application/json' } });
36
+ if (!res.ok) return null;
37
+ const body = await res.json();
38
+ return body && typeof body === 'object' && body.data && typeof body.data === 'object' ? body.data : null;
39
+ } catch {
40
+ return null;
41
+ } finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+
46
+ /** "www.jaitus.com/status", from the configured site. */
47
+ export function statusAddress(siteUrl = endpoints().siteUrl) {
48
+ return `${String(siteUrl).replace(/^https?:\/\//, '').replace(/\/+$/, '')}/status`;
49
+ }
50
+
51
+ // Text from the server is printed into a terminal: no escape codes, no control
52
+ // characters, one short line.
53
+ const clean = (t, max = 100) => {
54
+ const s = String(t ?? '')
55
+ // eslint-disable-next-line no-control-regex
56
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
57
+ // eslint-disable-next-line no-control-regex
58
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, ' ')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
62
+ };
63
+ const dropStop = (t) => t.replace(/[.!?…]+$/, '');
64
+
65
+ /** The start-up line for one notice from the server's `cli` list. */
66
+ export function lineFor(item, where = statusAddress()) {
67
+ if (!item || typeof item !== 'object') return null;
68
+ const title = dropStop(clean(item.title));
69
+ if (item.kind === 'incident') {
70
+ return `Jaitus is having problems${title ? `: ${title}` : ''}. Updates: ${where}`;
71
+ }
72
+ if (item.kind === 'maintenance' && item.phase === 'in_progress') {
73
+ const until = clean(item.until, 40);
74
+ if (item.overrun && until) return `Jaitus is down for maintenance, running past its planned end (${until}). Updates: ${where}`;
75
+ if (until) return `Jaitus is down for maintenance until ${until}. Updates: ${where}`;
76
+ return `Jaitus is down for maintenance. Updates: ${where}`;
77
+ }
78
+ if (item.kind === 'maintenance' && item.phase === 'upcoming') {
79
+ const when = clean(item.window, 60);
80
+ if (!when) return null;
81
+ return `Planned maintenance ${when}: Jaitus will not answer during it. Details: ${where}`;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ function readShown(file) {
87
+ try {
88
+ const v = JSON.parse(fs.readFileSync(file, 'utf8'));
89
+ return v && typeof v.shown === 'object' && v.shown ? v.shown : {};
90
+ } catch {
91
+ return {};
92
+ }
93
+ }
94
+
95
+ /**
96
+ * The one line to show at start-up, or null. A notice is shown at most once a day per
97
+ * machine; a planned window and the same window in progress are separate notices.
98
+ */
99
+ export async function startupNotice({ env = process.env, now = Date.now(), file = SHOWN_FILE, fetchNotices: get = fetchNotices, where } = {}) {
100
+ if (!noticeCheckEnabled(env)) return null;
101
+ const data = await get().catch(() => null);
102
+ const items = Array.isArray(data?.cli) ? data.cli : [];
103
+ if (!items.length) return null;
104
+ const shown = readShown(file);
105
+ for (const item of items) {
106
+ const key = `${item?.id}:${item?.phase}`;
107
+ if (shown[key] != null && now - Number(shown[key]) < DAY_MS) continue;
108
+ const line = lineFor(item, where);
109
+ if (!line) continue;
110
+ shown[key] = now;
111
+ // Forget anything a week old, so the file never grows.
112
+ for (const k of Object.keys(shown)) if (now - Number(shown[k]) > 7 * DAY_MS) delete shown[k];
113
+ try {
114
+ fs.mkdirSync(path.dirname(file), { recursive: true });
115
+ fs.writeFileSync(file, JSON.stringify({ shown }));
116
+ } catch { /* read-only home: it may show again next start */ }
117
+ return line;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ const EXPLAINED = new Set(['maintenance', 'unavailable', 'timeout']);
123
+
124
+ /**
125
+ * A request that finally failed: add what the status page knows to the error's hint —
126
+ * when Jaitus is back from maintenance, or which known problem this is. Mutates and
127
+ * returns `error`; leaves it alone when there is nothing to add or the check fails.
128
+ */
129
+ export async function explainFailure(error, { baseUrl, fetchNotices: get = fetchNotices, where } = {}) {
130
+ if (!error || !EXPLAINED.has(error.code)) return error;
131
+ const data = await get(baseUrl ? { baseUrl } : undefined).catch(() => null);
132
+ if (!data) return error;
133
+ const at = where || statusAddress();
134
+ const m = data.maintenance;
135
+ if (error.code === 'maintenance' || m?.enabled) {
136
+ const until = clean(m?.until, 40);
137
+ error.title = 'Down for maintenance';
138
+ error.hint = m?.enabled && until
139
+ ? (m.overrun
140
+ ? `It was planned to end at ${until} and is running late. Updates: ${at}`
141
+ : `Jaitus should be back by ${until}. Updates: ${at}`)
142
+ : `Try again shortly. Updates: ${at}`;
143
+ return error;
144
+ }
145
+ const incident = Array.isArray(data.incidents) ? data.incidents[0] : null;
146
+ const title = incident ? dropStop(clean(incident.title)) : '';
147
+ error.hint = title
148
+ ? `Known problem: ${title}. Updates: ${at}`
149
+ : `${error.hint ? `${dropStop(error.hint)}. ` : ''}Service status: ${at}`;
150
+ return error;
151
+ }
package/src/sessions.js CHANGED
@@ -9,6 +9,7 @@ import crypto from 'node:crypto';
9
9
  import { SESSIONS } from './config.js';
10
10
  import { c } from './ui.js';
11
11
  import { unifiedDiff } from './diff.js';
12
+ import { TOOLS } from './agent/tools.js';
12
13
 
13
14
  function safeId(id) {
14
15
  return String(id || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80);
@@ -195,6 +196,13 @@ export function replaySession(tui, messages) {
195
196
  /** The same one-line summary the live loop shows, without importing the tool table. */
196
197
  function summariseCall(call) {
197
198
  const input = call.input && typeof call.input === 'object' ? call.input : {};
199
+ // The same label the row had while the session ran. A second, guessed label here
200
+ // showed a resumed task list as `{"todos":[{"content":…` and a background read as
201
+ // `{"id":"bg1"}`.
202
+ const tool = TOOLS.find((t) => t.name === call.name);
203
+ if (tool?.summarise) {
204
+ try { const s = tool.summarise(input); if (s) return String(s); } catch { /* fall back below */ }
205
+ }
198
206
  if (input.command) return String(input.command);
199
207
  if (input.path) return String(input.path);
200
208
  if (input.pattern) return String(input.pattern);