lattris 0.1.4 → 0.2.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 (78) hide show
  1. package/README.md +2 -2
  2. package/dist/build.json +4 -1
  3. package/dist/datasets/capture.js +118 -0
  4. package/dist/datasets/page.js +64 -0
  5. package/dist/datasets/pieces.js +78 -0
  6. package/dist/dispatch-form.js +181 -0
  7. package/dist/dispatch-words.js +137 -0
  8. package/dist/dispatch.js +67 -0
  9. package/dist/entry/session.js +219 -0
  10. package/dist/entry/set.js +130 -0
  11. package/dist/entry/setup-boot.js +151 -0
  12. package/dist/entry/setup.js +145 -0
  13. package/dist/entry/use.js +127 -0
  14. package/dist/help.js +65 -0
  15. package/dist/main.js +143 -6887
  16. package/dist/memory/capture-common.js +67 -0
  17. package/dist/memory/capture-help.js +186 -0
  18. package/dist/memory/capture-note.js +190 -0
  19. package/dist/memory/capture-session.js +166 -0
  20. package/dist/memory/delete.js +88 -0
  21. package/dist/memory/erase.js +67 -0
  22. package/dist/{extract-claude-jsonl.js → memory/extract-claude-jsonl.js} +5 -94
  23. package/dist/memory/get-help.js +63 -0
  24. package/dist/memory/get.js +213 -0
  25. package/dist/memory/history.js +134 -0
  26. package/dist/memory/recording-update.js +128 -0
  27. package/dist/memory/recording.js +249 -0
  28. package/dist/memory/resend.js +82 -0
  29. package/dist/memory/restore.js +73 -0
  30. package/dist/memory/save-session-chain.js +59 -0
  31. package/dist/memory/transcript.js +85 -0
  32. package/dist/memory/update-help.js +126 -0
  33. package/dist/memory/update.js +220 -0
  34. package/dist/memory/views.js +138 -0
  35. package/dist/nouns/consent.js +29 -0
  36. package/dist/nouns/contact.js +142 -0
  37. package/dist/nouns/datasets.js +109 -0
  38. package/dist/nouns/edges.js +106 -0
  39. package/dist/nouns/form.js +2 -0
  40. package/dist/nouns/index.js +34 -0
  41. package/dist/nouns/invite.js +98 -0
  42. package/dist/nouns/questions.js +116 -0
  43. package/dist/nouns/share.js +105 -0
  44. package/dist/nouns/words.js +76 -0
  45. package/dist/registry/datasets.js +15 -0
  46. package/dist/registry/entry.js +51 -0
  47. package/dist/registry/index.js +17 -0
  48. package/dist/registry/memory.js +110 -0
  49. package/dist/render/datasets.js +77 -0
  50. package/dist/render/door.js +39 -0
  51. package/dist/render/output.js +19 -0
  52. package/dist/render/prompt.js +4 -0
  53. package/dist/render/rows.js +49 -0
  54. package/dist/render/text.js +21 -0
  55. package/dist/request/access-token.js +67 -0
  56. package/dist/request/api-fetch.js +76 -0
  57. package/dist/request/callback.js +86 -0
  58. package/dist/request/door.js +74 -0
  59. package/dist/request/flags.js +38 -0
  60. package/dist/request/io.js +2 -0
  61. package/dist/request/paste-and-device.js +99 -0
  62. package/dist/request/stdin.js +22 -0
  63. package/dist/request/token.js +66 -0
  64. package/dist/request/upload.js +6 -0
  65. package/dist/request/version-check.js +79 -0
  66. package/dist/store/chains.js +25 -0
  67. package/dist/store/drafts.js +48 -0
  68. package/dist/store/environment.js +57 -0
  69. package/dist/store/exit.js +13 -0
  70. package/dist/store/io.js +2 -0
  71. package/dist/store/json-file.js +9 -0
  72. package/dist/store/session-home.js +189 -0
  73. package/dist/store/session-lock.js +169 -0
  74. package/dist/store/session.js +70 -0
  75. package/dist/store/write-identity.js +153 -0
  76. package/dist/version.js +16 -0
  77. package/package.json +4 -5
  78. package/dist/save-session-chain.js +0 -136
