@inetafrica/open-claudia 2.0.2 → 2.0.3
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/bin/cli.js +10 -0
- package/bin/transcript-window.js +202 -0
- package/core/config.js +3 -1
- package/core/runner.js +33 -21
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -233,6 +233,12 @@ switch (command) {
|
|
|
233
233
|
break;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
case "tw":
|
|
237
|
+
case "transcript-window": {
|
|
238
|
+
require("./transcript-window").run(args.slice(1));
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
236
242
|
default:
|
|
237
243
|
console.log(`
|
|
238
244
|
Open Claudia — AI Coding Assistant via Telegram
|
|
@@ -252,6 +258,10 @@ Send tools (only work inside an active bot-spawned task):
|
|
|
252
258
|
open-claudia send-photo <path> [caption]
|
|
253
259
|
open-claudia send-voice <path>
|
|
254
260
|
|
|
261
|
+
Memory tools:
|
|
262
|
+
open-claudia transcript-window <pattern> Search project transcript, show hits with context
|
|
263
|
+
(alias: tw; --help for options)
|
|
264
|
+
|
|
255
265
|
Start options:
|
|
256
266
|
--web Also start the web UI
|
|
257
267
|
--quick Skip slow health checks (Claude auth, Telegram API)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Search the project transcript JSONL for a pattern and print each hit
|
|
2
|
+
// with surrounding turns of context, capped to keep the output bounded.
|
|
3
|
+
//
|
|
4
|
+
// One JSONL line = one user or assistant turn. Output is plain text:
|
|
5
|
+
// match header, separator-delimited context blocks (before, HIT, after).
|
|
6
|
+
//
|
|
7
|
+
// Path resolution:
|
|
8
|
+
// 1. --path <file> flag
|
|
9
|
+
// 2. OC_TRANSCRIPT_PATH env (injected by core/runner.js for bot-spawned tasks)
|
|
10
|
+
// 3. error out — we don't try to guess
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
before: 2,
|
|
16
|
+
after: 2,
|
|
17
|
+
maxTurns: 10,
|
|
18
|
+
maxChars: 1200,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const out = {
|
|
23
|
+
pattern: null,
|
|
24
|
+
before: DEFAULTS.before,
|
|
25
|
+
after: DEFAULTS.after,
|
|
26
|
+
maxTurns: DEFAULTS.maxTurns,
|
|
27
|
+
maxChars: DEFAULTS.maxChars,
|
|
28
|
+
regex: false,
|
|
29
|
+
path: null,
|
|
30
|
+
json: false,
|
|
31
|
+
help: false,
|
|
32
|
+
};
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === "-h" || a === "--help") { out.help = true; continue; }
|
|
36
|
+
if (a === "--regex") { out.regex = true; continue; }
|
|
37
|
+
if (a === "--json") { out.json = true; continue; }
|
|
38
|
+
if (a === "--before") { out.before = parseInt(argv[++i], 10); continue; }
|
|
39
|
+
if (a === "--after") { out.after = parseInt(argv[++i], 10); continue; }
|
|
40
|
+
if (a === "--max-turns") { out.maxTurns = parseInt(argv[++i], 10); continue; }
|
|
41
|
+
if (a === "--max-chars") { out.maxChars = parseInt(argv[++i], 10); continue; }
|
|
42
|
+
if (a === "--path") { out.path = argv[++i]; continue; }
|
|
43
|
+
if (a.startsWith("--")) {
|
|
44
|
+
console.error(`Unknown flag: ${a}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
if (out.pattern === null) { out.pattern = a; continue; }
|
|
48
|
+
console.error(`Unexpected argument: ${a}`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isFinite(out.before) || out.before < 0) out.before = DEFAULTS.before;
|
|
52
|
+
if (!Number.isFinite(out.after) || out.after < 0) out.after = DEFAULTS.after;
|
|
53
|
+
if (!Number.isFinite(out.maxTurns) || out.maxTurns < 1) out.maxTurns = DEFAULTS.maxTurns;
|
|
54
|
+
if (!Number.isFinite(out.maxChars) || out.maxChars < 100) out.maxChars = DEFAULTS.maxChars;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(`Usage: open-claudia transcript-window <pattern> [options]
|
|
60
|
+
|
|
61
|
+
Search the active project transcript and print each hit with surrounding turns.
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--before N Turns of context before each hit (default ${DEFAULTS.before})
|
|
65
|
+
--after N Turns of context after each hit (default ${DEFAULTS.after})
|
|
66
|
+
--max-turns K Stop after K hits (default ${DEFAULTS.maxTurns})
|
|
67
|
+
--max-chars C Cap per-turn text length (default ${DEFAULTS.maxChars})
|
|
68
|
+
--regex Treat pattern as a regex (default: literal, case-insensitive)
|
|
69
|
+
--path <file> Transcript path (default: OC_TRANSCRIPT_PATH env)
|
|
70
|
+
--json Emit JSONL of matched windows instead of pretty text
|
|
71
|
+
|
|
72
|
+
Exit codes: 0 hits found, 1 no hits, 2 usage error, 3 transcript unreadable.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveTranscriptPath(flagPath) {
|
|
76
|
+
if (flagPath) return flagPath;
|
|
77
|
+
if (process.env.OC_TRANSCRIPT_PATH) return process.env.OC_TRANSCRIPT_PATH;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadTurns(transcriptPath) {
|
|
82
|
+
const raw = fs.readFileSync(transcriptPath, "utf-8");
|
|
83
|
+
const turns = [];
|
|
84
|
+
let lineNo = 0;
|
|
85
|
+
for (const line of raw.split("\n")) {
|
|
86
|
+
lineNo++;
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
try {
|
|
89
|
+
const obj = JSON.parse(line);
|
|
90
|
+
turns.push({
|
|
91
|
+
lineNo,
|
|
92
|
+
timestamp: obj.timestamp || "",
|
|
93
|
+
role: obj.role || "",
|
|
94
|
+
text: typeof obj.text === "string" ? obj.text : "",
|
|
95
|
+
sessionId: obj.sessionId || "",
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// skip malformed line
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return turns;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildMatcher(pattern, regex) {
|
|
105
|
+
if (regex) {
|
|
106
|
+
try { return new RegExp(pattern); }
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(`Invalid regex: ${e.message}`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const lc = pattern.toLowerCase();
|
|
113
|
+
return { test: (s) => s.toLowerCase().includes(lc) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function truncate(text, maxChars) {
|
|
117
|
+
if (text.length <= maxChars) return text;
|
|
118
|
+
return text.slice(0, maxChars) + `\n[...truncated, ${text.length - maxChars} more chars]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatTurn(turn, label, maxChars) {
|
|
122
|
+
return [
|
|
123
|
+
`--- ${label} line ${turn.lineNo} ${turn.timestamp} ${turn.role} ---`,
|
|
124
|
+
truncate(turn.text, maxChars),
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function run(argv) {
|
|
129
|
+
const opts = parseArgs(argv);
|
|
130
|
+
if (opts.help || !opts.pattern) {
|
|
131
|
+
printHelp();
|
|
132
|
+
process.exit(opts.help ? 0 : 2);
|
|
133
|
+
}
|
|
134
|
+
const transcriptPath = resolveTranscriptPath(opts.path);
|
|
135
|
+
if (!transcriptPath) {
|
|
136
|
+
console.error("No transcript path. Pass --path <file>, or run inside an active bot-spawned task (OC_TRANSCRIPT_PATH).");
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
140
|
+
console.error(`Transcript not found: ${transcriptPath}`);
|
|
141
|
+
process.exit(3);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let turns;
|
|
145
|
+
try { turns = loadTurns(transcriptPath); }
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error(`Failed to read transcript: ${e.message}`);
|
|
148
|
+
process.exit(3);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const matcher = buildMatcher(opts.pattern, opts.regex);
|
|
152
|
+
const hits = [];
|
|
153
|
+
for (let i = 0; i < turns.length; i++) {
|
|
154
|
+
if (matcher.test(turns[i].text)) {
|
|
155
|
+
hits.push(i);
|
|
156
|
+
if (hits.length >= opts.maxTurns) break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (hits.length === 0) {
|
|
161
|
+
console.error(`No matches for ${JSON.stringify(opts.pattern)} in ${transcriptPath}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (opts.json) {
|
|
166
|
+
for (let h = 0; h < hits.length; h++) {
|
|
167
|
+
const idx = hits[h];
|
|
168
|
+
const window = [];
|
|
169
|
+
for (let j = Math.max(0, idx - opts.before); j <= Math.min(turns.length - 1, idx + opts.after); j++) {
|
|
170
|
+
window.push({ ...turns[j], offset: j - idx });
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(JSON.stringify({ matchIndex: h + 1, matchLine: turns[idx].lineNo, window }) + "\n");
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`Transcript: ${transcriptPath}`);
|
|
178
|
+
console.log(`Pattern: ${JSON.stringify(opts.pattern)}${opts.regex ? " (regex)" : ""}`);
|
|
179
|
+
console.log(`Hits: ${hits.length} Window: -${opts.before}/+${opts.after} turns Per-turn cap: ${opts.maxChars} chars`);
|
|
180
|
+
console.log("");
|
|
181
|
+
|
|
182
|
+
for (let h = 0; h < hits.length; h++) {
|
|
183
|
+
const idx = hits[h];
|
|
184
|
+
const start = Math.max(0, idx - opts.before);
|
|
185
|
+
const end = Math.min(turns.length - 1, idx + opts.after);
|
|
186
|
+
console.log(`===== match ${h + 1}/${hits.length} line ${turns[idx].lineNo} =====`);
|
|
187
|
+
for (let j = start; j <= end; j++) {
|
|
188
|
+
const offset = j - idx;
|
|
189
|
+
const label = offset === 0 ? "HIT" : (offset < 0 ? `ctx ${offset}` : `ctx +${offset}`);
|
|
190
|
+
console.log(formatTurn(turns[j], label, opts.maxChars));
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { run, parseArgs, loadTurns };
|
|
199
|
+
|
|
200
|
+
if (require.main === module) {
|
|
201
|
+
run(process.argv.slice(2));
|
|
202
|
+
}
|
package/core/config.js
CHANGED
|
@@ -47,7 +47,8 @@ const WORKSPACE = config.WORKSPACE;
|
|
|
47
47
|
const CLAUDE_PATH = config.CLAUDE_PATH;
|
|
48
48
|
const CURSOR_PATH = config.CURSOR_PATH || null;
|
|
49
49
|
const CODEX_PATH = config.CODEX_PATH || null;
|
|
50
|
-
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "
|
|
50
|
+
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "380000", 10);
|
|
51
|
+
const MIN_COMPACT_INTERVAL_MS = parseInt(config.MIN_COMPACT_INTERVAL_MS || process.env.MIN_COMPACT_INTERVAL_MS || "1800000", 10);
|
|
51
52
|
const PROJECT_TRANSCRIPTS = configTruthy(config.PROJECT_TRANSCRIPTS || process.env.PROJECT_TRANSCRIPTS, true);
|
|
52
53
|
const TRANSCRIPT_MAX_ENTRY_CHARS = parseInt(config.TRANSCRIPT_MAX_ENTRY_CHARS || process.env.TRANSCRIPT_MAX_ENTRY_CHARS || "12000", 10);
|
|
53
54
|
const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR || path.join(CONFIG_DIR, "transcripts");
|
|
@@ -185,6 +186,7 @@ module.exports = {
|
|
|
185
186
|
resolvedCursorPath,
|
|
186
187
|
resolvedCodexPath,
|
|
187
188
|
AUTO_COMPACT_TOKENS,
|
|
189
|
+
MIN_COMPACT_INTERVAL_MS,
|
|
188
190
|
PROJECT_TRANSCRIPTS,
|
|
189
191
|
TRANSCRIPT_MAX_ENTRY_CHARS,
|
|
190
192
|
TRANSCRIPTS_DIR,
|
package/core/runner.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const { spawn } = require("child_process");
|
|
7
7
|
const {
|
|
8
8
|
CLAUDE_PATH, resolvedCursorPath, resolvedCodexPath,
|
|
9
|
-
AUTO_COMPACT_TOKENS, MAX_PROCESS_TIMEOUT, botSubprocessEnv,
|
|
9
|
+
AUTO_COMPACT_TOKENS, MIN_COMPACT_INTERVAL_MS, MAX_PROCESS_TIMEOUT, botSubprocessEnv,
|
|
10
10
|
} = require("./config");
|
|
11
11
|
const { currentState, saveState, recordSession, userOwnsClaudeSession } = require("./state");
|
|
12
12
|
const { chatContext, currentChannelId, currentAdapter } = require("./context");
|
|
@@ -33,6 +33,8 @@ function chatEnvOverlay() {
|
|
|
33
33
|
overlay.OC_SEND_URL = lb.url;
|
|
34
34
|
overlay.OC_SEND_TOKEN = lb.token;
|
|
35
35
|
}
|
|
36
|
+
const tinfo = transcriptProjectInfo();
|
|
37
|
+
if (tinfo && tinfo.transcriptPath) overlay.OC_TRANSCRIPT_PATH = tinfo.transcriptPath;
|
|
36
38
|
return overlay;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -71,8 +73,11 @@ function getActiveSessionKey(state = currentState()) {
|
|
|
71
73
|
function shouldAutoCompact(state = currentState(), opts = {}) {
|
|
72
74
|
if (opts.skipAutoCompact || opts.fresh || opts.continueSession || state.isCompacting) return false;
|
|
73
75
|
if (!state[getActiveSessionKey(state)]) return false;
|
|
74
|
-
const threshold = Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS :
|
|
75
|
-
|
|
76
|
+
const threshold = Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS : 380000;
|
|
77
|
+
if ((state.sessionUsage?.lastInputTokens || 0) < threshold) return false;
|
|
78
|
+
const minInterval = Number.isFinite(MIN_COMPACT_INTERVAL_MS) ? MIN_COMPACT_INTERVAL_MS : 1800000;
|
|
79
|
+
if (state.lastCompactedAt && (Date.now() - state.lastCompactedAt) < minInterval) return false;
|
|
80
|
+
return true;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function buildClaudeArgs(prompt, opts = {}) {
|
|
@@ -181,13 +186,20 @@ function claudeEmptyFailureMessage(code, stderrText = "") {
|
|
|
181
186
|
function compactSummaryPrompt() {
|
|
182
187
|
return [
|
|
183
188
|
"Summarize this conversation for a fresh compacted continuation.",
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"-
|
|
188
|
-
"-
|
|
189
|
-
"-
|
|
190
|
-
"
|
|
189
|
+
"Write a brief that lets your future self resume work without re-asking the user.",
|
|
190
|
+
"",
|
|
191
|
+
"Include:",
|
|
192
|
+
"- Current user goal and any active side threads or sub-goals.",
|
|
193
|
+
"- Locked-in constraints, preferences, and decisions (with the WHY when non-obvious).",
|
|
194
|
+
"- Infrastructure / release facts: how things deploy, publish, or release; which CI handles what; required env or secrets; commands the user said NOT to run locally.",
|
|
195
|
+
"- Files and repos touched, current branch / commit / tag state, what is committed vs uncommitted, what is pushed vs local.",
|
|
196
|
+
"- Commands already run and their results — so you do not re-run completed setup and do not re-attempt failed steps.",
|
|
197
|
+
"- Open TODOs, blockers, and the exact next step.",
|
|
198
|
+
"- User pushback or corrections this session — your future self must not repeat the mistake. Quote the correction if short.",
|
|
199
|
+
"- Stable paths, IDs, PIDs, owner IDs, and reference URLs the work depends on.",
|
|
200
|
+
"",
|
|
201
|
+
"Do not include: secrets, raw tokens, full file dumps, or chat pleasantries.",
|
|
202
|
+
"Be concrete. Names of files, commands, flags, tags, and commits beat paraphrase. If a fact was load-bearing in this session, write it down verbatim.",
|
|
191
203
|
].join("\n");
|
|
192
204
|
}
|
|
193
205
|
|
|
@@ -197,6 +209,17 @@ function compactSeedPrompt(summary) {
|
|
|
197
209
|
"Treat the following summary as the prior conversation context. Do not repeat it back unless asked.",
|
|
198
210
|
"Continue from this state in future turns.",
|
|
199
211
|
"",
|
|
212
|
+
"Before telling the user you lack context on something they reference:",
|
|
213
|
+
"1. Check the summary below.",
|
|
214
|
+
"2. Search the project transcript with `open-claudia transcript-window <pattern>`.",
|
|
215
|
+
" It returns each hit with surrounding turns of context, capped per turn so it stays bounded.",
|
|
216
|
+
" Useful flags: --before N / --after N (default 2), --max-turns K (default 10), --regex.",
|
|
217
|
+
" Fall back to `grep -n -C 5 <pattern> <transcript-path>` (path in your system prompt under",
|
|
218
|
+
" 'Project Transcript Memory') only if the helper does not fit your search.",
|
|
219
|
+
"Only ask the user if both turn up nothing.",
|
|
220
|
+
"",
|
|
221
|
+
"If a fact in the summary contradicts current repo state (a file path, a command, a flag, a version), trust what you observe now and proceed without flagging it unless the user asks.",
|
|
222
|
+
"",
|
|
200
223
|
"Compacted summary:",
|
|
201
224
|
summary,
|
|
202
225
|
].join("\n");
|
|
@@ -366,17 +389,6 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
366
389
|
return;
|
|
367
390
|
}
|
|
368
391
|
|
|
369
|
-
if (shouldAutoCompact(state, opts)) {
|
|
370
|
-
try {
|
|
371
|
-
await compactActiveSession(cwd, {
|
|
372
|
-
notify: true,
|
|
373
|
-
message: "Context is getting large, compacting first so this stays fast…",
|
|
374
|
-
});
|
|
375
|
-
} catch (e) {
|
|
376
|
-
await send(`Compaction failed: ${redactSensitive(e.message)}\nContinuing in the existing session.`, { replyTo: replyToMsgId });
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
392
|
appendProjectTranscript("user", prompt, {
|
|
381
393
|
sourceMessageId: replyToMsgId || null,
|
|
382
394
|
fresh: !!opts.fresh,
|
package/package.json
CHANGED