@inetafrica/open-claudia 2.14.8 → 3.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 +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
package/core/web-auth.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
const { spawn } = require("child_process");
|
|
8
8
|
const os = require("os");
|
|
9
|
-
const {
|
|
9
|
+
const { discoverProviderExecutable } = require("./config");
|
|
10
|
+
const { inspectProviderConfiguration } = require("./provider-status");
|
|
10
11
|
const { redactSensitive, stripTerminalControls, extractUrls } = require("./redact");
|
|
11
12
|
const {
|
|
12
|
-
claudeSubprocessEnv, saveClaudeOAuthToken, getClaudeOAuthToken,
|
|
13
|
+
claudeSubprocessEnv, codexSubprocessEnv, saveClaudeOAuthToken, getClaudeOAuthToken,
|
|
13
14
|
extractClaudeToken, isClaudeAuthUrl, looksLikeClaudeToken, looksLikeOpenAIKey,
|
|
14
|
-
|
|
15
|
-
runCodexLoginStatus, saveCodexApiKeyWithCli,
|
|
15
|
+
saveCodexApiKeyWithCli,
|
|
16
16
|
} = require("./auth-flow");
|
|
17
17
|
|
|
18
18
|
const HOME = process.env.HOME || os.homedir();
|
|
@@ -22,7 +22,9 @@ let claude = null;
|
|
|
22
22
|
|
|
23
23
|
function startClaudeLogin() {
|
|
24
24
|
if (claude && claude.proc && !claude.done) return { ok: true, already: true };
|
|
25
|
-
const
|
|
25
|
+
const binary = discoverProviderExecutable("claude");
|
|
26
|
+
if (!binary) return { ok: false, error: "Claude Code CLI not found." };
|
|
27
|
+
const proc = spawn(binary, ["auth", "login", "--claudeai"], {
|
|
26
28
|
cwd: HOME, env: claudeSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
27
29
|
});
|
|
28
30
|
claude = { proc, url: null, log: "", awaitingCode: false, done: false, ok: false, error: null, tokenStored: false };
|
|
@@ -89,10 +91,11 @@ function saveClaudeToken(token) {
|
|
|
89
91
|
let codex = null;
|
|
90
92
|
|
|
91
93
|
function startCodexLogin() {
|
|
92
|
-
|
|
94
|
+
const binary = discoverProviderExecutable("codex");
|
|
95
|
+
if (!binary) return { ok: false, error: "Codex CLI not found." };
|
|
93
96
|
if (codex && codex.proc && !codex.done) return { ok: true, already: true };
|
|
94
|
-
const proc = spawn(
|
|
95
|
-
cwd: HOME, env:
|
|
97
|
+
const proc = spawn(binary, ["login", "--device-auth"], {
|
|
98
|
+
cwd: HOME, env: codexSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
96
99
|
});
|
|
97
100
|
codex = { proc, url: null, code: null, log: "", done: false, ok: false, error: null };
|
|
98
101
|
const onChunk = (buf) => {
|
|
@@ -135,7 +138,7 @@ function codexState() {
|
|
|
135
138
|
|
|
136
139
|
// Reliable path: save a pasted OpenAI API key via the Codex CLI.
|
|
137
140
|
async function saveCodexApiKey(key) {
|
|
138
|
-
if (!
|
|
141
|
+
if (!discoverProviderExecutable("codex")) return { ok: false, error: "Codex CLI not found." };
|
|
139
142
|
const k = String(key || "").trim();
|
|
140
143
|
if (!looksLikeOpenAIKey(k)) return { ok: false, error: "That does not look like an OpenAI API key." };
|
|
141
144
|
const result = await saveCodexApiKeyWithCli(k);
|
|
@@ -145,11 +148,24 @@ async function saveCodexApiKey(key) {
|
|
|
145
148
|
// ── Combined status ─────────────────────────────────────────────────
|
|
146
149
|
function authStatus() {
|
|
147
150
|
const tokenInfo = getClaudeOAuthToken();
|
|
148
|
-
const
|
|
149
|
-
const
|
|
151
|
+
const summary = inspectProviderConfiguration();
|
|
152
|
+
const claudeStatus = summary.providers.find((provider) => provider.id === "claude");
|
|
153
|
+
const codexStatus = summary.providers.find((provider) => provider.id === "codex");
|
|
150
154
|
return {
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
providers: summary.providers,
|
|
156
|
+
defaultProvider: summary.defaultProvider,
|
|
157
|
+
// Transitional fields keep existing dashboard clients working while the
|
|
158
|
+
// generic provider list is the authoritative status surface.
|
|
159
|
+
claude: {
|
|
160
|
+
configured: !!tokenInfo.value,
|
|
161
|
+
source: tokenInfo.source,
|
|
162
|
+
cliFound: claudeStatus?.availability.state === "available",
|
|
163
|
+
loggedIn: claudeStatus?.auth.state === "authenticated",
|
|
164
|
+
},
|
|
165
|
+
codex: {
|
|
166
|
+
cliFound: codexStatus?.availability.state === "available",
|
|
167
|
+
loggedIn: codexStatus?.auth.state === "authenticated",
|
|
168
|
+
},
|
|
153
169
|
};
|
|
154
170
|
}
|
|
155
171
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Revocable dashboard sessions. The web login used to set the cookie to a
|
|
2
|
+
// static sha256(password): identical for every session, valid until the password
|
|
3
|
+
// changed, and impossible to revoke — a leaked cookie or /otp link granted
|
|
4
|
+
// permanent access. Instead we mint a random 256-bit token per login, track it
|
|
5
|
+
// server-side with an expiry, and validate the cookie against that store. Tokens
|
|
6
|
+
// can be individually revoked, expire on their own, and are all dropped when the
|
|
7
|
+
// password changes. Persisted via the atomic writer (core/fsutil) so a login
|
|
8
|
+
// survives a bot restart/upgrade, and a torn write falls back to the last good
|
|
9
|
+
// file rather than logging everyone out.
|
|
10
|
+
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const CONFIG_DIR = require("../config-dir");
|
|
14
|
+
const { atomicWriteFileSync, readJsonWithFallback } = require("./fsutil");
|
|
15
|
+
|
|
16
|
+
const SESSIONS_FILE = path.join(CONFIG_DIR, "web-sessions.json");
|
|
17
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24h — matches the cookie Max-Age
|
|
18
|
+
|
|
19
|
+
function load() {
|
|
20
|
+
const raw = readJsonWithFallback(SESSIONS_FILE, {});
|
|
21
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function persist(sessions) {
|
|
25
|
+
try { atomicWriteFileSync(SESSIONS_FILE, JSON.stringify(sessions), { backup: true }); }
|
|
26
|
+
catch (e) { console.error("web-sessions: write failed:", e.message); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Drop expired (or malformed) entries in place. Returns true if anything went.
|
|
30
|
+
function sweep(sessions, now = Date.now()) {
|
|
31
|
+
let changed = false;
|
|
32
|
+
for (const token of Object.keys(sessions)) {
|
|
33
|
+
const meta = sessions[token];
|
|
34
|
+
if (!meta || typeof meta.expires !== "number" || meta.expires <= now) {
|
|
35
|
+
delete sessions[token];
|
|
36
|
+
changed = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return changed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mint and persist a fresh session token, returning it for the Set-Cookie.
|
|
43
|
+
function issue() {
|
|
44
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
45
|
+
const sessions = load();
|
|
46
|
+
sweep(sessions);
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
sessions[token] = { created: now, expires: now + TTL_MS };
|
|
49
|
+
persist(sessions);
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Is this cookie token a live, unexpired session? hasOwnProperty guards against
|
|
54
|
+
// an attacker presenting "__proto__"/"constructor" and hitting the prototype.
|
|
55
|
+
function valid(token) {
|
|
56
|
+
if (!token || typeof token !== "string") return false;
|
|
57
|
+
const sessions = load();
|
|
58
|
+
if (!Object.prototype.hasOwnProperty.call(sessions, token)) return false;
|
|
59
|
+
const meta = sessions[token];
|
|
60
|
+
if (!meta || typeof meta.expires !== "number" || meta.expires <= Date.now()) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function revoke(token) {
|
|
65
|
+
if (!token) return;
|
|
66
|
+
const sessions = load();
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(sessions, token)) {
|
|
68
|
+
delete sessions[token];
|
|
69
|
+
persist(sessions);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Kill every session — used when the password changes.
|
|
74
|
+
function revokeAll() {
|
|
75
|
+
persist({});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { issue, valid, revoke, revokeAll, sweep, TTL_MS, SESSIONS_FILE };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Open Claudia — Channel design (Phase 0/2)
|
|
2
|
+
|
|
3
|
+
> Provider-parity update: current direct and agent modes both run through the
|
|
4
|
+
> modular `bot.js` entrypoint. `bot-agent.js` is now only a compatibility shim.
|
|
5
|
+
> Channel adapters dispatch normalized turns to the provider registry and do
|
|
6
|
+
> not choose a provider or construct provider-specific CLI arguments.
|
|
7
|
+
> Provider state/session/job changes use the verified snapshot and rollback
|
|
8
|
+
> process in [PROVIDER_MIGRATION.md](./PROVIDER_MIGRATION.md). References below
|
|
9
|
+
> to leaving the old monolith unchanged describe the historical channel refactor.
|
|
10
|
+
|
|
11
|
+
Companion to MULTI_CHANNEL_PLAN.md. Decides the concrete shape of channels in code before refactor begins.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Channel-config mechanism
|
|
16
|
+
|
|
17
|
+
Channels are a configurable list. Each entry declares an adapter type + adapter-specific env keys. No hardcoded if/else in core.
|
|
18
|
+
|
|
19
|
+
`.env` example (both enabled):
|
|
20
|
+
```
|
|
21
|
+
CHANNELS=telegram,kazee
|
|
22
|
+
|
|
23
|
+
# telegram adapter
|
|
24
|
+
TELEGRAM_BOT_TOKEN=...
|
|
25
|
+
TELEGRAM_CHAT_ID=6251055967
|
|
26
|
+
|
|
27
|
+
# kazee adapter
|
|
28
|
+
KAZEE_URL=https://chat.kazee.africa
|
|
29
|
+
KAZEE_BOT_TOKEN=bot_...
|
|
30
|
+
KAZEE_BOT_USER_ID=64f8...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Backwards compat: if `CHANNELS` is unset, default to `telegram` so existing installs keep working (no migration on v1.21.0).
|
|
34
|
+
|
|
35
|
+
`core/config.js` exports `loadChannels()` returning `[{id, type, opts}]`. Bot bootstrap iterates and calls `createAdapter(type, opts)` per entry. Unknown type → log + skip, not fatal.
|
|
36
|
+
|
|
37
|
+
## 2. ChannelAdapter contract
|
|
38
|
+
|
|
39
|
+
One JS class per channel. All identified IO with the outside world (Telegram API, Kazee REST/socket) lives behind this surface — core code never touches transport SDKs directly.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// channels/types.js — JSDoc-only contract (no runtime base class)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} InboundEnvelope
|
|
46
|
+
* @property {string} channelId // raw transport id (telegram chatId, kazee chatId)
|
|
47
|
+
* @property {string} canonicalUserId // "telegram:123" | "kazee:abc"
|
|
48
|
+
* @property {string} userId // raw transport user id
|
|
49
|
+
* @property {string} [text]
|
|
50
|
+
* @property {string} [replyToId]
|
|
51
|
+
* @property {Array<MediaRef>} [mediaRefs]
|
|
52
|
+
* @property {string} type // "text" | "voice" | "audio" | "photo" | "document" | "action"
|
|
53
|
+
* @property {any} raw // original payload, adapter-specific
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} SendOpts
|
|
58
|
+
* @property {string} [parseMode]
|
|
59
|
+
* @property {Object} [keyboard] // {inline_keyboard:[[{text,callback_data}]]} OR
|
|
60
|
+
* // {buttons:[{id,label,style,payload}]} for kazee interactive
|
|
61
|
+
* @property {string|number} [replyTo]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every adapter implements:
|
|
66
|
+
* id: string "telegram" | "kazee"
|
|
67
|
+
* start(): Promise<void>
|
|
68
|
+
* stop(): Promise<void>
|
|
69
|
+
* on(event, fn): unsubscribe events: "message" | "action"
|
|
70
|
+
* send(channelId, text, opts): Promise<messageId|null>
|
|
71
|
+
* edit(channelId, messageId, text, opts): Promise<void>
|
|
72
|
+
* delete(channelId, messageId): Promise<void>
|
|
73
|
+
* sendVoice(channelId, oggPath): Promise<boolean>
|
|
74
|
+
* sendFile(channelId, filePath, caption?): Promise<boolean>
|
|
75
|
+
* typing(channelId): Promise<void>
|
|
76
|
+
* downloadMedia(mediaRef): Promise<localPath>
|
|
77
|
+
*/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Notes
|
|
81
|
+
- `keyboard` is the one leaky abstraction. Telegram's `inline_keyboard` and Kazee's `interactive.buttons` differ enough that core builds a portable representation and each adapter renders it natively. Core uses `{buttons: [{id, label, style?, payload?}]}` as the canonical shape; the telegram adapter converts to `inline_keyboard`.
|
|
82
|
+
- `channelId` is opaque to core. Adapters route on it. Same chatId may exist on different channels — disambiguation happens via the adapter the message arrived on.
|
|
83
|
+
- `canonicalUserId` is the only user identifier core code cares about. It maps to per-user state.
|
|
84
|
+
|
|
85
|
+
## 3. Channel-aware chat context
|
|
86
|
+
|
|
87
|
+
Today: `chatContext = new AsyncLocalStorage()` stores chatId as a plain string. Every `send/edit/...` reads `currentChatId()`.
|
|
88
|
+
|
|
89
|
+
After: context stores `{adapter, channelId, canonicalUserId}`. New helpers:
|
|
90
|
+
```js
|
|
91
|
+
core/context.js
|
|
92
|
+
runInChat({adapter, channelId, canonicalUserId}, fn)
|
|
93
|
+
currentAdapter() // returns the ChannelAdapter for the in-flight request
|
|
94
|
+
currentChannelId() // raw channel id
|
|
95
|
+
currentCanonicalUserId()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`send(text, opts)` becomes `currentAdapter().send(currentChannelId(), text, opts)`. All 19 Telegram primitives flatten down to ~7 adapter methods + utility helpers.
|
|
99
|
+
|
|
100
|
+
## 4. Command registry
|
|
101
|
+
|
|
102
|
+
`core/commands.js` exports a registry:
|
|
103
|
+
```js
|
|
104
|
+
register({
|
|
105
|
+
name: "model",
|
|
106
|
+
description: "Pick or show active model",
|
|
107
|
+
args: [{name: "model", required: false}],
|
|
108
|
+
handler: async (ctx, args) => { ... },
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
- `ctx` = `{canonicalUserId, channelId, adapter, raw, replyToId}`.
|
|
112
|
+
- `args` = `string[]` (raw tokens after `/cmd`), parser is intentionally dumb so handlers keep flexibility.
|
|
113
|
+
- The router parses `/cmd args` from inbound text and dispatches. Both adapters share one parser.
|
|
114
|
+
- Telegram-specific behaviour (regex multimatch like `/cron add "..." "..." "..."`) — handlers that need richer parsing can fall back to raw text via `ctx.raw.text`.
|
|
115
|
+
|
|
116
|
+
On startup the bot:
|
|
117
|
+
1. Loads registry.
|
|
118
|
+
2. Telegram adapter: no-op (Telegram has its own BotFather menu we don't auto-register; current behaviour preserved).
|
|
119
|
+
3. Kazee adapter: `PUT /bot/<botId>/commands` with `[{name, description, args}]` so web/mobile slash menus populate.
|
|
120
|
+
|
|
121
|
+
## 5. Refactor strategy (low-risk, incremental)
|
|
122
|
+
|
|
123
|
+
The 3544-line bot.js gets split across multiple commits, each shippable on its own and behaviour-preserving for Telegram.
|
|
124
|
+
|
|
125
|
+
| Step | Commit | Scope | Risk |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| A | extract config | `core/config.js` (loadEnv + loadChannels) | nil |
|
|
128
|
+
| B | extract identity | `core/identity.js` (canonicalForChannel, identities file IO) | nil |
|
|
129
|
+
| C | extract state | `core/state.js` (userStates, savedState, sessions persistence) | low |
|
|
130
|
+
| D | extract commands registry | `core/commands.js` empty registry + router that delegates to existing `bot.onText` handlers | nil (no behaviour change) |
|
|
131
|
+
| E | introduce ChannelAdapter | `channels/telegram/adapter.js` wraps existing `bot.sendMessage/...`. `chatContext` now stores `{adapter,channelId}`. All `send/edit/delete/sendVoice` in bot.js refactored to delegate to `currentAdapter()` | medium — touches every IO call site |
|
|
132
|
+
| F | migrate handlers to registry | Convert each `bot.onText` into a `register({...})` entry. Telegram adapter routes inbound text → registry. | medium — 35+ touchpoints, but mechanical |
|
|
133
|
+
| G | ship v1.21.0 | smoke test Telegram parity end-to-end | — |
|
|
134
|
+
| H | kazee adapter | `channels/kazee/adapter.js` — socket client, send/edit/delete/sendFile/sendVoice, interactive button shape | new code, no Telegram risk |
|
|
135
|
+
| I | ship v1.22.0 | kazee adapter behind `CHANNELS=telegram,kazee` once dev verified | — |
|
|
136
|
+
|
|
137
|
+
After step G the file tree looks like:
|
|
138
|
+
```
|
|
139
|
+
open-claudia/
|
|
140
|
+
bot.js # ~200 lines — bootstrap, signal handling, channel loop
|
|
141
|
+
core/
|
|
142
|
+
config.js
|
|
143
|
+
identity.js
|
|
144
|
+
state.js
|
|
145
|
+
context.js
|
|
146
|
+
commands.js
|
|
147
|
+
router.js # parses /cmd, dispatches registry, handles voice/media
|
|
148
|
+
transcripts.js # re-export of project-transcripts.js
|
|
149
|
+
channels/
|
|
150
|
+
types.js # JSDoc
|
|
151
|
+
telegram/
|
|
152
|
+
adapter.js
|
|
153
|
+
format.js # Telegram-MD quirks (single-asterisk bold)
|
|
154
|
+
bot-agent.js # unchanged
|
|
155
|
+
health.js # unchanged
|
|
156
|
+
web.js # unchanged
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
After step H:
|
|
160
|
+
```
|
|
161
|
+
channels/kazee/
|
|
162
|
+
adapter.js
|
|
163
|
+
format.js # standard markdown, no chunking
|
|
164
|
+
socket.js # socket.io-client wrapper
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 6. Open items I'm flagging now
|
|
168
|
+
|
|
169
|
+
1. **Telegram `parseMode: "Markdown"` vs `MarkdownV2`** — many existing `send(text, {parseMode: "Markdown"})` calls. Kazee adapter ignores parseMode (always standard MD). Need adapter-side normalisation if we ever ship the same text through both. For Phase 0, parseMode flows through unchanged for Telegram; Kazee adapter silently drops it.
|
|
170
|
+
2. **Reply IDs across channels** — `replyTo` is a Telegram `message_id`. Kazee uses Mongo `_id`. Already opaque in `SendOpts`. Fine.
|
|
171
|
+
3. **`bot.on("callback_query")`** in Telegram becomes a `{type:"action", actionId, payload}` envelope on the adapter. Same path Kazee uses for button presses. Core router sees both the same way.
|
|
172
|
+
4. **Voice download path** — `downloadFile(fileId, ext)` is Telegram-specific. Becomes `adapter.downloadMedia(mediaRef)` returning a local file path. Telegram adapter wraps `bot.getFile + https.get`; Kazee adapter does `GET <minio-url>`.
|
|
173
|
+
5. **Crons & notifyError** — both currently call `bot.sendMessage(CHAT_ID, ...)` directly. Need to route through the *owner's* preferred adapter. Cron entries store `canonicalUserId`; lookup their preferred channel via identities and dispatch via that adapter. (`notifyError` keeps Telegram-only fallback — last-resort crash channel.)
|
|
174
|
+
|
|
175
|
+
## 7. What I won't change in this refactor
|
|
176
|
+
|
|
177
|
+
- At that time, `bot-agent.js` stayed as the Claude runner subprocess wrapper; it is now a provider-neutral compatibility shim.
|
|
178
|
+
- `health.js`, `web.js`, `setup.js` — untouched.
|
|
179
|
+
- Historical subprocess management stayed unchanged during this channel-only refactor; provider convergence happened later.
|
|
180
|
+
- Vault, soul, crons schemas on disk — same files, same format. No migration.
|
|
181
|
+
- Identities file format — adds `kazee:<userId>` keys naturally; no schema change needed.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Open Claudia — Multi-Channel Plan (Telegram + Kazee Chat)
|
|
2
|
+
|
|
3
|
+
> This is a historical design plan. The target adapter architecture has since
|
|
4
|
+
> shipped; Section 1 records the current Open Claudia implementation, while the
|
|
5
|
+
> later rollout estimates and external-client notes are retained as design history.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
Make Open Claudia channel-agnostic, with Telegram as one adapter and **Kazee Chat** as a second equal-class adapter. Slash commands must work natively in both.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Current implementation
|
|
13
|
+
|
|
14
|
+
### open-claudia
|
|
15
|
+
- `bot.js` is the modular runtime bootstrap; Telegram, Kazee, and voice are channel adapters behind one normalized router and command registry.
|
|
16
|
+
- Async chat context carries the adapter, channel ID, and canonical user ID. Linked channels can resolve to one canonical identity and share provider/project state safely.
|
|
17
|
+
- All model-bearing paths call the provider-neutral runner. The provider registry owns Claude Code and OpenAI Codex capabilities, invocation arguments, event parsing, auth status, and native session semantics.
|
|
18
|
+
- Setup, web configuration, doctor, and status remain available with no provider installed. Model turns require one compatible authenticated provider.
|
|
19
|
+
- Provider-aware state, sessions, and scheduled jobs use the verified snapshot and rollback procedure in [PROVIDER_MIGRATION.md](./PROVIDER_MIGRATION.md).
|
|
20
|
+
- Shared operator stores such as the vault and soul remain intentionally global; conversations, settings, usage, and transcripts are canonical-user/project/provider scoped.
|
|
21
|
+
|
|
22
|
+
### Historical external-service baseline: chat-central (ActionHero + Socket.IO + Mongo + Redis)
|
|
23
|
+
- **Bot infrastructure already exists**:
|
|
24
|
+
- `User.type: "Bot"` with `botToken` field (unique, indexed).
|
|
25
|
+
- Auth middleware accepts `Authorization: Bearer bot_<token>` and bypasses Central JWT.
|
|
26
|
+
- `BotRole` model with `webhookUrl` (for external webhook fan-out).
|
|
27
|
+
- `createBot`, `listBots`, `createBotRole`, `listBotRoles` actions exist.
|
|
28
|
+
- Message model is rich: text/image/video/file/audio/poll/system, replyTo, mentions, reactions, edits, media[], read receipts.
|
|
29
|
+
- Real-time via Socket.IO V2 — events: `message:new`, `message:update`, `message:delete`, `typing`, `reaction:updated`, `pin:updated`, `chat:read`, etc.
|
|
30
|
+
- DMs: `isGroupChat: false`, 2 participants. Existing dedup query.
|
|
31
|
+
- File upload: `POST /chat/:chat_id/media` (MinIO-backed).
|
|
32
|
+
- **No native slash-command framework** — content is plain text. We need to build the slash menu UX in the clients.
|
|
33
|
+
|
|
34
|
+
### kazee-chat (web, Next.js + TipTap + Socket.IO V2)
|
|
35
|
+
- Rich text editor (TipTap) for input. Mentions autocomplete exists; **no slash menu yet**.
|
|
36
|
+
- Markdown rendering (react-markdown + rehype-highlight) — good for our outputs.
|
|
37
|
+
- File upload, voice recording, image rendering — all working.
|
|
38
|
+
|
|
39
|
+
### kazee-chat-mobile (React Native + Expo + Socket.IO V2)
|
|
40
|
+
- @-mention autocomplete in `ChatInputToolbar.tsx`; **no slash menu yet**.
|
|
41
|
+
- Voice recording via `expo-audio`; uploads via `mediaPipeline`.
|
|
42
|
+
- Markdown rendering via `react-native-markdown-display`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Target architecture
|
|
47
|
+
|
|
48
|
+
### 2.1 ChannelAdapter interface (open-claudia)
|
|
49
|
+
A single interface every transport implements. The bot core becomes transport-agnostic and only talks to adapters.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// channels/types.d.ts (conceptual)
|
|
53
|
+
class ChannelAdapter {
|
|
54
|
+
readonly id; // "telegram" | "kazee"
|
|
55
|
+
|
|
56
|
+
// Lifecycle
|
|
57
|
+
start(); // connect/poll
|
|
58
|
+
stop();
|
|
59
|
+
|
|
60
|
+
// Inbound (emit to core)
|
|
61
|
+
on("message", fn(InboundMessage));
|
|
62
|
+
on("command", fn(InboundCommand)); // parsed slash command
|
|
63
|
+
on("media", fn(InboundMedia)); // voice/photo/document
|
|
64
|
+
on("action", fn(InboundAction)); // button click / callback
|
|
65
|
+
|
|
66
|
+
// Outbound (called by core)
|
|
67
|
+
send(channelId, text, opts); // {parseMode, replyTo, buttons}
|
|
68
|
+
edit(channelId, messageId, text, opts);
|
|
69
|
+
delete(channelId, messageId);
|
|
70
|
+
sendFile(channelId, path, caption);
|
|
71
|
+
sendVoice(channelId, path);
|
|
72
|
+
typing(channelId);
|
|
73
|
+
|
|
74
|
+
// Identity
|
|
75
|
+
canonicalUserId(rawId); // "telegram:123" / "kazee:abc"
|
|
76
|
+
downloadMedia(mediaRef) -> localPath;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`InboundMessage` is a normalised envelope:
|
|
81
|
+
```ts
|
|
82
|
+
{ channelId, userId, canonicalUserId, text, replyToId, mediaRefs[], raw }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2.2 Bot core (transport-agnostic)
|
|
86
|
+
- `core/router.js` — receives normalised events, dispatches to command registry or chat handler.
|
|
87
|
+
- `core/commands.js` — explicit registry: `{name, description, args, handler}`. Both adapters consume the same registry; Telegram converts to `bot.onText` regex, Kazee exposes it via REST so clients can build the slash menu.
|
|
88
|
+
- `core/state.js` — already mostly transport-agnostic (sessions, vault, soul, crons). Stays.
|
|
89
|
+
- `core/identity.js` — `canonicalForChannel(transport, channelId)` already exists; generalise to `kazee:` prefix.
|
|
90
|
+
|
|
91
|
+
### 2.3 Refactor scope (bot.js → modules)
|
|
92
|
+
| New module | Lifted from bot.js (approx lines) | Notes |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `channels/telegram/index.js` | client init 228-232, polling 238-250, all `bot.onText` 755-3091, media handlers 3259-3339, callback_query 3102-3255, `send/edit/delete/getFile` wrappers 986-1113 | Becomes a thin adapter |
|
|
95
|
+
| `channels/kazee/index.js` | NEW | Socket.IO V2 client + REST send + media download |
|
|
96
|
+
| `core/router.js` | NEW — dispatch logic from `bot.on("message")` 3343-3497 |
|
|
97
|
+
| `core/commands.js` | extract command handlers + descriptions | Each handler becomes `{name, desc, args, fn}` |
|
|
98
|
+
| `core/identity.js` | 376-392, 487-491, 561-589 | Generalised |
|
|
99
|
+
| `core/transcripts.js` | already split (project-transcripts.js) | No change |
|
|
100
|
+
| `core/state.js` | 501-503, 597-649 | No change |
|
|
101
|
+
|
|
102
|
+
bot.js shrinks to ~200 lines of bootstrap.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 3. Kazee Chat adapter — concrete plan
|
|
107
|
+
|
|
108
|
+
### 3.1 Bot account setup (one-time, ops)
|
|
109
|
+
On chat-central:
|
|
110
|
+
1. `POST /botRole` → `{botName: "open-claudia", webhookUrl: null}` (we won't use webhook; we use Socket.IO).
|
|
111
|
+
2. `POST /bot` → creates `User{type:"Bot", botToken, botRole, name:"Open Claudia"}`.
|
|
112
|
+
3. Store `botToken` in open-claudia env (`KAZEE_BOT_TOKEN`) — never in code.
|
|
113
|
+
|
|
114
|
+
### 3.2 Connect (open-claudia → chat-central)
|
|
115
|
+
- Open Socket.IO V2 connection: `wss://<chat-central>/socket.io?version=v2` with `Authorization: Bearer bot_<token>`.
|
|
116
|
+
- On connect: `GET /chats?userId=<botUserId>` → list of chats the bot is in.
|
|
117
|
+
- Batch-join those chat rooms (same pattern web/mobile use).
|
|
118
|
+
- Listen to `message:new`. Filter: ignore own messages, process DMs + group messages mentioning bot.
|
|
119
|
+
|
|
120
|
+
### 3.3 Identity
|
|
121
|
+
- Canonical user ID: `kazee:<userId>` (chat-central User._id).
|
|
122
|
+
- Per-user transcripts: same isolation logic as Telegram — `projectHash` already userId-scoped.
|
|
123
|
+
|
|
124
|
+
### 3.4 Send / edit / delete
|
|
125
|
+
- Send: `POST /chat/:chatId/message` with `{content, type:"text"}` or `{type:"image", mediaIds}`.
|
|
126
|
+
- Edit: there's no documented edit endpoint in the explore report; we either (a) add `PATCH /message/:id` to chat-central, or (b) skip in-place edits and just send a follow-up. **Recommend (a)** — chat already supports `edited` + `editHistory` in the model.
|
|
127
|
+
- Delete: similar — likely needs a `DELETE /message/:id` action if not present. **Confirm in chat-central** before assuming.
|
|
128
|
+
|
|
129
|
+
### 3.5 Voice / files
|
|
130
|
+
- Receive: when `message:new` arrives with `type:"audio"`/`"voice"`, fetch media URL, download via REST, run Whisper (same as today's `transcribeAudio`).
|
|
131
|
+
- Send file: `POST /chat/:chatId/media` (returns mediaId) → `POST /chat/:chatId/message` with `mediaIds:[id]`.
|
|
132
|
+
|
|
133
|
+
### 3.6 Buttons / callback queries (the awkward gap)
|
|
134
|
+
Telegram inline keyboards have no Kazee equivalent today. Used in open-claudia for: auth approval, project picker, model/backend/effort selection, session switcher, onboarding style, cron presets.
|
|
135
|
+
|
|
136
|
+
Three options, ranked:
|
|
137
|
+
1. **Recommended — add a `type:"interactive"` message kind to chat-central** with a `buttons:[{label, callbackId}]` field, plus a `message:action` socket event when a user clicks. Render natively in web + mobile. Reuses existing message infra. Highest UX quality.
|
|
138
|
+
2. **Text-fallback** — bot sends `Reply with 1/2/3` style menus, parses replies. No frontend work, ugly UX, fragile when humans free-type.
|
|
139
|
+
3. **Web-only mini-form** — bot embeds a special markdown block (`:::buttons[...]:::`), web/mobile parse and render. Lighter than (1), heavier than (2). Drifts from chat-central message model.
|
|
140
|
+
|
|
141
|
+
Decision needed before implementation — flagging this as the biggest design choice.
|
|
142
|
+
|
|
143
|
+
### 3.7 Markdown
|
|
144
|
+
- Standard Markdown — react-markdown / react-native-markdown-display already render `**bold**`, `_italic_`, `` `code` ``, fenced code, lists. Bot's existing Telegram-Markdown output is mostly compatible; minor: Telegram uses `*bold*` (single asterisk). The adapter normalises asterisks on the way out.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 4. Slash commands — UX across clients
|
|
149
|
+
|
|
150
|
+
### 4.1 Server side (chat-central, new)
|
|
151
|
+
- Add `commands: [{name, description, args:[{name, required}]}]` to `User` (for Bot type) or to `BotRole`. Prefer `BotRole.commands`.
|
|
152
|
+
- New action: `GET /bot/:userId/commands` → returns the list.
|
|
153
|
+
- Bot registers/updates its command list on startup via `PUT /botRole/:id/commands`.
|
|
154
|
+
- Estimated effort: 1 model field, 2 actions, ~1 day in chat-central.
|
|
155
|
+
|
|
156
|
+
### 4.2 Web (kazee-chat) — slash menu in `ChatInput.tsx`
|
|
157
|
+
- TipTap extension or input-level handler that:
|
|
158
|
+
- Detects `/` as first character of an empty message.
|
|
159
|
+
- Calls `GET /bot/:botUserId/commands` if the other party is a Bot user.
|
|
160
|
+
- Shows a floating dropdown (same component family as mentions menu).
|
|
161
|
+
- On selection: inserts the slash command name, leaves cursor for args.
|
|
162
|
+
- Caches command list per bot for the session.
|
|
163
|
+
- Submit sends `content: "/cmd arg1 arg2"` as normal text — bot parses on its side.
|
|
164
|
+
- Estimated effort: ~2 days.
|
|
165
|
+
|
|
166
|
+
### 4.3 Mobile (kazee-chat-mobile) — slash menu in `ChatInputToolbar.tsx`
|
|
167
|
+
- Same logic, RN implementation. Reuse the mentions autocomplete pattern that already exists.
|
|
168
|
+
- Floating list above keyboard.
|
|
169
|
+
- Estimated effort: ~2 days.
|
|
170
|
+
|
|
171
|
+
### 4.4 Bot side (open-claudia)
|
|
172
|
+
- Parse `/cmd args` from inbound text. `core/commands.js` registry already handles this — no Telegram dependency.
|
|
173
|
+
- Same handlers work on both adapters.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 5. Phased rollout
|
|
178
|
+
|
|
179
|
+
**Phase 0 — prep (no behaviour change)**
|
|
180
|
+
- Refactor `bot.js` to extract `core/commands.js`, `core/router.js`, `channels/telegram/`. All existing functionality goes through the new abstraction. Ship as v1.21.0. Verify Telegram still works.
|
|
181
|
+
|
|
182
|
+
**Phase 1 — chat-central additions**
|
|
183
|
+
- Add `BotRole.commands` field + `GET /bot/:id/commands`.
|
|
184
|
+
- Add `PATCH /message/:id` (edit) + `DELETE /message/:id` (delete) actions, if not present.
|
|
185
|
+
- Decision on interactive buttons (§3.6). If option 1 (new message type), spec + implement.
|
|
186
|
+
|
|
187
|
+
**Phase 2 — kazee adapter**
|
|
188
|
+
- Build `channels/kazee/index.js` in open-claudia.
|
|
189
|
+
- Provision bot account in chat-central (dev env first).
|
|
190
|
+
- Connect, listen, send text. Parity with Telegram for plain text + file + voice.
|
|
191
|
+
- Ship as v1.22.0 behind `KAZEE_ENABLED=true` env flag.
|
|
192
|
+
|
|
193
|
+
**Phase 3 — slash menu UX**
|
|
194
|
+
- Web: slash menu in `ChatInput.tsx`.
|
|
195
|
+
- Mobile: slash menu in `ChatInputToolbar.tsx`.
|
|
196
|
+
- Bot registers its full command list (currently 35+).
|
|
197
|
+
|
|
198
|
+
**Phase 4 — interactive buttons (if option 1 chosen)**
|
|
199
|
+
- chat-central: `type:"interactive"` message + `message:action` event.
|
|
200
|
+
- Web + mobile: render buttons in `MessageBubble`, emit action on tap.
|
|
201
|
+
- Bot: emit interactive menus instead of text for auth/project/model selection.
|
|
202
|
+
|
|
203
|
+
**Phase 5 — production**
|
|
204
|
+
- Flip `KAZEE_ENABLED` on in prod.
|
|
205
|
+
- Document onboarding: how a Kazee user starts a chat with Open Claudia and authenticates.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 6. Onboarding flow (Kazee)
|
|
210
|
+
|
|
211
|
+
1. User opens Kazee, searches for "Open Claudia" in users.
|
|
212
|
+
2. Creates DM (existing flow: `createChat` with bot as participant).
|
|
213
|
+
3. Bot receives `message:new` for the first message → triggers `/start`-equivalent: replies with auth instructions.
|
|
214
|
+
4. Auth: same model as Telegram today (vault-stored credentials, OAuth flow). No changes — `canonicalUserId = kazee:<userId>` ensures isolation.
|
|
215
|
+
5. Subsequent messages flow normally.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 7. Risks & open questions
|
|
220
|
+
|
|
221
|
+
| # | Risk | Mitigation |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| 1 | Edit/delete endpoints may not exist in chat-central today | Verify before Phase 1; add if missing |
|
|
224
|
+
| 2 | Telegram inline keyboards have no equivalent — biggest design call | Decide §3.6 option before Phase 4 |
|
|
225
|
+
| 3 | Markdown dialect drift (Telegram `*bold*` vs standard `**bold**`) | Normalise in `channels/telegram/format.js` and `channels/kazee/format.js` |
|
|
226
|
+
| 4 | Long-message chunking (4KB Telegram limit) doesn't apply to Kazee | Move chunking into Telegram adapter only |
|
|
227
|
+
| 5 | Voice transcription path coupled to TEMP_DIR + ffmpeg + whisper | Already path-based; just changes how media is fetched (REST vs Telegram API) |
|
|
228
|
+
| 6 | Bot in group chats: noise if it processes every message | Only respond when @mentioned or in DM (chat-central `mentions[]` already supports this) |
|
|
229
|
+
| 7 | Onboarding identity binding — how do we know which Kazee user is which workspace operator? | `/auth` flow stays the same; first message after `/auth` binds canonical user to bot state |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 8. Work breakdown (rough)
|
|
234
|
+
|
|
235
|
+
| Area | Effort | Owner |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| Phase 0 — bot.js refactor | 3-4 days | open-claudia |
|
|
238
|
+
| Phase 1 — chat-central additions (commands list + edit/delete) | 2 days | chat-central |
|
|
239
|
+
| Phase 2 — Kazee adapter (open-claudia) | 3 days | open-claudia |
|
|
240
|
+
| Phase 3 — slash menu (web) | 2 days | kazee-chat |
|
|
241
|
+
| Phase 3 — slash menu (mobile) | 2 days | kazee-chat-mobile |
|
|
242
|
+
| Phase 4 — interactive buttons (optional but recommended) | 4 days across 3 repos | shared |
|
|
243
|
+
| **Total core (Phases 0–3)** | **~12 days** | |
|
|
244
|
+
| **With Phase 4** | **~16 days** | |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 9. Decisions to lock before starting
|
|
249
|
+
|
|
250
|
+
1. **Interactive buttons** — option 1, 2, or 3 from §3.6? Affects all three frontends and chat-central scope.
|
|
251
|
+
2. **Bot scope in group chats** — DMs only initially, or mention-driven in groups too?
|
|
252
|
+
3. **Command registration** — does bot register commands on every startup, or one-shot via ops script?
|
|
253
|
+
4. **Edit/delete endpoints in chat-central** — confirm they exist; if not, add to Phase 1.
|
|
254
|
+
5. **`KAZEE_ENABLED` flag** — env-flag rollout vs straight cutover.
|