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,59 @@
1
+ export class ChainSaveError extends Error {
2
+ epoch;
3
+ status;
4
+ body;
5
+ constructor(epoch, status, body) {
6
+ super(`epoch ${epoch} save failed (${status} ${body.code ?? ''}): ${body.message ?? 'no message'}`);
7
+ this.epoch = epoch;
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ }
12
+ export const driveChain = async (chain, receipts, judgment, save) => {
13
+ const results = [];
14
+ let previousBlockId;
15
+ for (let k = 0; k < chain.epochs.length; k++) {
16
+ const turns = chain.epochs[k];
17
+ const isLive = k === chain.epochs.length - 1;
18
+ const base = { turns, source: chain.source };
19
+ if (chain.conversationId !== null)
20
+ base.externalConversationId = chain.conversationId;
21
+ if (isLive && judgment.title !== undefined)
22
+ base.title = judgment.title;
23
+ if (isLive && judgment.aiSummary !== undefined)
24
+ base.aiSummary = judgment.aiSummary;
25
+ if (judgment.contributor !== undefined)
26
+ base.contributor = judgment.contributor;
27
+ const hinted = receipts.epochs[String(k)];
28
+ let outcome;
29
+ let forked = false;
30
+ if (hinted !== undefined) {
31
+ outcome = await save({ ...base, extends: hinted });
32
+ if (outcome.status === 409 && outcome.body.code === 'memory:session-diverged') {
33
+ outcome = await save({ ...base, asNewBlock: true, forkedFrom: hinted });
34
+ forked = true;
35
+ }
36
+ else if (outcome.status === 400 && outcome.body.code === 'memory:unknown-extends') {
37
+ outcome = await save(previousBlockId !== undefined ? { ...base, continuesFrom: previousBlockId } : base);
38
+ }
39
+ }
40
+ else {
41
+ outcome = await save(previousBlockId !== undefined && k > 0 ? { ...base, continuesFrom: previousBlockId } : base);
42
+ }
43
+ if (outcome.status !== 200 || !outcome.body.block?.id) {
44
+ throw new ChainSaveError(k, outcome.status, outcome.body);
45
+ }
46
+ const blockId = outcome.body.block.id;
47
+ receipts.epochs[String(k)] = blockId;
48
+ results.push({
49
+ epoch: k,
50
+ blockId,
51
+ outcome: outcome.body.outcome ?? 'unknown',
52
+ turnCount: outcome.body.turnCount ?? turns.length,
53
+ forked
54
+ });
55
+ previousBlockId = blockId;
56
+ }
57
+ return results;
58
+ };
59
+ //# sourceMappingURL=save-session-chain.js.map
@@ -0,0 +1,85 @@
1
+ import { sanitizeForTerminal } from "../render/text.js";
2
+ const AUDIO_MIME_BY_EXT = {
3
+ mp3: 'audio/mpeg',
4
+ m4a: 'audio/mp4',
5
+ mp4: 'audio/mp4',
6
+ wav: 'audio/wav',
7
+ ogg: 'audio/ogg',
8
+ oga: 'audio/ogg',
9
+ webm: 'audio/webm',
10
+ flac: 'audio/flac',
11
+ aac: 'audio/aac'
12
+ };
13
+ export const mimeForAudio = (filename, override) => {
14
+ if (override !== undefined)
15
+ return override;
16
+ const ext = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
17
+ return AUDIO_MIME_BY_EXT[ext] ?? null;
18
+ };
19
+ export const convertPlainTranscript = (raw) => {
20
+ const speakers = {};
21
+ const keyByLabel = new Map();
22
+ const turns = [];
23
+ const keyFor = (label) => {
24
+ const existing = keyByLabel.get(label);
25
+ if (existing !== undefined)
26
+ return existing;
27
+ const n = keyByLabel.size;
28
+ const key = String.fromCharCode(97 + (n % 26)) + (n >= 26 ? String(Math.floor(n / 26)) : '');
29
+ keyByLabel.set(label, key);
30
+ speakers[key] = { label };
31
+ return key;
32
+ };
33
+ for (const line of raw.split('\n')) {
34
+ const trimmed = line.trim();
35
+ if (trimmed === '')
36
+ continue;
37
+ const colon = trimmed.indexOf(':');
38
+ if (colon > 0 && colon < 40) {
39
+ const label = trimmed.slice(0, colon).trim();
40
+ const text = trimmed.slice(colon + 1).trim();
41
+ turns.push({ speaker: keyFor(label), text });
42
+ }
43
+ else if (turns.length > 0) {
44
+ turns[turns.length - 1].text += `\n${trimmed}`;
45
+ }
46
+ else {
47
+ turns.push({ speaker: keyFor('speaker'), text: trimmed });
48
+ }
49
+ }
50
+ return { speakers, turns };
51
+ };
52
+ export const TRANSCRIPT_CONVERTERS = {
53
+ json: (raw) => JSON.parse(raw),
54
+ a: (raw) => convertPlainTranscript(raw)
55
+ };
56
+ export const RECORDING_FORMATS = Object.keys(TRANSCRIPT_CONVERTERS);
57
+ export const buildRecordingBody = (input) => {
58
+ const body = {};
59
+ for (const [k, v] of Object.entries(input)) {
60
+ if (v === undefined)
61
+ continue;
62
+ if (k === 'allowBannedWords' && v !== true)
63
+ continue;
64
+ body[k] = v;
65
+ }
66
+ return body;
67
+ };
68
+ export const formatRecording = (r) => {
69
+ const lines = [`Saved recording ${r.id}`];
70
+ if (r.title)
71
+ lines.push(` title ${sanitizeForTerminal(r.title)}`);
72
+ const stands = [];
73
+ if (r.media)
74
+ stands.push(`audio${r.media.status ? ` (${r.media.status})` : ''}`);
75
+ if (r.sourceUrl)
76
+ stands.push('source');
77
+ if (r.transcript)
78
+ stands.push(`transcript${Array.isArray(r.transcript.turns) ? ` (${r.transcript.turns.length} turns)` : ''}`);
79
+ lines.push(` stands on ${stands.join(', ') || '(nothing yet)'}`);
80
+ if (r.durationSeconds)
81
+ lines.push(` duration ${r.durationSeconds}s`);
82
+ lines.push(` capturedAt ${r.capturedAt}`);
83
+ return lines.join('\n');
84
+ };
85
+ //# sourceMappingURL=transcript.js.map
@@ -0,0 +1,126 @@
1
+ export const UPDATE_HELP = [
2
+ 'update — correct a memory you already kept.',
3
+ '',
4
+ 'For fixing words: a typo, a wrong word, a better title. The record stays the',
5
+ 'same act, kept at the same moment, at the same address.',
6
+ '',
7
+ 'A new thought is a NEW capture, not an edit — growing a record into something',
8
+ 'it was not is how a record stops being true.',
9
+ '',
10
+ 'Never delete and re-capture to fix words. That burns the address and the',
11
+ 'original capture time, which are the two things a correction exists to keep.',
12
+ '',
13
+ 'Usage:',
14
+ ' echo "..." | lattris update memory <id> --base-fingerprint <fp> replace the words',
15
+ ' lattris update memory <photo-id> --expected-updated-at <time> --title "..." a non-note',
16
+ ' lattris update memory <id> --base-fingerprint <fp> --title "..." retitle only',
17
+ ' lattris update memory <id> --base-fingerprint <fp> --ai-summary "..."',
18
+ ' lattris update memory <id> --base-fingerprint <fp> --intent <word>',
19
+ ' lattris update memory <id> --base-fingerprint <fp> --clear-intent',
20
+ ' echo "..." | lattris update memory <id> --overwrite --reason "..." replace on purpose',
21
+ '',
22
+ ' --base-fingerprint <fp>',
23
+ ' a NOTE\u2019s guard: the fingerprint the read you built this edit',
24
+ ' on returned — `get memory <id>` prints it.',
25
+ ' --expected-updated-at <time>',
26
+ ' every OTHER type\u2019s guard (a photo, video, session, recording',
27
+ ' keeps no fingerprint yet): the updatedAt that read returned.',
28
+ ' One guard or --overwrite is required; missing, the edit is',
29
+ ' refused here before any words are read.',
30
+ ' --overwrite replace whatever is there now, on purpose. Needs --reason, which',
31
+ ' is recorded on the note’s history. Sends an Idempotency-Key minted',
32
+ ' for this run; a lost response keeps the request under drafts with',
33
+ ' that key, and --idempotency-key <uuid> resends it so a retry can',
34
+ ' never overwrite twice.',
35
+ '',
36
+ ' <id> a full id or a >=4-char prefix, like `get memory` — an',
37
+ ' ambiguous prefix refuses and lists the matches',
38
+ ' --title optional; one line, what this is about',
39
+ ' --ai-summary optional; the AI-written abstract beside the title — a',
40
+ ' finding aid, never the record; correctable forever',
41
+ ' --intent optional; a fresh claim about what the record is — the API',
42
+ ' owns the words (action/decision/truth/learning/noticed/…)',
43
+ ' and an unknown one refuses with the accepted set. A photo',
44
+ ' or video carries no intent and says so.',
45
+ ' --contributor optional; whose expression this is — a contact id, the same',
46
+ ' flag capture takes (`lattris contact list` shows yours)',
47
+ ' --clear-intent remove the declared intent (there is no magic value —',
48
+ ' "none" is not vocabulary and would refuse)',
49
+ ' --allow-banned-words',
50
+ ' keep flagged wording deliberately — without it, certain',
51
+ ' words in new words or a new title refuse, and the refusal',
52
+ ' names every word found. Rephrase first; override only when',
53
+ ' the wording is meant.',
54
+ '',
55
+ ' Content arrives on stdin, like capture — no quoting, up to 5MB. Pipe',
56
+ ' nothing and the words are left exactly as they were.',
57
+ '',
58
+ ' The write carries the fingerprint you read. If another session changed',
59
+ ' the note in between, the door refuses (memory:stale-write), nothing is',
60
+ ' written, your draft is kept under drafts/ in the machine dir (0600) and',
61
+ ' the refusal prints where the note stands now: read it again, re-apply',
62
+ ' your change to those words, and update with that fingerprint. If the',
63
+ ' note already carries exactly what you sent, the answer is "already',
64
+ ' saved" and the exit is 0. Two sessions editing one note never erase each',
65
+ ' other, and every change is kept: `lattris get history <id>`.',
66
+ '',
67
+ ' Every note write names its session (X-Lattris-Session-Id): at a terminal',
68
+ ' one is minted per terminal session; off one, set LATTRIS_SESSION_ID.',
69
+ ' LATTRIS_AGENT adds a readable label.',
70
+ '',
71
+ ' Send nothing at all and there is nothing to do — that is refused here,',
72
+ ' before the network, rather than spending a round trip to be told.',
73
+ '',
74
+ ' A session takes --title, --ai-summary, --intent and --clear-intent (its',
75
+ ' turns are never edited — new turns go through capture); its receipt is',
76
+ ' the index row, never the conversation echoed back.',
77
+ '',
78
+ 'An edge can be corrected too — its REASON only. (from, verb, to) is the',
79
+ 'edge’s identity and never changes here: a different verb is a different',
80
+ 'claim, made by deleting one edge and creating the other.',
81
+ '',
82
+ ' lattris update edge <id> --verb <verb> --to <id> --reason "..."',
83
+ '',
84
+ ' <id> the cause-side block — full id or a >=4-char prefix, like',
85
+ ' `update memory`',
86
+ ' --verb required; the edge’s verb, exactly as the read shows it',
87
+ ' --to required; the effect-side block — the FULL id from the edge',
88
+ ' as read, never a prefix (it names WHICH edge; it does not',
89
+ ' search)',
90
+ ' --reason required; the replacement annotation, one line, written to',
91
+ ' the future walker — when is this edge worth following.',
92
+ ' Replaces in full; nothing records the previous wording.',
93
+ '',
94
+ 'A recording’s transcript and speaker map are corrected too — see',
95
+ '`lattris update recording` for the full form:',
96
+ '',
97
+ ' lattris update recording transcript <id> <file> [--force]',
98
+ ' lattris update recording speakers <id> <key>=me|<contact-id>|clear',
99
+ '',
100
+ 'updated → prints the id, what changed, and the address.'
101
+ ].join('\n');
102
+ export const UPDATE_FLAG_NAMES = {
103
+ '--title': 'title',
104
+ '--ai-summary': 'aiSummary',
105
+ '--intent': 'intent',
106
+ '--base-fingerprint': 'baseFingerprint',
107
+ '--expected-updated-at': 'expectedUpdatedAt',
108
+ '--reason': 'reason',
109
+ '--idempotency-key': 'idempotencyKey',
110
+ '--contributor': 'contributor'
111
+ };
112
+ export const FINGERPRINT_SHAPE = /^[0-9a-f]{64}$/;
113
+ const ISO_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
114
+ export const isIsoInstant = (value) => {
115
+ const m = ISO_INSTANT.exec(value);
116
+ if (!m)
117
+ return false;
118
+ const [, y, mo, d, h, mi, s] = m.map(Number);
119
+ if (mo < 1 || mo > 12 || h > 23 || mi > 59 || s > 60)
120
+ return false;
121
+ const daysInMonth = new Date(Date.UTC(y, mo, 0)).getUTCDate();
122
+ if (d < 1 || d > daysInMonth)
123
+ return false;
124
+ return !Number.isNaN(Date.parse(value));
125
+ };
126
+ //# sourceMappingURL=update-help.js.map
@@ -0,0 +1,220 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { parseArgs } from "../request/flags.js";
3
+ import { retryWords } from "../memory/capture-common.js";
4
+ import { FINGERPRINT_SHAPE, UPDATE_FLAG_NAMES, isIsoInstant } from "../memory/update-help.js";
5
+ import { composeAddressFor, formatBlockLine, formatMemoryDetail } from "../memory/views.js";
6
+ import { emit } from "../render/output.js";
7
+ import { renderRunHint, sanitizeForTerminal } from "../render/text.js";
8
+ import { refusalOf } from "../request/api-fetch.js";
9
+ import { alreadySaved, saveEditDraft, saveRequestDraft } from "../store/drafts.js";
10
+ import { EXIT, isRefusal, refusal } from "../store/exit.js";
11
+ import { isUuidKey, noteWriteHeaders, requireWriteIdentity } from "../store/write-identity.js";
12
+ import { signedIn } from "../request/access-token.js";
13
+ import { ambiguousText } from "../render/rows.js";
14
+ export const updateMemory = async (cfg, io, args) => {
15
+ const clearIntent = args.includes('--clear-intent');
16
+ const allowBanned = args.includes('--allow-banned-words');
17
+ const overwrite = args.includes('--overwrite');
18
+ const { flags, positionals, unknown, notAnId } = parseArgs(UPDATE_FLAG_NAMES, args.filter((a) => a !== '--clear-intent' && a !== '--allow-banned-words' && a !== '--overwrite'), [0]);
19
+ if (unknown) {
20
+ return refusal(`Unknown flag: ${unknown}\n`);
21
+ }
22
+ if (notAnId)
23
+ return refusal(notAnId);
24
+ if (positionals.length !== 1) {
25
+ return refusal(`${UPDATE_USAGE}\n`);
26
+ }
27
+ if (overwrite && flags.baseFingerprint !== undefined) {
28
+ return refusal('--overwrite and --base-fingerprint contradict each other; pass one.\n');
29
+ }
30
+ if (overwrite && (flags.reason === undefined || flags.reason.trim().length === 0)) {
31
+ return refusal('--overwrite needs --reason "...": why the current words are being replaced on purpose. ' +
32
+ 'It is recorded on the note’s history.\n');
33
+ }
34
+ if (!overwrite && flags.reason !== undefined) {
35
+ return refusal('--reason rides --overwrite; pass both or neither.\n');
36
+ }
37
+ if (flags.baseFingerprint !== undefined && flags.expectedUpdatedAt !== undefined) {
38
+ return refusal('--base-fingerprint and --expected-updated-at are two guards; a note takes the first, ' +
39
+ 'every other type the second. Pass one.\n');
40
+ }
41
+ if (overwrite && flags.expectedUpdatedAt !== undefined) {
42
+ return refusal('--overwrite and --expected-updated-at contradict each other; pass one.\n');
43
+ }
44
+ if (!overwrite && flags.baseFingerprint === undefined && flags.expectedUpdatedAt === undefined) {
45
+ return refusal(`A guard is required: --base-fingerprint <fp> for a note (the fingerprint the read you ` +
46
+ `built this edit on returned — ${renderRunHint(['get', 'memory', positionals[0]])} prints it), ` +
47
+ 'or --expected-updated-at <time> for a photo, video, session or recording (the updatedAt ' +
48
+ 'that read returned). Nothing was read and nothing was written. To replace the words on ' +
49
+ 'purpose whatever is there now, pass --overwrite --reason "...".\n');
50
+ }
51
+ if (flags.expectedUpdatedAt !== undefined && !isIsoInstant(flags.expectedUpdatedAt)) {
52
+ return refusal(`--expected-updated-at must be the updatedAt a read printed, an ISO instant with its zone ` +
53
+ `(2026-09-03T05:02:40.118Z): ${flags.expectedUpdatedAt}\n`);
54
+ }
55
+ if (flags.baseFingerprint !== undefined && !FINGERPRINT_SHAPE.test(flags.baseFingerprint)) {
56
+ return refusal(`--base-fingerprint must be the 64 lowercase hex characters a read printed: ${flags.baseFingerprint}\n`);
57
+ }
58
+ if (flags.idempotencyKey !== undefined && !overwrite) {
59
+ return refusal('--idempotency-key rides --overwrite (a guarded edit needs no key).\n');
60
+ }
61
+ if (flags.idempotencyKey !== undefined && !isUuidKey(flags.idempotencyKey)) {
62
+ return refusal(`--idempotency-key must be the UUID a draft printed: ${flags.idempotencyKey}\n`);
63
+ }
64
+ const identity = requireWriteIdentity(io.store, io.stdin.isTTY);
65
+ if (isRefusal(identity))
66
+ return identity;
67
+ const piped = !io.stdin.isTTY;
68
+ const typed = piped ? await io.stdin.read() : '';
69
+ const content = typed.length > 0 ? typed : undefined;
70
+ const sendTitle = flags.title !== undefined;
71
+ const sendAiSummary = flags.aiSummary !== undefined;
72
+ const sendContent = content !== undefined;
73
+ const sendIntent = flags.intent !== undefined;
74
+ const sendContributor = flags.contributor !== undefined;
75
+ if (sendContributor && !isUuidKey(flags.contributor)) {
76
+ return refusal(`--contributor must be a valid UUID (contact id): ${flags.contributor}\n`);
77
+ }
78
+ if (sendIntent && clearIntent) {
79
+ return refusal('--intent and --clear-intent contradict each other; pass one.\n');
80
+ }
81
+ if (!sendTitle &&
82
+ !sendAiSummary &&
83
+ !sendContent &&
84
+ !sendIntent &&
85
+ !clearIntent &&
86
+ !sendContributor) {
87
+ return refusal('Nothing to update: pipe new words in, or pass --title, --ai-summary, --intent, --clear-intent or --contributor.\n');
88
+ }
89
+ const accessToken = await signedIn(io, cfg);
90
+ if (typeof accessToken !== 'string')
91
+ return accessToken;
92
+ const body = {};
93
+ if (sendTitle)
94
+ body.title = flags.title;
95
+ if (sendAiSummary)
96
+ body.aiSummary = flags.aiSummary;
97
+ if (sendContent)
98
+ body.content = content;
99
+ if (sendIntent)
100
+ body.intent = flags.intent;
101
+ if (clearIntent)
102
+ body.intent = null;
103
+ if (sendContributor)
104
+ body.contributor = flags.contributor;
105
+ if (allowBanned)
106
+ body.allowBannedWords = true;
107
+ if (overwrite)
108
+ body.overwrite = { reason: flags.reason.trim() };
109
+ else if (flags.baseFingerprint !== undefined)
110
+ body.baseFingerprint = flags.baseFingerprint;
111
+ else
112
+ body.expectedUpdatedAt = flags.expectedUpdatedAt;
113
+ const idempotencyKey = overwrite ? (flags.idempotencyKey ?? randomUUID()) : undefined;
114
+ const blockUrl = `${cfg.apiUrl}/memories/${encodeURIComponent(positionals[0])}`;
115
+ const editDraft = () => ({
116
+ door: `PATCH /memories/${positionals[0]}`,
117
+ sessionId: identity.sessionId,
118
+ agentLabel: identity.agentLabel,
119
+ baseFingerprint: body.baseFingerprint,
120
+ expectedUpdatedAt: body.expectedUpdatedAt,
121
+ overwrite: body.overwrite,
122
+ allowBannedWords: body.allowBannedWords,
123
+ fields: {
124
+ ...(sendTitle ? { title: body.title } : {}),
125
+ ...(sendAiSummary ? { aiSummary: body.aiSummary } : {}),
126
+ ...(sendIntent || clearIntent ? { intent: body.intent } : {}),
127
+ ...(sendContent ? { content: body.content } : {}),
128
+ ...(sendContributor ? { contributor: body.contributor } : {})
129
+ }
130
+ });
131
+ const keepWords = () => saveEditDraft(io.store, positionals[0], editDraft());
132
+ const requestDraft = overwrite
133
+ ? {
134
+ door: `PATCH /memories/${positionals[0]}`,
135
+ idempotencyKey: idempotencyKey,
136
+ sessionId: identity.sessionId,
137
+ agentLabel: identity.agentLabel,
138
+ body: body
139
+ }
140
+ : undefined;
141
+ const keepRequest = () => saveRequestDraft(io.store, 'overwrite', requestDraft);
142
+ const keptWhere = (kept) => kept.wordsPath
143
+ ? `Your draft is kept at ${kept.draftPath} (every field sent) and its words at ${kept.wordsPath}` +
144
+ ' — pipe that file back to restore them exactly.'
145
+ : `Your draft is kept at ${kept.draftPath} (every field sent).`;
146
+ let result;
147
+ try {
148
+ result = await io.api(accessToken, blockUrl, {
149
+ method: 'PATCH',
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ ...noteWriteHeaders(identity, idempotencyKey)
153
+ },
154
+ body: JSON.stringify(body)
155
+ });
156
+ }
157
+ catch (e) {
158
+ const lost = `The response was lost (${sanitizeForTerminal(e instanceof Error ? e.message : String(e))}). `;
159
+ if (overwrite) {
160
+ const path = keepRequest();
161
+ return refusal(`${lost}The request is kept at ${path}. Resend it exactly, same session and same key, with ` +
162
+ `${retryWords(path)} — if it landed, the door answers the same receipt and overwrites nothing again.\n`, EXIT.GENERIC);
163
+ }
164
+ return refusal(`${lost}${keptWhere(keepWords())} Read the block again and retry with its fingerprint.\n`, EXIT.GENERIC);
165
+ }
166
+ if (!result.ok) {
167
+ const matches = ambiguousText(result.body);
168
+ if (result.body.code === 'memory:stale-write') {
169
+ const detail = (result.body.detail ?? {});
170
+ const current = (detail.current ?? {});
171
+ if (alreadySaved(body, current)) {
172
+ return {
173
+ ...refusalOf(result, matches +
174
+ `Already saved: the block already carries these words (revision ${String(detail.currentRevisionNo ?? '?')}, ` +
175
+ `fingerprint ${String(detail.currentFingerprint ?? '?')}). Nothing to do.\n`),
176
+ exit: EXIT.OK
177
+ };
178
+ }
179
+ const kept = keepWords();
180
+ if (io.stdout.isTTY) {
181
+ return refusalOf(result, matches +
182
+ `The block now stands at revision ${String(detail.currentRevisionNo ?? '?')}, fingerprint ` +
183
+ `${String(detail.currentFingerprint ?? '?')}. Nothing was written. ${keptWhere(kept)} ` +
184
+ `Read it with ${renderRunHint(['get', 'memory', positionals[0]])}, re-apply your change to ` +
185
+ 'those words, and update again with that fingerprint — or replace them on purpose with ' +
186
+ '--overwrite --reason "...".\n' +
187
+ (typeof current.content === 'string'
188
+ ? `\n${sanitizeForTerminal(current.content, Infinity)}\n`
189
+ : ''));
190
+ }
191
+ io.stdout.write(`${JSON.stringify({ ...result.body, draftPath: kept.draftPath, wordsPath: kept.wordsPath ?? null, draft: editDraft().fields })}\n`);
192
+ return refusalOf(result, matches);
193
+ }
194
+ if (overwrite && result.exit !== EXIT.USAGE && result.exit !== EXIT.NOAUTH) {
195
+ const path = keepRequest();
196
+ return refusalOf(result, matches +
197
+ `The request is kept at ${path}. Once the cause above is settled, resend it exactly with ${retryWords(path)}\n`);
198
+ }
199
+ return refusalOf(result, matches);
200
+ }
201
+ let address;
202
+ if (io.stdout.isTTY && !('visibility' in result.data)) {
203
+ address = await composeAddressFor(cfg, io, accessToken, result.data);
204
+ }
205
+ const changed = [
206
+ sendTitle ? 'title' : undefined,
207
+ sendAiSummary ? 'ai summary' : undefined,
208
+ sendContent ? 'words' : undefined,
209
+ sendIntent ? 'intent' : undefined,
210
+ clearIntent ? 'intent (cleared)' : undefined
211
+ ]
212
+ .filter((c) => c !== undefined)
213
+ .join(' and ');
214
+ emit(io, result.data, result.rawText, (m) => 'visibility' in m
215
+ ? `Updated ${changed}.\n\n${formatBlockLine(m)}`
216
+ : `Updated ${changed}.\n\n${formatMemoryDetail(m, address)}`);
217
+ };
218
+ const UPDATE_USAGE = 'Usage: lattris update memory <id> (--base-fingerprint <fp> | --expected-updated-at <time> | --overwrite --reason "...") ' +
219
+ '[--title "..."] [--ai-summary "..."] [--intent <word>] [--clear-intent] [--allow-banned-words] [--idempotency-key <uuid>]';
220
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1,138 @@
1
+ import { formatAudiences } from "../render/rows.js";
2
+ import { sanitizeForTerminal } from "../render/text.js";
3
+ import { stripTrailingSlash } from "../store/environment.js";
4
+ export const formatDeletedLine = (b) => `${b.id} ${b.deletedAt ?? '-'} rev ${b.revisionNo ?? '-'} ${b.fingerprint ?? '-'} ${sanitizeForTerminal(b.title ?? '(untitled)')}`;
5
+ export const formatDeletedPage = (page) => page.items.map((i) => formatDeletedLine(i)).join('\n');
6
+ export const formatBlockLine = (b) => b.ownerHandle === undefined
7
+ ? `${b.id} ${b.handle} ${b.capturedAt} ${b.intent ?? '-'} ${b.visibility.public ? 'public' : '-'} ${sanitizeForTerminal(b.title ?? '(untitled)')}`
8
+ : `${b.id} ${b.handle} ${b.capturedAt} by ${sanitizeForTerminal(b.ownerDisplayName ?? b.ownerHandle)} (@${b.ownerHandle}) ${b.consented ? 'on graph' : 'off graph'} ${sanitizeForTerminal(b.title ?? '(untitled)')}`;
9
+ const SLUG_MAX = 60;
10
+ const slugify = (title) => (title ?? '')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '')
14
+ .slice(0, SLUG_MAX)
15
+ .replace(/-+$/g, '');
16
+ export const composeAddress = (webUrl, personHandle, block) => {
17
+ const slug = slugify(block.title);
18
+ const ref = slug ? `${slug}-${block.handle}` : block.handle;
19
+ return `${webUrl}/by/${personHandle}/memories/${ref}`;
20
+ };
21
+ export const parseWebsiteMemoryAddress = (webUrl, value) => {
22
+ try {
23
+ const base = new URL(webUrl);
24
+ const address = new URL(value);
25
+ if (address.origin !== base.origin || address.username || address.password)
26
+ return undefined;
27
+ const basePath = stripTrailingSlash(base.pathname);
28
+ const route = address.pathname.slice(basePath.length);
29
+ if (!address.pathname.startsWith(`${basePath}/by/`))
30
+ return undefined;
31
+ const parts = route.split('/');
32
+ if (parts.length !== 5 || parts[1] !== 'by' || parts[3] !== 'memories')
33
+ return undefined;
34
+ const ownerHandle = decodeURIComponent(parts[2]);
35
+ const ref = decodeURIComponent(parts[4]);
36
+ if (!ownerHandle || !ref || ownerHandle.includes('/') || ref.includes('/'))
37
+ return undefined;
38
+ return { ownerHandle, ref };
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ };
44
+ export const ownHandleFor = async (cfg, io, accessToken) => {
45
+ try {
46
+ const me = await io.api(accessToken, `${cfg.apiUrl}/person/me`);
47
+ if (!me.ok)
48
+ return undefined;
49
+ const { handle } = me.data;
50
+ return typeof handle === 'string' && handle.length > 0 ? handle : undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ };
56
+ export const composeAddressFor = async (cfg, io, accessToken, block) => {
57
+ const handle = await ownHandleFor(cfg, io, accessToken);
58
+ return handle ? composeAddress(cfg.webUrl, handle, block) : undefined;
59
+ };
60
+ export const formatSessionTurn = (t) => {
61
+ const header = `[${t.position}] ${sanitizeForTerminal(t.speaker)}${t.saidAt ? ` ${t.saidAt}` : ''}`;
62
+ const body = sanitizeForTerminal(t.text, Infinity);
63
+ return t.kind === 'compaction-summary'
64
+ ? `── compaction boundary ──\n${header}\n${body}`
65
+ : `${header}\n${body}`;
66
+ };
67
+ export const formatMemoryDetail = (m, address) => {
68
+ const lines = [`id ${m.id}`, `handle ${m.handle}`, `type ${m.type}`];
69
+ if (m.intent)
70
+ lines.push(`intent ${m.intent}`);
71
+ lines.push(`capturedAt ${m.capturedAt}`);
72
+ if (m.title)
73
+ lines.push(`title ${sanitizeForTerminal(m.title)}`);
74
+ if (m.source)
75
+ lines.push(`source ${sanitizeForTerminal(m.source)}`);
76
+ lines.push(`audiences ${formatAudiences(m.audiences)}`);
77
+ if (m.fingerprint)
78
+ lines.push(`fingerprint ${m.fingerprint}`);
79
+ if (m.revisionNo)
80
+ lines.push(`revisionNo ${m.revisionNo}`);
81
+ if (m.structureHash)
82
+ lines.push(`structure ${m.structureHash}`);
83
+ if (m.deletedAt)
84
+ lines.push(`deletedAt ${m.deletedAt}`);
85
+ if (address)
86
+ lines.push(`address ${address}`);
87
+ if (Array.isArray(m.turns)) {
88
+ lines.push('', m.turns.map((t) => formatSessionTurn(t)).join('\n\n'));
89
+ }
90
+ else {
91
+ lines.push('', sanitizeForTerminal(m.content ?? '', Infinity));
92
+ }
93
+ return lines.join('\n');
94
+ };
95
+ export const formatMemoryPage = (page) => page.items.map((i) => formatBlockLine(i)).join('\n');
96
+ export const formatFindingAid = (a) => {
97
+ const lines = [`id ${a.id}`, `handle ${a.handle}`, `type ${a.type}`];
98
+ if (a.title)
99
+ lines.push(`title ${sanitizeForTerminal(a.title)}`);
100
+ if (a.aiSummary)
101
+ lines.push(`aiSummary ${sanitizeForTerminal(a.aiSummary, Infinity)}`);
102
+ if (a.fingerprint)
103
+ lines.push(`fingerprint ${a.fingerprint}`);
104
+ if (a.revisionNo)
105
+ lines.push(`revisionNo ${a.revisionNo}`);
106
+ if (a.structureHash)
107
+ lines.push(`structure ${a.structureHash}`);
108
+ return lines.join('\n');
109
+ };
110
+ export const formatWalkLink = (l) => {
111
+ const arrow = l.direction === 'out' ? '\u2192' : '\u2190';
112
+ const title = l.node.title ? ` ${sanitizeForTerminal(l.node.title)}` : '';
113
+ const reason = l.reason ? ` \u2014 ${sanitizeForTerminal(l.reason, Infinity)}` : '';
114
+ const dir = l.direction === 'out' ? 'out' : 'in ';
115
+ return `${dir} ${sanitizeForTerminal(l.verb)} ${arrow} ${l.node.handle} (${l.node.type})${title}${reason}`;
116
+ };
117
+ export const formatNodeWalk = (w, address) => {
118
+ const parts = [
119
+ 'capturedAt' in w.node ? formatMemoryDetail(w.node, address) : formatFindingAid(w.node)
120
+ ];
121
+ if (w.links !== undefined) {
122
+ parts.push('', w.links.length === 0
123
+ ? 'links (none)'
124
+ : `links\n${w.links.map((l) => ` ${formatWalkLink(l)}`).join('\n')}`);
125
+ }
126
+ if (w.walk !== undefined) {
127
+ for (const n of w.walk) {
128
+ const title = n.node.title ? ` ${sanitizeForTerminal(n.node.title)}` : '';
129
+ parts.push('', `\u2500\u2500 ${n.node.handle} (${n.node.type})${title}`);
130
+ if (n.node.aiSummary)
131
+ parts.push(` ${sanitizeForTerminal(n.node.aiSummary, Infinity)}`);
132
+ for (const l of n.links)
133
+ parts.push(` ${formatWalkLink(l)}`);
134
+ }
135
+ }
136
+ return parts.join('\n');
137
+ };
138
+ //# sourceMappingURL=views.js.map
@@ -0,0 +1,29 @@
1
+ export const CONSENT_HELP = [
2
+ 'consent — choose whether a memory shared with you appears on your graph.',
3
+ '',
4
+ 'Usage:',
5
+ ' lattris consent <id-or-prefix>',
6
+ ' lattris consent <id-or-prefix> --withdraw',
7
+ '',
8
+ 'The choice is yours alone. Withdrawing a share hides the block regardless;',
9
+ 'if its owner shares it again later, your previous choice still stands.'
10
+ ].join('\n');
11
+ export const consent = {
12
+ verb: 'consent',
13
+ form: {
14
+ usage: 'Usage: lattris consent <id-or-prefix> [--withdraw]',
15
+ switches: ['--withdraw'],
16
+ positionals: 1,
17
+ idPositionals: [0],
18
+ request: {
19
+ method: 'PUT',
20
+ path: '/memories/{p0}/consent',
21
+ body: { consented: { switch: '--withdraw', value: false, otherwise: true } }
22
+ },
23
+ ambiguousRefusal: true,
24
+ render: (state, typed) => state.consented
25
+ ? `Memory ${typed.positionals[0]} may appear on your graph.`
26
+ : `Memory ${typed.positionals[0]} will stay off your graph.`
27
+ }
28
+ };
29
+ //# sourceMappingURL=consent.js.map