@inetafrica/open-claudia 1.20.0 → 2.0.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/.env.example +15 -1
- package/CHANGELOG.md +15 -0
- package/Dockerfile +3 -0
- package/README.md +2 -1
- package/bot.js +109 -3440
- package/channels/kazee/adapter.js +375 -0
- package/channels/kazee/format.js +9 -0
- package/channels/kazee/socket.js +28 -0
- package/channels/telegram/adapter.js +296 -0
- package/channels/telegram/format.js +12 -0
- package/channels/types.js +91 -0
- package/core/access.js +147 -0
- package/core/actions.js +180 -0
- package/core/adapter-registry.js +113 -0
- package/core/auth-flow.js +433 -0
- package/core/channel-wizard.js +174 -0
- package/core/commands.js +73 -0
- package/core/config.js +197 -0
- package/core/context.js +44 -0
- package/core/cron.js +77 -0
- package/core/doctor.js +126 -0
- package/core/handlers.js +995 -0
- package/core/identity.js +87 -0
- package/core/io.js +59 -0
- package/core/kazee-probe.js +48 -0
- package/core/media.js +39 -0
- package/core/onboarding.js +97 -0
- package/core/process-tree.js +40 -0
- package/core/projects.js +43 -0
- package/core/redact.js +26 -0
- package/core/router.js +309 -0
- package/core/runner.js +683 -0
- package/core/state.js +261 -0
- package/core/system-prompt.js +66 -0
- package/core/transcripts.js +71 -0
- package/core/vault-store.js +9 -0
- package/docker-entrypoint.sh +21 -4
- package/package.json +6 -3
- package/project-transcripts.js +17 -12
- package/setup.js +44 -3
package/core/state.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Per-user state buckets and persistence. State is keyed by canonical
|
|
2
|
+
// user id (`telegram:<chatId>` by default, `<email>` after /link) so
|
|
3
|
+
// linked channels share the same conversation, settings, and usage.
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { STATE_FILE, SESSIONS_FILE, CHAT_ID } = require("./config");
|
|
7
|
+
const {
|
|
8
|
+
normalizeCanonicalUserId,
|
|
9
|
+
canonicalForTelegram,
|
|
10
|
+
canonicalForStoredUserKey,
|
|
11
|
+
setIdentityMapping,
|
|
12
|
+
} = require("./identity");
|
|
13
|
+
const { currentChannelId, currentCanonicalUserId } = require("./context");
|
|
14
|
+
|
|
15
|
+
function loadStateFile() {
|
|
16
|
+
try { return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8")); } catch (e) { return {}; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mergeSavedState(existing, next) {
|
|
20
|
+
return {
|
|
21
|
+
...existing,
|
|
22
|
+
...next,
|
|
23
|
+
settings: { ...(existing.settings || {}), ...(next.settings || {}) },
|
|
24
|
+
sessionUsage: { ...(existing.sessionUsage || {}), ...(next.sessionUsage || {}) },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Multi-user state. Legacy top-level shapes are migrated to
|
|
29
|
+
// `{ users: { "<canonicalId>": {...} } }` on load.
|
|
30
|
+
const savedState = (() => {
|
|
31
|
+
const raw = loadStateFile();
|
|
32
|
+
const users = {};
|
|
33
|
+
if (raw && raw.users && typeof raw.users === "object") {
|
|
34
|
+
for (const [key, value] of Object.entries(raw.users)) {
|
|
35
|
+
const userId = canonicalForStoredUserKey(key);
|
|
36
|
+
users[userId] = mergeSavedState(users[userId] || {}, value || {});
|
|
37
|
+
}
|
|
38
|
+
return { users };
|
|
39
|
+
}
|
|
40
|
+
return { users: { [canonicalForTelegram(CHAT_ID || "")]: raw || {} } };
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
const userStates = new Map();
|
|
44
|
+
|
|
45
|
+
function freshSettings() {
|
|
46
|
+
return { model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function freshUsage() {
|
|
50
|
+
return { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createUserState(userId) {
|
|
54
|
+
const saved = (savedState.users && savedState.users[userId]) || {};
|
|
55
|
+
const settings = saved.settings || freshSettings();
|
|
56
|
+
if (!settings.backend) settings.backend = "claude";
|
|
57
|
+
return {
|
|
58
|
+
userId: String(userId),
|
|
59
|
+
channelId: currentChannelId(),
|
|
60
|
+
currentSession: saved.currentSession || null,
|
|
61
|
+
runningProcess: null,
|
|
62
|
+
statusMessageId: null,
|
|
63
|
+
streamBuffer: "",
|
|
64
|
+
streamInterval: null,
|
|
65
|
+
lastSessionId: saved.lastSessionId || null,
|
|
66
|
+
cursorSessionId: saved.cursorSessionId || null,
|
|
67
|
+
codexSessionId: saved.codexSessionId || null,
|
|
68
|
+
messageQueue: [],
|
|
69
|
+
isFirstMessage: !saved.lastSessionId,
|
|
70
|
+
lastInputWasVoice: false,
|
|
71
|
+
sessionUsage: saved.sessionUsage || freshUsage(),
|
|
72
|
+
settings,
|
|
73
|
+
onboardingStep: null,
|
|
74
|
+
onboardingData: {},
|
|
75
|
+
pendingVaultUnlock: false,
|
|
76
|
+
pendingVaultAction: null,
|
|
77
|
+
pendingClaudeAuthProcess: null,
|
|
78
|
+
pendingClaudeAuthLabel: null,
|
|
79
|
+
pendingCodexAuthProcess: null,
|
|
80
|
+
pendingCodexAuthLabel: null,
|
|
81
|
+
isCompacting: false,
|
|
82
|
+
lastCompactedAt: saved.lastCompactedAt || 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getUserState(userId) {
|
|
87
|
+
const id = normalizeCanonicalUserId(userId);
|
|
88
|
+
if (!userStates.has(id)) userStates.set(id, createUserState(id));
|
|
89
|
+
const state = userStates.get(id);
|
|
90
|
+
state.channelId = currentChannelId() || state.channelId;
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function currentState() {
|
|
95
|
+
return getUserState(currentCanonicalUserId() || canonicalForTelegram(CHAT_ID || ""));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resetSessionUsage(state = currentState()) {
|
|
99
|
+
state.sessionUsage = freshUsage();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resetSettings(state = currentState()) {
|
|
103
|
+
state.settings = freshSettings();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function saveState() {
|
|
107
|
+
const data = { users: { ...(savedState.users || {}) } };
|
|
108
|
+
for (const [id, s] of userStates) {
|
|
109
|
+
data.users[id] = {
|
|
110
|
+
currentSession: s.currentSession,
|
|
111
|
+
lastSessionId: s.lastSessionId,
|
|
112
|
+
cursorSessionId: s.cursorSessionId,
|
|
113
|
+
codexSessionId: s.codexSessionId,
|
|
114
|
+
settings: s.settings,
|
|
115
|
+
sessionUsage: s.sessionUsage,
|
|
116
|
+
lastCompactedAt: s.lastCompactedAt || 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
savedState.users = data.users;
|
|
120
|
+
try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Sessions ────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function loadSessions() {
|
|
126
|
+
let raw;
|
|
127
|
+
try { raw = JSON.parse(fs.readFileSync(SESSIONS_FILE, "utf-8")); } catch (e) { return {}; }
|
|
128
|
+
if (!raw || typeof raw !== "object") return {};
|
|
129
|
+
const looksLegacy = Object.values(raw).some((v) => Array.isArray(v));
|
|
130
|
+
if (looksLegacy) return { [canonicalForTelegram(CHAT_ID || "")]: raw };
|
|
131
|
+
const migrated = {};
|
|
132
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
133
|
+
const userId = canonicalForStoredUserKey(key);
|
|
134
|
+
migrated[userId] = { ...(migrated[userId] || {}) };
|
|
135
|
+
for (const [project, list] of Object.entries(value || {})) {
|
|
136
|
+
migrated[userId][project] = [
|
|
137
|
+
...(migrated[userId][project] || []),
|
|
138
|
+
...(Array.isArray(list) ? list : []),
|
|
139
|
+
].slice(-20);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return migrated;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function saveSessions(sessions) {
|
|
146
|
+
try { fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2)); } catch (e) {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function recordSession(userId, projectName, sessionId, title) {
|
|
150
|
+
const all = loadSessions();
|
|
151
|
+
const id = normalizeCanonicalUserId(userId);
|
|
152
|
+
if (!all[id]) all[id] = {};
|
|
153
|
+
if (!all[id][projectName]) all[id][projectName] = [];
|
|
154
|
+
const arr = all[id][projectName];
|
|
155
|
+
const existing = arr.find((s) => s.id === sessionId);
|
|
156
|
+
if (existing) {
|
|
157
|
+
if (title) existing.title = title;
|
|
158
|
+
existing.lastUsed = new Date().toISOString();
|
|
159
|
+
} else {
|
|
160
|
+
arr.push({
|
|
161
|
+
id: sessionId,
|
|
162
|
+
title: title || "Untitled",
|
|
163
|
+
created: new Date().toISOString(),
|
|
164
|
+
lastUsed: new Date().toISOString(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
all[id][projectName] = arr.slice(-20);
|
|
168
|
+
saveSessions(all);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getProjectSessions(userId, projectName) {
|
|
172
|
+
const all = loadSessions();
|
|
173
|
+
return ((all[normalizeCanonicalUserId(userId)] || {})[projectName] || []).slice().reverse();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getLastProjectSession(userId, projectName) {
|
|
177
|
+
const sessions = getProjectSessions(userId, projectName);
|
|
178
|
+
return sessions.length > 0 ? sessions[0] : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Cross-user leakage guard: only resume a Claude session id we have
|
|
182
|
+
// recorded for this user. Stops migration glitches from handing one
|
|
183
|
+
// person's conversation to another.
|
|
184
|
+
function userOwnsClaudeSession(userId, sessionId) {
|
|
185
|
+
if (!sessionId) return false;
|
|
186
|
+
const all = loadSessions();
|
|
187
|
+
const bucket = all[normalizeCanonicalUserId(userId)];
|
|
188
|
+
if (!bucket) return false;
|
|
189
|
+
for (const list of Object.values(bucket)) {
|
|
190
|
+
if (Array.isArray(list) && list.some((s) => s && s.id === sessionId)) return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function migrateUserData(fromUserId, toUserId) {
|
|
196
|
+
const from = normalizeCanonicalUserId(fromUserId);
|
|
197
|
+
const to = normalizeCanonicalUserId(toUserId);
|
|
198
|
+
if (!from || !to || from === to) return;
|
|
199
|
+
|
|
200
|
+
if (savedState.users && savedState.users[from]) {
|
|
201
|
+
savedState.users[to] = mergeSavedState(savedState.users[to] || {}, savedState.users[from]);
|
|
202
|
+
delete savedState.users[from];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (userStates.has(from)) {
|
|
206
|
+
const fromState = userStates.get(from);
|
|
207
|
+
if (userStates.has(to)) {
|
|
208
|
+
const toState = userStates.get(to);
|
|
209
|
+
Object.assign(toState, mergeSavedState(toState, fromState));
|
|
210
|
+
toState.userId = to;
|
|
211
|
+
} else {
|
|
212
|
+
fromState.userId = to;
|
|
213
|
+
userStates.set(to, fromState);
|
|
214
|
+
}
|
|
215
|
+
userStates.delete(from);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sessions = loadSessions();
|
|
219
|
+
if (sessions[from]) {
|
|
220
|
+
sessions[to] = { ...(sessions[to] || {}) };
|
|
221
|
+
for (const [project, list] of Object.entries(sessions[from])) {
|
|
222
|
+
sessions[to][project] = [
|
|
223
|
+
...(sessions[to][project] || []),
|
|
224
|
+
...(Array.isArray(list) ? list : []),
|
|
225
|
+
].slice(-20);
|
|
226
|
+
}
|
|
227
|
+
delete sessions[from];
|
|
228
|
+
saveSessions(sessions);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
saveState();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Wraps identity.setIdentityMapping with state migration so callers don't
|
|
235
|
+
// have to remember to migrate after re-linking.
|
|
236
|
+
function linkIdentity(transport, channelId, canonicalUserId) {
|
|
237
|
+
const result = setIdentityMapping(transport, channelId, canonicalUserId);
|
|
238
|
+
if (result.shouldMigrate) migrateUserData(result.previousUserId, result.userId);
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
userStates,
|
|
244
|
+
savedState,
|
|
245
|
+
freshSettings,
|
|
246
|
+
freshUsage,
|
|
247
|
+
createUserState,
|
|
248
|
+
getUserState,
|
|
249
|
+
currentState,
|
|
250
|
+
resetSessionUsage,
|
|
251
|
+
resetSettings,
|
|
252
|
+
saveState,
|
|
253
|
+
loadSessions,
|
|
254
|
+
saveSessions,
|
|
255
|
+
recordSession,
|
|
256
|
+
getProjectSessions,
|
|
257
|
+
getLastProjectSession,
|
|
258
|
+
userOwnsClaudeSession,
|
|
259
|
+
migrateUserData,
|
|
260
|
+
linkIdentity,
|
|
261
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Builds the soul + runtime context block prepended to every Claude
|
|
2
|
+
// request. Channel-aware: the "Interface" line names the actual channel
|
|
3
|
+
// the request is coming from.
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, WHISPER_CLI, FFMPEG } = require("./config");
|
|
8
|
+
const { currentState } = require("./state");
|
|
9
|
+
const { currentAdapter } = require("./context");
|
|
10
|
+
const { vault } = require("./vault-store");
|
|
11
|
+
const { transcriptPointerNote } = require("./transcripts");
|
|
12
|
+
|
|
13
|
+
function loadSoul() {
|
|
14
|
+
try { return fs.readFileSync(SOUL_FILE, "utf-8"); }
|
|
15
|
+
catch (e) { return "You are a helpful AI coding assistant."; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildSystemPrompt() {
|
|
19
|
+
const state = currentState();
|
|
20
|
+
const soul = loadSoul();
|
|
21
|
+
const hasVoice = WHISPER_CLI && FFMPEG;
|
|
22
|
+
const adapter = currentAdapter();
|
|
23
|
+
const channelLabel = adapter
|
|
24
|
+
? (adapter.type === "kazee" ? "Kazee Chat" : adapter.type === "telegram" ? "Telegram" : adapter.type)
|
|
25
|
+
: "Telegram";
|
|
26
|
+
|
|
27
|
+
return `
|
|
28
|
+
${soul}
|
|
29
|
+
|
|
30
|
+
## Runtime Context
|
|
31
|
+
- Interface: ${channelLabel} chat through Open Claudia.
|
|
32
|
+
- Active project path: ${state.currentSession ? state.currentSession.dir : "none"}
|
|
33
|
+
- Voice notes: ${hasVoice ? "enabled" : "disabled"}
|
|
34
|
+
- Vault: ${vault.isUnlocked() ? "unlocked" : "locked"}
|
|
35
|
+
- Session: ${state.lastSessionId ? "resuming existing conversation" : "new conversation"}
|
|
36
|
+
|
|
37
|
+
## Stable Local Paths
|
|
38
|
+
- Bot code: ${path.join(BOT_DIR, "bot.js")}
|
|
39
|
+
- Personality file: ${SOUL_FILE}
|
|
40
|
+
- Cron config: ${CRONS_FILE}
|
|
41
|
+
- Vault file: ${VAULT_FILE}
|
|
42
|
+
- Bot environment: ${path.join(BOT_DIR, ".env")} (sensitive; never expose values)
|
|
43
|
+
- Received user files directory: ${FILES_DIR}
|
|
44
|
+
|
|
45
|
+
${transcriptPointerNote(state)}
|
|
46
|
+
|
|
47
|
+
## Delivery
|
|
48
|
+
Reply normally in your final answer. If you must send a large file, image, or artifact directly, use the channel API/CLI; never print or embed bot tokens in prompts, commands, logs, or messages.
|
|
49
|
+
|
|
50
|
+
## Guidelines
|
|
51
|
+
- Keep responses concise — many users are on mobile.
|
|
52
|
+
- Markdown: *bold*, _italic_, \`code\`, \`\`\`code blocks\`\`\` work on both Telegram and Kazee. Skip headers (#) and links [text](url).
|
|
53
|
+
- For long output (logs, diffs, large code), save to a file and send it as an artifact instead of pasting walls of text.
|
|
54
|
+
- Act on screenshots (fix bugs, implement designs) — don't just describe what you see.
|
|
55
|
+
- When the user sends a file, it is saved in the received files directory above. Read it with the Read tool.
|
|
56
|
+
- When the user sends a credential, token, or API key, store it in the vault immediately using the vault CLI or bot commands. Tell them it's stored and that you've deleted their message for security.
|
|
57
|
+
- When asked to change your personality, edit the personality file above.
|
|
58
|
+
- When asked about yourself, you are Open Claudia — an AI coding assistant running Claude Code via Telegram or Kazee.
|
|
59
|
+
- If a task will take a while, let the user know upfront.
|
|
60
|
+
- Don't ask for confirmation on simple tasks — just do them.
|
|
61
|
+
- NEVER start long-running processes (dev servers, watchers, tails) in the foreground. They block all further messages. Instead, run them in the background: \`nohup command &\` or \`command &disown\`. Then report the PID so the user can stop it later.
|
|
62
|
+
- If asked to "start" or "run" a dev server, ALWAYS background it.
|
|
63
|
+
`.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { loadSoul, buildSystemPrompt };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Per-project transcript memory: thin wrapper around ProjectTranscripts
|
|
2
|
+
// that adds the channel/state context most callers want.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const { ProjectTranscripts } = require("../project-transcripts");
|
|
6
|
+
const {
|
|
7
|
+
CONFIG_DIR, PROJECT_TRANSCRIPTS, TRANSCRIPTS_DIR, TRANSCRIPT_MAX_ENTRY_CHARS,
|
|
8
|
+
} = require("./config");
|
|
9
|
+
const { redactSensitive } = require("./redact");
|
|
10
|
+
const { currentChannelId } = require("./context");
|
|
11
|
+
const { currentState } = require("./state");
|
|
12
|
+
|
|
13
|
+
const projectTranscripts = new ProjectTranscripts({
|
|
14
|
+
configDir: CONFIG_DIR,
|
|
15
|
+
enabled: PROJECT_TRANSCRIPTS,
|
|
16
|
+
transcriptsDir: TRANSCRIPTS_DIR,
|
|
17
|
+
maxEntryChars: TRANSCRIPT_MAX_ENTRY_CHARS,
|
|
18
|
+
redact: redactSensitive,
|
|
19
|
+
});
|
|
20
|
+
if (PROJECT_TRANSCRIPTS) {
|
|
21
|
+
try { fs.mkdirSync(projectTranscripts.transcriptsDir, { recursive: true, mode: 0o700 }); } catch (e) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function transcriptProjectInfo(state = currentState()) {
|
|
25
|
+
if (!state.currentSession) return null;
|
|
26
|
+
return projectTranscripts.pointer(state.currentSession.dir, state.currentSession.name, state.userId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function transcriptPointerNote(state = currentState()) {
|
|
30
|
+
if (!state.currentSession) return "";
|
|
31
|
+
return projectTranscripts.buildPointerNote(state.currentSession.dir, state.currentSession.name, state.userId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function appendProjectTranscript(role, text, metadata = {}, state = currentState(), getActiveSessionId) {
|
|
35
|
+
if (!state.currentSession) return null;
|
|
36
|
+
try {
|
|
37
|
+
const transport = (state.userId || "").split(":")[0] || "telegram";
|
|
38
|
+
return projectTranscripts.append({
|
|
39
|
+
role,
|
|
40
|
+
text,
|
|
41
|
+
userId: state.userId,
|
|
42
|
+
chat: { transport, id: String(currentChannelId() || "") },
|
|
43
|
+
projectName: state.currentSession.name,
|
|
44
|
+
projectPath: state.currentSession.dir,
|
|
45
|
+
backend: state.settings.backend,
|
|
46
|
+
sessionId: typeof getActiveSessionId === "function" ? getActiveSessionId() : null,
|
|
47
|
+
metadata,
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("Transcript write failed:", redactSensitive(e.message));
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function promptWithTranscriptPointer(prompt, state = currentState()) {
|
|
56
|
+
if (!state.currentSession) return prompt;
|
|
57
|
+
return projectTranscripts.withPointer(prompt, state.currentSession.dir, state.currentSession.name, state.userId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stripTranscriptPointerForStorage(prompt) {
|
|
61
|
+
return String(prompt || "").replace(/^## Project Transcript Memory\n[\s\S]*?\n\nCurrent user request:\n/, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
projectTranscripts,
|
|
66
|
+
transcriptProjectInfo,
|
|
67
|
+
transcriptPointerNote,
|
|
68
|
+
appendProjectTranscript,
|
|
69
|
+
promptWithTranscriptPointer,
|
|
70
|
+
stripTranscriptPointerForStorage,
|
|
71
|
+
};
|
package/docker-entrypoint.sh
CHANGED
|
@@ -6,13 +6,30 @@ ENV_FILE="$CONFIG_DIR/.env"
|
|
|
6
6
|
|
|
7
7
|
mkdir -p "$CONFIG_DIR"
|
|
8
8
|
|
|
9
|
-
# If .env doesn't exist and
|
|
10
|
-
|
|
9
|
+
# If .env doesn't exist and a channel token is set, auto-configure.
|
|
10
|
+
# CHANNELS env selects which channels to enable; defaults to telegram for
|
|
11
|
+
# backwards compat with v1 installs.
|
|
12
|
+
if [ ! -f "$ENV_FILE" ] && { [ -n "$TELEGRAM_BOT_TOKEN" ] || [ -n "$KAZEE_BOT_TOKEN" ]; }; then
|
|
11
13
|
echo "Auto-configuring from environment variables..."
|
|
12
14
|
|
|
15
|
+
if [ -z "$CHANNELS" ]; then
|
|
16
|
+
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$KAZEE_BOT_TOKEN" ]; then
|
|
17
|
+
CHANNELS="telegram,kazee"
|
|
18
|
+
elif [ -n "$KAZEE_BOT_TOKEN" ]; then
|
|
19
|
+
CHANNELS="kazee"
|
|
20
|
+
else
|
|
21
|
+
CHANNELS="telegram"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
|
|
13
25
|
cat > "$ENV_FILE" <<ENVEOF
|
|
14
|
-
|
|
26
|
+
CHANNELS=${CHANNELS}
|
|
27
|
+
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
|
15
28
|
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
|
29
|
+
KAZEE_URL=${KAZEE_URL:-}
|
|
30
|
+
KAZEE_BOT_TOKEN=${KAZEE_BOT_TOKEN:-}
|
|
31
|
+
KAZEE_BOT_USER_ID=${KAZEE_BOT_USER_ID:-}
|
|
32
|
+
KAZEE_OWNER_USER_ID=${KAZEE_OWNER_USER_ID:-}
|
|
16
33
|
WORKSPACE=${WORKSPACE:-$HOME/Workspace}
|
|
17
34
|
CLAUDE_PATH=${CLAUDE_PATH:-claude}
|
|
18
35
|
SOUL_FILE=$CONFIG_DIR/soul.md
|
|
@@ -30,7 +47,7 @@ ENVEOF
|
|
|
30
47
|
# Create default soul
|
|
31
48
|
if [ ! -f "$CONFIG_DIR/soul.md" ]; then
|
|
32
49
|
cat > "$CONFIG_DIR/soul.md" <<'SOUL'
|
|
33
|
-
You are a helpful AI coding assistant
|
|
50
|
+
You are a helpful AI coding assistant.
|
|
34
51
|
Keep responses concise — this is a mobile screen.
|
|
35
52
|
Act on screenshots, fix bugs, implement designs.
|
|
36
53
|
SOUL
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"open-claudia": "./bin/cli.js"
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"setup.js",
|
|
21
21
|
"web.js",
|
|
22
22
|
"config-dir.js",
|
|
23
|
+
"core/",
|
|
24
|
+
"channels/",
|
|
23
25
|
"bin/",
|
|
24
26
|
"k8s/",
|
|
25
27
|
"Dockerfile",
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"node-cron": "^4.2.1",
|
|
47
|
-
"node-telegram-bot-api": "^0.67.0"
|
|
49
|
+
"node-telegram-bot-api": "^0.67.0",
|
|
50
|
+
"socket.io-client": "^4.7.5"
|
|
48
51
|
}
|
|
49
52
|
}
|
package/project-transcripts.js
CHANGED
|
@@ -21,8 +21,10 @@ function safeName(value) {
|
|
|
21
21
|
return String(value || "project").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "project";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function projectHash(projectPath) {
|
|
25
|
-
|
|
24
|
+
function projectHash(projectPath, userId) {
|
|
25
|
+
const normalized = normalizeProjectPath(projectPath);
|
|
26
|
+
const seed = userId ? `${String(userId)} ${normalized}` : normalized;
|
|
27
|
+
return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 24);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function mkdirp(dir) {
|
|
@@ -39,13 +41,14 @@ class ProjectTranscripts {
|
|
|
39
41
|
this.redact = redact;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
projectInfo(projectPath, projectName) {
|
|
44
|
+
projectInfo(projectPath, projectName, userId) {
|
|
43
45
|
const normalizedPath = normalizeProjectPath(projectPath);
|
|
44
|
-
const hash = projectHash(normalizedPath);
|
|
46
|
+
const hash = projectHash(normalizedPath, userId);
|
|
45
47
|
const name = projectName || path.basename(normalizedPath) || "project";
|
|
46
48
|
const baseName = `${hash}-${safeName(name)}`;
|
|
47
49
|
return {
|
|
48
50
|
hash,
|
|
51
|
+
userId: userId || null,
|
|
49
52
|
projectName: name,
|
|
50
53
|
projectPath: normalizedPath,
|
|
51
54
|
transcriptPath: path.join(this.transcriptsDir, `${baseName}.jsonl`),
|
|
@@ -54,13 +57,13 @@ class ProjectTranscripts {
|
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
pointer(projectPath, projectName) {
|
|
60
|
+
pointer(projectPath, projectName, userId) {
|
|
58
61
|
if (!this.enabled || !projectPath) return null;
|
|
59
|
-
return this.projectInfo(projectPath, projectName);
|
|
62
|
+
return this.projectInfo(projectPath, projectName, userId);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
buildPointerNote(projectPath, projectName) {
|
|
63
|
-
const info = this.pointer(projectPath, projectName);
|
|
65
|
+
buildPointerNote(projectPath, projectName, userId) {
|
|
66
|
+
const info = this.pointer(projectPath, projectName, userId);
|
|
64
67
|
if (!info) return "";
|
|
65
68
|
return [
|
|
66
69
|
"## Project Transcript Memory",
|
|
@@ -71,8 +74,8 @@ class ProjectTranscripts {
|
|
|
71
74
|
].join("\n");
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
withPointer(prompt, projectPath, projectName) {
|
|
75
|
-
const note = this.buildPointerNote(projectPath, projectName);
|
|
77
|
+
withPointer(prompt, projectPath, projectName, userId) {
|
|
78
|
+
const note = this.buildPointerNote(projectPath, projectName, userId);
|
|
76
79
|
if (!note) return prompt;
|
|
77
80
|
return `${note}\n\nCurrent user request:\n${prompt}`;
|
|
78
81
|
}
|
|
@@ -96,13 +99,14 @@ class ProjectTranscripts {
|
|
|
96
99
|
|
|
97
100
|
append(entry) {
|
|
98
101
|
if (!this.enabled || !entry || !entry.projectPath) return null;
|
|
99
|
-
const
|
|
102
|
+
const canonicalUserId = entry.userId || entry.canonicalUserId || null;
|
|
103
|
+
const info = this.projectInfo(entry.projectPath, entry.projectName, canonicalUserId);
|
|
100
104
|
mkdirp(this.transcriptsDir);
|
|
101
105
|
const textData = this.truncateText(entry.text);
|
|
102
106
|
const now = new Date().toISOString();
|
|
103
107
|
const record = {
|
|
104
108
|
timestamp: entry.timestamp || now,
|
|
105
|
-
canonicalUserId
|
|
109
|
+
canonicalUserId,
|
|
106
110
|
chat: entry.chat || null,
|
|
107
111
|
project: { name: info.projectName, path: info.projectPath, hash: info.hash },
|
|
108
112
|
backend: entry.backend || null,
|
|
@@ -118,6 +122,7 @@ class ProjectTranscripts {
|
|
|
118
122
|
fs.appendFileSync(info.transcriptPath, JSON.stringify(record) + "\n", { mode: 0o600 });
|
|
119
123
|
const metadata = {
|
|
120
124
|
projectHash: info.hash,
|
|
125
|
+
canonicalUserId,
|
|
121
126
|
projectName: info.projectName,
|
|
122
127
|
projectPath: info.projectPath,
|
|
123
128
|
transcriptPath: info.transcriptPath,
|
package/setup.js
CHANGED
|
@@ -488,7 +488,9 @@ function updateEnvKey(key, value) {
|
|
|
488
488
|
|
|
489
489
|
// ── Main setup (resumable) ─────────────────────────────────────────
|
|
490
490
|
|
|
491
|
-
const STEPS = ["prerequisites", "telegram", "auth", "workspace", "vault", "config", "daemon"];
|
|
491
|
+
const STEPS = ["prerequisites", "telegram", "auth", "channels", "workspace", "vault", "config", "daemon"];
|
|
492
|
+
|
|
493
|
+
const { validateKazee } = require("./core/kazee-probe");
|
|
492
494
|
|
|
493
495
|
async function main() {
|
|
494
496
|
// Check if this is an auth subcommand
|
|
@@ -666,6 +668,36 @@ async function main() {
|
|
|
666
668
|
saveSetupState(state);
|
|
667
669
|
}
|
|
668
670
|
|
|
671
|
+
// ── Step: Extra channels (Kazee) ─────────────────────────────────
|
|
672
|
+
if (!state.completedSteps.includes("channels")) {
|
|
673
|
+
console.log("\n Extra Channels (optional)\n");
|
|
674
|
+
const addKazee = await ask(" Add a Kazee channel now? (y/n) [n]: ");
|
|
675
|
+
if (addKazee.toLowerCase() === "y") {
|
|
676
|
+
while (true) {
|
|
677
|
+
const url = (await ask(" Kazee bot API URL (e.g. https://chat.example.com/api): ")).trim();
|
|
678
|
+
if (!/^https?:\/\//i.test(url)) { console.log(" That doesn't look like a URL."); continue; }
|
|
679
|
+
const token = (await ask(" Kazee bot token: ")).trim();
|
|
680
|
+
if (!token) { console.log(" Empty token."); continue; }
|
|
681
|
+
console.log(" Validating against Kazee /me…");
|
|
682
|
+
const check = await validateKazee({ url, token });
|
|
683
|
+
if (!check.ok) {
|
|
684
|
+
console.log(` Validation failed: ${check.error}`);
|
|
685
|
+
const retry = await ask(" Try again? (y/n) [y]: ");
|
|
686
|
+
if (retry.toLowerCase() === "n") break;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const ownerUserId = (await ask(" Kazee owner user id (blank to skip): ")).trim();
|
|
690
|
+
state.data.kazee = { url: url.replace(/\/$/, ""), token, ownerUserId };
|
|
691
|
+
console.log(" Kazee channel configured.");
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
console.log(" Skipped — you can add a channel later with /channel add kazee.");
|
|
696
|
+
}
|
|
697
|
+
state.completedSteps.push("channels");
|
|
698
|
+
saveSetupState(state);
|
|
699
|
+
}
|
|
700
|
+
|
|
669
701
|
// ── Step: Workspace ──────────────────────────────────────────────
|
|
670
702
|
if (!state.completedSteps.includes("workspace")) {
|
|
671
703
|
const defaultWorkspace = path.join(CONFIG_DIR, "Workspace");
|
|
@@ -704,7 +736,9 @@ async function main() {
|
|
|
704
736
|
// ── Step: Write config ───────────────────────────────────────────
|
|
705
737
|
if (!state.completedSteps.includes("config")) {
|
|
706
738
|
const d = state.data;
|
|
707
|
-
const
|
|
739
|
+
const channels = ["telegram"];
|
|
740
|
+
if (d.kazee) channels.push("kazee");
|
|
741
|
+
const envLines = [
|
|
708
742
|
`TELEGRAM_BOT_TOKEN=${d.token}`,
|
|
709
743
|
`TELEGRAM_CHAT_ID=${d.chatId}`,
|
|
710
744
|
`WORKSPACE=${d.workspace}`,
|
|
@@ -716,8 +750,15 @@ async function main() {
|
|
|
716
750
|
`SOUL_FILE=${SOUL_FILE}`,
|
|
717
751
|
`CRONS_FILE=${CRONS_FILE}`,
|
|
718
752
|
`AUTH_FILE=${AUTH_FILE}`,
|
|
753
|
+
`CHANNELS=${channels.join(",")}`,
|
|
719
754
|
`ONBOARDED=false`,
|
|
720
|
-
]
|
|
755
|
+
];
|
|
756
|
+
if (d.kazee) {
|
|
757
|
+
envLines.push(`KAZEE_URL=${d.kazee.url}`);
|
|
758
|
+
envLines.push(`KAZEE_BOT_TOKEN=${d.kazee.token}`);
|
|
759
|
+
envLines.push(`KAZEE_OWNER_USER_ID=${d.kazee.ownerUserId || ""}`);
|
|
760
|
+
}
|
|
761
|
+
const env = envLines.join("\n");
|
|
721
762
|
|
|
722
763
|
fs.writeFileSync(ENV_FILE, env);
|
|
723
764
|
console.log(` Config saved: ${ENV_FILE}\n`);
|