@oortmem/claude-code 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +11 -2
- package/dist/deps.d.ts +41 -0
- package/dist/deps.js +2 -0
- package/dist/fsutil.d.ts +15 -0
- package/dist/fsutil.js +77 -0
- package/dist/hooks.d.ts +48 -0
- package/dist/hooks.js +321 -0
- package/dist/import.d.ts +71 -0
- package/dist/import.js +244 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/install.d.ts +55 -0
- package/dist/install.js +257 -0
- package/dist/log.d.ts +30 -0
- package/dist/log.js +65 -0
- package/dist/queue.d.ts +79 -0
- package/dist/queue.js +351 -0
- package/dist/transcript.d.ts +88 -0
- package/dist/transcript.js +441 -0
- package/package.json +57 -3
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { scrub } from '@oortmem/core';
|
|
2
|
+
/**
|
|
3
|
+
* XML-style blocks Claude Code writes into user rows on the harness's behalf:
|
|
4
|
+
* background-task reports, `!` shell mode, slash commands and their local
|
|
5
|
+
* output, and system reminders (which carry hook context). They are not the
|
|
6
|
+
* user's words and are stripped, tag and content, before a row becomes text.
|
|
7
|
+
*/
|
|
8
|
+
export const HARNESS_TAGS = [
|
|
9
|
+
'task-notification',
|
|
10
|
+
'bash-input',
|
|
11
|
+
'bash-stdout',
|
|
12
|
+
'bash-stderr',
|
|
13
|
+
'command-name',
|
|
14
|
+
'command-message',
|
|
15
|
+
'command-args',
|
|
16
|
+
'local-command-stdout',
|
|
17
|
+
'local-command-caveat',
|
|
18
|
+
'system-reminder',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Oort's own injected block (see `formatForInjection`). Stripped from user and
|
|
22
|
+
* assistant text alike, so recalled memories are never observed back into memory.
|
|
23
|
+
*/
|
|
24
|
+
export const OORT_MEMORY_TAG = 'oort-memory';
|
|
25
|
+
/** Attributes on an opening tag are scanned at most this far, so a `<tag` with no `>` costs O(1). */
|
|
26
|
+
const MAX_OPEN_TAG_ATTRS = 256;
|
|
27
|
+
function tagSet(tags) {
|
|
28
|
+
return {
|
|
29
|
+
names: new Set(tags),
|
|
30
|
+
open: new RegExp(`<(${tags.join('|')})(?:\\s[^>]{0,${MAX_OPEN_TAG_ATTRS}})?>`, 'y'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const USER_BLOCKS = tagSet([...HARNESS_TAGS, OORT_MEMORY_TAG]);
|
|
34
|
+
const ASSISTANT_BLOCKS = tagSet([OORT_MEMORY_TAG]);
|
|
35
|
+
/**
|
|
36
|
+
* Removes whole `<tag …>…</tag>` blocks for the tags in `set`: each block ends
|
|
37
|
+
* at the first matching close after it (non-greedy, across lines); an opening
|
|
38
|
+
* tag with no close is left as text. A linear scan: every tag's closing-tag
|
|
39
|
+
* search only moves forward, and a tag found to have no later close is never
|
|
40
|
+
* searched again, so unclosed tags cannot make it quadratic. Whitespace is
|
|
41
|
+
* tidied only when something was removed.
|
|
42
|
+
*/
|
|
43
|
+
function stripBlocks(text, set) {
|
|
44
|
+
if (!text.includes('<'))
|
|
45
|
+
return text;
|
|
46
|
+
/** Per tag: the next close found (start, end), or null once none remains. */
|
|
47
|
+
const closes = new Map();
|
|
48
|
+
const findClose = (tag, from) => {
|
|
49
|
+
const cached = closes.get(tag);
|
|
50
|
+
if (cached === null)
|
|
51
|
+
return null;
|
|
52
|
+
if (cached !== undefined && cached.start >= from)
|
|
53
|
+
return cached;
|
|
54
|
+
const needle = `</${tag}`;
|
|
55
|
+
let p = from;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const k = text.indexOf(needle, p);
|
|
58
|
+
if (k < 0) {
|
|
59
|
+
closes.set(tag, null);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
let e = k + needle.length;
|
|
63
|
+
while (e < text.length && /\s/.test(text[e] ?? ''))
|
|
64
|
+
e++;
|
|
65
|
+
if (text[e] === '>') {
|
|
66
|
+
const found = { start: k, end: e + 1 };
|
|
67
|
+
closes.set(tag, found);
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
p = k + 1;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
let out = '';
|
|
74
|
+
let i = 0;
|
|
75
|
+
let removed = false;
|
|
76
|
+
for (;;) {
|
|
77
|
+
const lt = text.indexOf('<', i);
|
|
78
|
+
if (lt < 0)
|
|
79
|
+
break;
|
|
80
|
+
set.open.lastIndex = lt;
|
|
81
|
+
const m = set.open.exec(text);
|
|
82
|
+
const tag = m?.[1];
|
|
83
|
+
if (m === null || tag === undefined || !set.names.has(tag)) {
|
|
84
|
+
out += text.slice(i, lt + 1);
|
|
85
|
+
i = lt + 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const afterOpen = lt + m[0].length;
|
|
89
|
+
const close = findClose(tag, afterOpen);
|
|
90
|
+
if (close === null) {
|
|
91
|
+
out += text.slice(i, afterOpen);
|
|
92
|
+
i = afterOpen;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
out += text.slice(i, lt);
|
|
96
|
+
i = close.end;
|
|
97
|
+
removed = true;
|
|
98
|
+
}
|
|
99
|
+
if (!removed)
|
|
100
|
+
return text;
|
|
101
|
+
out += text.slice(i);
|
|
102
|
+
return out
|
|
103
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
104
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
107
|
+
const TOOL_INPUT_CHARS = 120;
|
|
108
|
+
const TOOL_RESULT_CHARS = 200;
|
|
109
|
+
/**
|
|
110
|
+
* Scrubbing looks this far past the cut, so a secret that straddles the cut is
|
|
111
|
+
* seen whole (the longest secrets the scrubber knows, PEM keys, fit easily).
|
|
112
|
+
*/
|
|
113
|
+
const SCRUB_LOOKAHEAD = 8192;
|
|
114
|
+
const MARKER = /\[REDACTED:[a-z0-9-]*\]/g;
|
|
115
|
+
/**
|
|
116
|
+
* Scrubs `s` before cutting it to `n` chars: cutting first would leave a
|
|
117
|
+
* secret's prefix that the scrubber no longer recognises. A cut that would
|
|
118
|
+
* fall inside a `[REDACTED:…]` marker moves to just before it.
|
|
119
|
+
*/
|
|
120
|
+
export function scrubThenCut(s, n) {
|
|
121
|
+
const clean = scrub(s.length > n + SCRUB_LOOKAHEAD ? s.slice(0, n + SCRUB_LOOKAHEAD) : s).text;
|
|
122
|
+
if (clean.length <= n)
|
|
123
|
+
return clean;
|
|
124
|
+
let cut = n;
|
|
125
|
+
MARKER.lastIndex = 0;
|
|
126
|
+
for (let m = MARKER.exec(clean); m !== null && m.index < n; m = MARKER.exec(clean)) {
|
|
127
|
+
if (m.index + m[0].length > n)
|
|
128
|
+
cut = m.index;
|
|
129
|
+
}
|
|
130
|
+
return clean.slice(0, cut);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Cheap pre-filter: a message row always carries a top-level `"type":"user"` or
|
|
134
|
+
* `"type":"assistant"`. Lines without either (the bulky `api-request` rows, for
|
|
135
|
+
* one) are never handed to `JSON.parse`. Nested occurrences inside strings are
|
|
136
|
+
* escaped (`\"type\"`), so they never match.
|
|
137
|
+
*/
|
|
138
|
+
const MESSAGE_ROW = /"type":\s*"(?:user|assistant)"/;
|
|
139
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
140
|
+
function blocksOf(content) {
|
|
141
|
+
if (typeof content === 'string')
|
|
142
|
+
return content;
|
|
143
|
+
if (!Array.isArray(content))
|
|
144
|
+
return undefined;
|
|
145
|
+
return content.filter((b) => typeof b === 'object' && b !== null);
|
|
146
|
+
}
|
|
147
|
+
function resultText(content) {
|
|
148
|
+
if (typeof content === 'string')
|
|
149
|
+
return content;
|
|
150
|
+
if (!Array.isArray(content))
|
|
151
|
+
return '';
|
|
152
|
+
const parts = [];
|
|
153
|
+
for (const b of content) {
|
|
154
|
+
if (typeof b === 'object' && b !== null && b['type'] === 'text') {
|
|
155
|
+
const t = str(b['text']);
|
|
156
|
+
if (t !== undefined)
|
|
157
|
+
parts.push(t);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return parts.join('\n');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Scrubs every string leaf of a tool input before it is serialised:
|
|
164
|
+
* `JSON.stringify` turns a newline into a literal `\n`, and a secret right
|
|
165
|
+
* after it would then follow a letter. Only what can reach the summary is
|
|
166
|
+
* scrubbed, so a huge input stays cheap: a leaf is cut to the remaining budget
|
|
167
|
+
* and leaves past it are emptied (serialising only lengthens text, so the
|
|
168
|
+
* budget still covers the summary and the scrub lookahead after it).
|
|
169
|
+
*/
|
|
170
|
+
function scrubLeaves(v, budget) {
|
|
171
|
+
if (typeof v === 'string') {
|
|
172
|
+
if (budget.left <= 0)
|
|
173
|
+
return '';
|
|
174
|
+
const s = v.length > budget.left ? v.slice(0, budget.left) : v;
|
|
175
|
+
budget.left -= s.length;
|
|
176
|
+
return scrub(s).text;
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(v))
|
|
179
|
+
return v.map((x) => scrubLeaves(x, budget));
|
|
180
|
+
if (typeof v === 'object' && v !== null) {
|
|
181
|
+
const out = {};
|
|
182
|
+
for (const [k, x] of Object.entries(v)) {
|
|
183
|
+
budget.left -= k.length;
|
|
184
|
+
Object.defineProperty(out, k, {
|
|
185
|
+
value: scrubLeaves(x, budget),
|
|
186
|
+
enumerable: true,
|
|
187
|
+
writable: true,
|
|
188
|
+
configurable: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
return v;
|
|
194
|
+
}
|
|
195
|
+
function toolUseSummary(b) {
|
|
196
|
+
const name = str(b['name']) ?? 'tool';
|
|
197
|
+
let input;
|
|
198
|
+
try {
|
|
199
|
+
const budget = { left: TOOL_INPUT_CHARS + SCRUB_LOOKAHEAD };
|
|
200
|
+
input = JSON.stringify(scrubLeaves(b['input'] ?? {}, budget)) ?? '';
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
input = '';
|
|
204
|
+
}
|
|
205
|
+
return `${name}(${scrubThenCut(input, TOOL_INPUT_CHARS)})`;
|
|
206
|
+
}
|
|
207
|
+
function toolResultSummary(b) {
|
|
208
|
+
return `result: ${scrubThenCut(resultText(b['content']), TOOL_RESULT_CHARS)}`;
|
|
209
|
+
}
|
|
210
|
+
/** A row that is conversation: a user or assistant message on the main chain, not injected. */
|
|
211
|
+
function messageRow(line) {
|
|
212
|
+
if (!MESSAGE_ROW.test(line))
|
|
213
|
+
return undefined;
|
|
214
|
+
let r;
|
|
215
|
+
try {
|
|
216
|
+
r = JSON.parse(line);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
222
|
+
return undefined;
|
|
223
|
+
const row = r;
|
|
224
|
+
if (row['type'] !== 'user' && row['type'] !== 'assistant')
|
|
225
|
+
return undefined;
|
|
226
|
+
if (row['isSidechain'] === true)
|
|
227
|
+
return undefined;
|
|
228
|
+
// Injected rows: caveats, skill bodies, compaction summaries, synthetic API errors.
|
|
229
|
+
if (row['isMeta'] === true ||
|
|
230
|
+
row['isCompactSummary'] === true ||
|
|
231
|
+
row['isVisibleInTranscriptOnly'] === true ||
|
|
232
|
+
row['isApiErrorMessage'] === true) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
if (typeof row['uuid'] !== 'string')
|
|
236
|
+
return undefined;
|
|
237
|
+
const message = row['message'];
|
|
238
|
+
if (typeof message !== 'object' || message === null)
|
|
239
|
+
return undefined;
|
|
240
|
+
return row;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parses a transcript into alternating user and assistant turns, keeping the
|
|
244
|
+
* uuids of the rows behind each turn. Rules:
|
|
245
|
+
* - only `user`/`assistant` rows; sidechain, meta and malformed rows are skipped;
|
|
246
|
+
* - consecutive assistant rows (Claude Code writes one row per content block)
|
|
247
|
+
* merge into one assistant turn; `thinking` blocks are ignored;
|
|
248
|
+
* - `tool_use` blocks become `"<name>(<first 120 chars of input>)"` summaries;
|
|
249
|
+
* - a user row holding only `tool_result` blocks merges into the previous
|
|
250
|
+
* turn's summaries as `"result: <first 200 chars>"`; mixed rows contribute
|
|
251
|
+
* their results to the previous turn and their text to a new user turn;
|
|
252
|
+
* - `HARNESS_TAGS` blocks and `<oort-memory>` are stripped from user text, and
|
|
253
|
+
* `<oort-memory>` from assistant text; a user row holding only harness output
|
|
254
|
+
* becomes a user turn with empty text, so the reply to it is a turn of its own.
|
|
255
|
+
*/
|
|
256
|
+
export function parseTranscriptDetailed(jsonl) {
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const line of jsonl.split('\n')) {
|
|
259
|
+
const row = messageRow(line);
|
|
260
|
+
if (row === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
const role = row['type'];
|
|
263
|
+
const uuid = row['uuid'];
|
|
264
|
+
const content = blocksOf(row['message']['content']);
|
|
265
|
+
if (content === undefined)
|
|
266
|
+
continue;
|
|
267
|
+
const texts = [];
|
|
268
|
+
const summaries = [];
|
|
269
|
+
const results = [];
|
|
270
|
+
if (typeof content === 'string') {
|
|
271
|
+
texts.push(content);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
for (const b of content) {
|
|
275
|
+
const type = b['type'];
|
|
276
|
+
if (type === 'text') {
|
|
277
|
+
const t = str(b['text']);
|
|
278
|
+
if (t !== undefined)
|
|
279
|
+
texts.push(t);
|
|
280
|
+
}
|
|
281
|
+
else if (type === 'tool_use') {
|
|
282
|
+
summaries.push(toolUseSummary(b));
|
|
283
|
+
}
|
|
284
|
+
else if (type === 'tool_result') {
|
|
285
|
+
results.push(toolResultSummary(b));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const tags = role === 'user' ? USER_BLOCKS : ASSISTANT_BLOCKS;
|
|
290
|
+
const raw = texts.filter((t) => t.length > 0).join('\n');
|
|
291
|
+
const text = stripBlocks(raw, tags);
|
|
292
|
+
const prev = out.at(-1);
|
|
293
|
+
if (role === 'user') {
|
|
294
|
+
if (results.length > 0 && prev !== undefined)
|
|
295
|
+
prev.turn.toolSummaries.push(...results);
|
|
296
|
+
// A tool-result row (even one carrying a system reminder) stays part of the turn.
|
|
297
|
+
if (text.length === 0 && (raw.length === 0 || results.length > 0))
|
|
298
|
+
continue;
|
|
299
|
+
// A row of only harness output becomes an empty user turn: what Claude says
|
|
300
|
+
// next answers the harness, not the previous prompt.
|
|
301
|
+
out.push({
|
|
302
|
+
turn: {
|
|
303
|
+
uuid,
|
|
304
|
+
parentUuid: str(row['parentUuid']) ?? null,
|
|
305
|
+
role,
|
|
306
|
+
text,
|
|
307
|
+
toolSummaries: [],
|
|
308
|
+
timestamp: str(row['timestamp']) ?? '',
|
|
309
|
+
sessionId: str(row['sessionId']) ?? '',
|
|
310
|
+
cwd: str(row['cwd']) ?? '',
|
|
311
|
+
},
|
|
312
|
+
rowUuids: [uuid],
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (prev?.turn.role === 'assistant') {
|
|
317
|
+
const t = prev.turn;
|
|
318
|
+
if (text.length > 0)
|
|
319
|
+
t.text = t.text.length > 0 ? `${t.text}\n${text}` : text;
|
|
320
|
+
t.toolSummaries.push(...summaries);
|
|
321
|
+
t.uuid = uuid;
|
|
322
|
+
t.timestamp = str(row['timestamp']) ?? t.timestamp;
|
|
323
|
+
prev.rowUuids.push(uuid);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
out.push({
|
|
327
|
+
turn: {
|
|
328
|
+
uuid,
|
|
329
|
+
parentUuid: str(row['parentUuid']) ?? null,
|
|
330
|
+
role,
|
|
331
|
+
text,
|
|
332
|
+
toolSummaries: summaries,
|
|
333
|
+
timestamp: str(row['timestamp']) ?? '',
|
|
334
|
+
sessionId: str(row['sessionId']) ?? '',
|
|
335
|
+
cwd: str(row['cwd']) ?? '',
|
|
336
|
+
},
|
|
337
|
+
rowUuids: [uuid],
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
export function parseTranscript(jsonl) {
|
|
343
|
+
return parseTranscriptDetailed(jsonl).map((d) => d.turn);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Per turn, its own timestamp when it parses, else the nearest turn's that
|
|
347
|
+
* does (earlier first on a tie), else `""` (observe then uses its own time).
|
|
348
|
+
* Linear: one pass each way.
|
|
349
|
+
*/
|
|
350
|
+
function nearestKnownTimestamps(turns) {
|
|
351
|
+
const known = (t) => t !== undefined && t.timestamp !== '' && !Number.isNaN(Date.parse(t.timestamp));
|
|
352
|
+
const prev = [];
|
|
353
|
+
let last = -1;
|
|
354
|
+
turns.forEach((t, i) => {
|
|
355
|
+
if (known(t))
|
|
356
|
+
last = i;
|
|
357
|
+
prev.push(last);
|
|
358
|
+
});
|
|
359
|
+
const out = Array.from({ length: turns.length }, () => '');
|
|
360
|
+
let next = -1;
|
|
361
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
362
|
+
if (known(turns[i]))
|
|
363
|
+
next = i;
|
|
364
|
+
const p = prev[i];
|
|
365
|
+
const pick = p < 0 ? next : next < 0 ? p : i - p <= next - i ? p : next;
|
|
366
|
+
out[i] = pick < 0 ? '' : turns[pick].timestamp;
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* One observation per user→assistant pair, at the assistant turn's timestamp
|
|
372
|
+
* (or the nearest known one when it has none, see `nearestKnownTimestamps`).
|
|
373
|
+
* Consecutive user turns (an interrupted prompt and its retry) are joined; an
|
|
374
|
+
* assistant turn without a user turn before it, and a trailing unanswered user
|
|
375
|
+
* turn, yield nothing. A reply to harness-only rows (a background task report,
|
|
376
|
+
* say) is observed with `userText: ""`: no text is invented for the user. When
|
|
377
|
+
* both sides are empty after stripping, the pair yields nothing.
|
|
378
|
+
*/
|
|
379
|
+
export function pairTurns(turns) {
|
|
380
|
+
const at = nearestKnownTimestamps(turns);
|
|
381
|
+
const out = [];
|
|
382
|
+
let pending = [];
|
|
383
|
+
turns.forEach((t, i) => {
|
|
384
|
+
if (t.role === 'user') {
|
|
385
|
+
pending.push(t);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (pending.length === 0)
|
|
389
|
+
return;
|
|
390
|
+
const userText = pending
|
|
391
|
+
.map((u) => u.text)
|
|
392
|
+
.filter((x) => x.length > 0)
|
|
393
|
+
.join('\n');
|
|
394
|
+
if (userText.length === 0 && t.text.length === 0) {
|
|
395
|
+
pending = [];
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
out.push({
|
|
399
|
+
obs: {
|
|
400
|
+
userText,
|
|
401
|
+
assistantText: t.text,
|
|
402
|
+
toolSummaries: [...pending.flatMap((u) => u.toolSummaries), ...t.toolSummaries],
|
|
403
|
+
sessionId: t.sessionId,
|
|
404
|
+
turnRef: t.uuid,
|
|
405
|
+
at: at[i],
|
|
406
|
+
},
|
|
407
|
+
assistantIndex: i,
|
|
408
|
+
});
|
|
409
|
+
pending = [];
|
|
410
|
+
});
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
export function turnsToObservations(turns) {
|
|
414
|
+
return pairTurns(turns).map((p) => p.obs);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Observations not yet processed, given `cursor` (the uuid of the last
|
|
418
|
+
* processed assistant row). When the cursor sits inside a turn rather than at
|
|
419
|
+
* its end, the turn grew after it was processed (the transcript lagged, or a
|
|
420
|
+
* stop hook made Claude continue), so it is taken again.
|
|
421
|
+
*/
|
|
422
|
+
export function selectAfterCursor(detailed, cursor) {
|
|
423
|
+
const paired = pairTurns(detailed.map((d) => d.turn));
|
|
424
|
+
if (cursor === null)
|
|
425
|
+
return { fresh: paired.map((p) => p.obs), cursorFound: true };
|
|
426
|
+
for (let i = detailed.length - 1; i >= 0; i--) {
|
|
427
|
+
const d = detailed[i];
|
|
428
|
+
if (d === undefined || d.turn.role !== 'assistant')
|
|
429
|
+
continue;
|
|
430
|
+
const at = d.rowUuids.lastIndexOf(cursor);
|
|
431
|
+
if (at < 0)
|
|
432
|
+
continue;
|
|
433
|
+
const from = at === d.rowUuids.length - 1 ? i + 1 : i;
|
|
434
|
+
return {
|
|
435
|
+
fresh: paired.filter((p) => p.assistantIndex >= from).map((p) => p.obs),
|
|
436
|
+
cursorFound: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return { fresh: paired.map((p) => p.obs), cursorFound: false };
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=transcript.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oortmem/claude-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Oort for Claude Code: hooks that recall and save memories, and transcript import.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"oort",
|
|
7
|
+
"agent-memory",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"llm-memory",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"hooks",
|
|
12
|
+
"anthropic"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://oortcloud.dev",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/oortmem/oort/issues",
|
|
17
|
+
"email": "hello@oortcloud.dev"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/oortmem/oort.git",
|
|
22
|
+
"directory": "packages/claude-code"
|
|
23
|
+
},
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"author": "Oort <hello@oortcloud.dev>",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.15.0"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"!dist/**/*.map",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@oortmem/core": "0.1.1",
|
|
50
|
+
"zod": "4.6.7"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@oortmem/store-sqlite": "0.1.1",
|
|
54
|
+
"vitest": "^5.0.5"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
59
|
+
}
|
|
6
60
|
}
|