lattris 0.1.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 +86 -0
- package/dist/extract-claude-jsonl.js +253 -0
- package/dist/main.js +6587 -0
- package/dist/save-session-chain.js +136 -0
- package/package.json +29 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The whole-chain materialiser (session-block spec: "the extractor
|
|
2
|
+
// materialises the WHOLE chain oldest-epoch-first on every save — extend an
|
|
3
|
+
// epoch's block where it exists, create-and-link where it doesn't —
|
|
4
|
+
// complete, idempotent, regardless of when the person last saved"; block-
|
|
5
|
+
// graph spec: "The whole loop — epoch detection, chain materialisation,
|
|
6
|
+
// fork handling, edge writing, reasons — is a deterministic script reading
|
|
7
|
+
// the jsonl on one side and the CLI/API's own responses on the other").
|
|
8
|
+
//
|
|
9
|
+
// An internal module behind `lattris capture session <transcript.jsonl>`
|
|
10
|
+
// (main.ts), which owns the IO: the token, the POST, the receipts dir, the
|
|
11
|
+
// door rendering. What lives here is the deterministic drive loop and the
|
|
12
|
+
// receipts store — machine-read receipts and refusal CODES, never parsed
|
|
13
|
+
// prose. Per epoch, oldest first:
|
|
14
|
+
// - a RECEIPT HINT exists for this epoch (a prior run's own receipt,
|
|
15
|
+
// cached under the state dir): save with `extends` — the server's
|
|
16
|
+
// fingerprints decide; a divergence 409 is the scripted fork signal →
|
|
17
|
+
// re-submit `asNewBlock` + `forkedFrom` that block (the server verifies
|
|
18
|
+
// THAT claim too — the hint only ever nominates, per the law: "receipts
|
|
19
|
+
// and external ids are hints; the server's fingerprint comparison is
|
|
20
|
+
// the truth").
|
|
21
|
+
// - no hint: a plain save (`externalConversationId` nomination resolves a
|
|
22
|
+
// known epoch; otherwise create), with `continuesFrom` naming the
|
|
23
|
+
// PREVIOUS epoch's block from THIS run's receipts — the chain edge.
|
|
24
|
+
// A forked block carries its `forked_into` edge INSTEAD of a chain edge
|
|
25
|
+
// (the door's one-born-with-edge rule); continuity stays derivable
|
|
26
|
+
// through the origin's own chain — edge-economy, nothing duplicated.
|
|
27
|
+
// - the receipt (outcome + block id + turn count) is recorded back into
|
|
28
|
+
// the hint file, so the next run resumes exactly where the truth is.
|
|
29
|
+
//
|
|
30
|
+
// The AI-judgment inputs are --title, --ai-summary (applied to the LIVE —
|
|
31
|
+
// last — epoch's save only), and --contributor (applied to EVERY epoch's
|
|
32
|
+
// save — attribution is a whole-chain fact, not an epoch-specific one, and
|
|
33
|
+
// an extend's own save carries it so re-running with the flag heals older
|
|
34
|
+
// epochs of the same chain); everything else is mechanical.
|
|
35
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
// A save the drive loop could not scriptedly react to (anything beyond the
|
|
38
|
+
// divergence-fork and stale-hint outcomes above). Typed, carrying the wire's
|
|
39
|
+
// own status and body, so the command can render it through the CLI's one
|
|
40
|
+
// door rule (classifyApiError → renderDoor) with the house exit codes — a
|
|
41
|
+
// 401 mid-chain must read as NOAUTH, not a generic failure.
|
|
42
|
+
export class ChainSaveError extends Error {
|
|
43
|
+
// Explicit fields, not constructor parameter properties — those are
|
|
44
|
+
// TS-only syntax the no-build `node --experimental-strip-types` run
|
|
45
|
+
// cannot erase (strip-only mode refuses them outright).
|
|
46
|
+
epoch;
|
|
47
|
+
status;
|
|
48
|
+
body;
|
|
49
|
+
constructor(epoch, status, body) {
|
|
50
|
+
super(`epoch ${epoch} save failed (${status} ${body.code ?? ''}): ${body.message ?? 'no message'}`);
|
|
51
|
+
this.epoch = epoch;
|
|
52
|
+
this.status = status;
|
|
53
|
+
this.body = body;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const driveChain = async (chain, receipts, judgment, save) => {
|
|
57
|
+
const results = [];
|
|
58
|
+
let previousBlockId;
|
|
59
|
+
for (let k = 0; k < chain.epochs.length; k++) {
|
|
60
|
+
const turns = chain.epochs[k];
|
|
61
|
+
const isLive = k === chain.epochs.length - 1;
|
|
62
|
+
const base = { turns, source: chain.source };
|
|
63
|
+
if (chain.conversationId !== null)
|
|
64
|
+
base.externalConversationId = chain.conversationId;
|
|
65
|
+
if (isLive && judgment.title !== undefined)
|
|
66
|
+
base.title = judgment.title;
|
|
67
|
+
if (isLive && judgment.aiSummary !== undefined)
|
|
68
|
+
base.aiSummary = judgment.aiSummary;
|
|
69
|
+
// contributor rides EVERY epoch, not just the live one — attribution is a
|
|
70
|
+
// whole-chain fact, and an extend re-running with the flag heals older
|
|
71
|
+
// epochs whose original save predated the contact.
|
|
72
|
+
if (judgment.contributor !== undefined)
|
|
73
|
+
base.contributor = judgment.contributor;
|
|
74
|
+
const hinted = receipts.epochs[String(k)];
|
|
75
|
+
let outcome;
|
|
76
|
+
let forked = false;
|
|
77
|
+
if (hinted !== undefined) {
|
|
78
|
+
outcome = await save({ ...base, extends: hinted });
|
|
79
|
+
if (outcome.status === 409 && outcome.body.code === 'memory:session-diverged') {
|
|
80
|
+
// The scripted fork reaction: the walked path rewound-and-diverged
|
|
81
|
+
// from what that block holds — a NEW record, its origin claimed and
|
|
82
|
+
// server-verified. No continuesFrom here (the door's one-edge rule);
|
|
83
|
+
// the chain stays derivable through the origin.
|
|
84
|
+
outcome = await save({ ...base, asNewBlock: true, forkedFrom: hinted });
|
|
85
|
+
forked = true;
|
|
86
|
+
}
|
|
87
|
+
else if (outcome.status === 400 && outcome.body.code === 'memory:unknown-extends') {
|
|
88
|
+
// The hint went stale (the block was deleted) — hints never decide:
|
|
89
|
+
// fall back to the no-hint path.
|
|
90
|
+
outcome = await save(previousBlockId !== undefined ? { ...base, continuesFrom: previousBlockId } : base);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
outcome = await save(previousBlockId !== undefined && k > 0 ? { ...base, continuesFrom: previousBlockId } : base);
|
|
95
|
+
}
|
|
96
|
+
if (outcome.status !== 200 || !outcome.body.block?.id) {
|
|
97
|
+
throw new ChainSaveError(k, outcome.status, outcome.body);
|
|
98
|
+
}
|
|
99
|
+
const blockId = outcome.body.block.id;
|
|
100
|
+
receipts.epochs[String(k)] = blockId;
|
|
101
|
+
results.push({
|
|
102
|
+
epoch: k,
|
|
103
|
+
blockId,
|
|
104
|
+
outcome: outcome.body.outcome ?? 'unknown',
|
|
105
|
+
turnCount: outcome.body.turnCount ?? turns.length,
|
|
106
|
+
forked
|
|
107
|
+
});
|
|
108
|
+
previousBlockId = blockId;
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
};
|
|
112
|
+
// ─── the receipts store (hints, never decisions) ────────────────────────────
|
|
113
|
+
// Lives under the CLI's own state dir — the command passes sessionDir() in, so
|
|
114
|
+
// this module never re-derives the LATTRIS_HOME/XDG resolution main.ts owns.
|
|
115
|
+
// Same 0700/0600 hygiene as the session file: a receipt names block ids in the
|
|
116
|
+
// person's own record.
|
|
117
|
+
const receiptsPath = (dir, conversationId) => join(dir, 'chains', `${conversationId}.json`);
|
|
118
|
+
export const loadReceipts = (dir, conversationId) => {
|
|
119
|
+
if (conversationId === null)
|
|
120
|
+
return { epochs: {} };
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(readFileSync(receiptsPath(dir, conversationId), 'utf8'));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { epochs: {} };
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
export const saveReceipts = (dir, conversationId, receipts) => {
|
|
129
|
+
if (conversationId === null)
|
|
130
|
+
return;
|
|
131
|
+
mkdirSync(join(dir, 'chains'), { recursive: true, mode: 0o700 });
|
|
132
|
+
writeFileSync(receiptsPath(dir, conversationId), JSON.stringify(receipts, null, 2), {
|
|
133
|
+
mode: 0o600
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=save-session-chain.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lattris",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "First-party CLI for Lattris — sign in, capture notes and sessions, and read your own tree from the terminal.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"lattris",
|
|
8
|
+
"cli",
|
|
9
|
+
"notes",
|
|
10
|
+
"knowledge"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"homepage": "https://lattris.com",
|
|
14
|
+
"bin": {
|
|
15
|
+
"lattris": "./dist/main.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/main.js",
|
|
22
|
+
"dist/extract-claude-jsonl.js",
|
|
23
|
+
"dist/save-session-chain.js",
|
|
24
|
+
"dist/build.json"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|