@botcord/daemon 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-tracker.d.ts +43 -0
- package/dist/activity-tracker.js +110 -0
- package/dist/adapters/runtimes.d.ts +14 -0
- package/dist/adapters/runtimes.js +18 -0
- package/dist/agent-discovery.d.ts +81 -0
- package/dist/agent-discovery.js +181 -0
- package/dist/agent-workspace.d.ts +31 -0
- package/dist/agent-workspace.js +221 -0
- package/dist/config.d.ts +116 -0
- package/dist/config.js +180 -0
- package/dist/control-channel.d.ts +99 -0
- package/dist/control-channel.js +388 -0
- package/dist/cross-room.d.ts +23 -0
- package/dist/cross-room.js +55 -0
- package/dist/daemon-config-map.d.ts +61 -0
- package/dist/daemon-config-map.js +153 -0
- package/dist/daemon.d.ts +123 -0
- package/dist/daemon.js +349 -0
- package/dist/doctor.d.ts +89 -0
- package/dist/doctor.js +191 -0
- package/dist/gateway/channel-manager.d.ts +54 -0
- package/dist/gateway/channel-manager.js +292 -0
- package/dist/gateway/channels/botcord.d.ts +93 -0
- package/dist/gateway/channels/botcord.js +510 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +1 -0
- package/dist/gateway/channels/sanitize.d.ts +20 -0
- package/dist/gateway/channels/sanitize.js +56 -0
- package/dist/gateway/dispatcher.d.ts +73 -0
- package/dist/gateway/dispatcher.js +431 -0
- package/dist/gateway/gateway.d.ts +87 -0
- package/dist/gateway/gateway.js +158 -0
- package/dist/gateway/index.d.ts +15 -0
- package/dist/gateway/index.js +15 -0
- package/dist/gateway/log.d.ts +9 -0
- package/dist/gateway/log.js +20 -0
- package/dist/gateway/router.d.ts +10 -0
- package/dist/gateway/router.js +48 -0
- package/dist/gateway/runtimes/claude-code.d.ts +30 -0
- package/dist/gateway/runtimes/claude-code.js +162 -0
- package/dist/gateway/runtimes/codex.d.ts +83 -0
- package/dist/gateway/runtimes/codex.js +272 -0
- package/dist/gateway/runtimes/gemini.d.ts +15 -0
- package/dist/gateway/runtimes/gemini.js +29 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +43 -0
- package/dist/gateway/runtimes/ndjson-stream.js +169 -0
- package/dist/gateway/runtimes/probe.d.ts +17 -0
- package/dist/gateway/runtimes/probe.js +54 -0
- package/dist/gateway/runtimes/registry.d.ts +59 -0
- package/dist/gateway/runtimes/registry.js +94 -0
- package/dist/gateway/session-store.d.ts +39 -0
- package/dist/gateway/session-store.js +133 -0
- package/dist/gateway/types.d.ts +265 -0
- package/dist/gateway/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +854 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +44 -0
- package/dist/provision.d.ts +88 -0
- package/dist/provision.js +749 -0
- package/dist/room-context-fetcher.d.ts +18 -0
- package/dist/room-context-fetcher.js +101 -0
- package/dist/room-context.d.ts +53 -0
- package/dist/room-context.js +112 -0
- package/dist/sender-classify.d.ts +30 -0
- package/dist/sender-classify.js +32 -0
- package/dist/snapshot-writer.d.ts +37 -0
- package/dist/snapshot-writer.js +84 -0
- package/dist/status-render.d.ts +28 -0
- package/dist/status-render.js +97 -0
- package/dist/system-context.d.ts +57 -0
- package/dist/system-context.js +91 -0
- package/dist/turn-text.d.ts +36 -0
- package/dist/turn-text.js +57 -0
- package/dist/user-auth.d.ts +75 -0
- package/dist/user-auth.js +245 -0
- package/dist/working-memory.d.ts +46 -0
- package/dist/working-memory.js +274 -0
- package/package.json +39 -0
- package/src/__tests__/activity-tracker.test.ts +130 -0
- package/src/__tests__/agent-discovery.test.ts +191 -0
- package/src/__tests__/agent-workspace.test.ts +147 -0
- package/src/__tests__/control-channel.test.ts +327 -0
- package/src/__tests__/cross-room.test.ts +116 -0
- package/src/__tests__/daemon-config-map.test.ts +416 -0
- package/src/__tests__/daemon.test.ts +300 -0
- package/src/__tests__/device-code.test.ts +152 -0
- package/src/__tests__/doctor.test.ts +218 -0
- package/src/__tests__/protocol-core-reexport.test.ts +24 -0
- package/src/__tests__/provision.test.ts +922 -0
- package/src/__tests__/room-context.test.ts +233 -0
- package/src/__tests__/runtime-discovery.test.ts +173 -0
- package/src/__tests__/snapshot-writer.test.ts +141 -0
- package/src/__tests__/status-render.test.ts +137 -0
- package/src/__tests__/system-context.test.ts +315 -0
- package/src/__tests__/turn-text.test.ts +116 -0
- package/src/__tests__/user-auth.test.ts +125 -0
- package/src/__tests__/working-memory.test.ts +240 -0
- package/src/activity-tracker.ts +140 -0
- package/src/adapters/runtimes.ts +30 -0
- package/src/agent-discovery.ts +262 -0
- package/src/agent-workspace.ts +247 -0
- package/src/config.ts +290 -0
- package/src/control-channel.ts +455 -0
- package/src/cross-room.ts +89 -0
- package/src/daemon-config-map.ts +200 -0
- package/src/daemon.ts +478 -0
- package/src/doctor.ts +282 -0
- package/src/gateway/__tests__/.gitkeep +0 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +480 -0
- package/src/gateway/__tests__/channel-manager.test.ts +475 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +318 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +350 -0
- package/src/gateway/__tests__/dispatcher.test.ts +1159 -0
- package/src/gateway/__tests__/gateway-add-channel.test.ts +180 -0
- package/src/gateway/__tests__/gateway-managed-routes.test.ts +181 -0
- package/src/gateway/__tests__/gateway.test.ts +222 -0
- package/src/gateway/__tests__/router.test.ts +247 -0
- package/src/gateway/__tests__/sanitize.test.ts +193 -0
- package/src/gateway/__tests__/session-store.test.ts +235 -0
- package/src/gateway/channel-manager.ts +349 -0
- package/src/gateway/channels/botcord.ts +605 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/sanitize.ts +68 -0
- package/src/gateway/dispatcher.ts +554 -0
- package/src/gateway/gateway.ts +211 -0
- package/src/gateway/index.ts +29 -0
- package/src/gateway/log.ts +30 -0
- package/src/gateway/router.ts +60 -0
- package/src/gateway/runtimes/claude-code.ts +180 -0
- package/src/gateway/runtimes/codex.ts +312 -0
- package/src/gateway/runtimes/gemini.ts +43 -0
- package/src/gateway/runtimes/ndjson-stream.ts +225 -0
- package/src/gateway/runtimes/probe.ts +73 -0
- package/src/gateway/runtimes/registry.ts +143 -0
- package/src/gateway/session-store.ts +157 -0
- package/src/gateway/types.ts +325 -0
- package/src/index.ts +961 -0
- package/src/log.ts +47 -0
- package/src/provision.ts +879 -0
- package/src/room-context-fetcher.ts +124 -0
- package/src/room-context.ts +167 -0
- package/src/sender-classify.ts +46 -0
- package/src/snapshot-writer.ts +103 -0
- package/src/status-render.ts +132 -0
- package/src/system-context.ts +162 -0
- package/src/turn-text.ts +93 -0
- package/src/user-auth.ts +295 -0
- package/src/working-memory.ts +352 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, PID_PATH, SNAPSHOT_PATH, CONFIG_FILE_PATH, } from "./config.js";
|
|
7
|
+
import { resolveBootAgents } from "./agent-discovery.js";
|
|
8
|
+
import { startDaemon } from "./daemon.js";
|
|
9
|
+
import { log, LOG_FILE_PATH } from "./log.js";
|
|
10
|
+
import { detectRuntimes, getAdapterModule, listAdapterIds } from "./adapters/runtimes.js";
|
|
11
|
+
import { pollDeviceToken, requestDeviceCode, } from "@botcord/protocol-core";
|
|
12
|
+
import { AUTH_EXPIRED_FLAG_PATH, clearAuthExpiredFlag, loadUserAuth, saveUserAuth, userAuthFromTokenResponse, } from "./user-auth.js";
|
|
13
|
+
import { renderStatus } from "./status-render.js";
|
|
14
|
+
import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
|
|
15
|
+
import { clearWorkingMemory, readWorkingMemory, resolveMemoryDir, updateWorkingMemory, DEFAULT_SECTION, } from "./working-memory.js";
|
|
16
|
+
const ADAPTER_LIST = listAdapterIds().join("|");
|
|
17
|
+
const DEFAULT_HUB = "https://api.botcord.chat";
|
|
18
|
+
/**
|
|
19
|
+
* Fallback label when the operator doesn't pass `--label` at login.
|
|
20
|
+
* macOS hostnames often carry a `.local` mDNS suffix that's just noise in
|
|
21
|
+
* the dashboard — strip it. A null/empty hostname falls back to "daemon".
|
|
22
|
+
*/
|
|
23
|
+
function defaultLoginLabel() {
|
|
24
|
+
const raw = (hostname() || "").trim().replace(/\.local$/i, "");
|
|
25
|
+
return raw.length > 0 ? raw : "daemon";
|
|
26
|
+
}
|
|
27
|
+
const HELP = `botcord-daemon — BotCord local daemon
|
|
28
|
+
|
|
29
|
+
Usage: botcord-daemon <command> [options]
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
init [--agent <ag_xxx> ...] [--cwd <path>]
|
|
33
|
+
Create ~/.botcord/daemon/config.json.
|
|
34
|
+
Without --agent, the daemon discovers
|
|
35
|
+
identities from ~/.botcord/credentials
|
|
36
|
+
at startup (repeat --agent to pin).
|
|
37
|
+
start [--foreground] [--relogin] [--hub <url>] [--label <name>]
|
|
38
|
+
Start the daemon. Without credentials
|
|
39
|
+
and on a TTY, runs the interactive
|
|
40
|
+
device-code login first. --hub defaults
|
|
41
|
+
to ${DEFAULT_HUB} (or the URL stored in
|
|
42
|
+
a previous login). --relogin forces
|
|
43
|
+
re-login. --label is sent to the Hub
|
|
44
|
+
on connect for the dashboard device
|
|
45
|
+
list (defaults to hostname). Non-TTY
|
|
46
|
+
environments must mount a pre-existing
|
|
47
|
+
user-auth.json (plan §6.4).
|
|
48
|
+
stop Stop the running daemon (SIGTERM)
|
|
49
|
+
status Print daemon status (pid, agent)
|
|
50
|
+
logs [-f] Print log tail (use -f to follow)
|
|
51
|
+
route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
|
|
52
|
+
match flags (first match wins; at least one conversation/sender selector required):
|
|
53
|
+
--conversation-id <rm_xxx> (alias: --room <rm_xxx>)
|
|
54
|
+
--conversation-prefix <rm_oc_> (alias: --prefix <rm_oc_>)
|
|
55
|
+
--conversation-kind <direct|group>
|
|
56
|
+
--channel <channel_type> (default: botcord)
|
|
57
|
+
--account-id <ag_xxx>
|
|
58
|
+
--sender-id <ag_xxx>
|
|
59
|
+
--mentioned / --no-mentioned
|
|
60
|
+
route list
|
|
61
|
+
route remove --room <rm_xxx>|--prefix <rm_xxx>
|
|
62
|
+
config Print resolved config
|
|
63
|
+
doctor [--json] Scan local runtimes (${ADAPTER_LIST})
|
|
64
|
+
memory get [--agent <ag_xxx>] [--json] Show current working memory
|
|
65
|
+
memory set [--agent <ag_xxx>] --goal <text>
|
|
66
|
+
Pin/update the agent's work goal
|
|
67
|
+
memory set [--agent <ag_xxx>] --section <name> --content <text>
|
|
68
|
+
Upsert a section (empty --content deletes it)
|
|
69
|
+
memory delete [--agent <ag_xxx>] --section <name>
|
|
70
|
+
Remove a section
|
|
71
|
+
memory clear [--agent <ag_xxx>] Wipe all working memory
|
|
72
|
+
(--agent required if the daemon runs
|
|
73
|
+
more than one; optional otherwise)
|
|
74
|
+
|
|
75
|
+
Env:
|
|
76
|
+
BOTCORD_<RUNTIME>_BIN Override CLI path per runtime (e.g. BOTCORD_CODEX_BIN)
|
|
77
|
+
BOTCORD_DAEMON_DEBUG Enable debug logging
|
|
78
|
+
`;
|
|
79
|
+
/** Known boolean flags — never consume the following token as a value. */
|
|
80
|
+
const BOOLEAN_FLAGS = new Set([
|
|
81
|
+
"foreground",
|
|
82
|
+
"f",
|
|
83
|
+
"follow",
|
|
84
|
+
"json",
|
|
85
|
+
"help",
|
|
86
|
+
"h",
|
|
87
|
+
"mentioned",
|
|
88
|
+
"relogin",
|
|
89
|
+
]);
|
|
90
|
+
/** Flags that may be repeated on the command line; all values are collected. */
|
|
91
|
+
const LIST_FLAGS = new Set(["agent"]);
|
|
92
|
+
function parseArgs(argv) {
|
|
93
|
+
const [cmd, maybeSub, ...rest] = argv;
|
|
94
|
+
const flags = {};
|
|
95
|
+
const lists = {};
|
|
96
|
+
const positional = [];
|
|
97
|
+
let sub;
|
|
98
|
+
if (maybeSub && !maybeSub.startsWith("-")) {
|
|
99
|
+
sub = maybeSub;
|
|
100
|
+
}
|
|
101
|
+
else if (maybeSub) {
|
|
102
|
+
positional.unshift(maybeSub);
|
|
103
|
+
}
|
|
104
|
+
const args = [...positional, ...rest];
|
|
105
|
+
for (let i = 0; i < args.length; i++) {
|
|
106
|
+
const a = args[i];
|
|
107
|
+
if (!a.startsWith("--") && !a.startsWith("-"))
|
|
108
|
+
continue;
|
|
109
|
+
const key = a.replace(/^-+/, "");
|
|
110
|
+
// `--no-<bool>` → explicit false for a known boolean flag.
|
|
111
|
+
if (key.startsWith("no-")) {
|
|
112
|
+
const base = key.slice(3);
|
|
113
|
+
if (BOOLEAN_FLAGS.has(base)) {
|
|
114
|
+
flags[base] = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
119
|
+
flags[key] = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const next = args[i + 1];
|
|
123
|
+
if (next && !next.startsWith("-")) {
|
|
124
|
+
if (LIST_FLAGS.has(key)) {
|
|
125
|
+
(lists[key] ||= []).push(next);
|
|
126
|
+
}
|
|
127
|
+
flags[key] = next;
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
flags[key] = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { cmd: cmd ?? "", sub, flags, lists };
|
|
135
|
+
}
|
|
136
|
+
function readPid() {
|
|
137
|
+
if (!existsSync(PID_PATH))
|
|
138
|
+
return null;
|
|
139
|
+
const raw = readFileSync(PID_PATH, "utf8").trim();
|
|
140
|
+
const pid = Number(raw);
|
|
141
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
142
|
+
}
|
|
143
|
+
function pidAlive(pid) {
|
|
144
|
+
try {
|
|
145
|
+
process.kill(pid, 0);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function cmdInit(args) {
|
|
153
|
+
// `--agent` is optional as of P1: when omitted, the daemon discovers
|
|
154
|
+
// agent identities from `~/.botcord/credentials/*.json` at startup.
|
|
155
|
+
// Every repeated `--agent ag_xxx` still pins an explicit id (the
|
|
156
|
+
// canonical `agents: [...]` config shape).
|
|
157
|
+
const agents = args.lists.agent ?? [];
|
|
158
|
+
const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
|
|
159
|
+
log.info("cmd init", { agents, cwd });
|
|
160
|
+
const cfg = initDefaultConfig(agents, cwd);
|
|
161
|
+
saveConfig(cfg);
|
|
162
|
+
console.log(`wrote ${CONFIG_FILE_PATH}`);
|
|
163
|
+
if (agents.length === 0) {
|
|
164
|
+
console.log("no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials at start");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Read the current user-auth record without throwing on parse / permission
|
|
169
|
+
* errors — those are returned as `null` so the caller treats them like a
|
|
170
|
+
* missing file (and the device-code flow re-runs).
|
|
171
|
+
*/
|
|
172
|
+
function safeLoadUserAuth() {
|
|
173
|
+
try {
|
|
174
|
+
return loadUserAuth();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Sleep helper used by the device-code poll loop. */
|
|
181
|
+
function delay(ms) {
|
|
182
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run the device-code login flow against the given Hub. Polls every
|
|
186
|
+
* `interval` seconds (the Hub may bump this) until the user authorizes
|
|
187
|
+
* from the dashboard, the device_code expires, or SIGINT is received.
|
|
188
|
+
* Persists the token envelope to `user-auth.json` and returns the record.
|
|
189
|
+
*
|
|
190
|
+
* Plan §6.1.
|
|
191
|
+
*/
|
|
192
|
+
async function runDeviceCodeFlow(opts) {
|
|
193
|
+
log.info("device-code flow: requesting code", {
|
|
194
|
+
hubUrl: opts.hubUrl,
|
|
195
|
+
label: opts.label ?? null,
|
|
196
|
+
});
|
|
197
|
+
const dc = await requestDeviceCode(opts.hubUrl, opts.label ? { label: opts.label } : undefined);
|
|
198
|
+
const display = dc.verificationUriComplete ?? dc.verificationUri;
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(`Visit ${display}`);
|
|
201
|
+
console.log(`Code: ${dc.userCode}`);
|
|
202
|
+
console.log("Waiting for authorization (Ctrl-C to abort)...");
|
|
203
|
+
const expiresAt = Date.now() + dc.expiresIn * 1000;
|
|
204
|
+
let intervalSec = dc.interval;
|
|
205
|
+
while (Date.now() < expiresAt) {
|
|
206
|
+
await delay(intervalSec * 1000);
|
|
207
|
+
let res;
|
|
208
|
+
try {
|
|
209
|
+
res = await pollDeviceToken(opts.hubUrl, dc.deviceCode, opts.label ? { label: opts.label } : undefined);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// Network blips shouldn't kill the loop — surface, then retry on
|
|
213
|
+
// the next tick. A persistent failure still ends at expiry.
|
|
214
|
+
console.error(`device-code poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (res.status === "pending")
|
|
218
|
+
continue;
|
|
219
|
+
if (res.status === "slow_down") {
|
|
220
|
+
intervalSec = Math.max(intervalSec, res.interval);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Issued — persist and return.
|
|
224
|
+
const record = userAuthFromTokenResponse(res, opts.label ? { label: opts.label } : undefined);
|
|
225
|
+
saveUserAuth(record);
|
|
226
|
+
clearAuthExpiredFlag();
|
|
227
|
+
log.info("device-code flow: authorized", {
|
|
228
|
+
userId: record.userId,
|
|
229
|
+
hubUrl: record.hubUrl,
|
|
230
|
+
label: opts.label ?? null,
|
|
231
|
+
});
|
|
232
|
+
console.log(`Logged in as ${record.userId}`);
|
|
233
|
+
return record;
|
|
234
|
+
}
|
|
235
|
+
log.warn("device-code flow: expired without authorization", {
|
|
236
|
+
hubUrl: opts.hubUrl,
|
|
237
|
+
});
|
|
238
|
+
throw new Error("device-code expired without authorization");
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Resolve / acquire a valid user-auth record before the daemon process
|
|
242
|
+
* forks. Returns `null` when the daemon should proceed without a control
|
|
243
|
+
* plane (legacy P0 behavior — caller may still log a warning).
|
|
244
|
+
*
|
|
245
|
+
* Decision tree (plan §4.4 + §6.4):
|
|
246
|
+
* 1. `--relogin` → device-code login.
|
|
247
|
+
* 2. Have valid creds (not near expiry) → return existing record.
|
|
248
|
+
* 3. Have stale creds → leave as-is; the control channel will refresh.
|
|
249
|
+
* 4. No creds + TTY → device-code login.
|
|
250
|
+
* 5. No creds + no TTY → exit 1 with the §6.4 hint.
|
|
251
|
+
*/
|
|
252
|
+
async function ensureUserAuthForStart(args) {
|
|
253
|
+
const hubFlag = typeof args.flags.hub === "string" ? args.flags.hub : undefined;
|
|
254
|
+
const labelFlag = typeof args.flags.label === "string" ? args.flags.label : undefined;
|
|
255
|
+
const relogin = args.flags.relogin === true;
|
|
256
|
+
const existing = safeLoadUserAuth();
|
|
257
|
+
if (!relogin && existing) {
|
|
258
|
+
// A previously-set auth-expired flag is stale by definition once the
|
|
259
|
+
// operator runs `start` again — if creds genuinely don't work, the
|
|
260
|
+
// control channel will re-write the flag on the next 4401/4403.
|
|
261
|
+
// Clearing here keeps `status` from indefinitely warning about a
|
|
262
|
+
// recovery the daemon already made.
|
|
263
|
+
clearAuthExpiredFlag();
|
|
264
|
+
// Idempotent restart: if creds already exist, keep the stored label as
|
|
265
|
+
// the source of truth — operators wanting to rename must go through
|
|
266
|
+
// `--relogin`. Stale access tokens will be refreshed by
|
|
267
|
+
// UserAuthManager on first WS connect; nothing else to do here.
|
|
268
|
+
if (labelFlag && existing.label !== labelFlag) {
|
|
269
|
+
console.error(`note: --label "${labelFlag}" ignored (already logged in as "${existing.label ?? "<unset>"}"); pass --relogin to change it`);
|
|
270
|
+
}
|
|
271
|
+
return existing;
|
|
272
|
+
}
|
|
273
|
+
// Need a fresh login. Resolve hubUrl: explicit --hub > existing record > DEFAULT_HUB.
|
|
274
|
+
const hubUrl = hubFlag ?? existing?.hubUrl ?? DEFAULT_HUB;
|
|
275
|
+
if (!process.stdin.isTTY) {
|
|
276
|
+
// Plan §6.4 — non-interactive environment. Fail fast with actionable
|
|
277
|
+
// remediation; never block waiting for input that will never arrive.
|
|
278
|
+
console.error("error: not logged in and no TTY available");
|
|
279
|
+
console.error("hint: run `botcord-daemon start` once interactively to establish credentials,");
|
|
280
|
+
console.error(" or mount a valid `~/.botcord/daemon/user-auth.json`");
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
const label = labelFlag ?? defaultLoginLabel();
|
|
284
|
+
return runDeviceCodeFlow({ hubUrl, label });
|
|
285
|
+
}
|
|
286
|
+
async function cmdStart(args) {
|
|
287
|
+
const cfg = loadConfig();
|
|
288
|
+
const foreground = args.flags.foreground === true;
|
|
289
|
+
log.info("cmd start", {
|
|
290
|
+
foreground,
|
|
291
|
+
relogin: args.flags.relogin === true,
|
|
292
|
+
child: process.env.BOTCORD_DAEMON_CHILD === "1",
|
|
293
|
+
});
|
|
294
|
+
const existing = readPid();
|
|
295
|
+
if (existing && pidAlive(existing)) {
|
|
296
|
+
console.error(`daemon already running (pid ${existing})`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
// Login MUST happen before fork — once detached, stdio is gone and the
|
|
300
|
+
// user can't see the device code. We also run it for explicit
|
|
301
|
+
// --foreground so an interactive user can log in without the fork dance.
|
|
302
|
+
// The auto-spawned child (foreground re-exec) carries the marker env
|
|
303
|
+
// var so we don't try to re-prompt for credentials it already has.
|
|
304
|
+
if (process.env.BOTCORD_DAEMON_CHILD !== "1") {
|
|
305
|
+
await ensureUserAuthForStart(args);
|
|
306
|
+
}
|
|
307
|
+
if (!foreground) {
|
|
308
|
+
// Detached child re-exec in foreground mode. The child writes the PID
|
|
309
|
+
// file once it's up; the parent only polls to confirm startup so the
|
|
310
|
+
// two never race on the same file.
|
|
311
|
+
const child = spawn(process.execPath, [process.argv[1], "start", "--foreground"], {
|
|
312
|
+
detached: true,
|
|
313
|
+
stdio: "ignore",
|
|
314
|
+
env: { ...process.env, BOTCORD_DAEMON_CHILD: "1" },
|
|
315
|
+
});
|
|
316
|
+
child.unref();
|
|
317
|
+
const deadline = Date.now() + 500;
|
|
318
|
+
let observed = null;
|
|
319
|
+
while (Date.now() < deadline) {
|
|
320
|
+
const p = readPid();
|
|
321
|
+
if (p && pidAlive(p)) {
|
|
322
|
+
observed = p;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
326
|
+
}
|
|
327
|
+
if (!observed) {
|
|
328
|
+
console.error(`daemon did not record pid within 500ms (expected child pid ${child.pid})`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
console.log(`daemon started (pid ${observed})`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Foreground: we ARE the daemon.
|
|
335
|
+
writeFileSync(PID_PATH, String(process.pid), { mode: 0o600 });
|
|
336
|
+
const handle = await startDaemon({ config: cfg, configPath: CONFIG_FILE_PATH });
|
|
337
|
+
const shutdown = async (sig) => {
|
|
338
|
+
log.info("signal received", { sig });
|
|
339
|
+
await handle.stop(sig);
|
|
340
|
+
try {
|
|
341
|
+
unlinkSync(PID_PATH);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// ignore
|
|
345
|
+
}
|
|
346
|
+
process.exit(0);
|
|
347
|
+
};
|
|
348
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
349
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
350
|
+
// Gateway.start() resolves after channels are started. Keep the process
|
|
351
|
+
// alive until a signal arrives; the channel manager owns its own loops.
|
|
352
|
+
await new Promise(() => {
|
|
353
|
+
// Deliberately never resolves; `shutdown()` calls process.exit(0).
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function cmdStop() {
|
|
357
|
+
const pid = readPid();
|
|
358
|
+
log.info("cmd stop", { pid });
|
|
359
|
+
if (!pid) {
|
|
360
|
+
console.error("no pid file found");
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
if (!pidAlive(pid)) {
|
|
364
|
+
console.error(`pid ${pid} not alive; removing stale pid file`);
|
|
365
|
+
try {
|
|
366
|
+
unlinkSync(PID_PATH);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// ignore
|
|
370
|
+
}
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
process.kill(pid, "SIGTERM");
|
|
374
|
+
console.log(`sent SIGTERM to ${pid}`);
|
|
375
|
+
}
|
|
376
|
+
function readSnapshotFile() {
|
|
377
|
+
if (!existsSync(SNAPSHOT_PATH))
|
|
378
|
+
return null;
|
|
379
|
+
try {
|
|
380
|
+
const raw = readFileSync(SNAPSHOT_PATH, "utf8");
|
|
381
|
+
const parsed = JSON.parse(raw);
|
|
382
|
+
if (parsed &&
|
|
383
|
+
typeof parsed === "object" &&
|
|
384
|
+
parsed.version === 1 &&
|
|
385
|
+
typeof parsed.writtenAt === "number" &&
|
|
386
|
+
parsed.snapshot) {
|
|
387
|
+
return parsed;
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function cmdStatus(args) {
|
|
396
|
+
const pid = readPid();
|
|
397
|
+
const alive = pid ? pidAlive(pid) : false;
|
|
398
|
+
let agents = [];
|
|
399
|
+
let agentsSource = null;
|
|
400
|
+
let configPath = null;
|
|
401
|
+
try {
|
|
402
|
+
const cfg = loadConfig();
|
|
403
|
+
const boot = resolveBootAgents(cfg);
|
|
404
|
+
agents = boot.agents.map((a) => a.agentId);
|
|
405
|
+
agentsSource = boot.source;
|
|
406
|
+
configPath = CONFIG_FILE_PATH;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// config may not exist pre-init — that's fine
|
|
410
|
+
}
|
|
411
|
+
let userAuth = null;
|
|
412
|
+
try {
|
|
413
|
+
userAuth = loadUserAuth();
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// a broken user-auth shouldn't fail status; leave as null
|
|
417
|
+
}
|
|
418
|
+
const authExpired = existsSync(AUTH_EXPIRED_FLAG_PATH);
|
|
419
|
+
const file = readSnapshotFile();
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
const snapshotAgeMs = file ? now - file.writtenAt : null;
|
|
422
|
+
if (args.flags.json === true) {
|
|
423
|
+
const payload = {
|
|
424
|
+
pid,
|
|
425
|
+
alive,
|
|
426
|
+
agents,
|
|
427
|
+
agentsSource,
|
|
428
|
+
// Preserve the legacy scalar field in JSON output when exactly one
|
|
429
|
+
// agent is bound, so consumers pinned to `agentId` keep working.
|
|
430
|
+
agentId: agents.length === 1 ? agents[0] : null,
|
|
431
|
+
config: configPath,
|
|
432
|
+
userAuth: userAuth
|
|
433
|
+
? {
|
|
434
|
+
userId: userAuth.userId,
|
|
435
|
+
daemonInstanceId: userAuth.daemonInstanceId,
|
|
436
|
+
hubUrl: userAuth.hubUrl,
|
|
437
|
+
expiresAt: userAuth.expiresAt,
|
|
438
|
+
label: userAuth.label ?? null,
|
|
439
|
+
}
|
|
440
|
+
: null,
|
|
441
|
+
authExpired,
|
|
442
|
+
snapshot: file?.snapshot ?? null,
|
|
443
|
+
snapshotWrittenAt: file?.writtenAt ?? null,
|
|
444
|
+
snapshotAgeMs,
|
|
445
|
+
snapshotPath: SNAPSHOT_PATH,
|
|
446
|
+
};
|
|
447
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const input = {
|
|
451
|
+
pid,
|
|
452
|
+
alive,
|
|
453
|
+
agents,
|
|
454
|
+
agentsSource,
|
|
455
|
+
configPath,
|
|
456
|
+
snapshot: file?.snapshot ?? null,
|
|
457
|
+
snapshotAgeMs,
|
|
458
|
+
};
|
|
459
|
+
console.log(renderStatus(input, now));
|
|
460
|
+
if (userAuth) {
|
|
461
|
+
console.log(`logged in as ${userAuth.userId}${userAuth.label ? ` (${userAuth.label})` : ""}`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
console.log("not logged in (control plane disabled)");
|
|
465
|
+
}
|
|
466
|
+
if (authExpired) {
|
|
467
|
+
console.log("⚠ credentials revoked — run `botcord-daemon start --relogin` to re-authorize");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function cmdLogs(args) {
|
|
471
|
+
const follow = args.flags.f === true || args.flags.follow === true;
|
|
472
|
+
if (!existsSync(LOG_FILE_PATH)) {
|
|
473
|
+
console.error(`no log file at ${LOG_FILE_PATH}`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
if (follow) {
|
|
477
|
+
// `tail -f` is simpler and more robust than watching fs events ourselves.
|
|
478
|
+
const child = spawn("tail", ["-n", "100", "-f", LOG_FILE_PATH], { stdio: "inherit" });
|
|
479
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
480
|
+
await new Promise((resolve) => child.on("close", resolve));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const data = readFileSync(LOG_FILE_PATH, "utf8");
|
|
484
|
+
const lines = data.split("\n");
|
|
485
|
+
console.log(lines.slice(-100).join("\n"));
|
|
486
|
+
}
|
|
487
|
+
function formatRouteMatch(m) {
|
|
488
|
+
const parts = [];
|
|
489
|
+
if (m.channel)
|
|
490
|
+
parts.push(`channel=${m.channel}`);
|
|
491
|
+
if (m.accountId)
|
|
492
|
+
parts.push(`accountId=${m.accountId}`);
|
|
493
|
+
const convId = m.conversationId ?? m.roomId;
|
|
494
|
+
if (convId)
|
|
495
|
+
parts.push(`conversationId=${convId}`);
|
|
496
|
+
const convPrefix = m.conversationPrefix ?? m.roomPrefix;
|
|
497
|
+
if (convPrefix)
|
|
498
|
+
parts.push(`conversationPrefix=${convPrefix}`);
|
|
499
|
+
if (m.conversationKind)
|
|
500
|
+
parts.push(`conversationKind=${m.conversationKind}`);
|
|
501
|
+
if (m.senderId)
|
|
502
|
+
parts.push(`senderId=${m.senderId}`);
|
|
503
|
+
if (typeof m.mentioned === "boolean")
|
|
504
|
+
parts.push(`mentioned=${m.mentioned}`);
|
|
505
|
+
return parts.length > 0 ? parts.join(", ") : "(any)";
|
|
506
|
+
}
|
|
507
|
+
async function cmdRoute(args) {
|
|
508
|
+
const cfg = loadConfig();
|
|
509
|
+
const sub = args.sub;
|
|
510
|
+
if (sub === "list") {
|
|
511
|
+
if (args.flags.json === true) {
|
|
512
|
+
console.log(JSON.stringify({ default: cfg.defaultRoute, routes: cfg.routes }, null, 2));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const d = cfg.defaultRoute;
|
|
516
|
+
console.log(`default: runtime=${d.adapter} cwd=${d.cwd}${d.extraArgs?.length ? ` extraArgs=${JSON.stringify(d.extraArgs)}` : ""}`);
|
|
517
|
+
if (cfg.routes.length === 0) {
|
|
518
|
+
console.log("routes: (none)");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
console.log("routes:");
|
|
522
|
+
cfg.routes.forEach((r, i) => {
|
|
523
|
+
const tail = r.extraArgs?.length ? ` extraArgs=${JSON.stringify(r.extraArgs)}` : "";
|
|
524
|
+
console.log(` [${i}] runtime=${r.adapter} cwd=${r.cwd}${tail}`);
|
|
525
|
+
console.log(` match: ${formatRouteMatch(r.match)}`);
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (sub === "add") {
|
|
530
|
+
// Legacy aliases: --room → --conversation-id, --prefix → --conversation-prefix.
|
|
531
|
+
// Prefer the canonical field if both are provided.
|
|
532
|
+
const roomFlag = typeof args.flags.room === "string" ? args.flags.room : undefined;
|
|
533
|
+
const convIdFlag = typeof args.flags["conversation-id"] === "string"
|
|
534
|
+
? args.flags["conversation-id"]
|
|
535
|
+
: undefined;
|
|
536
|
+
const conversationId = convIdFlag ?? roomFlag;
|
|
537
|
+
const prefixFlag = typeof args.flags.prefix === "string" ? args.flags.prefix : undefined;
|
|
538
|
+
const convPrefixFlag = typeof args.flags["conversation-prefix"] === "string"
|
|
539
|
+
? args.flags["conversation-prefix"]
|
|
540
|
+
: undefined;
|
|
541
|
+
const conversationPrefix = convPrefixFlag ?? prefixFlag;
|
|
542
|
+
const channel = typeof args.flags.channel === "string" ? args.flags.channel : undefined;
|
|
543
|
+
const accountId = typeof args.flags["account-id"] === "string"
|
|
544
|
+
? args.flags["account-id"]
|
|
545
|
+
: undefined;
|
|
546
|
+
const senderId = typeof args.flags["sender-id"] === "string"
|
|
547
|
+
? args.flags["sender-id"]
|
|
548
|
+
: undefined;
|
|
549
|
+
const kindRaw = typeof args.flags["conversation-kind"] === "string"
|
|
550
|
+
? args.flags["conversation-kind"]
|
|
551
|
+
: undefined;
|
|
552
|
+
if (kindRaw !== undefined && kindRaw !== "direct" && kindRaw !== "group") {
|
|
553
|
+
console.error(`invalid --conversation-kind "${kindRaw}" (must be "direct" or "group")`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
const conversationKind = kindRaw;
|
|
557
|
+
const mentioned = typeof args.flags.mentioned === "boolean"
|
|
558
|
+
? args.flags.mentioned
|
|
559
|
+
: undefined;
|
|
560
|
+
const adapter = (typeof args.flags.adapter === "string" ? args.flags.adapter : "claude-code");
|
|
561
|
+
const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : "";
|
|
562
|
+
const hasAnyMatch = !!conversationId ||
|
|
563
|
+
!!conversationPrefix ||
|
|
564
|
+
!!channel ||
|
|
565
|
+
!!accountId ||
|
|
566
|
+
!!senderId ||
|
|
567
|
+
!!conversationKind ||
|
|
568
|
+
mentioned !== undefined;
|
|
569
|
+
if (!hasAnyMatch) {
|
|
570
|
+
console.error("at least one match flag required (--conversation-id/--room, --conversation-prefix/--prefix, --channel, --account-id, --sender-id, --conversation-kind, --mentioned)");
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
if (!cwd) {
|
|
574
|
+
console.error("--cwd required");
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
if (!getAdapterModule(adapter)) {
|
|
578
|
+
console.error(`unknown --adapter "${adapter}". Registered: ${ADAPTER_LIST}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
// Persist the canonical fields (conversationId/conversationPrefix) even
|
|
582
|
+
// when the user passed the legacy aliases, to avoid config drift.
|
|
583
|
+
const match = {};
|
|
584
|
+
if (channel)
|
|
585
|
+
match.channel = channel;
|
|
586
|
+
if (accountId)
|
|
587
|
+
match.accountId = accountId;
|
|
588
|
+
if (conversationId)
|
|
589
|
+
match.conversationId = conversationId;
|
|
590
|
+
if (conversationPrefix)
|
|
591
|
+
match.conversationPrefix = conversationPrefix;
|
|
592
|
+
if (conversationKind)
|
|
593
|
+
match.conversationKind = conversationKind;
|
|
594
|
+
if (senderId)
|
|
595
|
+
match.senderId = senderId;
|
|
596
|
+
if (mentioned !== undefined)
|
|
597
|
+
match.mentioned = mentioned;
|
|
598
|
+
cfg.routes.push({ match, adapter, cwd });
|
|
599
|
+
saveConfig(cfg);
|
|
600
|
+
console.log("route added");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (sub === "remove") {
|
|
604
|
+
const roomFlag = typeof args.flags.room === "string" ? args.flags.room : undefined;
|
|
605
|
+
const convIdFlag = typeof args.flags["conversation-id"] === "string"
|
|
606
|
+
? args.flags["conversation-id"]
|
|
607
|
+
: undefined;
|
|
608
|
+
const roomId = convIdFlag ?? roomFlag;
|
|
609
|
+
const prefixFlag = typeof args.flags.prefix === "string" ? args.flags.prefix : undefined;
|
|
610
|
+
const convPrefixFlag = typeof args.flags["conversation-prefix"] === "string"
|
|
611
|
+
? args.flags["conversation-prefix"]
|
|
612
|
+
: undefined;
|
|
613
|
+
const prefix = convPrefixFlag ?? prefixFlag;
|
|
614
|
+
const before = cfg.routes.length;
|
|
615
|
+
cfg.routes = cfg.routes.filter((r) => {
|
|
616
|
+
const rConv = r.match.conversationId ?? r.match.roomId;
|
|
617
|
+
const rPrefix = r.match.conversationPrefix ?? r.match.roomPrefix;
|
|
618
|
+
if (roomId && rConv === roomId)
|
|
619
|
+
return false;
|
|
620
|
+
if (prefix && rPrefix === prefix)
|
|
621
|
+
return false;
|
|
622
|
+
return true;
|
|
623
|
+
});
|
|
624
|
+
saveConfig(cfg);
|
|
625
|
+
console.log(`removed ${before - cfg.routes.length} route(s)`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
console.error(HELP);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
async function cmdConfig() {
|
|
632
|
+
const cfg = loadConfig();
|
|
633
|
+
// Surface the effective boot-agent list alongside the raw on-disk config
|
|
634
|
+
// so operators running `config` can tell which identities the daemon will
|
|
635
|
+
// actually bind — explicit list vs discovered credentials.
|
|
636
|
+
const explicit = resolveConfiguredAgentIds(cfg);
|
|
637
|
+
let boot = null;
|
|
638
|
+
try {
|
|
639
|
+
boot = resolveBootAgents(cfg);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
boot = null;
|
|
643
|
+
}
|
|
644
|
+
const payload = {
|
|
645
|
+
config: cfg,
|
|
646
|
+
effective: boot
|
|
647
|
+
? {
|
|
648
|
+
agents: boot.agents.map((a) => ({
|
|
649
|
+
agentId: a.agentId,
|
|
650
|
+
credentialsFile: a.credentialsFile,
|
|
651
|
+
...(a.displayName ? { displayName: a.displayName } : {}),
|
|
652
|
+
})),
|
|
653
|
+
source: explicit ? "config" : "credentials",
|
|
654
|
+
credentialsDir: boot.credentialsDir,
|
|
655
|
+
warnings: boot.warnings,
|
|
656
|
+
}
|
|
657
|
+
: null,
|
|
658
|
+
};
|
|
659
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Select which agent a `memory` subcommand targets.
|
|
663
|
+
*
|
|
664
|
+
* - `--agent <ag_xxx>` explicitly chooses an agent; it must be one listed
|
|
665
|
+
* in the resolved config.
|
|
666
|
+
* - If the daemon is bound to exactly one agent, `--agent` is optional and
|
|
667
|
+
* defaults to that agent.
|
|
668
|
+
* - If multiple agents are configured and no `--agent` is passed, we bail
|
|
669
|
+
* with an explicit message listing the options — too easy to footgun a
|
|
670
|
+
* memory write against the wrong agent otherwise.
|
|
671
|
+
*/
|
|
672
|
+
function resolveMemoryTargetAgent(args, cfg) {
|
|
673
|
+
const boot = resolveBootAgents(cfg);
|
|
674
|
+
const agents = boot.agents.map((a) => a.agentId);
|
|
675
|
+
if (agents.length === 0) {
|
|
676
|
+
console.error("memory: no agents configured or discovered (add `--agent` to `init` or drop a credentials JSON in the discovery dir)");
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
const flagAgent = typeof args.flags.agent === "string" ? args.flags.agent : undefined;
|
|
680
|
+
if (flagAgent) {
|
|
681
|
+
if (!agents.includes(flagAgent)) {
|
|
682
|
+
console.error(`--agent "${flagAgent}" is not configured. Configured agents: ${agents.join(", ")}`);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
return flagAgent;
|
|
686
|
+
}
|
|
687
|
+
if (agents.length === 1)
|
|
688
|
+
return agents[0];
|
|
689
|
+
console.error(`memory: --agent <ag_xxx> is required when the daemon is bound to multiple agents. Configured: ${agents.join(", ")}`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
async function cmdMemory(args) {
|
|
693
|
+
const cfg = loadConfig();
|
|
694
|
+
const agentId = resolveMemoryTargetAgent(args, cfg);
|
|
695
|
+
const sub = args.sub;
|
|
696
|
+
if (!sub || sub === "get") {
|
|
697
|
+
const memory = readWorkingMemory(agentId);
|
|
698
|
+
if (args.flags.json === true) {
|
|
699
|
+
console.log(JSON.stringify({ agentId, memory, dir: resolveMemoryDir(agentId) }, null, 2));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!memory) {
|
|
703
|
+
console.log(`(empty — no working memory for ${agentId})`);
|
|
704
|
+
console.log(`path: ${resolveMemoryDir(agentId)}/working-memory.json`);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (memory.goal)
|
|
708
|
+
console.log(`goal: ${memory.goal}`);
|
|
709
|
+
const entries = Object.entries(memory.sections);
|
|
710
|
+
if (entries.length === 0) {
|
|
711
|
+
console.log("(no sections)");
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
for (const [name, content] of entries) {
|
|
715
|
+
console.log(`\n[section: ${name}]`);
|
|
716
|
+
console.log(content);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
console.log(`\nupdatedAt: ${memory.updatedAt}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (sub === "set") {
|
|
723
|
+
const goal = typeof args.flags.goal === "string" ? args.flags.goal : undefined;
|
|
724
|
+
const section = typeof args.flags.section === "string" ? args.flags.section : undefined;
|
|
725
|
+
const content = typeof args.flags.content === "string" ? args.flags.content : undefined;
|
|
726
|
+
if (goal === undefined && content === undefined) {
|
|
727
|
+
console.error("memory set: provide --goal or --content");
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const res = updateWorkingMemory(agentId, { goal, section, content });
|
|
732
|
+
const status = { ok: true, totalChars: res.totalChars };
|
|
733
|
+
if (goal !== undefined)
|
|
734
|
+
status.goal = goal === "" ? null : goal;
|
|
735
|
+
if (content !== undefined) {
|
|
736
|
+
status.section = section ?? DEFAULT_SECTION;
|
|
737
|
+
status.sectionPresent = res.sectionPresent;
|
|
738
|
+
}
|
|
739
|
+
console.log(JSON.stringify(status, null, 2));
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (sub === "delete") {
|
|
748
|
+
const section = typeof args.flags.section === "string" ? args.flags.section : undefined;
|
|
749
|
+
if (!section) {
|
|
750
|
+
console.error("memory delete: --section required");
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
updateWorkingMemory(agentId, { section, content: "" });
|
|
755
|
+
console.log(`section "${section}" removed`);
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (sub === "clear") {
|
|
764
|
+
clearWorkingMemory(agentId);
|
|
765
|
+
console.log(`cleared working memory for ${agentId}`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
console.error(HELP);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
const fsFileReader = {
|
|
772
|
+
readFile(p) {
|
|
773
|
+
if (!existsSync(p))
|
|
774
|
+
return null;
|
|
775
|
+
try {
|
|
776
|
+
return readFileSync(p, "utf8");
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
async function cmdDoctor(args) {
|
|
784
|
+
const entries = detectRuntimes();
|
|
785
|
+
// Doctor should not hard-fail when no config exists yet; channel probes
|
|
786
|
+
// simply produce an empty list in that case.
|
|
787
|
+
let channels = [];
|
|
788
|
+
try {
|
|
789
|
+
const cfg = loadConfig();
|
|
790
|
+
channels = channelsFromDaemonConfig(cfg);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
channels = [];
|
|
794
|
+
}
|
|
795
|
+
const credentialsPath = (accountId) => path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
|
|
796
|
+
const input = await runDoctor(entries, channels, {
|
|
797
|
+
credentialsPath,
|
|
798
|
+
fileReader: fsFileReader,
|
|
799
|
+
fetcher: defaultHttpFetcher,
|
|
800
|
+
timeoutMs: 5_000,
|
|
801
|
+
});
|
|
802
|
+
if (args.flags.json === true) {
|
|
803
|
+
console.log(JSON.stringify(input, null, 2));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
console.log(renderDoctor(input));
|
|
807
|
+
}
|
|
808
|
+
async function main() {
|
|
809
|
+
const args = parseArgs(process.argv.slice(2));
|
|
810
|
+
if (!args.cmd || args.flags.help === true || args.flags.h === true) {
|
|
811
|
+
console.log(HELP);
|
|
812
|
+
process.exit(args.cmd ? 0 : 1);
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
switch (args.cmd) {
|
|
816
|
+
case "init":
|
|
817
|
+
await cmdInit(args);
|
|
818
|
+
break;
|
|
819
|
+
case "start":
|
|
820
|
+
await cmdStart(args);
|
|
821
|
+
break;
|
|
822
|
+
case "stop":
|
|
823
|
+
await cmdStop();
|
|
824
|
+
break;
|
|
825
|
+
case "status":
|
|
826
|
+
await cmdStatus(args);
|
|
827
|
+
break;
|
|
828
|
+
case "logs":
|
|
829
|
+
await cmdLogs(args);
|
|
830
|
+
break;
|
|
831
|
+
case "route":
|
|
832
|
+
await cmdRoute(args);
|
|
833
|
+
break;
|
|
834
|
+
case "config":
|
|
835
|
+
await cmdConfig();
|
|
836
|
+
break;
|
|
837
|
+
case "doctor":
|
|
838
|
+
await cmdDoctor(args);
|
|
839
|
+
break;
|
|
840
|
+
case "memory":
|
|
841
|
+
await cmdMemory(args);
|
|
842
|
+
break;
|
|
843
|
+
default:
|
|
844
|
+
console.error(`unknown command: ${args.cmd}`);
|
|
845
|
+
console.error(HELP);
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
main();
|