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,88 @@
1
+ import { parseArgs } from "../request/flags.js";
2
+ import { confirmAction } from "../render/prompt.js";
3
+ import { renderRunHint, sanitizeForTerminal } from "../render/text.js";
4
+ import { EXIT, isRefusal, refusal } from "../store/exit.js";
5
+ import { noteWriteHeaders, requireWriteIdentity } from "../store/write-identity.js";
6
+ import { refusalOf } from "../request/api-fetch.js";
7
+ import { signedIn } from "../request/access-token.js";
8
+ import { ambiguousText } from "../render/rows.js";
9
+ const deleteOne = async (cfg, io, idOrPrefix, yes) => {
10
+ const identity = requireWriteIdentity(io.store, io.stdin.isTTY);
11
+ if (isRefusal(identity))
12
+ return identity;
13
+ const accessToken = await signedIn(io, cfg);
14
+ if (typeof accessToken !== 'string')
15
+ return accessToken;
16
+ const fetched = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(idOrPrefix)}`);
17
+ if (!fetched.ok)
18
+ return refusalOf(fetched, ambiguousText(fetched.body));
19
+ const title = sanitizeForTerminal(fetched.data.title ?? '(untitled)');
20
+ const deleteId = fetched.data.id;
21
+ if (typeof fetched.data.fingerprint === 'string') {
22
+ io.stderr.write(`Moving "${title}" (${deleteId}) to deleted items. Its history stays; restore returns it private.\n`);
23
+ const result = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(deleteId)}`, {
24
+ method: 'DELETE',
25
+ headers: {
26
+ ...noteWriteHeaders(identity),
27
+ 'X-Lattris-Base-Fingerprint': fetched.data.fingerprint
28
+ }
29
+ });
30
+ if (!result.ok)
31
+ return refusalOf(result);
32
+ io.stderr.write(`Moved memory ${deleteId} to deleted items. ${renderRunHint(['get', 'deleted'])} lists it; ` +
33
+ `${renderRunHint(['restore', 'memory', deleteId])} brings it back.\n`);
34
+ return;
35
+ }
36
+ if (!io.stdin.isTTY && !yes) {
37
+ return refusal('Refusing to permanently delete without confirmation on a non-interactive session. Pass --yes to skip the prompt.\n');
38
+ }
39
+ if (io.stdin.isTTY) {
40
+ const confirmed = await confirmAction(io, `Delete "${title}" (${deleteId}) permanently? [y/N] `);
41
+ if (confirmed === 'interrupted') {
42
+ return refusal('Cancelled.\n', EXIT.GENERIC);
43
+ }
44
+ if (confirmed === 'no') {
45
+ io.stderr.write('Cancelled.\n');
46
+ return;
47
+ }
48
+ }
49
+ const result = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(deleteId)}`, { method: 'DELETE' });
50
+ if (!result.ok)
51
+ return refusalOf(result, ambiguousText(result.body));
52
+ io.stderr.write(`Deleted memory ${deleteId}\n`);
53
+ };
54
+ export const DELETE_HELP = [
55
+ 'delete — remove one memory.',
56
+ '',
57
+ 'A NOTE moves to deleted items. Its history stays; restore returns it private.',
58
+ 'The note is read first and its fingerprint sent with the removal, so a note',
59
+ 'that changed since you read it is refused and nothing changes. No prompt:',
60
+ 'nothing is lost. `lattris get deleted` lists what is there; `lattris restore`',
61
+ 'brings one back; `lattris erase` is the separate, permanent second step.',
62
+ '',
63
+ 'Every other type (a photo, a video, a session, a recording) is still',
64
+ 'permanently deleted here, with the prompt a permanent act keeps.',
65
+ '',
66
+ 'Usage:',
67
+ ' lattris delete memory <id-or-prefix> [--yes]',
68
+ '',
69
+ ' --yes for a permanent delete only: required when not at a terminal (there',
70
+ ' is no prompt to answer there); at a terminal you are always asked.',
71
+ ' A note never asks and never needs it.',
72
+ '',
73
+ 'Alias: remove → delete'
74
+ ].join('\n');
75
+ export const deleteMemory = async (cfg, io, rest) => {
76
+ const yes = rest.includes('--yes');
77
+ const { positionals, unknown, notAnId } = parseArgs({}, rest.filter((a) => a !== '--yes'), [0]);
78
+ if (unknown) {
79
+ return refusal(`Unknown flag: ${unknown}\n`);
80
+ }
81
+ if (notAnId)
82
+ return refusal(notAnId);
83
+ if (positionals.length !== 1) {
84
+ return refusal(`Usage: lattris delete memory <id> [--yes]\n`);
85
+ }
86
+ return deleteOne(cfg, io, positionals[0], yes);
87
+ };
88
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1,67 @@
1
+ import { parseArgs } from "../request/flags.js";
2
+ import { confirmAction } from "../render/prompt.js";
3
+ import { FINGERPRINT_SHAPE } from "../memory/update-help.js";
4
+ import { EXIT, isRefusal, refusal } from "../store/exit.js";
5
+ import { noteWriteHeaders, requireWriteIdentity } from "../store/write-identity.js";
6
+ import { refusalOf } from "../request/api-fetch.js";
7
+ import { signedIn } from "../request/access-token.js";
8
+ import { ambiguousText } from "../render/rows.js";
9
+ export const ERASE_HELP = [
10
+ 'erase — permanently erase a note that is already in deleted items. No undo.',
11
+ '',
12
+ 'Usage:',
13
+ ' lattris erase memory <id-or-prefix> --base-fingerprint <head> [--yes]',
14
+ '',
15
+ ' --base-fingerprint required; the fingerprint `lattris get deleted` shows for it.',
16
+ ' --yes required when not at a terminal; at a terminal you are asked.',
17
+ '',
18
+ 'Only a note in deleted items can be erased: delete it first. Erase purges its',
19
+ 'words and every revision; the id and handle remain as a marker, and links that',
20
+ 'pointed at it keep their reasons. There is no undo.'
21
+ ].join('\n');
22
+ const ERASE_FLAG_NAMES = {
23
+ '--base-fingerprint': 'baseFingerprint'
24
+ };
25
+ export const eraseMemory = async (cfg, io, rest) => {
26
+ const yes = rest.includes('--yes');
27
+ const { flags, positionals, unknown, notAnId } = parseArgs(ERASE_FLAG_NAMES, rest.filter((a) => a !== '--yes'), [0]);
28
+ if (unknown) {
29
+ return refusal(`Unknown flag: ${unknown}\n`);
30
+ }
31
+ if (notAnId)
32
+ return refusal(notAnId);
33
+ if (positionals.length !== 1 || flags.baseFingerprint === undefined) {
34
+ return refusal(`Usage: lattris erase memory <id> --base-fingerprint <head> [--yes]\n`);
35
+ }
36
+ if (!FINGERPRINT_SHAPE.test(flags.baseFingerprint)) {
37
+ return refusal(`--base-fingerprint must be the 64 lowercase hex characters the deleted list printed: ${flags.baseFingerprint}\n`);
38
+ }
39
+ if (!io.stdin.isTTY && !yes) {
40
+ return refusal('Refusing to erase without confirmation on a non-interactive session. Pass --yes to skip the prompt. No undo.\n');
41
+ }
42
+ const identity = requireWriteIdentity(io.store, io.stdin.isTTY);
43
+ if (isRefusal(identity))
44
+ return identity;
45
+ if (io.stdin.isTTY) {
46
+ const confirmed = await confirmAction(io, `Erase ${positionals[0]} permanently? Its words and every revision go; the id stays as a marker. No undo. [y/N] `);
47
+ if (confirmed === 'interrupted') {
48
+ return refusal('Cancelled.\n', EXIT.GENERIC);
49
+ }
50
+ if (confirmed === 'no') {
51
+ io.stderr.write('Cancelled.\n');
52
+ return;
53
+ }
54
+ }
55
+ const accessToken = await signedIn(io, cfg);
56
+ if (typeof accessToken !== 'string')
57
+ return accessToken;
58
+ const result = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(positionals[0])}/erase`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json', ...noteWriteHeaders(identity) },
61
+ body: JSON.stringify({ baseFingerprint: flags.baseFingerprint })
62
+ });
63
+ if (!result.ok)
64
+ return refusalOf(result, ambiguousText(result.body));
65
+ io.stderr.write(`Erased memory ${positionals[0]}. No undo.\n`);
66
+ };
67
+ //# sourceMappingURL=erase.js.map
@@ -1,47 +1,3 @@
1
- // Deterministic extractor: a Claude Code transcript
2
- // (~/.claude/projects/<project>/<session>.jsonl) in, ordered epoch turns out
3
- // (session-block spec §Skill — each rule verified against a real
4
- // transcript corpus, never inferred from vendor docs).
5
- // Extraction is MECHANICAL — one place for this vendor's shape, zero
6
- // judgment: the same file always yields the same epochs, byte for byte.
7
- // The AI-judgment touchpoints (when to save, title, ai_summary) live in the
8
- // skill, never here.
9
- //
10
- // An internal module behind `lattris capture session <transcript.jsonl>`
11
- // (main.ts) — not a command of its own.
12
- //
13
- // The settled extraction rules, in the spec's own words:
14
- // - The transcript is a TREE, not a list: every entry carries uuid/
15
- // parentUuid; the conversation is the walk from the live leaf up, never
16
- // file line order. Dead branches (rewinds) stay in the file and are simply
17
- // not on the walked path.
18
- // - The chain break at compaction: the system/compact_boundary entry has
19
- // parentUuid null; continuity is its logicalParentUuid — a naive
20
- // parent-walk silently truncates at the last compaction, so the walk
21
- // bridges it. Each boundary starts a NEW EPOCH; the summary rides the
22
- // isCompactSummary user entry and becomes the next epoch's opening
23
- // kind: 'compaction-summary' marker turn.
24
- // - Most "user" entries are not the person: promptSource 'typed' +
25
- // origin.kind 'human' is the person; tool_result carriers, meta entries
26
- // and task notifications are machinery; isSidechain entries are subagent
27
- // noise — all stripped.
28
- // - Thinking blocks are OUT (private scratchpad, not dialogue); an
29
- // assistant turn is its message's text blocks only, and one API message
30
- // spans MULTIPLE jsonl entries (one content block each, same message.id)
31
- // — concatenated in path order.
32
- // - Peer-session messages are stamped as their own speaker
33
- // (peer:<origin.name>), never the owner; the body is origin.body (the
34
- // raw message, without the harness's delivery wrapper).
35
- // - Two file shapes. A MAIN transcript is the person's session; sidechain
36
- // entries inside it are a subagent's turns interleaved, and noise. A
37
- // SUBAGENT transcript (…/<session>/subagents/agent-<id>.jsonl) is that
38
- // subagent's own file: every path entry is isSidechain with an agentId,
39
- // its first user entry is the parent's prompt (no promptSource, no
40
- // origin), and its later user entries are tool_result carriers. The
41
- // shape is read from the file and named on the result, and the same
42
- // chain of epochs comes out of either.
43
- //
44
- // Zero imports beyond node built-ins, mirroring main.ts's own discipline.
45
1
  const parseLines = (jsonl) => {
46
2
  const entries = [];
47
3
  for (const line of jsonl.split('\n')) {
@@ -52,24 +8,13 @@ const parseLines = (jsonl) => {
52
8
  if (parsed && typeof parsed === 'object')
53
9
  entries.push(parsed);
54
10
  }
55
- catch {
56
- // A torn tail line (the file is appended live) is not a conversation
57
- // fact — skipped, deterministically.
58
- }
11
+ catch { }
59
12
  }
60
13
  return entries;
61
14
  };
