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,168 @@
|
|
|
1
|
+
import { extractChain } from "./extract-claude-jsonl.js";
|
|
2
|
+
import { ChainSaveError, driveChain } from "./save-session-chain.js";
|
|
3
|
+
import { loadReceipts, saveReceipts } from "../store/chains.js";
|
|
4
|
+
import { allowWords, isRefusalValue, needsYesWords, resolveBornShares, withShares } from "../memory/capture-common.js";
|
|
5
|
+
import { emit } from "../render/output.js";
|
|
6
|
+
import { sanitizeForTerminal } from "../render/text.js";
|
|
7
|
+
import { classifyApiError } from "../request/door.js";
|
|
8
|
+
import { EXIT, refusal } from "../store/exit.js";
|
|
9
|
+
import { refusalOf } from "../request/api-fetch.js";
|
|
10
|
+
import { signedIn } from "../request/access-token.js";
|
|
11
|
+
export const formatSavedSession = (r) => {
|
|
12
|
+
const lines = [
|
|
13
|
+
`Saved session ${r.block.id} (outcome: ${r.outcome}, turns stored: ${r.turnCount})`
|
|
14
|
+
];
|
|
15
|
+
if (r.block.title)
|
|
16
|
+
lines.push(` title ${sanitizeForTerminal(r.block.title)}`);
|
|
17
|
+
lines.push(` capturedAt ${r.block.capturedAt}`);
|
|
18
|
+
return lines.join('\n');
|
|
19
|
+
};
|
|
20
|
+
export const buildCaptureSessionBody = (turns, flags, asNew, shares) => {
|
|
21
|
+
const body = { turns };
|
|
22
|
+
if (flags.title !== undefined)
|
|
23
|
+
body.title = flags.title;
|
|
24
|
+
if (flags.intent !== undefined)
|
|
25
|
+
body.intent = flags.intent;
|
|
26
|
+
if (flags.capturedAt !== undefined)
|
|
27
|
+
body.capturedAt = flags.capturedAt;
|
|
28
|
+
if (flags.source !== undefined)
|
|
29
|
+
body.source = flags.source;
|
|
30
|
+
if (flags.conversationId !== undefined)
|
|
31
|
+
body.externalConversationId = flags.conversationId;
|
|
32
|
+
if (flags.extends !== undefined)
|
|
33
|
+
body.extends = flags.extends;
|
|
34
|
+
if (flags.from !== undefined)
|
|
35
|
+
body.from = Number(flags.from);
|
|
36
|
+
if (asNew)
|
|
37
|
+
body.asNewBlock = true;
|
|
38
|
+
withShares(body, shares);
|
|
39
|
+
if (flags.continuesFrom !== undefined)
|
|
40
|
+
body.continuesFrom = flags.continuesFrom;
|
|
41
|
+
if (flags.forkedFrom !== undefined)
|
|
42
|
+
body.forkedFrom = flags.forkedFrom;
|
|
43
|
+
if (flags.aiSummary !== undefined)
|
|
44
|
+
body.aiSummary = flags.aiSummary;
|
|
45
|
+
if (flags.contributor !== undefined)
|
|
46
|
+
body.contributor = flags.contributor;
|
|
47
|
+
return body;
|
|
48
|
+
};
|
|
49
|
+
export const captureSession = async (cfg, io, content, flags, opts) => {
|
|
50
|
+
let turns;
|
|
51
|
+
try {
|
|
52
|
+
turns = JSON.parse(content);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return refusal('stdin must be a JSON array of turns — [{"speaker":"user","text":"…"}, …]. ' +
|
|
56
|
+
'Run `lattris capture memory --type session` for the form.\n');
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(turns) || turns.length === 0) {
|
|
59
|
+
return refusal('stdin must be a NON-EMPTY JSON array of turns — a session is at least one turn.\n');
|
|
60
|
+
}
|
|
61
|
+
if (flags.from !== undefined && !/^\d+$/.test(flags.from)) {
|
|
62
|
+
return refusal(`--from must be a non-negative integer, got: ${flags.from}\n`);
|
|
63
|
+
}
|
|
64
|
+
if (opts.shareWith.length > 0 && !opts.yes) {
|
|
65
|
+
return refusal(needsYesWords('session', allowWords(opts.shareWith)));
|
|
66
|
+
}
|
|
67
|
+
const accessToken = await signedIn(io, cfg);
|
|
68
|
+
if (typeof accessToken !== 'string')
|
|
69
|
+
return accessToken;
|
|
70
|
+
let shares;
|
|
71
|
+
if (opts.shareWith.length > 0 || opts.deny.length > 0) {
|
|
72
|
+
const resolved = await resolveBornShares(cfg, io, accessToken, opts.shareWith, opts.deny);
|
|
73
|
+
if (isRefusalValue(resolved))
|
|
74
|
+
return resolved;
|
|
75
|
+
shares = resolved;
|
|
76
|
+
}
|
|
77
|
+
const result = await io.api(accessToken, `${cfg.apiUrl}/memories/sessions`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify(buildCaptureSessionBody(turns, flags, opts.asNew, shares))
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok)
|
|
83
|
+
return refusalOf(result);
|
|
84
|
+
io.stdout.write(`${formatSavedSession(result.data)}\n`);
|
|
85
|
+
};
|
|
86
|
+
const TRANSCRIPT_INAPPLICABLE_FLAGS = [
|
|
87
|
+
['--intent', 'intent'],
|
|
88
|
+
['--captured-at', 'capturedAt'],
|
|
89
|
+
['--source', 'source'],
|
|
90
|
+
['--conversation-id', 'conversationId'],
|
|
91
|
+
['--extends', 'extends'],
|
|
92
|
+
['--from', 'from'],
|
|
93
|
+
['--continues-from', 'continuesFrom'],
|
|
94
|
+
['--forked-from', 'forkedFrom']
|
|
95
|
+
];
|
|
96
|
+
const formatChainEpoch = (r) => `epoch ${r.epoch}: ${r.outcome}${r.forked ? ' (forked)' : ''} — block ${r.blockId}, ${r.turnCount} turns stored`;
|
|
97
|
+
export const captureSessionTranscript = async (cfg, io, file, flags, asNew, sharesNamed = false) => {
|
|
98
|
+
for (const [name, key] of TRANSCRIPT_INAPPLICABLE_FLAGS) {
|
|
99
|
+
if (flags[key] !== undefined) {
|
|
100
|
+
return refusal(`${name} does not apply when capturing from a transcript — the chain drive ` +
|
|
101
|
+
`derives it. Only --title and --ai-summary ride this form.\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (sharesNamed) {
|
|
105
|
+
return refusal('--share-with and --deny do not apply when capturing from a transcript — the chain ' +
|
|
106
|
+
'drive derives it. Only --title and --ai-summary ride this form.\n');
|
|
107
|
+
}
|
|
108
|
+
if (asNew) {
|
|
109
|
+
return refusal('--as-new does not apply when capturing from a transcript — a fork is detected ' +
|
|
110
|
+
'and saved by the drive itself.\n');
|
|
111
|
+
}
|
|
112
|
+
let jsonl;
|
|
113
|
+
try {
|
|
114
|
+
jsonl = io.store.files.readFileSync(file, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
const code = e.code ?? 'error';
|
|
118
|
+
return refusal(`Cannot read transcript ${file} (${code}).\n`);
|
|
119
|
+
}
|
|
120
|
+
const chain = extractChain(jsonl);
|
|
121
|
+
const shapeLine = chain.shape === 'subagent'
|
|
122
|
+
? `Read a subagent transcript (agent ${chain.agentId ?? '?'}): ${chain.epochs.length} epoch(s).\n`
|
|
123
|
+
: `Read a main-session transcript: ${chain.epochs.length} epoch(s).\n`;
|
|
124
|
+
if (chain.shape === 'subagent' || io.stdout.isTTY)
|
|
125
|
+
io.stderr.write(shapeLine);
|
|
126
|
+
if (chain.epochs.length === 0) {
|
|
127
|
+
return refusal('No dialogue found on the active path — nothing to save.\n', EXIT.GENERIC);
|
|
128
|
+
}
|
|
129
|
+
const accessToken = await signedIn(io, cfg);
|
|
130
|
+
if (typeof accessToken !== 'string')
|
|
131
|
+
return accessToken;
|
|
132
|
+
const save = async (body) => {
|
|
133
|
+
const res = await io.api(accessToken, `${cfg.apiUrl}/memories/sessions`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify(body)
|
|
137
|
+
});
|
|
138
|
+
const text = res.rawText;
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
const b = JSON.parse(text);
|
|
142
|
+
parsed = b && typeof b === 'object' ? b : {};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
parsed = { message: text.slice(0, 200) };
|
|
146
|
+
}
|
|
147
|
+
return { status: res.status, body: parsed };
|
|
148
|
+
};
|
|
149
|
+
const receipts = loadReceipts(io.store, chain.conversationId);
|
|
150
|
+
let results;
|
|
151
|
+
try {
|
|
152
|
+
results = await driveChain(chain, receipts, { title: flags.title, aiSummary: flags.aiSummary, contributor: flags.contributor }, save);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
if (e instanceof ChainSaveError) {
|
|
156
|
+
const { message, next, exit } = classifyApiError(e.status, e.body);
|
|
157
|
+
return {
|
|
158
|
+
message: '',
|
|
159
|
+
exit,
|
|
160
|
+
answer: { kind: 'door', message: `epoch ${e.epoch}: ${message}`, next }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
saveReceipts(io.store, chain.conversationId, receipts);
|
|
166
|
+
emit(io, results, JSON.stringify(results), (rs) => rs.map(formatChainEpoch).join('\n'));
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=capture-session.js.map
|
|
@@ -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,72 @@
|
|
|
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
|
+
' access may one contact, or one number, see a memory, and because of what',
|
|
11
|
+
'',
|
|
12
|
+
'Usage:',
|
|
13
|
+
' lattris get memories [--shared-with-me] [--q <words>] [--type <name>] [--intent <name>] [--with <audience>] [--for <contact> | --for-phone <number>] [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
14
|
+
' lattris get memories --by <owner-handle> [--limit <n>] [--cursor <token>] [--content 0|1]',
|
|
15
|
+
' lattris get memory <website-address|id> [--by <owner-handle>] [--links 0|1] [--depth <n>] [--content 0|1]',
|
|
16
|
+
' lattris get memory <id-or-prefix> --revision <n> one revision, as history kept it',
|
|
17
|
+
' lattris get history <id-or-prefix> [--limit <n>] [--before <operation-id>]',
|
|
18
|
+
' lattris get deleted [--limit <n>] [--cursor <token>]',
|
|
19
|
+
' lattris get questions [--mine]',
|
|
20
|
+
' lattris get access <memory-id-or-prefix> --for <contact | number>',
|
|
21
|
+
'',
|
|
22
|
+
' --q <words> only memories whose title or content contains these words,',
|
|
23
|
+
' matched as one literal phrase, case-insensitive. Needs 3+',
|
|
24
|
+
' characters and at least one distinctive word — a query of',
|
|
25
|
+
' only common words ("the", "and", ...) is refused; pick a',
|
|
26
|
+
' name or a particular noun instead.',
|
|
27
|
+
' --type <name> filter to one type',
|
|
28
|
+
' --intent <name> filter to one intent',
|
|
29
|
+
' --with <audience> your own footprint: public (has that grant) or nobody',
|
|
30
|
+
' (private — no grant at all)',
|
|
31
|
+
' --limit <n> page size, 1-100 (default 50)',
|
|
32
|
+
' --cursor <token> the nextCursor from a previous page',
|
|
33
|
+
' --by <handle> one wall on the website address; an id without a',
|
|
34
|
+
' session needs it, while a full address carries it',
|
|
35
|
+
' --shared-with-me list memories other people granted to you; each row',
|
|
36
|
+
' names its owner and whether it is on your graph',
|
|
37
|
+
' --for <contact> what one of your contacts may see of your memories: an',
|
|
38
|
+
' exact name or id from `lattris contact list`. The rows',
|
|
39
|
+
' carry what they would see; the first line names them',
|
|
40
|
+
' --for-phone <number> the same for a phone number in full international',
|
|
41
|
+
' form; a number no contact holds is a stranger, who',
|
|
42
|
+
' sees public memories only',
|
|
43
|
+
'',
|
|
44
|
+
' --links 0|1 edges on the one memory (default 1): direction, verb,',
|
|
45
|
+
' reason, and each neighbour\u2019s finding aid. --links 0',
|
|
46
|
+
' restores the bare read, byte-identical on the wire.',
|
|
47
|
+
' --depth <n> expand neighbours recursively (finding aid + links',
|
|
48
|
+
' past the anchor; the API owns the range) \u2014 needs links',
|
|
49
|
+
' --content 0|1 0 answers the finding aid only (id, handle, type,',
|
|
50
|
+
' title, aiSummary) \u2014 the cheap look before a full read;',
|
|
51
|
+
' on the list, 0 drops the excerpts and keeps every other',
|
|
52
|
+
' field a writer needs (fingerprint, revisionNo, structureHash)',
|
|
53
|
+
' --mine (questions) the ones YOU asked, with how each closed',
|
|
54
|
+
'',
|
|
55
|
+
' --revision <n> the snapshot numbered n: title, words, metadata, its',
|
|
56
|
+
' fingerprint and who recorded it. Restore it with',
|
|
57
|
+
' `lattris restore memory <id> --revision <n> --base-fingerprint <head>`.',
|
|
58
|
+
' --before <op-id> (history) the page before that operation \u2014 the',
|
|
59
|
+
' nextBeforeOperation a previous page printed',
|
|
60
|
+
'',
|
|
61
|
+
'Every read of a note prints its fingerprint (the head of its history) and',
|
|
62
|
+
'revisionNo; every read prints structureHash, a hash of the block\u2019s links and',
|
|
63
|
+
'grants as this reader sees them. An edit sends the fingerprint back as --base-fingerprint.',
|
|
64
|
+
'Your own read of a memory also prints `access`: every allow and deny on it.',
|
|
65
|
+
'',
|
|
66
|
+
"A memory's web address is <site>/by/<owner-handle>/memories/<slug>-<memory-handle>.",
|
|
67
|
+
'A /by read asks for JSON with Accept: application/json; it sends a bearer',
|
|
68
|
+
'only when the session has a live one. `get memory` prints the address at a TTY.',
|
|
69
|
+
'',
|
|
70
|
+
'Aliases: read, show, view, fetch → get'
|
|
71
|
+
].join('\n');
|
|
72
|
+
//# sourceMappingURL=get-help.js.map
|