lattris 0.1.3 → 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 -6877
  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,105 @@
1
+ import { formatAudiences } from "../render/rows.js";
2
+ import { consent } from "./consent.js";
3
+ const NOT_LINKED = 'This contact is not linked to an account yet. Invite them from this contact; accepting links it.\n';
4
+ export const SHARE_HELP = [
5
+ 'share — grant public or a person contact access to a memory.',
6
+ '',
7
+ 'Usage:',
8
+ ' lattris share memory <id-or-prefix> --with <public-or-contact> [--yes]',
9
+ '',
10
+ ' --with public, or an exact contact name or id from `lattris contact list`.',
11
+ ' --yes required when not at a terminal; at a terminal you',
12
+ ' are always asked, --yes or not.',
13
+ '',
14
+ 'Idempotent: sharing with an audience that already has access succeeds and',
15
+ 'changes nothing.',
16
+ '',
17
+ 'Alias: publish → share'
18
+ ].join('\n');
19
+ const shareVerb = {
20
+ verb: 'share',
21
+ nouns: ['memory', 'memories'],
22
+ form: {
23
+ usage: 'Usage: lattris share memory <id> --with <audience> [--yes]',
24
+ flags: { '--with': 'with' },
25
+ switches: ['--yes'],
26
+ positionals: 1,
27
+ idPositionals: [0],
28
+ steps: {
29
+ requireFlag: {
30
+ key: 'with',
31
+ message: '--with is required. Use public or one of your contact names or ids.\n'
32
+ },
33
+ pipedNeedsYes: {
34
+ message: 'Refusing to share without confirmation on a non-interactive session. Pass --yes to skip the prompt.\n'
35
+ },
36
+ contact: { from: ['flag:with'], publicWord: 'public', mustBeLinked: NOT_LINKED },
37
+ memory: 'at-tty',
38
+ confirm: {
39
+ question: 'Share "{title}" ({memory.id}) with {audience}? [y/N] ',
40
+ onNo: { text: 'Cancelled.\n', to: 'stderr' },
41
+ onInterrupted: { text: 'Cancelled.\n', exit: 1 }
42
+ }
43
+ },
44
+ request: {
45
+ method: 'POST',
46
+ path: '/memories/{memory.id}/grants',
47
+ body: {
48
+ kind: { value: 'person' },
49
+ contactId: { context: 'contact.id' }
50
+ },
51
+ bodyWithPublic: { audience: 'public' }
52
+ },
53
+ ambiguousRefusal: true,
54
+ render: (grant, context) => `Shared memory ${context.memory?.id ?? context.positionals[0]} with ${context.contact?.displayName ?? grant.kind}`
55
+ }
56
+ };
57
+ export const UNSHARE_HELP = [
58
+ 'unshare — withdraw an audience from a memory. No confirmation — withdrawal',
59
+ 'is the safe direction.',
60
+ '',
61
+ 'Usage:',
62
+ ' lattris unshare memory <id-or-prefix> --with <public-or-contact>',
63
+ '',
64
+ ' --with public, or an exact contact name or id from `lattris contact list`.',
65
+ '',
66
+ 'Bare (no --with): refuses and shows what the memory is currently shared',
67
+ 'with, so you can see the state and the fix in one line.',
68
+ '',
69
+ 'Alias: unpublish → unshare'
70
+ ].join('\n');
71
+ const unshareVerb = {
72
+ verb: 'unshare',
73
+ nouns: ['memory', 'memories'],
74
+ form: {
75
+ usage: 'Usage: lattris unshare memory <id> [--with <audience>]',
76
+ flags: { '--with': 'with' },
77
+ positionals: 1,
78
+ idPositionals: [0],
79
+ steps: {
80
+ contact: { from: ['flag:with'], publicWord: 'public', mustBeLinked: NOT_LINKED },
81
+ memory: 'always',
82
+ noAudienceReport: {
83
+ key: 'with',
84
+ first: '--with is required. Use public or one of your contact names or ids.\n',
85
+ second: '"{title}" ({memory.id}) is currently shared with: {audiences}\n'
86
+ }
87
+ },
88
+ request: {
89
+ method: 'DELETE',
90
+ path: '/memories/{memory.id}/grants/person/{contact.claimedPersonId}',
91
+ pathWithPublic: '/memories/{memory.id}/grants/public'
92
+ },
93
+ ambiguousRefusal: true,
94
+ renderTo: 'stderr',
95
+ render: (_data, context) => `Unshared memory ${context.memory?.id ?? ''} from ${context.contact?.displayName ?? 'public'}`
96
+ }
97
+ };
98
+ export const audiencesOf = (memory) => formatAudiences(memory.audiences);
99
+ export const grants = {
100
+ name: 'grants',
101
+ words: { singular: 'grant', plural: 'grants' },
102
+ help: SHARE_HELP,
103
+ verbs: [shareVerb, unshareVerb, consent]
104
+ };
105
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1,76 @@
1
+ export const VERB_ALIASES = {
2
+ read: 'get',
3
+ show: 'get',
4
+ view: 'get',
5
+ fetch: 'get',
6
+ remove: 'delete',
7
+ publish: 'share',
8
+ unpublish: 'unshare'
9
+ };
10
+ export const USAGE_LINES = [
11
+ ' lattris login Sign in via the browser (PKCE) and store the session',
12
+ ' lattris setup Transition this repo’s Claude to Lattris as its memory system',
13
+ ' lattris use <name|path> Bind this directory to an actor (.lattris; bare `use` shows, --clear removes)',
14
+ ' lattris whoami Print "<name> · #<lattris_no>" for the signed-in person',
15
+ ' lattris set name "<value>" Set the name shown on your public page',
16
+ ' lattris set root <id> Set the block a walker starts from on your tree (whoami shows it)',
17
+ ' lattris set banned-words The words your own notes refuse (bare shows; words given replace; --clear)',
18
+ ' lattris capture memory Capture a memory — `echo "…" | lattris capture memory`',
19
+ ' lattris capture session <transcript> Save an AI conversation — the whole chain from its file',
20
+ ' lattris capture recording Keep a recording \u2014 held audio, a source, or a transcript file',
21
+ ' lattris get memories List your memories, newest first, with filters',
22
+ ' lattris contact create "<name>" Record a person or an AI you work with (--ai); add, list, get, update',
23
+ ' lattris invite <contact> Mint an invitation for one of your person contacts',
24
+ ' lattris get invites List the invitations you have minted',
25
+ ' lattris get memory <id> Read one memory back \u2014 words and edges (--links/--depth/--content)',
26
+ ' lattris ask <handle> Leave a question on someone\u2019s tree \u2014 `echo "\u2026" | lattris ask <handle>`',
27
+ ' lattris get questions The questions on your tree, open first (--mine: ones you asked)',
28
+ ' lattris close question <id> Close a question (--close-as updated|declined|already-answered)',
29
+ ' lattris share memory <id> Grant an audience access (--with public)',
30
+ ' lattris unshare memory <id> Withdraw an audience (--with public)',
31
+ ' lattris consent <id> Put a memory shared with you on your graph (--withdraw reverses)',
32
+ ' lattris link <from> <to> State that one memory leads to another (--verb, --reason)',
33
+ ' lattris unlink <from> <to> Remove that claim (--verb; the blocks stay)',
34
+ ' lattris update edge <from> Rewrite an edge\u2019s reason (--verb, --to, --reason)',
35
+ ' lattris update memory <id> Correct a memory — `echo "…" | lattris update memory <id> --base-fingerprint <fp>`',
36
+ ' lattris update recording Replace a recording\u2019s transcript, or name its speakers',
37
+ ' lattris get history <id> A note\u2019s history: every change, newest first, and the head fingerprint',
38
+ ' lattris delete memory <id> Move a note to deleted items (its history stays); other types delete',
39
+ ' lattris get deleted Your deleted items, with the fingerprint restore and erase need',
40
+ ' lattris restore memory <id> Bring a note back, or an earlier revision (--revision, --base-fingerprint)',
41
+ ' lattris erase memory <id> Permanently erase a note in deleted items. No undo.',
42
+ ' lattris resend <request.json> Send a kept request again, exactly (a lost capture or overwrite)',
43
+ ' lattris get datasets The datasets the API keeps, whether you hold rows, and their time range',
44
+ ' lattris get dataset <name> One dataset: its identity, filters, fields and an example line',
45
+ ' lattris get rows <name> One page of a dataset\u2019s rows (field=value filters, --from, --to, --limit)',
46
+ ' lattris get row <name> <id> One row of a dataset',
47
+ ' lattris capture rows <name> Save rows \u2014 newline-delimited JSON on stdin, sent in pieces of 5,000 lines',
48
+ ' lattris delete row <name> <id> Remove one row of a dataset, permanently',
49
+ ' lattris logout Forget the stored session'
50
+ ];
51
+ export const COMMAND_WORDS = new Set([
52
+ 'login',
53
+ 'setup',
54
+ 'use',
55
+ 'whoami',
56
+ 'set',
57
+ 'capture',
58
+ 'get',
59
+ 'ask',
60
+ 'close',
61
+ 'delete',
62
+ 'update',
63
+ 'link',
64
+ 'unlink',
65
+ 'invite',
66
+ 'share',
67
+ 'unshare',
68
+ 'consent',
69
+ 'restore',
70
+ 'erase',
71
+ 'resend',
72
+ 'contact',
73
+ 'logout',
74
+ '--as'
75
+ ]);
76
+ //# sourceMappingURL=words.js.map
@@ -0,0 +1,15 @@
1
+ import { captureRows } from "../datasets/capture.js";
2
+ import { getRows } from "../datasets/page.js";
3
+ import { GET_DATASETS_HELP } from "../nouns/datasets.js";
4
+ export const datasetNouns = [
5
+ {
6
+ name: 'rows',
7
+ words: { singular: 'row', plural: 'rows' },
8
+ help: GET_DATASETS_HELP,
9
+ verbs: [
10
+ { verb: 'get', nouns: ['rows'], handler: getRows },
11
+ { verb: 'capture', nouns: ['rows'], handler: captureRows }
12
+ ]
13
+ }
14
+ ];
15
+ //# sourceMappingURL=datasets.js.map
@@ -0,0 +1,51 @@
1
+ import { LOGIN_HELP, WHOAMI_HELP, login, logout, whoami } from "../entry/session.js";
2
+ import { SET_HELP, setBannedWords, setName, setRoot } from "../entry/set.js";
3
+ import { setup } from "../entry/setup.js";
4
+ import { use } from "../entry/use.js";
5
+ export const entryNouns = [
6
+ {
7
+ name: 'session',
8
+ words: { singular: 'session', plural: 'sessions' },
9
+ help: LOGIN_HELP,
10
+ verbs: [
11
+ { verb: 'login', handler: login },
12
+ { verb: 'logout', handler: async (_cfg, io) => logout(io) },
13
+ { verb: 'whoami', handler: whoami },
14
+ { verb: 'setup', handler: setup },
15
+ { verb: 'use', handler: use }
16
+ ]
17
+ },
18
+ {
19
+ name: 'person',
20
+ words: { singular: 'person', plural: 'persons' },
21
+ help: SET_HELP,
22
+ verbs: [
23
+ { verb: 'set', nouns: ['name'], handler: setName },
24
+ { verb: 'set', nouns: ['root'], handler: setRoot },
25
+ { verb: 'set', nouns: ['banned-words'], handler: setBannedWords }
26
+ ]
27
+ }
28
+ ];
29
+ export const entryVerbs = {
30
+ login: { help: LOGIN_HELP, helpBeforeAuth: true },
31
+ logout: {
32
+ help: [
33
+ 'Usage: lattris logout',
34
+ '',
35
+ 'Signs this machine dir out: removes its session file, so the next',
36
+ 'command that needs a session asks for lattris login. Nothing is sent',
37
+ 'to the server; with no session there it still prints "Logged out."',
38
+ 'Exit 1 when the session file cannot be removed.'
39
+ ].join('\n'),
40
+ helpBeforeAuth: true
41
+ },
42
+ whoami: { help: WHOAMI_HELP, helpBeforeAuth: true },
43
+ setup: {},
44
+ use: {},
45
+ set: {
46
+ help: SET_HELP,
47
+ bareIsHelp: true,
48
+ badNoun: { stderr: 'Unknown field: {noun}\n\n', stdout: `${SET_HELP}\n` }
49
+ }
50
+ };
51
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1,17 @@
1
+ import { datasetNouns } from "./datasets.js";
2
+ import { entryNouns, entryVerbs } from "./entry.js";
3
+ import { memoryNouns, memoryVerbs } from "./memory.js";
4
+ import { listNouns, listVerbs } from "../nouns/index.js";
5
+ export const registry = {
6
+ nouns: [...listNouns, ...memoryNouns, ...entryNouns, ...datasetNouns],
7
+ verbs: { ...listVerbs, ...memoryVerbs, ...entryVerbs }
8
+ };
9
+ export const commandWordsOf = (r) => [
10
+ ...new Set([
11
+ ...r.nouns
12
+ .filter((n) => n.grammar === 'noun-first')
13
+ .flatMap((n) => [n.words.singular, ...(n.words.aliases ?? [])]),
14
+ ...r.nouns.filter((n) => n.grammar !== 'noun-first').flatMap((n) => n.verbs.map((v) => v.verb))
15
+ ])
16
+ ];
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,110 @@
1
+ import { CAPTURE_ROWS_HELP } from "../datasets/capture.js";
2
+ import { DELETE_ROW_HELP, GET_DATASETS_HELP } from "../nouns/datasets.js";
3
+ import { CAPTURE_INDEX, CAPTURE_NOTE_HELP, CAPTURE_SESSION_HELP } from "../memory/capture-help.js";
4
+ import { asksForSessionForm, captureMemory } from "../memory/capture-note.js";
5
+ import { DELETE_HELP, deleteMemory } from "../memory/delete.js";
6
+ import { ERASE_HELP, eraseMemory } from "../memory/erase.js";
7
+ import { GET_HELP } from "../memory/get-help.js";
8
+ import { getMemories } from "../memory/get.js";
9
+ import { getDeleted, getHistory } from "../memory/history.js";
10
+ import { CAPTURE_RECORDING_HELP, captureRecording } from "../memory/recording.js";
11
+ import { updateRecording } from "../memory/recording-update.js";
12
+ import { RESEND_HELP, resend } from "../memory/resend.js";
13
+ import { RESTORE_HELP, restoreMemory } from "../memory/restore.js";
14
+ import { UPDATE_HELP } from "../memory/update-help.js";
15
+ import { updateMemory } from "../memory/update.js";
16
+ export const memoryNouns = [
17
+ {
18
+ name: 'memories',
19
+ words: { singular: 'memory', plural: 'memories', aliases: ['note'] },
20
+ help: GET_HELP,
21
+ verbs: [
22
+ { verb: 'capture', nouns: ['note', 'memory'], handler: captureMemory },
23
+ {
24
+ verb: 'capture',
25
+ nouns: ['session'],
26
+ handler: (cfg, io, rest) => captureMemory(cfg, io, ['--type', 'session', ...rest])
27
+ },
28
+ {
29
+ verb: 'get',
30
+ nouns: ['memory'],
31
+ handler: (cfg, io, rest) => getMemories(cfg, io, rest, 'memory')
32
+ },
33
+ {
34
+ verb: 'get',
35
+ nouns: ['memories'],
36
+ handler: (cfg, io, rest) => getMemories(cfg, io, rest, 'memories')
37
+ },
38
+ { verb: 'get', nouns: ['history'], handler: getHistory },
39
+ { verb: 'get', nouns: ['deleted'], handler: getDeleted },
40
+ { verb: 'update', nouns: ['memory', 'memories'], handler: updateMemory },
41
+ { verb: 'delete', nouns: ['memory', 'memories'], handler: deleteMemory },
42
+ { verb: 'restore', nouns: ['memory', 'memories', 'note'], handler: restoreMemory },
43
+ { verb: 'erase', nouns: ['memory', 'memories', 'note'], handler: eraseMemory },
44
+ { verb: 'resend', handler: resend }
45
+ ]
46
+ },
47
+ {
48
+ name: 'recordings',
49
+ words: { singular: 'recording', plural: 'recordings' },
50
+ help: CAPTURE_RECORDING_HELP,
51
+ verbs: [
52
+ { verb: 'capture', nouns: ['recording'], handler: captureRecording },
53
+ { verb: 'update', nouns: ['recording', 'recordings'], handler: updateRecording }
54
+ ]
55
+ }
56
+ ];
57
+ const captureHelp = (args) => {
58
+ if (args[0] === 'rows')
59
+ return CAPTURE_ROWS_HELP;
60
+ if (args[0] === 'session' || asksForSessionForm(args))
61
+ return CAPTURE_SESSION_HELP;
62
+ if (args[0] === 'note' || args[0] === 'memory')
63
+ return CAPTURE_NOTE_HELP;
64
+ if (args[0] === 'recording')
65
+ return CAPTURE_RECORDING_HELP;
66
+ return CAPTURE_INDEX;
67
+ };
68
+ export const memoryVerbs = {
69
+ capture: {
70
+ help: captureHelp,
71
+ helpBeforeAuth: true,
72
+ bareIsHelp: true,
73
+ badNoun: { stderr: 'Unknown capture form: {noun}\n\n', stdout: `${CAPTURE_INDEX}\n` }
74
+ },
75
+ get: {
76
+ help: (args) => ['datasets', 'dataset', 'rows', 'row'].includes(args[0]) ? GET_DATASETS_HELP : GET_HELP,
77
+ bareIsHelp: true,
78
+ badNoun: {
79
+ stderr: 'Usage: lattris get memories | lattris get memory <id> | lattris get history <id> | lattris get deleted | lattris get invites | lattris get questions | lattris get datasets | lattris get dataset <name> | lattris get rows <name> | lattris get row <name> <id>\n'
80
+ }
81
+ },
82
+ update: {
83
+ help: UPDATE_HELP,
84
+ bareIsHelp: true,
85
+ badNoun: { stderr: 'Usage: lattris update memory <id>\n' }
86
+ },
87
+ delete: {
88
+ help: (args) => (args[0] === 'row' ? DELETE_ROW_HELP : DELETE_HELP),
89
+ bareIsHelp: true,
90
+ badNoun: {
91
+ stderr: 'Usage: lattris delete memory <id> [--yes] | lattris delete row <name> <id> [--yes]\n'
92
+ }
93
+ },
94
+ restore: {
95
+ help: RESTORE_HELP,
96
+ helpBeforeAuth: true,
97
+ bareIsHelp: true,
98
+ badNoun: {
99
+ stderr: 'Usage: lattris restore memory <id> --revision <n> --base-fingerprint <head>\n'
100
+ }
101
+ },
102
+ erase: {
103
+ help: ERASE_HELP,
104
+ helpBeforeAuth: true,
105
+ bareIsHelp: true,
106
+ badNoun: { stderr: 'Usage: lattris erase memory <id> --base-fingerprint <head> [--yes]\n' }
107
+ },
108
+ resend: { help: RESEND_HELP, helpBeforeAuth: true }
109
+ };
110
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,77 @@
1
+ import { sanitizeForTerminal } from "./text.js";
2
+ const LABEL = 13;
3
+ const valueText = (value, max = 200) => typeof value === 'string'
4
+ ? sanitizeForTerminal(value, max)
5
+ : sanitizeForTerminal(JSON.stringify(value) ?? 'null', max);
6
+ const wordsText = (words) => words.length === 0 ? '(none)' : words.map((w) => sanitizeForTerminal(w)).join(', ');
7
+ const widest = (cells) => Math.max(0, ...cells.map((c) => c.length));
8
+ const table = (rows) => {
9
+ const widths = rows[0]?.map((_, i) => widest(rows.map((r) => r[i] ?? ''))) ?? [];
10
+ return rows
11
+ .map((r) => r
12
+ .map((cell, i) => (i === r.length - 1 ? cell : cell.padEnd(widths[i])))
13
+ .join(' ')
14
+ .trimEnd())
15
+ .join('\n');
16
+ };
17
+ export const formatCatalogue = (entries) => entries.length === 0
18
+ ? 'No datasets.'
19
+ : table(entries.map((e) => [
20
+ sanitizeForTerminal(e.name),
21
+ sanitizeForTerminal(e.storage),
22
+ e.holds ? 'holds rows' : 'no rows',
23
+ valueText(e.earliest ?? '-'),
24
+ valueText(e.latest ?? '-')
25
+ ]));
26
+ const boundText = (value) => Array.isArray(value) ? value.map((v) => valueText(v)).join(', ') : valueText(value);
27
+ const fieldCells = (f) => {
28
+ const { name, type, required, nullable, ...bounds } = f;
29
+ const rest = [
30
+ required ? 'required' : 'optional',
31
+ ...(nullable ? ['nullable'] : []),
32
+ ...Object.entries(bounds).map(([k, v]) => `${sanitizeForTerminal(k)} ${boundText(v)}`)
33
+ ];
34
+ return [` ${sanitizeForTerminal(name)}`, sanitizeForTerminal(type), rest.join(' ')];
35
+ };
36
+ export const formatDescription = (d) => {
37
+ const lines = [
38
+ `${'dataset'.padEnd(LABEL)}${sanitizeForTerminal(d.name)} (${sanitizeForTerminal(d.storage)})`,
39
+ `${'identity'.padEnd(LABEL)}${wordsText(d.identity)}`,
40
+ `${'filters'.padEnd(LABEL)}required ${wordsText(d.filters.required)}`,
41
+ `${''.padEnd(LABEL)}optional ${wordsText(d.filters.optional)}`
42
+ ];
43
+ if (d.page) {
44
+ const limits = d.limits ? `, ${d.limits.default} rows a page, at most ${d.limits.maximum}` : '';
45
+ lines.push(`${'page'.padEnd(LABEL)}by ${valueText(d.page.by)} on ${valueText(d.page.field)}${limits}`);
46
+ }
47
+ lines.push('', 'fields', table(d.fields.map(fieldCells)));
48
+ if (d.example) {
49
+ lines.push('', 'example line', ` ${valueText(JSON.stringify(d.example), Infinity)}`);
50
+ }
51
+ return lines.join('\n');
52
+ };
53
+ export const formatRow = (row) => {
54
+ const width = widest(Object.keys(row)) + 2;
55
+ return Object.entries(row)
56
+ .map(([k, v]) => `${sanitizeForTerminal(k).padEnd(width)}${valueText(v, Infinity)}`)
57
+ .join('\n');
58
+ };
59
+ const formatRowLine = (row) => [
60
+ valueText(row.id),
61
+ ...Object.entries(row)
62
+ .filter(([k, v]) => k !== 'id' && k !== 'ownerId' && v !== null)
63
+ .map(([k, v]) => `${sanitizeForTerminal(k)}=${valueText(v)}`)
64
+ ].join(' ');
65
+ export const formatRowPage = (page) => page.items.length === 0 ? 'No rows.' : page.items.map(formatRowLine).join('\n');
66
+ export const formatCaptured = (name, c) => {
67
+ const lines = [
68
+ `Sent ${c.sent} ${c.sent === 1 ? 'line' : 'lines'} to ${sanitizeForTerminal(name)}: ` +
69
+ `${c.added} added, ${c.replaced} replaced, ${c.refused} refused.`,
70
+ ...c.refusals.map((r) => ` ${valueText(r.message ?? '', Infinity)}`)
71
+ ];
72
+ if (c.refused > c.refusals.length) {
73
+ lines.push(` ${c.refused - c.refusals.length} more refused, counted and not named.`);
74
+ }
75
+ return lines.join('\n');
76
+ };
77
+ //# sourceMappingURL=datasets.js.map
@@ -0,0 +1,39 @@
1
+ import { renderRunHint, sanitizeForTerminal } from "./text.js";
2
+ const EDGE_REFUSAL_EXCERPT_BYTES = 200;
3
+ export const truncateToUtf8Bytes = (s, max) => {
4
+ let bytes = 0;
5
+ let out = '';
6
+ for (const ch of s) {
7
+ const n = Buffer.byteLength(ch, 'utf8');
8
+ if (bytes + n > max)
9
+ break;
10
+ bytes += n;
11
+ out += ch;
12
+ }
13
+ return out;
14
+ };
15
+ export const doorText = (commands, message, next, messageLimit = 200) => {
16
+ let text = `${sanitizeForTerminal(message, messageLimit)}\n`;
17
+ if (next?.action) {
18
+ const argv = next.action.trim().split(/\s+/);
19
+ text += commands.has(argv[0])
20
+ ? `Run: ${renderRunHint(argv)}\n`
21
+ : `The door suggests: ${sanitizeForTerminal(next.action)}\n`;
22
+ }
23
+ return text;
24
+ };
25
+ export const edgeText = (excerpt) => {
26
+ const shown = truncateToUtf8Bytes(sanitizeForTerminal(excerpt.replace(/\r/g, ''), Infinity), EDGE_REFUSAL_EXCERPT_BYTES);
27
+ return ('Refused at the edge before reaching Lattris (403). The words or the request shape ' +
28
+ "tripped the gateway's screen; nothing was written." +
29
+ (shown.length > 0 ? `\n${shown}` : ''));
30
+ };
31
+ export const answerText = (commands, answer) => {
32
+ if (answer.kind === 'door') {
33
+ return doorText(commands, answer.message, answer.next, answer.whole ? Infinity : 200);
34
+ }
35
+ if (answer.kind === 'edge')
36
+ return `${edgeText(answer.excerpt)}\n`;
37
+ return `The server answered with a web page, not the API — check the environment’s \`${answer.address}\` URL.\n`;
38
+ };
39
+ //# sourceMappingURL=door.js.map
@@ -0,0 +1,19 @@
1
+ import { answerText } from "./door.js";
2
+ export const emit = (term, data, rawText, renderHuman) => {
3
+ if (term.stdout.isTTY) {
4
+ const human = renderHuman(data);
5
+ if (human.length > 0)
6
+ term.stdout.write(`${human}\n`);
7
+ }
8
+ else {
9
+ term.stdout.write(rawText.endsWith('\n') ? rawText : `${rawText}\n`);
10
+ }
11
+ };
12
+ export const renderRefusal = (term, refused) => {
13
+ if (refused.answer)
14
+ term.stderr.write(answerText(term.commands, refused.answer));
15
+ if (refused.message.length > 0)
16
+ term.stderr.write(refused.message);
17
+ term.exit(refused.exit);
18
+ };
19
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,4 @@
1
+ export const readYesNo = (answer) => answer === null ? 'interrupted' : /^y(es)?$/i.test(answer.trim()) ? 'yes' : 'no';
2
+ export const confirmAction = async (term, prompt) => readYesNo(await term.stdin.ask(prompt));
3
+ export const promptLine = async (term, question) => (await term.stdin.ask(question))?.trim() ?? null;
4
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1,49 @@
1
+ import { sanitizeForTerminal } from "./text.js";
2
+ export const formatContact = (c) => `${sanitizeForTerminal(c.displayName)} · ${c.type} · ${c.id}` +
3
+ `${c.claimedPersonId ? ' · linked' : ''}` +
4
+ `${c.email ? ` · email ${sanitizeForTerminal(c.email)}` : ''}` +
5
+ `${c.rootBlockId ? ` · root ${c.rootBlockId}` : ''}`;
6
+ export const formatQuestion = (q) => {
7
+ const lines = [`id ${q.id}`, `createdAt ${q.createdAt}`];
8
+ if (q.kind === 'share') {
9
+ lines.push(`shared by @${sanitizeForTerminal(q.sharerHandle ?? '(unknown)')}`, `memory ${q.aboutBlockId ?? '(missing)'}`, `seen ${q.seenAt ?? '(unseen)'}`);
10
+ return lines.join('\n');
11
+ }
12
+ if (q.aboutBlockId)
13
+ lines.push(`about ${q.aboutBlockId}`);
14
+ lines.push(q.closedAs === null
15
+ ? 'closed as (open)'
16
+ : `closed as ${sanitizeForTerminal(q.closedAs)}${q.closeBlockId ? ` → ${q.closeBlockId}` : ''}${q.closedAt ? ` ${q.closedAt}` : ''}`);
17
+ if (q.closeNote)
18
+ lines.push(`note ${sanitizeForTerminal(q.closeNote, Infinity)}`);
19
+ lines.push('', sanitizeForTerminal(q.body ?? '', Infinity));
20
+ return lines.join('\n');
21
+ };
22
+ export const formatQuestionList = (qs) => qs.map((q) => formatQuestion(q)).join('\n\n');
23
+ export const formatAsked = (q) => {
24
+ const lines = [`Sent question ${q.id}`];
25
+ if (q.aboutBlockId)
26
+ lines.push(` about ${q.aboutBlockId}`);
27
+ lines.push(` createdAt ${q.createdAt}`);
28
+ return lines.join('\n');
29
+ };
30
+ export const formatClosed = (q) => `Closed question ${q.id} (${sanitizeForTerminal(q.closedAs ?? '')})${q.closeBlockId ? ` → ${q.closeBlockId}` : ''}`;
31
+ export const formatEdgeReceipt = (heading, e) => [
32
+ `${heading} (verb: ${sanitizeForTerminal(e.verb)})`,
33
+ ` from ${e.fromBlockId} (${e.fromType})`,
34
+ ` to ${e.toBlockId} (${e.toType})`,
35
+ ` reason ${sanitizeForTerminal(e.reason, Infinity)}`
36
+ ].join('\n');
37
+ export const formatAmendedEdge = (e) => formatEdgeReceipt('Updated edge reason.', e);
38
+ export const formatAudiences = (audiences) => audiences.length === 0 ? '(private)' : audiences.map((a) => a.kind).join(', ');
39
+ export const ambiguousText = (body) => {
40
+ if (body.code !== 'memory:ambiguous-id')
41
+ return '';
42
+ const matches = body.detail?.matches;
43
+ if (!Array.isArray(matches))
44
+ return '';
45
+ return matches
46
+ .map((m) => ` ${m.id} ${sanitizeForTerminal(m.title ?? '(untitled)')}\n`)
47
+ .join('');
48
+ };
49
+ //# sourceMappingURL=rows.js.map
@@ -0,0 +1,21 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ export const base64url = (b) => b.toString('base64url');
3
+ export const makePkce = () => {
4
+ const verifier = base64url(randomBytes(32));
5
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
6
+ return { verifier, challenge };
7
+ };
8
+ /* eslint-disable no-control-regex */
9
+ const ESCAPE_SEQUENCES = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[PX^_][^\x1b]*\x1b\\|\x1b\[[0-9:;<=>?]*[ -/]*[@-~]|\x1b[@-Z\\^_]/g;
10
+ const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\u0080-\u009f]/g;
11
+ /* eslint-enable no-control-regex */
12
+ const BIDI_FORMAT_CHARS = /[\u202a-\u202e\u2066-\u2069]/g;
13
+ export const sanitizeForTerminal = (s, maxLen = 200) => s
14
+ .replace(ESCAPE_SEQUENCES, '')
15
+ .replace(CONTROL_CHARS, '')
16
+ .replace(BIDI_FORMAT_CHARS, '')
17
+ .slice(0, maxLen);
18
+ export const shellQuote = (token) => `'${sanitizeForTerminal(token, Infinity).replace(/'/g, `'\\''`)}'`;
19
+ const SAFE_HINT_TOKEN = /^[A-Za-z0-9@._:/-]+$/;
20
+ export const renderRunHint = (argv) => ['lattris', ...argv.map((t) => (SAFE_HINT_TOKEN.test(t) ? t : shellQuote(t)))].join(' ');
21
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1,67 @@
1
+ import { EXIT, refusal } from "../store/exit.js";
2
+ import { RENEWAL_TIMEOUT_MS, SessionLockHeld, WAIT_AT_MOST_MS, withSessionLock } from "../store/session-lock.js";
3
+ import { loadSession, notLoggedInWords, saveSession } from "../store/session.js";
4
+ import { postToken, refreshBody, toSession } from "./token.js";
5
+ const SKEW_SECONDS = 30;
6
+ const stillValid = (session, now) => session.expiresAt > Math.floor(now / 1000) + SKEW_SECONDS;
7
+ const renewFrom = async (io, cfg, session) => {
8
+ if (!session)
9
+ throw new Error('not-logged-in');
10
+ if (stillValid(session, io.store.now()))
11
+ return session.accessToken;
12
+ if (!session.refreshToken)
13
+ throw new Error('not-logged-in');
14
+ const token = await postToken(io.network, cfg, refreshBody(cfg, session.refreshToken), io.store.timeout(RENEWAL_TIMEOUT_MS));
15
+ const refreshed = toSession(token, io.store.now());
16
+ refreshed.refreshToken = refreshed.refreshToken ?? session.refreshToken;
17
+ refreshed.personId = session.personId;
18
+ refreshed.handle = session.handle;
19
+ saveSession(io.store, refreshed);
20
+ return refreshed.accessToken;
21
+ };
22
+ export const validAccessToken = async (io, cfg) => {
23
+ const session = loadSession(io.store);
24
+ if (!session)
25
+ throw new Error('not-logged-in');
26
+ if (stillValid(session, io.store.now()))
27
+ return session.accessToken;
28
+ if (!session.refreshToken)
29
+ throw new Error('not-logged-in');
30
+ try {
31
+ return await withSessionLock(io.store, () => renewFrom(io, cfg, loadSession(io.store)), () => io.stderr.write('Waiting for another lattris command to finish renewing the sign-in…\n'));
32
+ }
33
+ catch (e) {
34
+ if (e instanceof SessionLockHeld)
35
+ throw e;
36
+ throw new Error('not-logged-in');
37
+ }
38
+ };
39
+ export const optionalAccessToken = async (io, cfg) => {
40
+ try {
41
+ return await io.token(cfg);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ };
47
+ const notLoggedIn = (io) => ({
48
+ message: '',
49
+ exit: EXIT.NOAUTH,
50
+ answer: {
51
+ kind: 'door',
52
+ message: notLoggedInWords(io.store),
53
+ next: { action: 'login' },
54
+ whole: true
55
+ }
56
+ });
57
+ const gaveUpWaiting = (lock) => refusal(`Gave up after ${WAIT_AT_MOST_MS / 1000} seconds waiting for another lattris command to renew the sign-in. ` +
58
+ `If none is running, remove ${lock} and run this again.\n`, EXIT.GENERIC);
59
+ export const signedIn = async (io, cfg) => {
60
+ try {
61
+ return await io.token(cfg);
62
+ }
63
+ catch (e) {
64
+ return e instanceof SessionLockHeld ? gaveUpWaiting(e.path) : notLoggedIn(io);
65
+ }
66
+ };
67
+ //# sourceMappingURL=access-token.js.map