jaitus 0.3.0

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.
Files changed (55) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +248 -0
  3. package/bin/jaitus.js +60 -0
  4. package/package.json +46 -0
  5. package/src/agent/loop.js +548 -0
  6. package/src/agent/permissions.js +218 -0
  7. package/src/agent/rules.js +190 -0
  8. package/src/agent/tools.js +923 -0
  9. package/src/agent/websearch.js +522 -0
  10. package/src/api.js +72 -0
  11. package/src/background.js +85 -0
  12. package/src/brand.js +232 -0
  13. package/src/commands/code-review.js +8 -0
  14. package/src/commands/commit.js +73 -0
  15. package/src/commands/completion.js +68 -0
  16. package/src/commands/doctor.js +185 -0
  17. package/src/commands/explain.js +33 -0
  18. package/src/commands/init.js +62 -0
  19. package/src/commands/login.js +201 -0
  20. package/src/commands/logout.js +25 -0
  21. package/src/commands/research.js +60 -0
  22. package/src/commands/review.js +80 -0
  23. package/src/commands/security-review.js +8 -0
  24. package/src/commands/simplify.js +8 -0
  25. package/src/commands/test.js +68 -0
  26. package/src/commands/update.js +97 -0
  27. package/src/config.js +95 -0
  28. package/src/diff.js +129 -0
  29. package/src/errors.js +101 -0
  30. package/src/hooks.js +55 -0
  31. package/src/index.js +2503 -0
  32. package/src/ledger.js +73 -0
  33. package/src/logo-data.js +33 -0
  34. package/src/mcp.js +160 -0
  35. package/src/open.js +50 -0
  36. package/src/project.js +0 -0
  37. package/src/rewind.js +130 -0
  38. package/src/session.js +207 -0
  39. package/src/sessions.js +179 -0
  40. package/src/sixel.js +131 -0
  41. package/src/skills.js +62 -0
  42. package/src/term-sim.js +103 -0
  43. package/src/terminal.js +90 -0
  44. package/src/tui/ansi.js +287 -0
  45. package/src/tui/entries.js +366 -0
  46. package/src/tui/highlight.js +211 -0
  47. package/src/tui/logo.js +93 -0
  48. package/src/tui/markdown.js +280 -0
  49. package/src/tui/mouse.js +125 -0
  50. package/src/tui/theme.js +201 -0
  51. package/src/tui/widgets.js +226 -0
  52. package/src/tui/wordmark.js +135 -0
  53. package/src/tui-ink.js +1927 -0
  54. package/src/tui.js +2555 -0
  55. package/src/ui.js +562 -0