62
- // Path-bearing entries: the tree the walk runs over. Everything else
63
- // (attachments, file-history, queue bookkeeping…) never carries dialogue.
64
15
  const isPathEntry = (e) => typeof e.uuid === 'string' &&
65
16
  (e.type === 'user' || e.type === 'assistant' || e.type === 'system');
66
- // The ACTIVE path, live leaf up, oldest-first on return.
67
- // The live leaf is the LAST path-bearing entry in file order — the file is
68
- // an append log, so its tail is the current tip; every dead branch hangs off
69
- // an earlier fork and is unreachable walking UP from here. The compaction
70
- // bridge: parentUuid null + logicalParentUuid set continues the walk into
71
- // the previous epoch.
72
- export const walkActivePath = (entries) => {
17
+ const walkActivePath = (entries) => {
73
18
  const byUuid = new Map();
74
19
  for (const e of entries) {
75
20
  if (typeof e.uuid === 'string')
@@ -80,7 +25,7 @@ export const walkActivePath = (entries) => {
80
25
  const seen = new Set();
81
26
  while (cursor) {
82
27
  if (typeof cursor.uuid !== 'string' || seen.has(cursor.uuid))
83
- break; // cycle guard
28
+ break;
84
29
  seen.add(cursor.uuid);
85
30
  path.push(cursor);
86
31
  const parentId = cursor.parentUuid ?? cursor.logicalParentUuid;
@@ -98,14 +43,7 @@ const textOf = (content) => {
98
43
  .map((b) => b.text)
99
44
  .join('\n\n');
100
45
  };
101
- // The shape of a FILE, judged over every path-bearing entry before any
102
- // walk: a subagent's own file has all of them marked isSidechain with an
103
- // agentId; anything else is a main transcript, where a sidechain entry is
104
- // another agent's turn and noise, even at the file's tail. Judged before
105
- // the walk because the walk starts at the last path entry in file order,
106
- // and a main file whose tail is a subagent's appended turns would
107
- // otherwise be walked from the subagent's leaf.
108
- export const shapeOf = (entries) => {
46
+ const shapeOf = (entries) => {
109
47
  const bearing = entries.filter(isPathEntry);
110
48
  return bearing.length > 0 &&
111
49
  bearing.every((e) => e.isSidechain === true && typeof e.agentId === 'string')
@@ -114,7 +52,7 @@ export const shapeOf = (entries) => {
114
52
  };
115
53
  const carriesToolResult = (content) => Array.isArray(content) &&
116
54
  content.some((b) => b && typeof b === 'object' && b.type === 'tool_result');
117
- export const classify = (e, shape = 'main') => {
55
+ const classify = (e, shape = 'main') => {
118
56
  if (shape === 'main' && e.isSidechain === true)
119
57
  return { kind: 'noise' };
120
58
  if (e.type === 'system') {
@@ -123,16 +61,12 @@ export const classify = (e, shape = 'main') => {
123
61
  : { kind: 'noise' };
124
62
  }
125
63
  if (e.type === 'user') {
126
- // The compaction summary — identified by its flag BEFORE any
127
- // person/machinery classification (it is neither).
128
64
  if (e.isCompactSummary === true) {
129
65
  const text = textOf(e.message?.content);
130
66
  return text.trim().length > 0 ? { kind: 'marker', text, entry: e } : { kind: 'noise' };
131
67
  }
132
68
  if (e.origin?.kind === 'peer') {
133
69
  const name = typeof e.origin.name === 'string' && e.origin.name.length > 0 ? e.origin.name : 'unknown';
134
- // origin.body is the raw peer message; the entry's own content wraps
135
- // it in the harness's delivery boilerplate, which is machinery.
136
70
  const body = typeof e.origin.body === 'string' ? e.origin.body : textOf(e.message?.content);
137
71
  return body.trim().length > 0
138
72
  ? { kind: 'peer', speaker: `peer:${name}`, text: body, entry: e }
@@ -142,22 +76,13 @@ export const classify = (e, shape = 'main') => {
142
76
  const text = textOf(e.message?.content);
143
77
  return text.trim().length > 0 ? { kind: 'person', text, entry: e } : { kind: 'noise' };
144
78
  }
145
- // In a subagent's own file the parent's prompt carries no promptSource
146
- // and no origin: it is the user entry that is words and not a
147
- // tool_result carrier.
148
79
  if (shape === 'subagent' && e.isMeta !== true && !carriesToolResult(e.message?.content)) {
149
80
  const text = textOf(e.message?.content);
150
81
  return text.trim().length > 0 ? { kind: 'person', text, entry: e } : { kind: 'noise' };
151
82
  }
152
- // tool_result carriers, meta entries, task notifications, queued
153
- // machinery — not dialogue.
154
83
  return { kind: 'noise' };
155
84
  }
156
85
  if (e.type === 'assistant') {
157
- // Text blocks only — thinking is OUT (deliberate: private scratchpad,
158
- // never dialogue), tool_use is
159
- // machinery. One entry carries one block; same-message entries merge in
160
- // the caller.
161
86
  const text = textOf(e.message?.content);
162
87
  return text.trim().length > 0
163
88
  ? { kind: 'assistant-block', messageId: e.message?.id, text, entry: e }
@@ -178,31 +103,19 @@ const toTurn = (speaker, text, entry, kind) => {
178
103
  turn.externalParentMessageId = parent;
179
104
  return turn;
180
105
  };
181
- // The whole extraction: entries → active path → epochs of ordered turns.
182
106
  export const extractChain = (jsonl) => {
183
107
  const entries = parseLines(jsonl);
184
108
  const shape = shapeOf(entries);
185
- // In a main file the sidechain entries are noise before the walk too, so
186
- // the live leaf is the person's last word and never a subagent's.
187
109
  const path = walkActivePath(shape === 'main' ? entries.filter((e) => e.isSidechain !== true) : entries);
188
110
  const agentId = shape === 'subagent'
189
111
  ? (path.map((e) => e.agentId).find((a) => typeof a === 'string') ?? null)
190
112
  : null;
191
113
  const sessionId = path.map((e) => e.sessionId).find((s) => typeof s === 'string') ?? null;
192
- // A subagent's conversation identity is its own: the parent's session id
193
- // and its agent id together, distinct from the parent's and from every
194
- // sibling's, the same on every capture of the same file. The receipts and
195
- // the conversation nomination key on it, so two siblings captured under
196
- // one session never extend or fork each other. Joined with a separator a
197
- // file name can carry, since the receipts file is named by it.
198
114
  const conversationId = shape === 'subagent' && sessionId !== null && agentId !== null
199
115
  ? `${sessionId}--agent-${agentId}`
200
116
  : sessionId;
201
117
  const epochs = [];
202
118
  let current = [];
203
- // The assistant merge: consecutive path entries sharing message.id are
204
- // one API message — their text blocks concatenate into ONE turn (path
205
- // order), stamped with the FIRST entry's ids/time.
206
119
  let pendingAssistant = null;
207
120
  const flushAssistant = () => {
208
121
  if (!pendingAssistant)
@@ -228,8 +141,6 @@ export const extractChain = (jsonl) => {
228
141
  continue;
229
142
  flushAssistant();
230
143
  if (c.kind === 'epoch-boundary') {
231
- // A new epoch begins. The boundary itself carries no words — the
232
- // summary marker (the next epoch's first classified entry) does.
233
144
  if (current.length > 0)
234
145
  epochs.push(current);
235
146
  current = [];
@@ -0,0 +1,63 @@
1
+ export const GET_HELP = [
2
+ 'get — read your memories or a public wall.',
3
+ '',
4
+ 'Nouns:',
5
+ ' memories your list, or one wall with --by <owner-handle>',
6
+ ' memory one, by website address or id with --by <owner-handle> \u2014 its edges ride along',
7
+ ' history one note\u2019s history: every change, newest first, and the head fingerprint',
8
+ ' deleted your deleted items, with the fingerprint restore and erase need',
9
+ ' questions the questions left on your tree, open first',
10
+ '',
11
+ 'Usage:',
12
+ ' lattris get memories [--shared-with-me] [--q <words>] [--type <name>] [--intent <name>] [--with <audience>] [--limit <n>] [--cursor <token>] [--content 0|1]',
13
+ ' lattris get memories --by <owner-handle> [--limit <n>] [--cursor <token>] [--content 0|1]',
14
+ ' lattris get memory <website-address|id> [--by <owner-handle>] [--links 0|1] [--depth <n>] [--content 0|1]',
15
+ ' lattris get memory <id-or-prefix> --revision <n> one revision, as history kept it',
16
+ ' lattris get history <id-or-prefix> [--limit <n>] [--before <operation-id>]',
17
+ ' lattris get deleted [--limit <n>] [--cursor <token>]',
18
+ ' lattris get questions [--mine]',
19
+ '',
20
+ ' --q <words> only memories whose title or content contains these words,',
21
+ ' matched as one literal phrase, case-insensitive. Needs 3+',
22
+ ' characters and at least one distinctive word — a query of',
23
+ ' only common words ("the", "and", ...) is refused; pick a',
24
+ ' name or a particular noun instead.',
25
+ ' --type <name> filter to one type',
26
+ ' --intent <name> filter to one intent',
27
+ ' --with <audience> your own footprint: public (has that grant) or nobody',
28
+ ' (private — no grant at all)',
29
+ ' --limit <n> page size, 1-100 (default 50)',
30
+ ' --cursor <token> the nextCursor from a previous page',
31
+ ' --by <handle> one wall on the website address; an id without a',
32
+ ' session needs it, while a full address carries it',
33
+ ' --shared-with-me list memories other people granted to you; each row',
34
+ ' names its owner and whether it is on your graph',
35
+ '',
36
+ ' --links 0|1 edges on the one memory (default 1): direction, verb,',
37
+ ' reason, and each neighbour\u2019s finding aid. --links 0',
38
+ ' restores the bare read, byte-identical on the wire.',
39
+ ' --depth <n> expand neighbours recursively (finding aid + links',
40
+ ' past the anchor; the API owns the range) \u2014 needs links',
41
+ ' --content 0|1 0 answers the finding aid only (id, handle, type,',
42
+ ' title, aiSummary) \u2014 the cheap look before a full read;',
43
+ ' on the list, 0 drops the excerpts and keeps every other',
44
+ ' field a writer needs (fingerprint, revisionNo, structureHash)',
45
+ ' --mine (questions) the ones YOU asked, with how each closed',
46
+ '',
47
+ ' --revision <n> the snapshot numbered n: title, words, metadata, its',
48
+ ' fingerprint and who recorded it. Restore it with',
49
+ ' `lattris restore memory <id> --revision <n> --base-fingerprint <head>`.',
50
+ ' --before <op-id> (history) the page before that operation \u2014 the',
51
+ ' nextBeforeOperation a previous page printed',
52
+ '',
53
+ 'Every read of a note prints its fingerprint (the head of its history) and',
54
+ 'revisionNo; every read prints structureHash, a hash of the block\u2019s links and',
55
+ 'grants as this reader sees them. An edit sends the fingerprint back as --base-fingerprint.',
56
+ '',
57
+ "A memory's web address is <site>/by/<owner-handle>/memories/<slug>-<memory-handle>.",
58
+ 'A /by read asks for JSON with Accept: application/json; it sends a bearer',
59
+ 'only when the session has a live one. `get memory` prints the address at a TTY.',
60
+ '',
61
+ 'Aliases: read, show, view, fetch → get'
62
+ ].join('\n');
63
+ //# sourceMappingURL=get-help.js.map
@@ -0,0 +1,213 @@
1
+ import { parseArgs } from "../request/flags.js";
2
+ import { getRevision } from "../memory/history.js";
3
+ import { composeAddress, formatMemoryDetail, formatMemoryPage, formatNodeWalk, ownHandleFor, parseWebsiteMemoryAddress } from "../memory/views.js";
4
+ import { emit } from "../render/output.js";
5
+ import { ambiguousText } from "../render/rows.js";
6
+ import { renderRunHint } from "../render/text.js";
7
+ import { optionalAccessToken, signedIn } from "../request/access-token.js";
8
+ import { refusalOf } from "../request/api-fetch.js";
9
+ import { refusal } from "../store/exit.js";
10
+ import { loadSession, saveSession } from "../store/session.js";
11
+ export const buildGetMemoryUrl = (webUrl, ownerHandle, ref, flags) => {
12
+ const url = new URL(`${webUrl}/by/${encodeURIComponent(ownerHandle)}/memories/${encodeURIComponent(ref)}`);
13
+ const links = flags.links ?? '1';
14
+ const bare = links === '0' && flags.depth === undefined && flags.content === undefined;
15
+ if (!bare) {
16
+ url.searchParams.set('links', links);
17
+ if (flags.depth !== undefined)
18
+ url.searchParams.set('depth', flags.depth);
19
+ if (flags.content !== undefined)
20
+ url.searchParams.set('content', flags.content);
21
+ }
22
+ return url;
23
+ };
24
+ const getMemory = async (cfg, io, idOrAddress, flags, by) => {
25
+ const isAddress = /^https?:\/\//i.test(idOrAddress);
26
+ const address = isAddress ? parseWebsiteMemoryAddress(cfg.webUrl, idOrAddress) : undefined;
27
+ if (isAddress && !address) {
28
+ return refusal(`The memory address must be on ${cfg.webUrl}/by/<owner>/memories/<ref>.\n`);
29
+ }
30
+ if (address && by && address.ownerHandle !== by) {
31
+ return refusal('--by must name the same owner as the memory address.\n');
32
+ }
33
+ const storedSession = loadSession(io.store);
34
+ let ownerHandle = address?.ownerHandle ?? by ?? storedSession?.handle;
35
+ let accessToken;
36
+ if (!ownerHandle && storedSession) {
37
+ accessToken = await optionalAccessToken(io, cfg);
38
+ if (accessToken) {
39
+ ownerHandle = await ownHandleFor(cfg, io, accessToken);
40
+ if (ownerHandle) {
41
+ const currentSession = loadSession(io.store);
42
+ if (currentSession && currentSession.handle !== ownerHandle) {
43
+ saveSession(io.store, { ...currentSession, handle: ownerHandle });
44
+ }
45
+ }
46
+ }
47
+ }
48
+ if (!ownerHandle) {
49
+ return refusal('A memory id needs its wall: use get memory <website-address> or get memory <id> --by <owner-handle>.\n');
50
+ }
51
+ accessToken ??= await optionalAccessToken(io, cfg);
52
+ const url = buildGetMemoryUrl(cfg.webUrl, ownerHandle, address?.ref ?? idOrAddress, flags);
53
+ const enveloped = url.search !== '';
54
+ const result = await io.api(accessToken, url, {
55
+ headers: { Accept: 'application/json' },
56
+ retryAnonymousOn401: true
57
+ }, 'web');
58
+ if (!result.ok) {
59
+ return refusalOf(result, ambiguousText(result.body) +
60
+ (result.body.code === 'memory:not-found' && !address && !by
61
+ ? 'For another wall, use get memory <website-address> or get memory <id> --by <owner-handle>.\n'
62
+ : ''));
63
+ }
64
+ const record = enveloped ? result.data.node : result.data;
65
+ const displayAddress = io.stdout.isTTY
66
+ ? composeAddress(cfg.webUrl, ownerHandle, record)
67
+ : undefined;
68
+ emit(io, result.data, result.rawText, () => enveloped
69
+ ? formatNodeWalk(result.data, displayAddress)
70
+ : formatMemoryDetail(result.data, displayAddress));
71
+ };
72
+ const GET_FLAG_NAMES = {
73
+ '--by': 'by',
74
+ '--revision': 'revision',
75
+ '--type': 'type',
76
+ '--intent': 'intent',
77
+ '--limit': 'limit',
78
+ '--cursor': 'cursor',
79
+ '--q': 'q',
80
+ '--with': 'with',
81
+ '--links': 'links',
82
+ '--depth': 'depth',
83
+ '--content': 'content'
84
+ };
85
+ const LIST_ONLY_FLAGS = ['type', 'intent', 'limit', 'cursor', 'q', 'with', 'sharedWithMe'];
86
+ const WALK_ONLY_FLAGS = ['links', 'depth', 'revision'];
87
+ const listMemories = async (cfg, io, flags) => {
88
+ const accessToken = await signedIn(io, cfg);
89
+ if (typeof accessToken !== 'string')
90
+ return accessToken;
91
+ const url = new URL(`${cfg.apiUrl}/memories`);
92
+ if (flags.type !== undefined)
93
+ url.searchParams.set('type', flags.type);
94
+ if (flags.intent !== undefined)
95
+ url.searchParams.set('intent', flags.intent);
96
+ if (flags.limit !== undefined)
97
+ url.searchParams.set('limit', flags.limit);
98
+ if (flags.cursor !== undefined)
99
+ url.searchParams.set('cursor', flags.cursor);
100
+ if (flags.q !== undefined)
101
+ url.searchParams.set('q', flags.q);
102
+ if (flags.with !== undefined)
103
+ url.searchParams.set('audience', flags.with);
104
+ if (flags.content !== undefined)
105
+ url.searchParams.set('content', flags.content);
106
+ if (flags.sharedWithMe)
107
+ url.searchParams.set('sharedWithMe', '1');
108
+ const result = await io.api(accessToken, url);
109
+ if (!result.ok)
110
+ return refusalOf(result);
111
+ emit(io, result.data, result.rawText, formatMemoryPage);
112
+ if (result.data.nextCursor) {
113
+ const argv = ['get', 'memories'];
114
+ if (flags.type !== undefined)
115
+ argv.push('--type', flags.type);
116
+ if (flags.intent !== undefined)
117
+ argv.push('--intent', flags.intent);
118
+ if (flags.q !== undefined)
119
+ argv.push('--q', flags.q);
120
+ if (flags.with !== undefined)
121
+ argv.push('--with', flags.with);
122
+ if (flags.limit !== undefined)
123
+ argv.push('--limit', flags.limit);
124
+ if (flags.content !== undefined)
125
+ argv.push('--content', flags.content);
126
+ if (flags.sharedWithMe)
127
+ argv.push('--shared-with-me');
128
+ argv.push('--cursor', result.data.nextCursor);
129
+ io.stderr.write(`More: ${renderRunHint(argv)}\n`);
130
+ }
131
+ };
132
+ const getWallMemories = async (cfg, io, ownerHandle, flags) => {
133
+ const accessToken = await optionalAccessToken(io, cfg);
134
+ const url = new URL(`${cfg.webUrl}/by/${encodeURIComponent(ownerHandle)}/memories`);
135
+ if (flags.limit !== undefined)
136
+ url.searchParams.set('limit', flags.limit);
137
+ if (flags.cursor !== undefined)
138
+ url.searchParams.set('cursor', flags.cursor);
139
+ if (flags.content !== undefined)
140
+ url.searchParams.set('content', flags.content);
141
+ const result = await io.api(accessToken, url, {
142
+ headers: { Accept: 'application/json' },
143
+ retryAnonymousOn401: true
144
+ }, 'web');
145
+ if (!result.ok)
146
+ return refusalOf(result);
147
+ emit(io, result.data, result.rawText, formatMemoryPage);
148
+ if (result.data.nextCursor) {
149
+ const argv = ['get', 'memories', '--by', ownerHandle];
150
+ if (flags.limit !== undefined)
151
+ argv.push('--limit', flags.limit);
152
+ if (flags.content !== undefined)
153
+ argv.push('--content', flags.content);
154
+ argv.push('--cursor', result.data.nextCursor);
155
+ io.stderr.write(`More: ${renderRunHint(argv)}\n`);
156
+ }
157
+ };
158
+ export const getMemories = async (cfg, io, rest, noun) => {
159
+ const sharedWithMeCount = rest.filter((arg) => arg === '--shared-with-me').length;
160
+ if (sharedWithMeCount > 1) {
161
+ return refusal('--shared-with-me was given more than once. Send it once.\n');
162
+ }
163
+ const { flags, positionals, unknown, notAnId } = parseArgs(GET_FLAG_NAMES, rest.filter((arg) => arg !== '--shared-with-me'), [0]);
164
+ if (sharedWithMeCount === 1)
165
+ flags.sharedWithMe = '1';
166
+ if (unknown) {
167
+ return refusal(`Unknown flag: ${unknown}\n`);
168
+ }
169
+ if (notAnId && !/^https?:\/\//i.test(positionals[0]))
170
+ return refusal(notAnId);
171
+ if (positionals.length > 1) {
172
+ return refusal(`Unexpected argument: ${positionals[1]}\n`);
173
+ }
174
+ const id = positionals[0];
175
+ if (id !== undefined) {
176
+ const inapplicable = LIST_ONLY_FLAGS.find((k) => flags[k] !== undefined);
177
+ if (inapplicable !== undefined) {
178
+ return refusal(`--${inapplicable} does not apply to a single memory.\n`);
179
+ }
180
+ if (flags.revision !== undefined) {
181
+ if (flags.by !== undefined || /^https?:\/\//i.test(id)) {
182
+ return refusal('A revision read uses your own id or prefix; --by and website addresses do not apply.\n');
183
+ }
184
+ const walkFlag = ['links', 'depth', 'content'].find((k) => flags[k] !== undefined);
185
+ if (walkFlag !== undefined) {
186
+ return refusal(`--${walkFlag} does not apply to a revision read.\n`);
187
+ }
188
+ return getRevision(cfg, io, id, flags.revision);
189
+ }
190
+ return getMemory(cfg, io, id, { links: flags.links, depth: flags.depth, content: flags.content }, flags.by);
191
+ }
192
+ else if (noun === 'memories') {
193
+ const walkFlag = WALK_ONLY_FLAGS.find((k) => flags[k] !== undefined);
194
+ if (walkFlag !== undefined) {
195
+ return refusal(`--${walkFlag} applies to a single memory, not the list.\n`);
196
+ }
197
+ if (flags.by !== undefined) {
198
+ const ownerOnly = ['type', 'intent', 'q', 'with', 'sharedWithMe'].find((key) => flags[key] !== undefined);
199
+ if (ownerOnly !== undefined) {
200
+ const flagName = ownerOnly === 'sharedWithMe' ? 'shared-with-me' : ownerOnly;
201
+ return refusal(`--${flagName} does not apply to a wall listing.\n`);
202
+ }
203
+ return getWallMemories(cfg, io, flags.by, flags);
204
+ }
205
+ else {
206
+ return listMemories(cfg, io, flags);
207
+ }
208
+ }
209
+ else {
210
+ return refusal('A website address or id is required for a single memory. To list your own: lattris get memories\n');
211
+ }
212
+ };
213
+ //# sourceMappingURL=get.js.map