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.
- package/README.md +2 -2
- package/dist/build.json +4 -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 +181 -0
- package/dist/dispatch-words.js +137 -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/capture-common.js +67 -0
- package/dist/memory/capture-help.js +186 -0
- package/dist/memory/capture-note.js +190 -0
- package/dist/memory/capture-session.js +166 -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 +63 -0
- package/dist/memory/get.js +213 -0
- package/dist/memory/history.js +134 -0
- package/dist/memory/recording-update.js +128 -0
- package/dist/memory/recording.js +249 -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 +85 -0
- package/dist/memory/update-help.js +126 -0
- package/dist/memory/update.js +220 -0
- package/dist/memory/views.js +138 -0
- package/dist/nouns/consent.js +29 -0
- package/dist/nouns/contact.js +142 -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 +34 -0
- package/dist/nouns/invite.js +98 -0
- package/dist/nouns/questions.js +116 -0
- package/dist/nouns/share.js +105 -0
- package/dist/nouns/words.js +76 -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 +110 -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 +49 -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 +4 -5
- 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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|