lattris 0.1.4 → 0.2.1
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.
- package/README.md +2 -2
- package/dist/build.json +1 -1
- package/dist/datasets/capture.js +118 -0
- package/dist/datasets/page.js +64 -0
- package/dist/datasets/pieces.js +78 -0
- package/dist/dispatch-form.js +202 -0
- package/dist/dispatch-words.js +133 -0
- package/dist/dispatch.js +67 -0
- package/dist/entry/session.js +219 -0
- package/dist/entry/set.js +130 -0
- package/dist/entry/setup-boot.js +151 -0
- package/dist/entry/setup.js +145 -0
- package/dist/entry/use.js +127 -0
- package/dist/help.js +65 -0
- package/dist/main.js +143 -6887
- package/dist/memory/access.js +88 -0
- package/dist/memory/capture-common.js +171 -0
- package/dist/memory/capture-help.js +195 -0
- package/dist/memory/capture-note.js +200 -0
- package/dist/memory/capture-session.js +168 -0
- package/dist/memory/delete.js +88 -0
- package/dist/memory/erase.js +67 -0
- package/dist/{extract-claude-jsonl.js → memory/extract-claude-jsonl.js} +5 -94
- package/dist/memory/get-help.js +72 -0
- package/dist/memory/get.js +246 -0
- package/dist/memory/history.js +134 -0
- package/dist/memory/recording-update.js +128 -0
- package/dist/memory/recording.js +258 -0
- package/dist/memory/resend.js +82 -0
- package/dist/memory/restore.js +73 -0
- package/dist/memory/save-session-chain.js +59 -0
- package/dist/memory/transcript.js +88 -0
- package/dist/memory/update-help.js +126 -0
- package/dist/memory/update.js +220 -0
- package/dist/memory/views.js +146 -0
- package/dist/nouns/consent.js +29 -0
- package/dist/nouns/contact.js +203 -0
- package/dist/nouns/datasets.js +109 -0
- package/dist/nouns/edges.js +106 -0
- package/dist/nouns/form.js +2 -0
- package/dist/nouns/index.js +48 -0
- package/dist/nouns/invite.js +98 -0
- package/dist/nouns/questions.js +116 -0
- package/dist/nouns/share.js +266 -0
- package/dist/nouns/words.js +81 -0
- package/dist/registry/datasets.js +15 -0
- package/dist/registry/entry.js +51 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/memory.js +116 -0
- package/dist/render/datasets.js +77 -0
- package/dist/render/door.js +39 -0
- package/dist/render/output.js +19 -0
- package/dist/render/prompt.js +4 -0
- package/dist/render/rows.js +82 -0
- package/dist/render/text.js +21 -0
- package/dist/request/access-token.js +67 -0
- package/dist/request/api-fetch.js +76 -0
- package/dist/request/callback.js +86 -0
- package/dist/request/door.js +74 -0
- package/dist/request/flags.js +38 -0
- package/dist/request/io.js +2 -0
- package/dist/request/paste-and-device.js +99 -0
- package/dist/request/stdin.js +22 -0
- package/dist/request/token.js +66 -0
- package/dist/request/upload.js +6 -0
- package/dist/request/version-check.js +79 -0
- package/dist/store/chains.js +25 -0
- package/dist/store/drafts.js +48 -0
- package/dist/store/environment.js +57 -0
- package/dist/store/exit.js +13 -0
- package/dist/store/io.js +2 -0
- package/dist/store/json-file.js +9 -0
- package/dist/store/session-home.js +189 -0
- package/dist/store/session-lock.js +169 -0
- package/dist/store/session.js +70 -0
- package/dist/store/write-identity.js +153 -0
- package/dist/version.js +16 -0
- package/package.json +3 -4
- package/dist/save-session-chain.js +0 -136
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseArgs } from "../request/flags.js";
|
|
3
|
+
import { HELP_TOKENS } from "../request/flags.js";
|
|
4
|
+
import { RECORDING_FORMATS, TRANSCRIPT_CONVERTERS, buildRecordingBody, formatRecording, mimeForAudio } from "../memory/transcript.js";
|
|
5
|
+
import { allowWords, checkShareWords, isRefusalValue, needsYesWords, resolveBornShares, takeShareFlags } from "../memory/capture-common.js";
|
|
6
|
+
import { emit } from "../render/output.js";
|
|
7
|
+
import { confirmAction } from "../render/prompt.js";
|
|
8
|
+
import { sanitizeForTerminal } from "../render/text.js";
|
|
9
|
+
import { classifyApiError, parseErrorBody } from "../request/door.js";
|
|
10
|
+
import { putToSignedUploadUrl } from "../request/upload.js";
|
|
11
|
+
import { EXIT, refusal } from "../store/exit.js";
|
|
12
|
+
import { refusalOf } from "../request/api-fetch.js";
|
|
13
|
+
import { signedIn } from "../request/access-token.js";
|
|
14
|
+
export const CAPTURE_RECORDING_HELP = `capture recording — keep an audio recording, its source, and its transcript.
|
|
15
|
+
|
|
16
|
+
A recording stands on any of THREE substances, at least one:
|
|
17
|
+
• held audio the file itself (--audio) — uploaded, then confirmed
|
|
18
|
+
• a source where it lives elsewhere (--source-url)
|
|
19
|
+
• a transcript the words (a file, in a format the CLI converts)
|
|
20
|
+
|
|
21
|
+
The API meets ONE transcript shape; vendor formats convert HERE. Formats:
|
|
22
|
+
json the file is already {speakers, turns, …} — passed through
|
|
23
|
+
a a plain transcript, one turn per line as \`Label: text\`; a line with
|
|
24
|
+
no colon continues the turn above it
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
lattris capture recording <transcript-file> --format a --title "…"
|
|
28
|
+
lattris capture recording <transcript.json> --format json
|
|
29
|
+
lattris capture recording --audio talk.m4a --title "…"
|
|
30
|
+
lattris capture recording --source-url https://… --source-name "…"
|
|
31
|
+
|
|
32
|
+
<transcript-file> optional; the transcript to convert (--format picks how)
|
|
33
|
+
--format <name> json | a (default json when the file ends .json)
|
|
34
|
+
--audio <path> the audio file to hold — its size and sha256 are computed
|
|
35
|
+
locally, the type comes from the extension (or --mime-type)
|
|
36
|
+
--mime-type <type> override the audio content type (an unknown extension)
|
|
37
|
+
--source-url <url> where the recording lives, if referenced not held
|
|
38
|
+
--source-name <s> the source's name --title <s> one line, what it is
|
|
39
|
+
--title-at-source its title at the source --creator <s> who made it (text)
|
|
40
|
+
--published-at <t> when the source published it (ISO 8601)
|
|
41
|
+
--captured-at <t> when it was recorded (ISO 8601; the future is refused)
|
|
42
|
+
--duration <n> length in seconds
|
|
43
|
+
--span <uuid> group it into one of your spans
|
|
44
|
+
--share-with <who> who may see it from birth: public, all-contacts, or a contact
|
|
45
|
+
(repeatable); asks at a terminal, needs --yes on a pipe
|
|
46
|
+
--deny <who> who may not, whatever else allows it: all-contacts or a
|
|
47
|
+
contact (repeatable); asks nothing
|
|
48
|
+
--contributor <id> whose expression it is — a contact id
|
|
49
|
+
--allow-banned-words keep a flagged word in the title deliberately
|
|
50
|
+
|
|
51
|
+
saved → prints the id, what it stands on, and the address. Held audio uploads
|
|
52
|
+
the bytes and confirms before the receipt.`;
|
|
53
|
+
const RECORDING_FLAG_NAMES = {
|
|
54
|
+
'--format': 'format',
|
|
55
|
+
'--audio': 'audio',
|
|
56
|
+
'--mime-type': 'mimeType',
|
|
57
|
+
'--source-url': 'sourceUrl',
|
|
58
|
+
'--source-name': 'sourceName',
|
|
59
|
+
'--title': 'title',
|
|
60
|
+
'--title-at-source': 'titleAtSource',
|
|
61
|
+
'--creator': 'creator',
|
|
62
|
+
'--published-at': 'publishedAt',
|
|
63
|
+
'--captured-at': 'capturedAt',
|
|
64
|
+
'--duration': 'duration',
|
|
65
|
+
'--span': 'span',
|
|
66
|
+
'--contributor': 'contributor'
|
|
67
|
+
};
|
|
68
|
+
export const captureRecording = async (cfg, io, args) => {
|
|
69
|
+
if (args.length === 0 || args.some((t) => HELP_TOKENS.has(t))) {
|
|
70
|
+
io.stdout.write(`${CAPTURE_RECORDING_HELP}\n`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const allowBanned = args.includes('--allow-banned-words');
|
|
74
|
+
const yes = args.includes('--yes');
|
|
75
|
+
const taken = takeShareFlags(args.filter((a) => a !== '--allow-banned-words' && a !== '--yes'));
|
|
76
|
+
const { flags, positionals, unknown } = parseArgs(RECORDING_FLAG_NAMES, taken.rest);
|
|
77
|
+
if (unknown ?? taken.unknown) {
|
|
78
|
+
return refusal(`Unknown flag: ${unknown ?? taken.unknown}\n`);
|
|
79
|
+
}
|
|
80
|
+
if (positionals.length > 1) {
|
|
81
|
+
return refusal(`Unexpected argument: ${positionals[1]}\n`);
|
|
82
|
+
}
|
|
83
|
+
const transcriptFile = positionals[0];
|
|
84
|
+
const badShareWords = checkShareWords(taken.shareWith, taken.deny);
|
|
85
|
+
if (badShareWords)
|
|
86
|
+
return badShareWords;
|
|
87
|
+
const allows = allowWords(taken.shareWith);
|
|
88
|
+
if (allows.length > 0 && !io.stdin.isTTY && !yes) {
|
|
89
|
+
return refusal(needsYesWords('recording', allows));
|
|
90
|
+
}
|
|
91
|
+
if (transcriptFile === undefined && flags.audio === undefined && flags.sourceUrl === undefined) {
|
|
92
|
+
return refusal('A recording needs at least one of: a transcript file, --audio, or --source-url.\n');
|
|
93
|
+
}
|
|
94
|
+
let transcript;
|
|
95
|
+
if (transcriptFile !== undefined) {
|
|
96
|
+
const format = flags.format ?? (transcriptFile.toLowerCase().endsWith('.json') ? 'json' : 'a');
|
|
97
|
+
const convert = TRANSCRIPT_CONVERTERS[format];
|
|
98
|
+
if (convert === undefined) {
|
|
99
|
+
return refusal(`Unknown --format: ${format}. Accepted: ${RECORDING_FORMATS.join(', ')}.\n`);
|
|
100
|
+
}
|
|
101
|
+
let raw;
|
|
102
|
+
try {
|
|
103
|
+
raw = io.store.files.readFileSync(transcriptFile, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return refusal(`Cannot read transcript file: ${transcriptFile}\n`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
transcript = convert(raw);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return refusal(`Could not parse ${transcriptFile} as --format ${format}: ${e instanceof Error ? e.message : 'invalid'}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let upload;
|
|
116
|
+
let audioBytes;
|
|
117
|
+
if (flags.audio !== undefined) {
|
|
118
|
+
const mimeType = mimeForAudio(flags.audio, flags.mimeType);
|
|
119
|
+
if (mimeType === null) {
|
|
120
|
+
return refusal(`Can't tell the audio type of ${flags.audio} from its extension — pass --mime-type <type>.\n`);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
audioBytes = io.store.files.readFileSync(flags.audio);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return refusal(`Cannot read audio file: ${flags.audio}\n`);
|
|
127
|
+
}
|
|
128
|
+
upload = {
|
|
129
|
+
filename: flags.audio.slice(flags.audio.lastIndexOf('/') + 1),
|
|
130
|
+
mimeType,
|
|
131
|
+
size: audioBytes.length,
|
|
132
|
+
sha256: createHash('sha256').update(audioBytes).digest('hex')
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (flags.duration !== undefined && !/^\d+$/.test(flags.duration)) {
|
|
136
|
+
return refusal(`--duration must be a whole number of seconds: ${flags.duration}\n`);
|
|
137
|
+
}
|
|
138
|
+
if (allows.length > 0 && io.stdin.isTTY) {
|
|
139
|
+
const answer = await confirmAction(io, `Share "${sanitizeForTerminal(flags.title ?? '(untitled)')}" with ${allows.join(', ')}? [y/N] `);
|
|
140
|
+
if (answer === 'interrupted')
|
|
141
|
+
return refusal('Cancelled.\n', EXIT.GENERIC);
|
|
142
|
+
if (answer === 'no') {
|
|
143
|
+
io.stderr.write('Cancelled.\n');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const accessToken = await signedIn(io, cfg);
|
|
148
|
+
if (typeof accessToken !== 'string')
|
|
149
|
+
return accessToken;
|
|
150
|
+
let shares;
|
|
151
|
+
if (taken.shareWith.length > 0 || taken.deny.length > 0) {
|
|
152
|
+
const resolved = await resolveBornShares(cfg, io, accessToken, taken.shareWith, taken.deny);
|
|
153
|
+
if (isRefusalValue(resolved))
|
|
154
|
+
return resolved;
|
|
155
|
+
shares = resolved;
|
|
156
|
+
}
|
|
157
|
+
const body = buildRecordingBody({
|
|
158
|
+
transcript,
|
|
159
|
+
upload,
|
|
160
|
+
sourceUrl: flags.sourceUrl,
|
|
161
|
+
title: flags.title,
|
|
162
|
+
capturedAt: flags.capturedAt,
|
|
163
|
+
spanId: flags.span,
|
|
164
|
+
audience: shares?.audience,
|
|
165
|
+
access: shares !== undefined && shares.access.length > 0 ? shares.access : undefined,
|
|
166
|
+
contributor: flags.contributor,
|
|
167
|
+
sourceName: flags.sourceName,
|
|
168
|
+
titleAtSource: flags.titleAtSource,
|
|
169
|
+
creator: flags.creator,
|
|
170
|
+
publishedAt: flags.publishedAt,
|
|
171
|
+
durationSeconds: flags.duration === undefined ? undefined : Number(flags.duration),
|
|
172
|
+
allowBannedWords: allowBanned
|
|
173
|
+
});
|
|
174
|
+
const created = await io.api(accessToken, `${cfg.apiUrl}/memories/recordings`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify(body)
|
|
178
|
+
});
|
|
179
|
+
if (!created.ok)
|
|
180
|
+
return refusalOf(created);
|
|
181
|
+
if ('signedUploadUrl' in created.data) {
|
|
182
|
+
const pending = created.data;
|
|
183
|
+
const putOnce = async () => {
|
|
184
|
+
try {
|
|
185
|
+
const res = await putToSignedUploadUrl(io.network, pending.signedUploadUrl, upload.mimeType, new Uint8Array(audioBytes));
|
|
186
|
+
if (res.ok)
|
|
187
|
+
return { ok: true };
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
why: res.status === 403
|
|
191
|
+
? 'the signed upload URL was rejected (it may have expired)'
|
|
192
|
+
: `HTTP ${res.status}`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
return { ok: false, why: e instanceof Error ? e.message : 'a network error' };
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
let put = await putOnce();
|
|
200
|
+
if (!put.ok)
|
|
201
|
+
put = await putOnce();
|
|
202
|
+
if (!put.ok) {
|
|
203
|
+
return refusal(`The recording was reserved (${pending.block.id}) but the audio upload failed — ${put.why}.\n` +
|
|
204
|
+
`That reservation has no confirmed audio and is cleared server-side; running the capture ` +
|
|
205
|
+
`again creates a NEW recording — it does not adopt this one.\n`, EXIT.GENERIC);
|
|
206
|
+
}
|
|
207
|
+
const confirmUrl = `${cfg.apiUrl}/memories/${encodeURIComponent(pending.block.id)}/confirm`;
|
|
208
|
+
const confirmOnce = async () => {
|
|
209
|
+
try {
|
|
210
|
+
const res = await io.api(accessToken, confirmUrl, { method: 'POST' });
|
|
211
|
+
const rawText = res.rawText;
|
|
212
|
+
if (res.ok || res.answer.kind === 'page')
|
|
213
|
+
return { kind: 'ok', data: JSON.parse(rawText), rawText };
|
|
214
|
+
return { kind: 'http', status: res.status, rawText };
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
return { kind: 'transport', message: e instanceof Error ? e.message : 'a network error' };
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
let confirmed = await confirmOnce();
|
|
221
|
+
const firstLost = confirmed.kind === 'transport';
|
|
222
|
+
if (firstLost)
|
|
223
|
+
confirmed = await confirmOnce();
|
|
224
|
+
if (confirmed.kind === 'ok') {
|
|
225
|
+
emit(io, confirmed.data, confirmed.rawText, formatRecording);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (firstLost) {
|
|
229
|
+
try {
|
|
230
|
+
const probe = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(pending.block.id)}`);
|
|
231
|
+
if (probe.ok) {
|
|
232
|
+
const rawText = probe.rawText;
|
|
233
|
+
const rec = JSON.parse(rawText);
|
|
234
|
+
if (rec.media != null) {
|
|
235
|
+
emit(io, rec, rawText, formatRecording);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
if (confirmed.kind === 'http') {
|
|
243
|
+
const { message, next, exit } = classifyApiError(confirmed.status, parseErrorBody(confirmed.rawText));
|
|
244
|
+
return {
|
|
245
|
+
message: `The recording was reserved (${pending.block.id}); the audio uploaded but the confirm did ` +
|
|
246
|
+
`not land. That reservation is incomplete and cleared server-side; running the capture ` +
|
|
247
|
+
`again creates a NEW recording — it does not finish this one.\n`,
|
|
248
|
+
exit,
|
|
249
|
+
answer: { kind: 'door', message, next }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return refusal(`The audio uploaded but confirming the recording (${pending.block.id}) failed — ${confirmed.message}.\n` +
|
|
253
|
+
`That reservation is incomplete and cleared server-side; running the capture again creates ` +
|
|
254
|
+
`a NEW recording — it does not finish this one.\n`, EXIT.GENERIC);
|
|
255
|
+
}
|
|
256
|
+
emit(io, created.data, created.rawText, formatRecording);
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=recording.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HELP_TOKENS } from "../request/flags.js";
|
|
2
|
+
import { formatSaved } from "../memory/capture-common.js";
|
|
3
|
+
import { formatBlockLine, formatMemoryDetail } from "../memory/views.js";
|
|
4
|
+
import { emit } from "../render/output.js";
|
|
5
|
+
import { sanitizeForTerminal } from "../render/text.js";
|
|
6
|
+
import { EXIT, refusal } from "../store/exit.js";
|
|
7
|
+
import { AGENT_LABEL_SHAPE, SESSION_ID_SHAPE, isUuidKey, noteWriteHeaders } from "../store/write-identity.js";
|
|
8
|
+
import { refusalOf } from "../request/api-fetch.js";
|
|
9
|
+
import { signedIn } from "../request/access-token.js";
|
|
10
|
+
import { ambiguousText } from "../render/rows.js";
|
|
11
|
+
export const RESEND_HELP = [
|
|
12
|
+
'resend — send a kept request again, exactly: the same door, the same session, the same',
|
|
13
|
+
'key, the same body. What a lost response on `capture memory` or `update memory --overwrite`',
|
|
14
|
+
'leaves under drafts/ in the machine dir.',
|
|
15
|
+
'',
|
|
16
|
+
'Usage:',
|
|
17
|
+
' lattris resend <path-to-request.json>',
|
|
18
|
+
'',
|
|
19
|
+
'If the first attempt landed, the door answers the same receipt and writes nothing; if it',
|
|
20
|
+
'did not, the write happens now, once. The file’s own session is used, whatever',
|
|
21
|
+
'LATTRIS_SESSION_ID says here.'
|
|
22
|
+
].join('\n');
|
|
23
|
+
const RESEND_DOORS = /^(POST \/memories\/notes|PATCH \/memories\/[^/\s]+)$/;
|
|
24
|
+
export const resend = async (cfg, io, args) => {
|
|
25
|
+
if (args.length === 0 || args.some((t) => HELP_TOKENS.has(t))) {
|
|
26
|
+
io.stdout.write(`${RESEND_HELP}\n`);
|
|
27
|
+
if (args.length > 0 && !args.some((t) => HELP_TOKENS.has(t)))
|
|
28
|
+
return refusal('', EXIT.USAGE);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (args.length !== 1) {
|
|
32
|
+
return refusal('Usage: lattris resend <path-to-request.json>\n');
|
|
33
|
+
}
|
|
34
|
+
let draft;
|
|
35
|
+
try {
|
|
36
|
+
draft = JSON.parse(io.store.files.readFileSync(args[0], 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return refusal(`Could not read a kept request at ${args[0]}: ${sanitizeForTerminal(e instanceof Error ? e.message : String(e))}\n`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof draft !== 'object' ||
|
|
42
|
+
draft === null ||
|
|
43
|
+
typeof draft.door !== 'string' ||
|
|
44
|
+
!RESEND_DOORS.test(draft.door) ||
|
|
45
|
+
typeof draft.idempotencyKey !== 'string' ||
|
|
46
|
+
!isUuidKey(draft.idempotencyKey) ||
|
|
47
|
+
typeof draft.sessionId !== 'string' ||
|
|
48
|
+
!SESSION_ID_SHAPE.test(draft.sessionId) ||
|
|
49
|
+
typeof draft.body !== 'object' ||
|
|
50
|
+
draft.body === null) {
|
|
51
|
+
return refusal(`${args[0]} is not a kept request (door, idempotencyKey, sessionId and body are what one carries).\n`);
|
|
52
|
+
}
|
|
53
|
+
if (draft.agentLabel !== undefined &&
|
|
54
|
+
(typeof draft.agentLabel !== 'string' || !AGENT_LABEL_SHAPE.test(draft.agentLabel))) {
|
|
55
|
+
return refusal(`The kept request at ${args[0]} names the agent label "${sanitizeForTerminal(String(draft.agentLabel))}", ` +
|
|
56
|
+
'which a request header cannot carry: an agent label is 1 to 128 printable ASCII characters. ' +
|
|
57
|
+
'Nothing was sent.\n');
|
|
58
|
+
}
|
|
59
|
+
const accessToken = await signedIn(io, cfg);
|
|
60
|
+
if (typeof accessToken !== 'string')
|
|
61
|
+
return accessToken;
|
|
62
|
+
const [method, path] = draft.door.split(' ');
|
|
63
|
+
const identity = { sessionId: draft.sessionId, agentLabel: draft.agentLabel };
|
|
64
|
+
const result = await io.api(accessToken, `${cfg.apiUrl}${path}`, {
|
|
65
|
+
method,
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
...noteWriteHeaders(identity, draft.idempotencyKey)
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(draft.body)
|
|
71
|
+
});
|
|
72
|
+
if (!result.ok)
|
|
73
|
+
return refusalOf(result, ambiguousText(result.body));
|
|
74
|
+
if (method === 'POST') {
|
|
75
|
+
io.stdout.write(`${formatSaved(result.data)}\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
emit(io, result.data, result.rawText, (m) => 'visibility' in m
|
|
79
|
+
? `Resent.\n\n${formatBlockLine(m)}`
|
|
80
|
+
: `Resent.\n\n${formatMemoryDetail(m)}`);
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=resend.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { parseArgs } from "../request/flags.js";
|
|
2
|
+
import { REVISION_NO_SHAPE } from "../memory/history.js";
|
|
3
|
+
import { FINGERPRINT_SHAPE } from "../memory/update-help.js";
|
|
4
|
+
import { composeAddressFor, formatMemoryDetail } from "../memory/views.js";
|
|
5
|
+
import { emit } from "../render/output.js";
|
|
6
|
+
import { noteWriteHeaders, requireWriteIdentity } from "../store/write-identity.js";
|
|
7
|
+
import { isRefusal, refusal } from "../store/exit.js";
|
|
8
|
+
import { refusalOf } from "../request/api-fetch.js";
|
|
9
|
+
import { signedIn } from "../request/access-token.js";
|
|
10
|
+
import { ambiguousText } from "../render/rows.js";
|
|
11
|
+
export const RESTORE_HELP = [
|
|
12
|
+
'restore — bring a note back from deleted items, or bring an earlier revision back as the head.',
|
|
13
|
+
'',
|
|
14
|
+
'Usage:',
|
|
15
|
+
' lattris restore memory <id-or-prefix> --revision <n> --base-fingerprint <head> [--clear-missing]',
|
|
16
|
+
'',
|
|
17
|
+
' --revision required; the revision to bring back, from `lattris get history <id>`.',
|
|
18
|
+
' The note\u2019s own current number un-archives it with its last words.',
|
|
19
|
+
' --base-fingerprint required; the head fingerprint history printed. A head that moved',
|
|
20
|
+
' since is refused and nothing changes.',
|
|
21
|
+
' --clear-missing restore even when the revision names a span or contact that no',
|
|
22
|
+
' longer exists, clearing those references; without it the door',
|
|
23
|
+
' refuses and names them.',
|
|
24
|
+
'',
|
|
25
|
+
'History is never rewritten: restoring earlier words appends them as a new revision',
|
|
26
|
+
'that says where it came from. A note back from deleted items returns private.'
|
|
27
|
+
].join('\n');
|
|
28
|
+
const RESTORE_FLAG_NAMES = {
|
|
29
|
+
'--revision': 'revision',
|
|
30
|
+
'--base-fingerprint': 'baseFingerprint'
|
|
31
|
+
};
|
|
32
|
+
export const restoreMemory = async (cfg, io, rest) => {
|
|
33
|
+
const clearMissing = rest.includes('--clear-missing');
|
|
34
|
+
const { flags, positionals, unknown, notAnId } = parseArgs(RESTORE_FLAG_NAMES, rest.filter((a) => a !== '--clear-missing'), [0]);
|
|
35
|
+
if (unknown) {
|
|
36
|
+
return refusal(`Unknown flag: ${unknown}\n`);
|
|
37
|
+
}
|
|
38
|
+
if (notAnId)
|
|
39
|
+
return refusal(notAnId);
|
|
40
|
+
if (positionals.length !== 1 ||
|
|
41
|
+
flags.revision === undefined ||
|
|
42
|
+
flags.baseFingerprint === undefined) {
|
|
43
|
+
return refusal(`Usage: lattris restore memory <id> --revision <n> --base-fingerprint <head>\n`);
|
|
44
|
+
}
|
|
45
|
+
if (!REVISION_NO_SHAPE.test(flags.revision)) {
|
|
46
|
+
return refusal(`--revision must be a positive number from the history: ${flags.revision}\n`);
|
|
47
|
+
}
|
|
48
|
+
if (!FINGERPRINT_SHAPE.test(flags.baseFingerprint)) {
|
|
49
|
+
return refusal(`--base-fingerprint must be the 64 lowercase hex characters history printed: ${flags.baseFingerprint}\n`);
|
|
50
|
+
}
|
|
51
|
+
const identity = requireWriteIdentity(io.store, io.stdin.isTTY);
|
|
52
|
+
if (isRefusal(identity))
|
|
53
|
+
return identity;
|
|
54
|
+
const accessToken = await signedIn(io, cfg);
|
|
55
|
+
if (typeof accessToken !== 'string')
|
|
56
|
+
return accessToken;
|
|
57
|
+
const result = await io.api(accessToken, `${cfg.apiUrl}/memories/${encodeURIComponent(positionals[0])}/restore`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json', ...noteWriteHeaders(identity) },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
revisionNo: flags.revision,
|
|
62
|
+
baseFingerprint: flags.baseFingerprint,
|
|
63
|
+
...(clearMissing ? { missingReferences: 'clear' } : {})
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
if (!result.ok)
|
|
67
|
+
return refusalOf(result, ambiguousText(result.body));
|
|
68
|
+
let address;
|
|
69
|
+
if (io.stdout.isTTY)
|
|
70
|
+
address = await composeAddressFor(cfg, io, accessToken, result.data);
|
|
71
|
+
emit(io, result.data, result.rawText, (m) => `Restored.\n\n${formatMemoryDetail(m, address)}`);
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=restore.js.map
|
|
@@ -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,88 @@
|
|
|
1
|
+
import { waitsForNameLines } from "../render/rows.js";
|
|
2
|
+
import { sanitizeForTerminal } from "../render/text.js";
|
|
3
|
+
const AUDIO_MIME_BY_EXT = {
|
|
4
|
+
mp3: 'audio/mpeg',
|
|
5
|
+
m4a: 'audio/mp4',
|
|
6
|
+
mp4: 'audio/mp4',
|
|
7
|
+
wav: 'audio/wav',
|
|
8
|
+
ogg: 'audio/ogg',
|
|
9
|
+
oga: 'audio/ogg',
|
|
10
|
+
webm: 'audio/webm',
|
|
11
|
+
flac: 'audio/flac',
|
|
12
|
+
aac: 'audio/aac'
|
|
13
|
+
};
|
|
14
|
+
export const mimeForAudio = (filename, override) => {
|
|
15
|
+
if (override !== undefined)
|
|
16
|
+
return override;
|
|
17
|
+
const ext = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
|
|
18
|
+
return AUDIO_MIME_BY_EXT[ext] ?? null;
|
|
19
|
+
};
|
|
20
|
+
export const convertPlainTranscript = (raw) => {
|
|
21
|
+
const speakers = {};
|
|
22
|
+
const keyByLabel = new Map();
|
|
23
|
+
const turns = [];
|
|
24
|
+
const keyFor = (label) => {
|
|
25
|
+
const existing = keyByLabel.get(label);
|
|
26
|
+
if (existing !== undefined)
|
|
27
|
+
return existing;
|
|
28
|
+
const n = keyByLabel.size;
|
|
29
|
+
const key = String.fromCharCode(97 + (n % 26)) + (n >= 26 ? String(Math.floor(n / 26)) : '');
|
|
30
|
+
keyByLabel.set(label, key);
|
|
31
|
+
speakers[key] = { label };
|
|
32
|
+
return key;
|
|
33
|
+
};
|
|
34
|
+
for (const line of raw.split('\n')) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (trimmed === '')
|
|
37
|
+
continue;
|
|
38
|
+
const colon = trimmed.indexOf(':');
|
|
39
|
+
if (colon > 0 && colon < 40) {
|
|
40
|
+
const label = trimmed.slice(0, colon).trim();
|
|
41
|
+
const text = trimmed.slice(colon + 1).trim();
|
|
42
|
+
turns.push({ speaker: keyFor(label), text });
|
|
43
|
+
}
|
|
44
|
+
else if (turns.length > 0) {
|
|
45
|
+
turns[turns.length - 1].text += `\n${trimmed}`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
turns.push({ speaker: keyFor('speaker'), text: trimmed });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { speakers, turns };
|
|
52
|
+
};
|
|
53
|
+
export const TRANSCRIPT_CONVERTERS = {
|
|
54
|
+
json: (raw) => JSON.parse(raw),
|
|
55
|
+
a: (raw) => convertPlainTranscript(raw)
|
|
56
|
+
};
|
|
57
|
+
export const RECORDING_FORMATS = Object.keys(TRANSCRIPT_CONVERTERS);
|
|
58
|
+
export const buildRecordingBody = (input) => {
|
|
59
|
+
const body = {};
|
|
60
|
+
for (const [k, v] of Object.entries(input)) {
|
|
61
|
+
if (v === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
if (k === 'allowBannedWords' && v !== true)
|
|
64
|
+
continue;
|
|
65
|
+
body[k] = v;
|
|
66
|
+
}
|
|
67
|
+
return body;
|
|
68
|
+
};
|
|
69
|
+
export const formatRecording = (r) => {
|
|
70
|
+
const lines = [`Saved recording ${r.id}`];
|
|
71
|
+
if (r.title)
|
|
72
|
+
lines.push(` title ${sanitizeForTerminal(r.title)}`);
|
|
73
|
+
const stands = [];
|
|
74
|
+
if (r.media)
|
|
75
|
+
stands.push(`audio${r.media.status ? ` (${r.media.status})` : ''}`);
|
|
76
|
+
if (r.sourceUrl)
|
|
77
|
+
stands.push('source');
|
|
78
|
+
if (r.transcript)
|
|
79
|
+
stands.push(`transcript${Array.isArray(r.transcript.turns) ? ` (${r.transcript.turns.length} turns)` : ''}`);
|
|
80
|
+
lines.push(` stands on ${stands.join(', ') || '(nothing yet)'}`);
|
|
81
|
+
if (r.durationSeconds)
|
|
82
|
+
lines.push(` duration ${r.durationSeconds}s`);
|
|
83
|
+
lines.push(` capturedAt ${r.capturedAt}`);
|
|
84
|
+
for (const sentence of waitsForNameLines(r.access))
|
|
85
|
+
lines.push(` website ${sentence}`);
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=transcript.js.map
|