package/src/diff.js ADDED
@@ -0,0 +1,129 @@
1
+ // ─── Unified diff ───────────────────────────────────────────────────────────────
2
+ //
3
+ // "Edited src/index.js" tells you nothing about what changed, and nobody re-reads a
4
+ // file to find out — they accept the claim and move on. Showing the actual hunks is
5
+ // what makes an agent's edit reviewable at the moment it happens rather than at the
6
+ // end of the session, and it costs one small algorithm rather than a dependency.
7
+ //
8
+ // Myers would be the textbook answer; this is the simpler Hunt–Szymanski shape built
9
+ // on an LCS table, bounded so a pathological pair of large files degrades to a
10
+ // whole-file replacement instead of allocating a gigabyte.
11
+
12
+ const MAX_CELLS = 4_000_000; // ~2000×2000 lines before falling back
13
+
14
+ /** Longest common subsequence of two line arrays, as index pairs. */
15
+ function lcs(a, b) {
16
+ const n = a.length;
17
+ const m = b.length;
18
+ if (n * m > MAX_CELLS) return null;
19
+
20
+ // Trim the common head and tail first: most edits touch a few lines of a long file,
21
+ // and this turns the usual case into a table of a handful of cells.
22
+ let head = 0;
23
+ while (head < n && head < m && a[head] === b[head]) head++;
24
+ let tail = 0;
25
+ while (tail < n - head && tail < m - head && a[n - 1 - tail] === b[m - 1 - tail]) tail++;
26
+
27
+ const ai = a.slice(head, n - tail);
28
+ const bi = b.slice(head, m - tail);
29
+ const rows = ai.length;
30
+ const cols = bi.length;
31
+
32
+ const table = new Uint32Array((rows + 1) * (cols + 1));
33
+ const at = (i, j) => i * (cols + 1) + j;
34
+ for (let i = rows - 1; i >= 0; i--) {
35
+ for (let j = cols - 1; j >= 0; j--) {
36
+ table[at(i, j)] = ai[i] === bi[j]
37
+ ? table[at(i + 1, j + 1)] + 1
38
+ : Math.max(table[at(i + 1, j)], table[at(i, j + 1)]);
39
+ }
40
+ }
41
+
42
+ const pairs = [];
43
+ for (let i = 0; i < head; i++) pairs.push([i, i]);
44
+ let i = 0;
45
+ let j = 0;
46
+ while (i < rows && j < cols) {
47
+ if (ai[i] === bi[j]) { pairs.push([head + i, head + j]); i++; j++; }
48
+ else if (table[at(i + 1, j)] >= table[at(i, j + 1)]) i++;
49
+ else j++;
50
+ }
51
+ for (let k = 0; k < tail; k++) pairs.push([n - tail + k, m - tail + k]);
52
+ return pairs;
53
+ }
54
+
55
+ /**
56
+ * A unified diff of two strings.
57
+ *
58
+ * Returns '' when nothing changed, which callers use to skip rendering entirely: an
59
+ * edit that produced no change is worth knowing about, but it is not worth a hunk.
60
+ */
61
+ export function unifiedDiff(before, after, { context = 3, file = '' } = {}) {
62
+ const a = String(before ?? '').replace(/\r\n?/g, '\n').split('\n');
63
+ const b = String(after ?? '').replace(/\r\n?/g, '\n').split('\n');
64
+ if (a.length === b.length && a.every((line, i) => line === b[i])) return '';
65
+
66
+ const pairs = lcs(a, b);
67
+ if (!pairs) {
68
+ // Too large to diff honestly. Saying so beats inventing hunks.
69
+ return `@@ ${file || 'file'} @@\n-[${a.length} lines replaced]\n+[${b.length} lines]`;
70
+ }
71
+
72
+ // Walk both sides together, emitting -/+/space operations in order.
73
+ const ops = [];
74
+ let ai = 0;
75
+ let bi = 0;
76
+ for (const [pa, pb] of pairs) {
77
+ while (ai < pa) ops.push(['-', a[ai++]]);
78
+ while (bi < pb) ops.push(['+', b[bi++]]);
79
+ ops.push([' ', a[pa]]);
80
+ ai = pa + 1;
81
+ bi = pb + 1;
82
+ }
83
+ while (ai < a.length) ops.push(['-', a[ai++]]);
84
+ while (bi < b.length) ops.push(['+', b[bi++]]);
85
+
86
+ // Group changes into hunks with `context` unchanged lines either side.
87
+ const changed = ops.map(([sign]) => sign !== ' ');
88
+ const keep = new Array(ops.length).fill(false);
89
+ for (let i = 0; i < ops.length; i++) {
90
+ if (!changed[i]) continue;
91
+ for (let j = Math.max(0, i - context); j <= Math.min(ops.length - 1, i + context); j++) keep[j] = true;
92
+ }
93
+
94
+ const lines = [];
95
+ let oldLine = 1;
96
+ let newLine = 1;
97
+ let hunk = null;
98
+ const flush = () => {
99
+ if (!hunk) return;
100
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`);
101
+ lines.push(...hunk.body);
102
+ hunk = null;
103
+ };
104
+
105
+ for (let i = 0; i < ops.length; i++) {
106
+ const [sign, text] = ops[i];
107
+ if (keep[i]) {
108
+ if (!hunk) hunk = { oldStart: oldLine, newStart: newLine, oldCount: 0, newCount: 0, body: [] };
109
+ hunk.body.push(`${sign}${text}`);
110
+ if (sign !== '+') hunk.oldCount++;
111
+ if (sign !== '-') hunk.newCount++;
112
+ } else {
113
+ flush();
114
+ }
115
+ if (sign !== '+') oldLine++;
116
+ if (sign !== '-') newLine++;
117
+ }
118
+ flush();
119
+ return lines.join('\n');
120
+ }
121
+
122
+ /** Added and removed line counts, for a one-line summary. */
123
+ export function diffStat(patch) {
124
+ const lines = String(patch || '').split('\n');
125
+ return {
126
+ added: lines.filter((l) => l.startsWith('+') && !l.startsWith('+++')).length,
127
+ removed: lines.filter((l) => l.startsWith('-') && !l.startsWith('---')).length,
128
+ };
129
+ }
package/src/errors.js ADDED
@@ -0,0 +1,101 @@
1
+ // ─── What the CLI says when something fails ─────────────────────────────────────
2
+ //
3
+ // The gateway answers every failure with a `jaitus_*` code from one catalog. This file
4
+ // is the CLI's half of that contract: a code maps to a title and a hint written here,
5
+ // and anything WITHOUT a known code — an HTML page from a proxy, a body in some other
6
+ // shape, a code this build has never heard of — becomes "something went wrong in
7
+ // Jaitus" plus the request id. The CLI never prints an error body it does not
8
+ // recognise, because the only text that is safe to show is text we wrote.
9
+
10
+ const CATALOG = {
11
+ auth_required: { title: 'Not signed in', hint: 'Run `jaitus login` to sign in.' },
12
+ session_expired: { title: 'Signed out', hint: 'Run `jaitus login` to sign in again.' },
13
+ account_suspended: { title: 'Account suspended', hint: 'Contact support if you think this is a mistake.' },
14
+ email_unverified: { title: 'Email not verified', hint: 'Open the verification link in your inbox, then retry.' },
15
+ free_limit_reached: { title: 'Free prompts used up', hint: 'Subscribe on the website, then retry this turn.' },
16
+ free_request_too_large: { title: 'Too large for the free plan', hint: 'Run /compact or start a new session, or subscribe.' },
17
+ plan_limit_reached: { title: 'Monthly allowance used', hint: 'It resets at the start of your next period, or upgrade on the website. /usage shows where you are.' },
18
+ rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
19
+ context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
20
+ content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
21
+ bad_request: { title: 'Request not accepted', hint: 'Try again; if it repeats, run /debug and contact support.' },
22
+ model_unavailable: { title: 'Model unavailable', hint: 'Run /model to pick another.' },
23
+ busy: { title: 'Jaitus is busy', hint: 'Try again in a minute.' },
24
+ unavailable: { title: 'Jaitus is unavailable', hint: 'Try again shortly. `jaitus doctor` checks your side.' },
25
+ maintenance: { title: 'Down for maintenance', hint: 'Try again shortly.' },
26
+ timeout: { title: 'Timed out', hint: 'Try again.' },
27
+ interrupted: { title: 'Response interrupted', hint: 'Try again.' },
28
+ internal: { title: 'Something went wrong', hint: 'Try again; if it keeps happening, contact support with the request id.' },
29
+ };
30
+
31
+ const GENERIC = 'Something went wrong in Jaitus.';
32
+
33
+ export class JaitusError extends Error {
34
+ constructor({ code = 'internal', message, status = 0, requestId = '', retryAfter = null } = {}) {
35
+ const known = Object.prototype.hasOwnProperty.call(CATALOG, code) ? code : 'internal';
36
+ super(message || GENERIC);
37
+ this.name = 'JaitusError';
38
+ this.code = known;
39
+ this.status = status;
40
+ this.requestId = requestId;
41
+ this.retryAfter = retryAfter;
42
+ this.title = CATALOG[known].title;
43
+ this.hint = CATALOG[known].hint;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * An error body from the gateway → a JaitusError.
49
+ *
50
+ * The gateway's message is shown ONLY when the body carries a recognised `jaitus_`
51
+ * code, because only then is it guaranteed to be our wording (it may carry specifics,
52
+ * like how many free prompts there were). Otherwise the message is ours, generic.
53
+ */
54
+ export function fromGatewayBody(status, body, { requestId = '', retryAfter = null } = {}) {
55
+ let parsed = body;
56
+ if (typeof body === 'string') {
57
+ try { parsed = JSON.parse(body); } catch { parsed = null; }
58
+ }
59
+ const err = parsed?.error || null;
60
+ const rawCode = String(err?.code || '');
61
+ const code = rawCode.startsWith('jaitus_') ? rawCode.slice(7) : '';
62
+ const known = code && Object.prototype.hasOwnProperty.call(CATALOG, code);
63
+ const id = String(parsed?.request_id || requestId || '');
64
+ return new JaitusError({
65
+ code: known ? code : 'internal',
66
+ message: known && typeof err?.message === 'string' && err.message.trim() ? err.message.trim() : GENERIC,
67
+ status,
68
+ requestId: id,
69
+ retryAfter,
70
+ });
71
+ }
72
+
73
+ /** A failed fetch Response → a JaitusError, reading the body once. */
74
+ export async function fromResponse(res) {
75
+ const text = await res.text().catch(() => '');
76
+ const requestId = res.headers?.get?.('x-request-id') || '';
77
+ const retryAfter = Number(res.headers?.get?.('retry-after')) || null;
78
+ return fromGatewayBody(res.status, text, { requestId, retryAfter });
79
+ }
80
+
81
+ /**
82
+ * fetch() threw before any response — the network, not the service. This is the one
83
+ * failure where the true cause is on the user's side and worth naming plainly.
84
+ */
85
+ export function fromNetwork(cause) {
86
+ if (cause?.name === 'AbortError') return cause;
87
+ const error = new JaitusError({
88
+ code: 'unavailable',
89
+ message: 'Could not reach Jaitus. Check your internet connection.',
90
+ });
91
+ error.title = 'Cannot reach Jaitus';
92
+ error.hint = 'Check your connection, then run `jaitus doctor`.';
93
+ error.cause = cause;
94
+ return error;
95
+ }
96
+
97
+ export function isJaitusError(e) {
98
+ return e instanceof JaitusError || e?.name === 'JaitusError';
99
+ }
100
+
101
+ export const ERROR_CODES = Object.freeze(Object.keys(CATALOG));
package/src/hooks.js ADDED
@@ -0,0 +1,55 @@
1
+ // ─── Local lifecycle hooks ──────────────────────────────────────────────────────
2
+ //
3
+ // Hooks are explicit project configuration, not hidden magic. A hook receives a small
4
+ // JSON event on stdin and may block the lifecycle by exiting non-zero. Hook stdout is
5
+ // intentionally not echoed: hooks often inspect repositories and must not accidentally
6
+ // publish credentials or noisy logs into the conversation.
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+
12
+ function load(cwd) {
13
+ // Only Jaitus's own file. Hooks are shell commands: ones written for another tool
14
+ // must never start running because Jaitus happened to find them.
15
+ const candidates = [
16
+ path.join(cwd, '.jaitus', 'hooks.json'),
17
+ ];
18
+ for (const file of candidates) {
19
+ try {
20
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ if (data?.hooks && typeof data.hooks === 'object') return data.hooks;
22
+ } catch { /* the next supported location may exist */ }
23
+ }
24
+ return {};
25
+ }
26
+
27
+ function commandsFor(value) {
28
+ if (typeof value === 'string') return [value];
29
+ if (!Array.isArray(value)) return [];
30
+ return value.flatMap((item) => typeof item === 'string' ? [item] : item?.command ? [item.command] : []);
31
+ }
32
+
33
+ export async function runHook(cwd, event, payload = {}) {
34
+ const commands = commandsFor(load(cwd)[event]);
35
+ for (const command of commands) {
36
+ const code = await new Promise((resolve) => {
37
+ const child = spawn(command, {
38
+ cwd,
39
+ shell: true,
40
+ env: { ...process.env, JAITUS_HOOK_EVENT: event },
41
+ stdio: ['pipe', 'ignore', 'ignore'],
42
+ });
43
+ const timer = setTimeout(() => { child.kill(); resolve(124); }, 30_000);
44
+ child.on('error', () => { clearTimeout(timer); resolve(127); });
45
+ child.on('close', (exitCode) => { clearTimeout(timer); resolve(exitCode ?? 1); });
46
+ child.stdin.end(JSON.stringify({ event, cwd, ...payload }));
47
+ });
48
+ if (code !== 0) return { ok: false, event, code, command };
49
+ }
50
+ return { ok: true, event };
51
+ }
52
+
53
+ export function configuredHookEvents(cwd) {
54
+ return Object.keys(load(cwd));
55
+ }