engrm 0.4.28 → 0.4.29
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/dist/hooks/post-tool-use.js +137 -10
- package/dist/hooks/pre-compact.js +137 -10
- package/dist/hooks/session-start.js +13 -3
- package/dist/hooks/stop.js +138 -11
- package/dist/hooks/user-prompt-submit.js +137 -10
- package/dist/server.js +323 -81
- package/package.json +1 -1
|
@@ -3721,6 +3721,7 @@ function parseJsonArray(value) {
|
|
|
3721
3721
|
}
|
|
3722
3722
|
|
|
3723
3723
|
// src/capture/transcript.ts
|
|
3724
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3724
3725
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3725
3726
|
import { join as join4 } from "node:path";
|
|
3726
3727
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -3774,23 +3775,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
3774
3775
|
}
|
|
3775
3776
|
return messages;
|
|
3776
3777
|
}
|
|
3778
|
+
function resolveHistoryPath(historyPath) {
|
|
3779
|
+
if (historyPath)
|
|
3780
|
+
return historyPath;
|
|
3781
|
+
const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
|
|
3782
|
+
if (override)
|
|
3783
|
+
return override;
|
|
3784
|
+
return join4(homedir3(), ".claude", "history.jsonl");
|
|
3785
|
+
}
|
|
3786
|
+
function readHistoryFallback(sessionId, cwd, opts) {
|
|
3787
|
+
const path = resolveHistoryPath(opts?.historyPath);
|
|
3788
|
+
if (!existsSync4(path))
|
|
3789
|
+
return [];
|
|
3790
|
+
let raw;
|
|
3791
|
+
try {
|
|
3792
|
+
raw = readFileSync4(path, "utf-8");
|
|
3793
|
+
} catch {
|
|
3794
|
+
return [];
|
|
3795
|
+
}
|
|
3796
|
+
const targetCanonical = detectProject(cwd).canonical_id;
|
|
3797
|
+
const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
|
|
3798
|
+
const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
|
|
3799
|
+
const entries = [];
|
|
3800
|
+
for (const line of raw.split(`
|
|
3801
|
+
`)) {
|
|
3802
|
+
if (!line.trim())
|
|
3803
|
+
continue;
|
|
3804
|
+
let entry;
|
|
3805
|
+
try {
|
|
3806
|
+
entry = JSON.parse(line);
|
|
3807
|
+
} catch {
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
|
|
3811
|
+
continue;
|
|
3812
|
+
const createdAtEpoch = Math.floor(entry.timestamp / 1000);
|
|
3813
|
+
entries.push({
|
|
3814
|
+
display: entry.display.trim(),
|
|
3815
|
+
project: typeof entry.project === "string" ? entry.project : "",
|
|
3816
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
|
|
3817
|
+
timestamp: createdAtEpoch
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
|
|
3821
|
+
if (bySession.length > 0) {
|
|
3822
|
+
return dedupeHistoryMessages(bySession.map((entry) => ({
|
|
3823
|
+
role: "user",
|
|
3824
|
+
text: entry.display,
|
|
3825
|
+
createdAtEpoch: entry.timestamp
|
|
3826
|
+
})));
|
|
3827
|
+
}
|
|
3828
|
+
const byProjectAndWindow = entries.filter((entry) => {
|
|
3829
|
+
if (entry.display.length === 0)
|
|
3830
|
+
return false;
|
|
3831
|
+
if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
|
|
3832
|
+
return false;
|
|
3833
|
+
if (!entry.project)
|
|
3834
|
+
return false;
|
|
3835
|
+
return detectProject(entry.project).canonical_id === targetCanonical;
|
|
3836
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
3837
|
+
return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
|
|
3838
|
+
role: "user",
|
|
3839
|
+
text: entry.display,
|
|
3840
|
+
createdAtEpoch: entry.timestamp
|
|
3841
|
+
})));
|
|
3842
|
+
}
|
|
3777
3843
|
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
3778
|
-
const
|
|
3844
|
+
const session = db.getSessionById(sessionId);
|
|
3845
|
+
const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
3779
3846
|
...message,
|
|
3780
3847
|
text: message.text.trim()
|
|
3781
3848
|
})).filter((message) => message.text.length > 0);
|
|
3849
|
+
const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
|
|
3850
|
+
...message,
|
|
3851
|
+
sourceKind: "transcript",
|
|
3852
|
+
transcriptIndex: index + 1,
|
|
3853
|
+
createdAtEpoch: null,
|
|
3854
|
+
remoteSourceId: null
|
|
3855
|
+
})) : readHistoryFallback(sessionId, cwd, {
|
|
3856
|
+
startedAtEpoch: session?.started_at_epoch ?? null,
|
|
3857
|
+
completedAtEpoch: session?.completed_at_epoch ?? null
|
|
3858
|
+
}).map((message) => ({
|
|
3859
|
+
role: message.role,
|
|
3860
|
+
text: message.text,
|
|
3861
|
+
sourceKind: "hook",
|
|
3862
|
+
transcriptIndex: null,
|
|
3863
|
+
createdAtEpoch: message.createdAtEpoch,
|
|
3864
|
+
remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
|
|
3865
|
+
}));
|
|
3782
3866
|
if (messages.length === 0)
|
|
3783
3867
|
return { imported: 0, total: 0 };
|
|
3784
|
-
const session = db.getSessionById(sessionId);
|
|
3785
3868
|
const projectId = session?.project_id ?? null;
|
|
3786
3869
|
const now = Math.floor(Date.now() / 1000);
|
|
3787
3870
|
let imported = 0;
|
|
3788
3871
|
for (let index = 0;index < messages.length; index++) {
|
|
3789
|
-
const transcriptIndex = index + 1;
|
|
3790
|
-
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
3791
|
-
continue;
|
|
3792
3872
|
const message = messages[index];
|
|
3793
|
-
const
|
|
3873
|
+
const transcriptIndex = message.transcriptIndex ?? index + 1;
|
|
3874
|
+
if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
|
|
3875
|
+
continue;
|
|
3876
|
+
}
|
|
3877
|
+
if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
|
|
3878
|
+
continue;
|
|
3879
|
+
}
|
|
3880
|
+
const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
|
|
3794
3881
|
const row = db.insertChatMessage({
|
|
3795
3882
|
session_id: sessionId,
|
|
3796
3883
|
project_id: projectId,
|
|
@@ -3800,10 +3887,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
3800
3887
|
device_id: config.device_id,
|
|
3801
3888
|
agent: "claude-code",
|
|
3802
3889
|
created_at_epoch: createdAtEpoch,
|
|
3803
|
-
|
|
3804
|
-
|
|
3890
|
+
remote_source_id: message.remoteSourceId,
|
|
3891
|
+
source_kind: message.sourceKind,
|
|
3892
|
+
transcript_index: message.transcriptIndex
|
|
3805
3893
|
});
|
|
3806
3894
|
db.addToOutbox("chat_message", row.id);
|
|
3895
|
+
if (message.role === "user") {
|
|
3896
|
+
db.insertUserPrompt({
|
|
3897
|
+
session_id: sessionId,
|
|
3898
|
+
project_id: projectId,
|
|
3899
|
+
prompt: message.text,
|
|
3900
|
+
cwd,
|
|
3901
|
+
user_id: config.user_id,
|
|
3902
|
+
device_id: config.device_id,
|
|
3903
|
+
agent: "claude-code",
|
|
3904
|
+
created_at_epoch: createdAtEpoch
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3807
3907
|
if (db.vecAvailable) {
|
|
3808
3908
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
3809
3909
|
if (embedding) {
|
|
@@ -3814,6 +3914,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
3814
3914
|
}
|
|
3815
3915
|
return { imported, total: messages.length };
|
|
3816
3916
|
}
|
|
3917
|
+
function dedupeHistoryMessages(messages) {
|
|
3918
|
+
const deduped = [];
|
|
3919
|
+
for (const message of messages) {
|
|
3920
|
+
const compact = message.text.replace(/\s+/g, " ").trim();
|
|
3921
|
+
if (!compact)
|
|
3922
|
+
continue;
|
|
3923
|
+
const previous = deduped[deduped.length - 1];
|
|
3924
|
+
if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
|
|
3925
|
+
continue;
|
|
3926
|
+
deduped.push({ ...message, text: compact });
|
|
3927
|
+
}
|
|
3928
|
+
return deduped;
|
|
3929
|
+
}
|
|
3930
|
+
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
3931
|
+
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
3932
|
+
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
3933
|
+
}
|
|
3817
3934
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
3818
3935
|
const lines = [];
|
|
3819
3936
|
for (const msg of messages) {
|
|
@@ -3889,6 +4006,16 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
|
3889
4006
|
return saved;
|
|
3890
4007
|
}
|
|
3891
4008
|
|
|
4009
|
+
// src/tools/recent-chat.ts
|
|
4010
|
+
function getChatCaptureOrigin(message) {
|
|
4011
|
+
if (message.source_kind === "transcript")
|
|
4012
|
+
return "transcript";
|
|
4013
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
4014
|
+
return "history";
|
|
4015
|
+
}
|
|
4016
|
+
return "hook";
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3892
4019
|
// src/tools/session-story.ts
|
|
3893
4020
|
function getSessionStory(db, input) {
|
|
3894
4021
|
const session = db.getSessionById(input.session_id);
|
|
@@ -4011,9 +4138,9 @@ function collectProvenanceSummary(observations) {
|
|
|
4011
4138
|
}
|
|
4012
4139
|
function summarizeChatSources(messages) {
|
|
4013
4140
|
return messages.reduce((summary, message) => {
|
|
4014
|
-
summary[message
|
|
4141
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
4015
4142
|
return summary;
|
|
4016
|
-
}, { transcript: 0, hook: 0 });
|
|
4143
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
4017
4144
|
}
|
|
4018
4145
|
|
|
4019
4146
|
// src/tools/handoffs.ts
|
|
@@ -2203,6 +2203,16 @@ function computeObservationPriority(obs, nowEpoch) {
|
|
|
2203
2203
|
return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
|
|
2204
2204
|
}
|
|
2205
2205
|
|
|
2206
|
+
// src/tools/recent-chat.ts
|
|
2207
|
+
function getChatCaptureOrigin(message) {
|
|
2208
|
+
if (message.source_kind === "transcript")
|
|
2209
|
+
return "transcript";
|
|
2210
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
2211
|
+
return "history";
|
|
2212
|
+
}
|
|
2213
|
+
return "hook";
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2206
2216
|
// src/tools/session-story.ts
|
|
2207
2217
|
function getSessionStory(db, input) {
|
|
2208
2218
|
const session = db.getSessionById(input.session_id);
|
|
@@ -2325,9 +2335,9 @@ function collectProvenanceSummary(observations) {
|
|
|
2325
2335
|
}
|
|
2326
2336
|
function summarizeChatSources(messages) {
|
|
2327
2337
|
return messages.reduce((summary, message) => {
|
|
2328
|
-
summary[message
|
|
2338
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
2329
2339
|
return summary;
|
|
2330
|
-
}, { transcript: 0, hook: 0 });
|
|
2340
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
2331
2341
|
}
|
|
2332
2342
|
|
|
2333
2343
|
// src/tools/save.ts
|
|
@@ -3974,6 +3984,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
3974
3984
|
}
|
|
3975
3985
|
|
|
3976
3986
|
// src/capture/transcript.ts
|
|
3987
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3977
3988
|
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
3978
3989
|
import { join as join3 } from "node:path";
|
|
3979
3990
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -4027,23 +4038,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4027
4038
|
}
|
|
4028
4039
|
return messages;
|
|
4029
4040
|
}
|
|
4041
|
+
function resolveHistoryPath(historyPath) {
|
|
4042
|
+
if (historyPath)
|
|
4043
|
+
return historyPath;
|
|
4044
|
+
const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
|
|
4045
|
+
if (override)
|
|
4046
|
+
return override;
|
|
4047
|
+
return join3(homedir2(), ".claude", "history.jsonl");
|
|
4048
|
+
}
|
|
4049
|
+
function readHistoryFallback(sessionId, cwd, opts) {
|
|
4050
|
+
const path = resolveHistoryPath(opts?.historyPath);
|
|
4051
|
+
if (!existsSync3(path))
|
|
4052
|
+
return [];
|
|
4053
|
+
let raw;
|
|
4054
|
+
try {
|
|
4055
|
+
raw = readFileSync3(path, "utf-8");
|
|
4056
|
+
} catch {
|
|
4057
|
+
return [];
|
|
4058
|
+
}
|
|
4059
|
+
const targetCanonical = detectProject(cwd).canonical_id;
|
|
4060
|
+
const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
|
|
4061
|
+
const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
|
|
4062
|
+
const entries = [];
|
|
4063
|
+
for (const line of raw.split(`
|
|
4064
|
+
`)) {
|
|
4065
|
+
if (!line.trim())
|
|
4066
|
+
continue;
|
|
4067
|
+
let entry;
|
|
4068
|
+
try {
|
|
4069
|
+
entry = JSON.parse(line);
|
|
4070
|
+
} catch {
|
|
4071
|
+
continue;
|
|
4072
|
+
}
|
|
4073
|
+
if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
|
|
4074
|
+
continue;
|
|
4075
|
+
const createdAtEpoch = Math.floor(entry.timestamp / 1000);
|
|
4076
|
+
entries.push({
|
|
4077
|
+
display: entry.display.trim(),
|
|
4078
|
+
project: typeof entry.project === "string" ? entry.project : "",
|
|
4079
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
|
|
4080
|
+
timestamp: createdAtEpoch
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
|
|
4084
|
+
if (bySession.length > 0) {
|
|
4085
|
+
return dedupeHistoryMessages(bySession.map((entry) => ({
|
|
4086
|
+
role: "user",
|
|
4087
|
+
text: entry.display,
|
|
4088
|
+
createdAtEpoch: entry.timestamp
|
|
4089
|
+
})));
|
|
4090
|
+
}
|
|
4091
|
+
const byProjectAndWindow = entries.filter((entry) => {
|
|
4092
|
+
if (entry.display.length === 0)
|
|
4093
|
+
return false;
|
|
4094
|
+
if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
|
|
4095
|
+
return false;
|
|
4096
|
+
if (!entry.project)
|
|
4097
|
+
return false;
|
|
4098
|
+
return detectProject(entry.project).canonical_id === targetCanonical;
|
|
4099
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
4100
|
+
return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
|
|
4101
|
+
role: "user",
|
|
4102
|
+
text: entry.display,
|
|
4103
|
+
createdAtEpoch: entry.timestamp
|
|
4104
|
+
})));
|
|
4105
|
+
}
|
|
4030
4106
|
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
4031
|
-
const
|
|
4107
|
+
const session = db.getSessionById(sessionId);
|
|
4108
|
+
const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
4032
4109
|
...message,
|
|
4033
4110
|
text: message.text.trim()
|
|
4034
4111
|
})).filter((message) => message.text.length > 0);
|
|
4112
|
+
const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
|
|
4113
|
+
...message,
|
|
4114
|
+
sourceKind: "transcript",
|
|
4115
|
+
transcriptIndex: index + 1,
|
|
4116
|
+
createdAtEpoch: null,
|
|
4117
|
+
remoteSourceId: null
|
|
4118
|
+
})) : readHistoryFallback(sessionId, cwd, {
|
|
4119
|
+
startedAtEpoch: session?.started_at_epoch ?? null,
|
|
4120
|
+
completedAtEpoch: session?.completed_at_epoch ?? null
|
|
4121
|
+
}).map((message) => ({
|
|
4122
|
+
role: message.role,
|
|
4123
|
+
text: message.text,
|
|
4124
|
+
sourceKind: "hook",
|
|
4125
|
+
transcriptIndex: null,
|
|
4126
|
+
createdAtEpoch: message.createdAtEpoch,
|
|
4127
|
+
remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
|
|
4128
|
+
}));
|
|
4035
4129
|
if (messages.length === 0)
|
|
4036
4130
|
return { imported: 0, total: 0 };
|
|
4037
|
-
const session = db.getSessionById(sessionId);
|
|
4038
4131
|
const projectId = session?.project_id ?? null;
|
|
4039
4132
|
const now = Math.floor(Date.now() / 1000);
|
|
4040
4133
|
let imported = 0;
|
|
4041
4134
|
for (let index = 0;index < messages.length; index++) {
|
|
4042
|
-
const transcriptIndex = index + 1;
|
|
4043
|
-
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
4044
|
-
continue;
|
|
4045
4135
|
const message = messages[index];
|
|
4046
|
-
const
|
|
4136
|
+
const transcriptIndex = message.transcriptIndex ?? index + 1;
|
|
4137
|
+
if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
|
|
4138
|
+
continue;
|
|
4139
|
+
}
|
|
4140
|
+
if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
|
|
4141
|
+
continue;
|
|
4142
|
+
}
|
|
4143
|
+
const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
|
|
4047
4144
|
const row = db.insertChatMessage({
|
|
4048
4145
|
session_id: sessionId,
|
|
4049
4146
|
project_id: projectId,
|
|
@@ -4053,10 +4150,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4053
4150
|
device_id: config.device_id,
|
|
4054
4151
|
agent: "claude-code",
|
|
4055
4152
|
created_at_epoch: createdAtEpoch,
|
|
4056
|
-
|
|
4057
|
-
|
|
4153
|
+
remote_source_id: message.remoteSourceId,
|
|
4154
|
+
source_kind: message.sourceKind,
|
|
4155
|
+
transcript_index: message.transcriptIndex
|
|
4058
4156
|
});
|
|
4059
4157
|
db.addToOutbox("chat_message", row.id);
|
|
4158
|
+
if (message.role === "user") {
|
|
4159
|
+
db.insertUserPrompt({
|
|
4160
|
+
session_id: sessionId,
|
|
4161
|
+
project_id: projectId,
|
|
4162
|
+
prompt: message.text,
|
|
4163
|
+
cwd,
|
|
4164
|
+
user_id: config.user_id,
|
|
4165
|
+
device_id: config.device_id,
|
|
4166
|
+
agent: "claude-code",
|
|
4167
|
+
created_at_epoch: createdAtEpoch
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4060
4170
|
if (db.vecAvailable) {
|
|
4061
4171
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
4062
4172
|
if (embedding) {
|
|
@@ -4067,6 +4177,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4067
4177
|
}
|
|
4068
4178
|
return { imported, total: messages.length };
|
|
4069
4179
|
}
|
|
4180
|
+
function dedupeHistoryMessages(messages) {
|
|
4181
|
+
const deduped = [];
|
|
4182
|
+
for (const message of messages) {
|
|
4183
|
+
const compact = message.text.replace(/\s+/g, " ").trim();
|
|
4184
|
+
if (!compact)
|
|
4185
|
+
continue;
|
|
4186
|
+
const previous = deduped[deduped.length - 1];
|
|
4187
|
+
if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
|
|
4188
|
+
continue;
|
|
4189
|
+
deduped.push({ ...message, text: compact });
|
|
4190
|
+
}
|
|
4191
|
+
return deduped;
|
|
4192
|
+
}
|
|
4193
|
+
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
4194
|
+
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
4195
|
+
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4196
|
+
}
|
|
4070
4197
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4071
4198
|
const lines = [];
|
|
4072
4199
|
for (const msg of messages) {
|
|
@@ -473,6 +473,16 @@ function normalizeItem(value) {
|
|
|
473
473
|
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
// src/tools/recent-chat.ts
|
|
477
|
+
function getChatCaptureOrigin(message) {
|
|
478
|
+
if (message.source_kind === "transcript")
|
|
479
|
+
return "transcript";
|
|
480
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
481
|
+
return "history";
|
|
482
|
+
}
|
|
483
|
+
return "hook";
|
|
484
|
+
}
|
|
485
|
+
|
|
476
486
|
// src/tools/session-story.ts
|
|
477
487
|
function getSessionStory(db, input) {
|
|
478
488
|
const session = db.getSessionById(input.session_id);
|
|
@@ -595,9 +605,9 @@ function collectProvenanceSummary(observations) {
|
|
|
595
605
|
}
|
|
596
606
|
function summarizeChatSources(messages) {
|
|
597
607
|
return messages.reduce((summary, message) => {
|
|
598
|
-
summary[message
|
|
608
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
599
609
|
return summary;
|
|
600
|
-
}, { transcript: 0, hook: 0 });
|
|
610
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
601
611
|
}
|
|
602
612
|
|
|
603
613
|
// src/tools/save.ts
|
|
@@ -3060,7 +3070,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
3060
3070
|
import { join as join3 } from "node:path";
|
|
3061
3071
|
import { homedir } from "node:os";
|
|
3062
3072
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
3063
|
-
var CLIENT_VERSION = "0.4.
|
|
3073
|
+
var CLIENT_VERSION = "0.4.29";
|
|
3064
3074
|
function hashFile(filePath) {
|
|
3065
3075
|
try {
|
|
3066
3076
|
if (!existsSync3(filePath))
|
package/dist/hooks/stop.js
CHANGED
|
@@ -3082,7 +3082,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3082
3082
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3083
3083
|
risk_score: riskScore,
|
|
3084
3084
|
stacks_detected: stacks,
|
|
3085
|
-
client_version: "0.4.
|
|
3085
|
+
client_version: "0.4.29",
|
|
3086
3086
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3087
3087
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3088
3088
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3270,6 +3270,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
|
3270
3270
|
}
|
|
3271
3271
|
|
|
3272
3272
|
// src/capture/transcript.ts
|
|
3273
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3273
3274
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3274
3275
|
import { join as join5 } from "node:path";
|
|
3275
3276
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -3983,23 +3984,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
3983
3984
|
}
|
|
3984
3985
|
return messages;
|
|
3985
3986
|
}
|
|
3987
|
+
function resolveHistoryPath(historyPath) {
|
|
3988
|
+
if (historyPath)
|
|
3989
|
+
return historyPath;
|
|
3990
|
+
const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
|
|
3991
|
+
if (override)
|
|
3992
|
+
return override;
|
|
3993
|
+
return join5(homedir3(), ".claude", "history.jsonl");
|
|
3994
|
+
}
|
|
3995
|
+
function readHistoryFallback(sessionId, cwd, opts) {
|
|
3996
|
+
const path = resolveHistoryPath(opts?.historyPath);
|
|
3997
|
+
if (!existsSync4(path))
|
|
3998
|
+
return [];
|
|
3999
|
+
let raw;
|
|
4000
|
+
try {
|
|
4001
|
+
raw = readFileSync4(path, "utf-8");
|
|
4002
|
+
} catch {
|
|
4003
|
+
return [];
|
|
4004
|
+
}
|
|
4005
|
+
const targetCanonical = detectProject(cwd).canonical_id;
|
|
4006
|
+
const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
|
|
4007
|
+
const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
|
|
4008
|
+
const entries = [];
|
|
4009
|
+
for (const line of raw.split(`
|
|
4010
|
+
`)) {
|
|
4011
|
+
if (!line.trim())
|
|
4012
|
+
continue;
|
|
4013
|
+
let entry;
|
|
4014
|
+
try {
|
|
4015
|
+
entry = JSON.parse(line);
|
|
4016
|
+
} catch {
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
|
|
4020
|
+
continue;
|
|
4021
|
+
const createdAtEpoch = Math.floor(entry.timestamp / 1000);
|
|
4022
|
+
entries.push({
|
|
4023
|
+
display: entry.display.trim(),
|
|
4024
|
+
project: typeof entry.project === "string" ? entry.project : "",
|
|
4025
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
|
|
4026
|
+
timestamp: createdAtEpoch
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
|
|
4030
|
+
if (bySession.length > 0) {
|
|
4031
|
+
return dedupeHistoryMessages(bySession.map((entry) => ({
|
|
4032
|
+
role: "user",
|
|
4033
|
+
text: entry.display,
|
|
4034
|
+
createdAtEpoch: entry.timestamp
|
|
4035
|
+
})));
|
|
4036
|
+
}
|
|
4037
|
+
const byProjectAndWindow = entries.filter((entry) => {
|
|
4038
|
+
if (entry.display.length === 0)
|
|
4039
|
+
return false;
|
|
4040
|
+
if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
|
|
4041
|
+
return false;
|
|
4042
|
+
if (!entry.project)
|
|
4043
|
+
return false;
|
|
4044
|
+
return detectProject(entry.project).canonical_id === targetCanonical;
|
|
4045
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
4046
|
+
return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
|
|
4047
|
+
role: "user",
|
|
4048
|
+
text: entry.display,
|
|
4049
|
+
createdAtEpoch: entry.timestamp
|
|
4050
|
+
})));
|
|
4051
|
+
}
|
|
3986
4052
|
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
3987
|
-
const
|
|
4053
|
+
const session = db.getSessionById(sessionId);
|
|
4054
|
+
const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
3988
4055
|
...message,
|
|
3989
4056
|
text: message.text.trim()
|
|
3990
4057
|
})).filter((message) => message.text.length > 0);
|
|
4058
|
+
const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
|
|
4059
|
+
...message,
|
|
4060
|
+
sourceKind: "transcript",
|
|
4061
|
+
transcriptIndex: index + 1,
|
|
4062
|
+
createdAtEpoch: null,
|
|
4063
|
+
remoteSourceId: null
|
|
4064
|
+
})) : readHistoryFallback(sessionId, cwd, {
|
|
4065
|
+
startedAtEpoch: session?.started_at_epoch ?? null,
|
|
4066
|
+
completedAtEpoch: session?.completed_at_epoch ?? null
|
|
4067
|
+
}).map((message) => ({
|
|
4068
|
+
role: message.role,
|
|
4069
|
+
text: message.text,
|
|
4070
|
+
sourceKind: "hook",
|
|
4071
|
+
transcriptIndex: null,
|
|
4072
|
+
createdAtEpoch: message.createdAtEpoch,
|
|
4073
|
+
remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
|
|
4074
|
+
}));
|
|
3991
4075
|
if (messages.length === 0)
|
|
3992
4076
|
return { imported: 0, total: 0 };
|
|
3993
|
-
const session = db.getSessionById(sessionId);
|
|
3994
4077
|
const projectId = session?.project_id ?? null;
|
|
3995
4078
|
const now = Math.floor(Date.now() / 1000);
|
|
3996
4079
|
let imported = 0;
|
|
3997
4080
|
for (let index = 0;index < messages.length; index++) {
|
|
3998
|
-
const transcriptIndex = index + 1;
|
|
3999
|
-
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
4000
|
-
continue;
|
|
4001
4081
|
const message = messages[index];
|
|
4002
|
-
const
|
|
4082
|
+
const transcriptIndex = message.transcriptIndex ?? index + 1;
|
|
4083
|
+
if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
|
|
4084
|
+
continue;
|
|
4085
|
+
}
|
|
4086
|
+
if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
|
|
4087
|
+
continue;
|
|
4088
|
+
}
|
|
4089
|
+
const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
|
|
4003
4090
|
const row = db.insertChatMessage({
|
|
4004
4091
|
session_id: sessionId,
|
|
4005
4092
|
project_id: projectId,
|
|
@@ -4009,10 +4096,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4009
4096
|
device_id: config.device_id,
|
|
4010
4097
|
agent: "claude-code",
|
|
4011
4098
|
created_at_epoch: createdAtEpoch,
|
|
4012
|
-
|
|
4013
|
-
|
|
4099
|
+
remote_source_id: message.remoteSourceId,
|
|
4100
|
+
source_kind: message.sourceKind,
|
|
4101
|
+
transcript_index: message.transcriptIndex
|
|
4014
4102
|
});
|
|
4015
4103
|
db.addToOutbox("chat_message", row.id);
|
|
4104
|
+
if (message.role === "user") {
|
|
4105
|
+
db.insertUserPrompt({
|
|
4106
|
+
session_id: sessionId,
|
|
4107
|
+
project_id: projectId,
|
|
4108
|
+
prompt: message.text,
|
|
4109
|
+
cwd,
|
|
4110
|
+
user_id: config.user_id,
|
|
4111
|
+
device_id: config.device_id,
|
|
4112
|
+
agent: "claude-code",
|
|
4113
|
+
created_at_epoch: createdAtEpoch
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4016
4116
|
if (db.vecAvailable) {
|
|
4017
4117
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
4018
4118
|
if (embedding) {
|
|
@@ -4023,6 +4123,23 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4023
4123
|
}
|
|
4024
4124
|
return { imported, total: messages.length };
|
|
4025
4125
|
}
|
|
4126
|
+
function dedupeHistoryMessages(messages) {
|
|
4127
|
+
const deduped = [];
|
|
4128
|
+
for (const message of messages) {
|
|
4129
|
+
const compact = message.text.replace(/\s+/g, " ").trim();
|
|
4130
|
+
if (!compact)
|
|
4131
|
+
continue;
|
|
4132
|
+
const previous = deduped[deduped.length - 1];
|
|
4133
|
+
if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
|
|
4134
|
+
continue;
|
|
4135
|
+
deduped.push({ ...message, text: compact });
|
|
4136
|
+
}
|
|
4137
|
+
return deduped;
|
|
4138
|
+
}
|
|
4139
|
+
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
4140
|
+
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
4141
|
+
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4142
|
+
}
|
|
4026
4143
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4027
4144
|
const lines = [];
|
|
4028
4145
|
for (const msg of messages) {
|
|
@@ -4098,6 +4215,16 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
|
4098
4215
|
return saved;
|
|
4099
4216
|
}
|
|
4100
4217
|
|
|
4218
|
+
// src/tools/recent-chat.ts
|
|
4219
|
+
function getChatCaptureOrigin(message) {
|
|
4220
|
+
if (message.source_kind === "transcript")
|
|
4221
|
+
return "transcript";
|
|
4222
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
4223
|
+
return "history";
|
|
4224
|
+
}
|
|
4225
|
+
return "hook";
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4101
4228
|
// src/tools/session-story.ts
|
|
4102
4229
|
function getSessionStory(db, input) {
|
|
4103
4230
|
const session = db.getSessionById(input.session_id);
|
|
@@ -4220,9 +4347,9 @@ function collectProvenanceSummary(observations) {
|
|
|
4220
4347
|
}
|
|
4221
4348
|
function summarizeChatSources(messages) {
|
|
4222
4349
|
return messages.reduce((summary, message) => {
|
|
4223
|
-
summary[message
|
|
4350
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
4224
4351
|
return summary;
|
|
4225
|
-
}, { transcript: 0, hook: 0 });
|
|
4352
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
4226
4353
|
}
|
|
4227
4354
|
|
|
4228
4355
|
// src/tools/handoffs.ts
|