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/package.json +2 -2
- package/src/agent/loop.js +9 -1
- package/src/agent/tools.js +11 -5
- package/src/api.js +174 -174
- package/src/commands/update.js +75 -49
- package/src/index.js +7 -3
- package/src/serviceNotice.js +151 -0
- package/src/sessions.js +8 -0
- package/src/tui-ink.js +258 -36
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'
|
|
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,
|
|
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
|
|
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);
|