@@ -0,0 +1,76 @@
1
+ import { EXIT } from "../store/exit.js";
2
+ import { classifyApiError, edgeExcerpt, parseErrorBody } from "./door.js";
3
+ const HTTP_NO_CONTENT = 204;
4
+ export const apiFetch = async (network, accessToken, url, init = {}, addressKind = 'api') => {
5
+ const { retryAnonymousOn401 = false, ...requestInit } = init;
6
+ const request = (token) => {
7
+ const headers = { ...requestInit.headers };
8
+ if (token !== undefined)
9
+ headers.Authorization = `Bearer ${token}`;
10
+ else if (retryAnonymousOn401)
11
+ delete headers.Authorization;
12
+ return network(url, { ...requestInit, headers });
13
+ };
14
+ let res = await request(accessToken);
15
+ const isRead = requestInit.method === undefined || requestInit.method.toUpperCase() === 'GET';
16
+ if (retryAnonymousOn401 && isRead && accessToken !== undefined && res.status === 401) {
17
+ await res.body?.cancel().catch(() => { });
18
+ res = await request(undefined);
19
+ }
20
+ if (!res.ok) {
21
+ const bytes = new Uint8Array(await res.arrayBuffer().catch(() => new ArrayBuffer(0)));
22
+ const rawText = new TextDecoder().decode(bytes);
23
+ const excerpt = edgeExcerpt(res.status, { contentType: res.headers.get('content-type'), via: res.headers.get('via') }, bytes);
24
+ if (excerpt !== null) {
25
+ return {
26
+ ok: false,
27
+ body: {},
28
+ exit: EXIT.GENERIC,
29
+ answer: { kind: 'edge', excerpt },
30
+ status: res.status,
31
+ rawText
32
+ };
33
+ }
34
+ const body = parseErrorBody(rawText);
35
+ const { message, next, exit } = classifyApiError(res.status, body);
36
+ return {
37
+ ok: false,
38
+ body,
39
+ exit,
40
+ answer: { kind: 'door', message, next },
41
+ status: res.status,
42
+ rawText
43
+ };
44
+ }
45
+ if (res.status === HTTP_NO_CONTENT) {
46
+ return { ok: true, data: undefined, rawText: '', status: res.status };
47
+ }
48
+ let rawText;
49
+ try {
50
+ rawText = await res.text();
51
+ }
52
+ catch (e) {
53
+ throw new TypeError('fetch failed', { cause: e });
54
+ }
55
+ let data;
56
+ try {
57
+ data = JSON.parse(rawText);
58
+ }
59
+ catch {
60
+ return {
61
+ ok: false,
62
+ body: {},
63
+ exit: EXIT.GENERIC,
64
+ answer: { kind: 'page', address: addressKind },
65
+ status: res.status,
66
+ rawText
67
+ };
68
+ }
69
+ return { ok: true, data, rawText, status: res.status };
70
+ };
71
+ export const refusalOf = (failed, message = '') => ({
72
+ message,
73
+ exit: failed.exit,
74
+ answer: failed.answer
75
+ });
76
+ //# sourceMappingURL=api-fetch.js.map
@@ -0,0 +1,86 @@
1
+ import { createServer } from 'node:http';
2
+ export const htmlEscape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
3
+ export class AuthorizationError extends Error {
4
+ error;
5
+ constructor(error) {
6
+ super(`authorization error: ${error}`);
7
+ this.error = error;
8
+ }
9
+ }
10
+ export const decideCallback = (params, expectedState) => {
11
+ const state = params.get('state');
12
+ if (!state || state !== expectedState)
13
+ return { kind: 'state-mismatch' };
14
+ const error = params.get('error');
15
+ if (error)
16
+ return { kind: 'error', error };
17
+ const code = params.get('code');
18
+ if (!code)
19
+ return { kind: 'missing-code' };
20
+ return { kind: 'success', code };
21
+ };
22
+ const callbackHtml = (ok, message) => `<!doctype html><meta charset="utf-8"><title>lattris</title>` +
23
+ `<body style="font:16px system-ui;padding:3rem;text-align:center">` +
24
+ `<h1>${ok ? '✓ Signed in' : '✗ Sign-in failed'}</h1>` +
25
+ `<p>${htmlEscape(message)}</p>` +
26
+ `<p style="color:#888">You can close this tab and return to the terminal.</p>`;
27
+ export const waitForCode = (port, expectedState, timeoutMs = 300_000, handle) => new Promise((resolve, reject) => {
28
+ const server = createServer((req, res) => {
29
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
30
+ if (url.pathname !== '/callback') {
31
+ res.writeHead(404).end('Not found');
32
+ return;
33
+ }
34
+ const decision = decideCallback(url.searchParams, expectedState);
35
+ const corsHeaders = (verdict) => ({
36
+ 'Content-Type': 'text/html',
37
+ 'Access-Control-Allow-Origin': '*',
38
+ 'Access-Control-Expose-Headers': 'X-Lattris-Login',
39
+ 'X-Lattris-Login': verdict
40
+ });
41
+ if (decision.kind === 'state-mismatch') {
42
+ res.writeHead(400, corsHeaders('refused'));
43
+ res.end(callbackHtml(false, 'Invalid or missing state; ignoring this callback.'));
44
+ return;
45
+ }
46
+ const settle = (status, ok, message) => {
47
+ res.writeHead(status, corsHeaders(ok ? 'ok' : 'refused'));
48
+ res.end(callbackHtml(ok, message));
49
+ shutdown();
50
+ };
51
+ if (decision.kind === 'success') {
52
+ settle(200, true, 'Your terminal now holds your Lattris session.');
53
+ resolve(decision.code);
54
+ return;
55
+ }
56
+ if (decision.kind === 'error') {
57
+ settle(400, false, `Authorization server returned: ${htmlEscape(decision.error)}`);
58
+ reject(new AuthorizationError(decision.error));
59
+ return;
60
+ }
61
+ settle(400, false, 'Missing authorization code.');
62
+ reject(new Error('missing authorization code'));
63
+ });
64
+ const shutdown = () => {
65
+ server.close();
66
+ server.closeAllConnections();
67
+ server.unref();
68
+ };
69
+ server.on('error', reject);
70
+ server.listen(port, '127.0.0.1');
71
+ if (handle)
72
+ handle.close = shutdown;
73
+ setTimeout(() => {
74
+ shutdown();
75
+ reject(new Error('timed out waiting for the sign-in redirect'));
76
+ }, timeoutMs).unref();
77
+ });
78
+ export const openBrowser = (machine, url) => {
79
+ const [cmd, args] = machine.platform === 'darwin'
80
+ ? ['open', [url]]
81
+ : machine.platform === 'win32'
82
+ ? ['cmd', ['/c', 'start', '', url]]
83
+ : ['xdg-open', [url]];
84
+ machine.detach(cmd, args);
85
+ };
86
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1,74 @@
1
+ import { EXIT } from "../store/exit.js";
2
+ export const parseErrorBody = (text) => {
3
+ try {
4
+ const b = JSON.parse(text);
5
+ return b && typeof b === 'object' ? b : {};
6
+ }
7
+ catch {
8
+ return {};
9
+ }
10
+ };
11
+ export const classifyApiError = (status, body) => {
12
+ if (body.next?.action) {
13
+ return { message: body.message ?? 'Action required.', next: body.next, exit: EXIT.DOOR };
14
+ }
15
+ if (status === 401) {
16
+ return {
17
+ message: body.message ?? 'Session invalid or expired.',
18
+ next: { action: 'login' },
19
+ exit: EXIT.NOAUTH
20
+ };
21
+ }
22
+ if (status === 403) {
23
+ return {
24
+ message: body.message ?? 'Your session lacks a required scope.',
25
+ next: { action: 'login' },
26
+ exit: EXIT.NOAUTH
27
+ };
28
+ }
29
+ return {
30
+ message: body.message ?? `Unexpected response from lattris-api: ${status}`,
31
+ exit: EXIT.GENERIC
32
+ };
33
+ };
34
+ const EDGE_REFUSAL_EXCERPT_BYTES = 200;
35
+ export const trimCutUtf8 = (bytes) => {
36
+ let i = bytes.length - 1;
37
+ let back = 0;
38
+ while (i >= 0 && back < 3 && (bytes[i] & 0xc0) === 0x80) {
39
+ i--;
40
+ back++;
41
+ }
42
+ if (i < 0)
43
+ return bytes;
44
+ const lead = bytes[i];
45
+ const need = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
46
+ const have = bytes.length - i;
47
+ return have < need ? bytes.subarray(0, i) : bytes;
48
+ };
49
+ const looksLikeHouseError = (text) => {
50
+ try {
51
+ const b = JSON.parse(text);
52
+ return (b !== null &&
53
+ typeof b === 'object' &&
54
+ (typeof b.code === 'string' || typeof b.message === 'string'));
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ };
60
+ export const edgeExcerpt = (status, headers, bytes) => {
61
+ if (status !== 403)
62
+ return null;
63
+ const text = new TextDecoder().decode(bytes);
64
+ if (looksLikeHouseError(text))
65
+ return null;
66
+ const contentType = (headers.contentType ?? '').toLowerCase();
67
+ const via = (headers.via ?? '').toLowerCase();
68
+ const isHtml = contentType.includes('text/html') || /^\s*<(!doctype|html)/i.test(text);
69
+ if (!isHtml && !via.includes('1.1 google'))
70
+ return null;
71
+ const prefix = trimCutUtf8(bytes.subarray(0, EDGE_REFUSAL_EXCERPT_BYTES));
72
+ return new TextDecoder().decode(prefix);
73
+ };
74
+ //# sourceMappingURL=door.js.map
@@ -0,0 +1,38 @@
1
+ export const HELP_TOKENS = new Set(['--help', '-h', 'help']);
2
+ export const parseArgs = (flagNames, args, ids = []) => {
3
+ const flags = {};
4
+ const positionals = [];
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ if (!arg.startsWith('--')) {
8
+ positionals.push(arg);
9
+ continue;
10
+ }
11
+ const eq = arg.indexOf('=');
12
+ const name = eq === -1 ? arg : arg.slice(0, eq);
13
+ const key = flagNames[name];
14
+ if (!key)
15
+ return { flags, positionals, unknown: name };
16
+ if (eq !== -1) {
17
+ flags[key] = arg.slice(eq + 1);
18
+ continue;
19
+ }
20
+ const value = args[i + 1];
21
+ if (value === undefined || value.startsWith('--')) {
22
+ return { flags, positionals, unknown: name };
23
+ }
24
+ flags[key] = value;
25
+ i++;
26
+ }
27
+ const notAnId = ids
28
+ .map((k) => positionals[k])
29
+ .find((v) => v === '.' || v === '..' || v?.includes('/'));
30
+ return notAnId === undefined
31
+ ? { flags, positionals }
32
+ : {
33
+ flags,
34
+ positionals,
35
+ notAnId: `Not an id: ${notAnId} — an id is never . or .. and never holds a /.\n`
36
+ };
37
+ };
38
+ //# sourceMappingURL=flags.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=io.js.map
@@ -0,0 +1,99 @@
1
+ import { createInterface } from 'node:readline';
2
+ export const parsePastedAuth = (line, expectedState) => {
3
+ const params = new URLSearchParams(line.trim().replace(/^\?/, ''));
4
+ const code = params.get('code');
5
+ const state = params.get('state');
6
+ if (!code || !state)
7
+ return { kind: 'malformed' };
8
+ if (state !== expectedState)
9
+ return { kind: 'mismatch' };
10
+ return { kind: 'code', code };
11
+ };
12
+ export const waitForPastedCode = (expectedState, input, stderr) => {
13
+ const rl = createInterface({ input });
14
+ const code = new Promise((resolve) => {
15
+ rl.on('line', (line) => {
16
+ if (line.trim() === '')
17
+ return;
18
+ const verdict = parsePastedAuth(line, expectedState);
19
+ if (verdict.kind === 'code') {
20
+ rl.close();
21
+ resolve(verdict.code);
22
+ return;
23
+ }
24
+ stderr.write(verdict.kind === 'mismatch'
25
+ ? 'That code is from a different sign-in attempt — copy it again from the current browser page.\n'
26
+ : 'That doesn’t look like the code — copy the whole box from the browser page.\n');
27
+ });
28
+ });
29
+ return {
30
+ code,
31
+ close: () => {
32
+ rl.close();
33
+ input.unref?.();
34
+ }
35
+ };
36
+ };
37
+ export const deviceAuthBody = (cfg) => ({
38
+ client_id: cfg.clientId,
39
+ scope: cfg.scopes,
40
+ audience: cfg.audience
41
+ });
42
+ export const requestDeviceAuth = async (network, cfg) => {
43
+ const res = await network(`${cfg.issuer}/oauth2/device/auth`, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
46
+ body: new URLSearchParams(deviceAuthBody(cfg)).toString()
47
+ });
48
+ if (!res.ok) {
49
+ const detail = await res.text().catch(() => '');
50
+ throw new Error(`device authorization endpoint ${res.status}: ${detail.slice(0, 300)}`);
51
+ }
52
+ return (await res.json());
53
+ };
54
+ const DEVICE_EXPIRED_MESSAGE = 'The sign-in code expired — run lattris login --device again.';
55
+ const deviceTokenAttempt = async (network, cfg, deviceCode) => {
56
+ const res = await network(`${cfg.issuer}/oauth2/token`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
59
+ body: new URLSearchParams({
60
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
61
+ device_code: deviceCode,
62
+ client_id: cfg.clientId
63
+ }).toString()
64
+ });
65
+ const body = (await res.json().catch(() => ({})));
66
+ if (res.ok)
67
+ return { kind: 'token', token: body };
68
+ switch (body.error) {
69
+ case 'authorization_pending':
70
+ return { kind: 'pending' };
71
+ case 'slow_down':
72
+ return { kind: 'slow_down' };
73
+ case 'expired_token':
74
+ return { kind: 'fail', message: DEVICE_EXPIRED_MESSAGE };
75
+ case 'access_denied':
76
+ return { kind: 'fail', message: 'Sign-in was denied.' };
77
+ default:
78
+ return {
79
+ kind: 'fail',
80
+ message: `token endpoint ${res.status}: ${String(body.error_description ?? body.error ?? '').slice(0, 200)}`
81
+ };
82
+ }
83
+ };
84
+ export const pollDeviceToken = async (io, cfg, auth, sleepFn = (ms) => new Promise((r) => setTimeout(r, ms))) => {
85
+ let interval = (auth.interval ?? 5) * 1000;
86
+ const deadline = io.now() + auth.expires_in * 1000;
87
+ while (io.now() < deadline) {
88
+ const verdict = await deviceTokenAttempt(io.network, cfg, auth.device_code);
89
+ if (verdict.kind === 'token')
90
+ return verdict.token;
91
+ if (verdict.kind === 'fail')
92
+ throw new Error(verdict.message);
93
+ if (verdict.kind === 'slow_down')
94
+ interval += 5000;
95
+ await sleepFn(interval);
96
+ }
97
+ throw new Error(DEVICE_EXPIRED_MESSAGE);
98
+ };
99
+ //# sourceMappingURL=paste-and-device.js.map
@@ -0,0 +1,22 @@
1
+ import { createInterface } from 'node:readline';
2
+ export const readAll = async (input) => {
3
+ const chunks = [];
4
+ for await (const chunk of input)
5
+ chunks.push(chunk);
6
+ const text = Buffer.concat(chunks).toString('utf8');
7
+ return text.endsWith('\n') ? text.slice(0, -1) : text;
8
+ };
9
+ export const ask = (input, output, question) => new Promise((resolve) => {
10
+ const rl = createInterface({ input, output });
11
+ let answered = false;
12
+ rl.once('close', () => {
13
+ if (!answered)
14
+ resolve(null);
15
+ });
16
+ rl.question(question, (answer) => {
17
+ answered = true;
18
+ rl.close();
19
+ resolve(answer);
20
+ });
21
+ });
22
+ //# sourceMappingURL=stdin.js.map
@@ -0,0 +1,66 @@
1
+ export const emailFromIdToken = (idToken) => {
2
+ try {
3
+ const payload = JSON.parse(Buffer.from(idToken.split('.')[1] ?? '', 'base64url').toString());
4
+ return typeof payload.email === 'string' && payload.email !== '' ? payload.email : null;
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ };
10
+ export const loginIdentityGate = (homeDir, tokenEmail) => {
11
+ const dirName = homeDir.split('/').filter(Boolean).at(-1) ?? '';
12
+ const homeEmail = dirName.includes('@') ? dirName : null;
13
+ if (!homeEmail)
14
+ return { save: true };
15
+ if (!tokenEmail) {
16
+ return {
17
+ save: false,
18
+ message: `Refusing to save: this directory is keyed to ${homeEmail}, but the sign-in ` +
19
+ `returned no email evidence. Nothing was written — retry the login, or use a ` +
20
+ `non-email --as home.\n`
21
+ };
22
+ }
23
+ if (homeEmail.toLowerCase() !== tokenEmail.toLowerCase()) {
24
+ return {
25
+ save: false,
26
+ message: `Refusing to save: this directory belongs to ${homeEmail}, but you signed in as ` +
27
+ `${tokenEmail}. Nothing was written — log in again as ${homeEmail}, ` +
28
+ `or point --as at ${tokenEmail} instead (\`lattris login\`).\n`
29
+ };
30
+ }
31
+ return { save: true };
32
+ };
33
+ export const toSession = (t, now) => ({
34
+ accessToken: t.access_token,
35
+ refreshToken: t.refresh_token,
36
+ tokenType: t.token_type || 'bearer',
37
+ expiresAt: Math.floor(now / 1000) + (t.expires_in ?? 0)
38
+ });
39
+ export const postToken = async (network, cfg, body, signal) => {
40
+ const res = await network(`${cfg.issuer}/oauth2/token`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
43
+ body: new URLSearchParams(body).toString(),
44
+ ...(signal ? { signal } : {})
45
+ });
46
+ if (!res.ok) {
47
+ const detail = await res.text().catch(() => '');
48
+ throw new Error(`token endpoint ${res.status}: ${detail.slice(0, 300)}`);
49
+ }
50
+ return (await res.json());
51
+ };
52
+ export const refreshBody = (cfg, refreshToken) => ({
53
+ grant_type: 'refresh_token',
54
+ refresh_token: refreshToken,
55
+ client_id: cfg.clientId,
56
+ scope: cfg.scopes
57
+ });
58
+ export const authCodeExchangeBody = (cfg, code, codeVerifier, redirectUri) => ({
59
+ grant_type: 'authorization_code',
60
+ code,
61
+ redirect_uri: redirectUri,
62
+ client_id: cfg.clientId,
63
+ code_verifier: codeVerifier
64
+ });
65
+ export const hostedRedirectUri = (cfg) => `${cfg.webUrl}/cli/auth`;
66
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,6 @@
1
+ export const putToSignedUploadUrl = (network, signedUploadUrl, contentType, bytes) => network(signedUploadUrl, {
2
+ method: 'PUT',
3
+ headers: { 'Content-Type': contentType },
4
+ body: bytes
5
+ });
6
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1,79 @@
1
+ import { join } from 'node:path';
2
+ import { readJsonObject } from "../store/json-file.js";
3
+ const VERSION_CHECK_URL = 'https://registry.npmjs.org/lattris/latest';
4
+ const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
5
+ const VERSION_CHECK_TIMEOUT_MS = 1500;
6
+ const parseVersion = (version) => {
7
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
8
+ if (!match)
9
+ return undefined;
10
+ return {
11
+ core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
12
+ prerelease: match[4] === undefined ? null : match[4].split('.')
13
+ };
14
+ };
15
+ const compareVersions = (left, right) => {
16
+ const a = parseVersion(left);
17
+ const b = parseVersion(right);
18
+ if (!a || !b)
19
+ return undefined;
20
+ for (let i = 0; i < a.core.length; i++) {
21
+ if (a.core[i] < b.core[i])
22
+ return -1;
23
+ if (a.core[i] > b.core[i])
24
+ return 1;
25
+ }
26
+ if (a.prerelease === null && b.prerelease === null)
27
+ return 0;
28
+ if (a.prerelease === null)
29
+ return 1;
30
+ if (b.prerelease === null)
31
+ return -1;
32
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
33
+ for (let i = 0; i < length; i++) {
34
+ const av = a.prerelease[i];
35
+ const bv = b.prerelease[i];
36
+ if (av === undefined)
37
+ return -1;
38
+ if (bv === undefined)
39
+ return 1;
40
+ if (av === bv)
41
+ continue;
42
+ const aNumber = /^\d+$/.test(av);
43
+ const bNumber = /^\d+$/.test(bv);
44
+ if (aNumber && bNumber)
45
+ return BigInt(av) < BigInt(bv) ? -1 : 1;
46
+ if (aNumber !== bNumber)
47
+ return aNumber ? -1 : 1;
48
+ return av < bv ? -1 : 1;
49
+ }
50
+ return 0;
51
+ };
52
+ export const checkForNewVersion = async (io, { currentVersion, stateDir, now }) => {
53
+ try {
54
+ const cachePath = join(stateDir, 'version-check.json');
55
+ const cache = readJsonObject(io.files, cachePath);
56
+ if (typeof cache?.checkedAt === 'number' && now - cache.checkedAt <= VERSION_CHECK_TTL_MS) {
57
+ return;
58
+ }
59
+ const response = await io.network(VERSION_CHECK_URL, {
60
+ headers: { Accept: 'application/json' },
61
+ signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS)
62
+ });
63
+ if (!response.ok)
64
+ return;
65
+ const body = (await response.json());
66
+ if (typeof body.version !== 'string' || parseVersion(body.version) === undefined)
67
+ return;
68
+ try {
69
+ io.files.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
70
+ io.files.writeFileSync(cachePath, `${JSON.stringify({ checkedAt: now, latestVersion: body.version })}\n`, { mode: 0o600 });
71
+ }
72
+ catch { }
73
+ if (compareVersions(currentVersion, body.version) === -1) {
74
+ io.stderr.write(`lattris ${currentVersion} → ${body.version} available: npm update -g lattris\n`);
75
+ }
76
+ }
77
+ catch { }
78
+ };
79
+ //# sourceMappingURL=version-check.js.map
@@ -0,0 +1,25 @@
1
+ import { join } from 'node:path';
2
+ import { safeFileName, sessionDir } from "./session-home.js";
3
+ const receiptsPath = (store, conversationId) => {
4
+ const name = conversationId === null ? undefined : safeFileName(conversationId);
5
+ return name === undefined ? undefined : join(sessionDir(store), 'chains', `${name}.json`);
6
+ };
7
+ export const loadReceipts = (store, conversationId) => {
8
+ const path = receiptsPath(store, conversationId);
9
+ if (path === undefined)
10
+ return { epochs: {} };
11
+ try {
12
+ return JSON.parse(store.files.readFileSync(path, 'utf8'));
13
+ }
14
+ catch {
15
+ return { epochs: {} };
16
+ }
17
+ };
18
+ export const saveReceipts = (store, conversationId, receipts) => {
19
+ const path = receiptsPath(store, conversationId);
20
+ if (path === undefined)
21
+ return;
22
+ store.files.mkdirSync(join(sessionDir(store), 'chains'), { recursive: true, mode: 0o700 });
23
+ store.files.writeFileSync(path, JSON.stringify(receipts, null, 2), { mode: 0o600 });
24
+ };
25
+ //# sourceMappingURL=chains.js.map
@@ -0,0 +1,48 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { safeFileName, sessionDir } from "./session-home.js";
4
+ const draftsDir = (store) => join(sessionDir(store), 'drafts');
5
+ const draftStamp = (store) => new Date(store.now()).toISOString().replace(/[:.]/g, '-');
6
+ const saveDraft = (store, stem, extension, contents) => {
7
+ const dir = draftsDir(store);
8
+ const name = safeFileName(stem);
9
+ if (name === undefined)
10
+ throw new Error(`could not keep a draft under ${dir}`);
11
+ const { chmodSync, mkdirSync, writeFileSync } = store.files;
12
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
13
+ for (let attempt = 0; attempt < 5; attempt += 1) {
14
+ const file = join(dir, `${name}-${draftStamp(store)}-${randomBytes(4).toString('hex')}.${extension}`);
15
+ try {
16
+ writeFileSync(file, contents, { mode: 0o600, flag: 'wx' });
17
+ chmodSync(file, 0o600);
18
+ return file;
19
+ }
20
+ catch (e) {
21
+ if (e.code !== 'EEXIST')
22
+ throw e;
23
+ }
24
+ }
25
+ throw new Error(`could not keep a draft under ${dir}`);
26
+ };
27
+ export const saveRequestDraft = (store, kind, draft) => saveDraft(store, kind, 'json', `${JSON.stringify(draft, null, 2)}\n`);
28
+ export const saveEditDraft = (store, blockRef, draft) => {
29
+ const wordsPath = draft.fields.content !== undefined
30
+ ? saveDraft(store, blockRef, 'md', `${draft.fields.content}\n`)
31
+ : undefined;
32
+ const draftPath = saveDraft(store, blockRef, 'json', `${JSON.stringify({ ...draft, wordsFile: wordsPath }, null, 2)}\n`);
33
+ return { draftPath, wordsPath };
34
+ };
35
+ export const alreadySaved = (sent, current) => {
36
+ if (sent.content !== undefined && sent.content !== current.content)
37
+ return false;
38
+ if (sent.title !== undefined && sent.title !== (current.title ?? undefined))
39
+ return false;
40
+ if (sent.aiSummary !== undefined && sent.aiSummary !== (current.aiSummary ?? undefined))
41
+ return false;
42
+ if (sent.intent !== undefined && (sent.intent ?? null) !== (current.intent ?? null))
43
+ return false;
44
+ if (sent.contributor !== undefined && sent.contributor !== (current.contributor?.id ?? null))
45
+ return false;
46
+ return true;
47
+ };
48
+ //# sourceMappingURL=drafts.js.map