instar 0.1.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/.claude/settings.local.json +7 -0
- package/.claude/skills/setup-wizard/skill.md +343 -0
- package/.github/workflows/ci.yml +78 -0
- package/CLAUDE.md +82 -0
- package/README.md +194 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +141 -0
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +568 -0
- package/dist/commands/job.d.ts +20 -0
- package/dist/commands/job.js +84 -0
- package/dist/commands/server.d.ts +19 -0
- package/dist/commands/server.js +273 -0
- package/dist/commands/setup.d.ts +24 -0
- package/dist/commands/setup.js +865 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.js +114 -0
- package/dist/commands/user.d.ts +17 -0
- package/dist/commands/user.js +53 -0
- package/dist/core/Config.d.ts +16 -0
- package/dist/core/Config.js +144 -0
- package/dist/core/Prerequisites.d.ts +28 -0
- package/dist/core/Prerequisites.js +159 -0
- package/dist/core/RelationshipManager.d.ts +73 -0
- package/dist/core/RelationshipManager.js +318 -0
- package/dist/core/SessionManager.d.ts +89 -0
- package/dist/core/SessionManager.js +326 -0
- package/dist/core/StateManager.d.ts +28 -0
- package/dist/core/StateManager.js +96 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/types.js +8 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -0
- package/dist/messaging/TelegramAdapter.d.ts +73 -0
- package/dist/messaging/TelegramAdapter.js +288 -0
- package/dist/monitoring/HealthChecker.d.ts +38 -0
- package/dist/monitoring/HealthChecker.js +148 -0
- package/dist/scaffold/bootstrap.d.ts +21 -0
- package/dist/scaffold/bootstrap.js +110 -0
- package/dist/scaffold/templates.d.ts +34 -0
- package/dist/scaffold/templates.js +187 -0
- package/dist/scheduler/JobLoader.d.ts +18 -0
- package/dist/scheduler/JobLoader.js +70 -0
- package/dist/scheduler/JobScheduler.d.ts +111 -0
- package/dist/scheduler/JobScheduler.js +402 -0
- package/dist/server/AgentServer.d.ts +40 -0
- package/dist/server/AgentServer.js +73 -0
- package/dist/server/middleware.d.ts +12 -0
- package/dist/server/middleware.js +50 -0
- package/dist/server/routes.d.ts +25 -0
- package/dist/server/routes.js +224 -0
- package/dist/users/UserManager.d.ts +45 -0
- package/dist/users/UserManager.js +113 -0
- package/docs/dawn-audit-report.md +412 -0
- package/docs/positioning-vs-openclaw.md +246 -0
- package/package.json +52 -0
- package/src/cli.ts +169 -0
- package/src/commands/init.ts +654 -0
- package/src/commands/job.ts +110 -0
- package/src/commands/server.ts +325 -0
- package/src/commands/setup.ts +958 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/user.ts +71 -0
- package/src/core/Config.ts +161 -0
- package/src/core/Prerequisites.ts +187 -0
- package/src/core/RelationshipManager.ts +366 -0
- package/src/core/SessionManager.ts +385 -0
- package/src/core/StateManager.ts +121 -0
- package/src/core/types.ts +320 -0
- package/src/index.ts +58 -0
- package/src/messaging/TelegramAdapter.ts +365 -0
- package/src/monitoring/HealthChecker.ts +172 -0
- package/src/scaffold/bootstrap.ts +122 -0
- package/src/scaffold/templates.ts +204 -0
- package/src/scheduler/JobLoader.ts +85 -0
- package/src/scheduler/JobScheduler.ts +476 -0
- package/src/server/AgentServer.ts +93 -0
- package/src/server/middleware.ts +58 -0
- package/src/server/routes.ts +278 -0
- package/src/templates/default-jobs.json +47 -0
- package/src/templates/hooks/compaction-recovery.sh +23 -0
- package/src/templates/hooks/dangerous-command-guard.sh +35 -0
- package/src/templates/hooks/grounding-before-messaging.sh +22 -0
- package/src/templates/hooks/session-start.sh +37 -0
- package/src/templates/hooks/settings-template.json +45 -0
- package/src/templates/scripts/health-watchdog.sh +63 -0
- package/src/templates/scripts/telegram-reply.sh +54 -0
- package/src/users/UserManager.ts +129 -0
- package/tests/e2e/lifecycle.test.ts +376 -0
- package/tests/fixtures/test-repo/CLAUDE.md +3 -0
- package/tests/fixtures/test-repo/README.md +1 -0
- package/tests/helpers/setup.ts +209 -0
- package/tests/integration/fresh-install.test.ts +218 -0
- package/tests/integration/scheduler-basic.test.ts +109 -0
- package/tests/integration/server-full.test.ts +284 -0
- package/tests/integration/session-lifecycle.test.ts +181 -0
- package/tests/unit/Config.test.ts +22 -0
- package/tests/unit/HealthChecker.test.ts +168 -0
- package/tests/unit/JobLoader.test.ts +151 -0
- package/tests/unit/JobScheduler.test.ts +267 -0
- package/tests/unit/Prerequisites.test.ts +59 -0
- package/tests/unit/RelationshipManager.test.ts +345 -0
- package/tests/unit/StateManager.test.ts +143 -0
- package/tests/unit/TelegramAdapter.test.ts +165 -0
- package/tests/unit/UserManager.test.ts +131 -0
- package/tests/unit/bootstrap.test.ts +28 -0
- package/tests/unit/commands.test.ts +138 -0
- package/tests/unit/middleware.test.ts +92 -0
- package/tests/unit/relationship-routes.test.ts +131 -0
- package/tests/unit/scaffold-templates.test.ts +132 -0
- package/tests/unit/server.test.ts +163 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.e2e.config.ts +9 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API routes — health, status, sessions, jobs, events.
|
|
3
|
+
*
|
|
4
|
+
* Extracted/simplified from Dawn's 2267-line routes.ts.
|
|
5
|
+
* All the observability you need, none of the complexity you don't.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import type { SessionManager } from '../core/SessionManager.js';
|
|
9
|
+
import type { StateManager } from '../core/StateManager.js';
|
|
10
|
+
import type { JobScheduler } from '../scheduler/JobScheduler.js';
|
|
11
|
+
import type { AgentKitConfig } from '../core/types.js';
|
|
12
|
+
import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
13
|
+
import type { RelationshipManager } from '../core/RelationshipManager.js';
|
|
14
|
+
interface RouteContext {
|
|
15
|
+
config: AgentKitConfig;
|
|
16
|
+
sessionManager: SessionManager;
|
|
17
|
+
state: StateManager;
|
|
18
|
+
scheduler: JobScheduler | null;
|
|
19
|
+
telegram: TelegramAdapter | null;
|
|
20
|
+
relationships: RelationshipManager | null;
|
|
21
|
+
startTime: Date;
|
|
22
|
+
}
|
|
23
|
+
export declare function createRoutes(ctx: RouteContext): Router;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API routes — health, status, sessions, jobs, events.
|
|
3
|
+
*
|
|
4
|
+
* Extracted/simplified from Dawn's 2267-line routes.ts.
|
|
5
|
+
* All the observability you need, none of the complexity you don't.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { execSync as execSyncFn } from 'node:child_process';
|
|
9
|
+
export function createRoutes(ctx) {
|
|
10
|
+
const router = Router();
|
|
11
|
+
// ── Health ──────────────────────────────────────────────────────
|
|
12
|
+
router.get('/health', (_req, res) => {
|
|
13
|
+
const uptimeMs = Date.now() - ctx.startTime.getTime();
|
|
14
|
+
res.json({
|
|
15
|
+
status: 'ok',
|
|
16
|
+
uptime: uptimeMs,
|
|
17
|
+
uptimeHuman: formatUptime(uptimeMs),
|
|
18
|
+
version: '0.1.0',
|
|
19
|
+
project: ctx.config.projectName,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
// ── Status ──────────────────────────────────────────────────────
|
|
23
|
+
router.get('/status', (_req, res) => {
|
|
24
|
+
const sessions = ctx.sessionManager.listRunningSessions();
|
|
25
|
+
const schedulerStatus = ctx.scheduler?.getStatus() ?? null;
|
|
26
|
+
res.json({
|
|
27
|
+
sessions: {
|
|
28
|
+
running: sessions.length,
|
|
29
|
+
max: ctx.config.sessions.maxSessions,
|
|
30
|
+
list: sessions.map(s => ({ id: s.id, name: s.name, jobSlug: s.jobSlug })),
|
|
31
|
+
},
|
|
32
|
+
scheduler: schedulerStatus,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// ── Sessions ────────────────────────────────────────────────────
|
|
36
|
+
router.get('/sessions', (req, res) => {
|
|
37
|
+
const status = req.query.status;
|
|
38
|
+
const sessions = status
|
|
39
|
+
? ctx.state.listSessions({ status: status })
|
|
40
|
+
: ctx.state.listSessions();
|
|
41
|
+
res.json(sessions);
|
|
42
|
+
});
|
|
43
|
+
router.get('/sessions/:name/output', (req, res) => {
|
|
44
|
+
const lines = parseInt(req.query.lines) || 100;
|
|
45
|
+
const output = ctx.sessionManager.captureOutput(req.params.name, lines);
|
|
46
|
+
if (output === null) {
|
|
47
|
+
res.status(404).json({ error: `Session "${req.params.name}" not found or not running` });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.json({ session: req.params.name, output });
|
|
51
|
+
});
|
|
52
|
+
router.post('/sessions/:name/input', (req, res) => {
|
|
53
|
+
const { text } = req.body;
|
|
54
|
+
if (!text || typeof text !== 'string') {
|
|
55
|
+
res.status(400).json({ error: 'Request body must include "text" field' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const success = ctx.sessionManager.sendInput(req.params.name, text);
|
|
59
|
+
if (!success) {
|
|
60
|
+
res.status(404).json({ error: `Session "${req.params.name}" not found or not running` });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.json({ ok: true });
|
|
64
|
+
});
|
|
65
|
+
router.post('/sessions/spawn', (req, res) => {
|
|
66
|
+
const { name, prompt, model, jobSlug } = req.body;
|
|
67
|
+
if (!name || !prompt) {
|
|
68
|
+
res.status(400).json({ error: '"name" and "prompt" are required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const session = ctx.sessionManager.spawnSession({ name, prompt, model, jobSlug });
|
|
73
|
+
// spawnSession is async but we want to handle errors,
|
|
74
|
+
// so we use .then/.catch
|
|
75
|
+
session.then(s => res.status(201).json(s)).catch(err => {
|
|
76
|
+
res.status(500).json({ error: err.message });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
res.status(500).json({ error: err.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
router.delete('/sessions/:id', (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const killed = ctx.sessionManager.killSession(req.params.id);
|
|
86
|
+
if (!killed) {
|
|
87
|
+
res.status(404).json({ error: `Session "${req.params.id}" not found` });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
res.json({ ok: true, killed: req.params.id });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
res.status(400).json({ error: err.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// ── Jobs ────────────────────────────────────────────────────────
|
|
97
|
+
router.get('/jobs', (_req, res) => {
|
|
98
|
+
if (!ctx.scheduler) {
|
|
99
|
+
res.json({ jobs: [], scheduler: null });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const jobs = ctx.scheduler.getJobs().map(job => {
|
|
103
|
+
const jobState = ctx.state.getJobState(job.slug);
|
|
104
|
+
return { ...job, state: jobState };
|
|
105
|
+
});
|
|
106
|
+
res.json({ jobs, queue: ctx.scheduler.getQueue() });
|
|
107
|
+
});
|
|
108
|
+
router.post('/jobs/:slug/trigger', (req, res) => {
|
|
109
|
+
if (!ctx.scheduler) {
|
|
110
|
+
res.status(503).json({ error: 'Scheduler not running' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const reason = req.body?.reason || 'manual';
|
|
114
|
+
try {
|
|
115
|
+
const result = ctx.scheduler.triggerJob(req.params.slug, reason);
|
|
116
|
+
res.json({ slug: req.params.slug, result });
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
res.status(404).json({ error: err.message });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// ── Telegram ────────────────────────────────────────────────────
|
|
123
|
+
router.post('/telegram/reply/:topicId', async (req, res) => {
|
|
124
|
+
if (!ctx.telegram) {
|
|
125
|
+
res.status(503).json({ error: 'Telegram not configured' });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const topicId = parseInt(req.params.topicId);
|
|
129
|
+
const { text } = req.body;
|
|
130
|
+
if (!text || typeof text !== 'string') {
|
|
131
|
+
res.status(400).json({ error: '"text" field required' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await ctx.telegram.sendToTopic(topicId, text);
|
|
136
|
+
res.json({ ok: true, topicId });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
res.status(500).json({ error: err.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// ── tmux Sessions (raw) ─────────────────────────────────────────
|
|
143
|
+
router.get('/sessions/tmux', (_req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const tmuxPath = ctx.config.sessions.tmuxPath;
|
|
146
|
+
const output = execSyncFn(`${tmuxPath} list-sessions -F '#{session_name}' 2>/dev/null || true`, {
|
|
147
|
+
encoding: 'utf-8',
|
|
148
|
+
}).trim();
|
|
149
|
+
const sessions = output
|
|
150
|
+
? output.split('\n').filter(Boolean).map((name) => ({ name }))
|
|
151
|
+
: [];
|
|
152
|
+
res.json({ sessions });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
res.json({ sessions: [] });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// ── Relationships ─────────────────────────────────────────────────
|
|
159
|
+
router.get('/relationships', (_req, res) => {
|
|
160
|
+
if (!ctx.relationships) {
|
|
161
|
+
res.json({ relationships: [] });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const sortBy = _req.query.sort || 'significance';
|
|
165
|
+
res.json({ relationships: ctx.relationships.getAll(sortBy) });
|
|
166
|
+
});
|
|
167
|
+
// Stale must be before :id to avoid "stale" matching as a param
|
|
168
|
+
router.get('/relationships/stale', (req, res) => {
|
|
169
|
+
if (!ctx.relationships) {
|
|
170
|
+
res.json({ stale: [] });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const days = parseInt(req.query.days) || 14;
|
|
174
|
+
res.json({ stale: ctx.relationships.getStaleRelationships(days) });
|
|
175
|
+
});
|
|
176
|
+
router.get('/relationships/:id', (req, res) => {
|
|
177
|
+
if (!ctx.relationships) {
|
|
178
|
+
res.status(503).json({ error: 'Relationships not configured' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const record = ctx.relationships.get(req.params.id);
|
|
182
|
+
if (!record) {
|
|
183
|
+
res.status(404).json({ error: 'Relationship not found' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
res.json(record);
|
|
187
|
+
});
|
|
188
|
+
router.get('/relationships/:id/context', (req, res) => {
|
|
189
|
+
if (!ctx.relationships) {
|
|
190
|
+
res.status(503).json({ error: 'Relationships not configured' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const context = ctx.relationships.getContextForPerson(req.params.id);
|
|
194
|
+
if (!context) {
|
|
195
|
+
res.status(404).json({ error: 'Relationship not found' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
res.json({ context });
|
|
199
|
+
});
|
|
200
|
+
// ── Events ──────────────────────────────────────────────────────
|
|
201
|
+
router.get('/events', (req, res) => {
|
|
202
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
203
|
+
const type = req.query.type;
|
|
204
|
+
const sinceHours = parseInt(req.query.since) || 24;
|
|
205
|
+
const since = new Date(Date.now() - sinceHours * 60 * 60 * 1000);
|
|
206
|
+
const events = ctx.state.queryEvents({ since, type, limit });
|
|
207
|
+
res.json(events);
|
|
208
|
+
});
|
|
209
|
+
return router;
|
|
210
|
+
}
|
|
211
|
+
function formatUptime(ms) {
|
|
212
|
+
const seconds = Math.floor(ms / 1000);
|
|
213
|
+
const minutes = Math.floor(seconds / 60);
|
|
214
|
+
const hours = Math.floor(minutes / 60);
|
|
215
|
+
const days = Math.floor(hours / 24);
|
|
216
|
+
if (days > 0)
|
|
217
|
+
return `${days}d ${hours % 24}h`;
|
|
218
|
+
if (hours > 0)
|
|
219
|
+
return `${hours}h ${minutes % 60}m`;
|
|
220
|
+
if (minutes > 0)
|
|
221
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
222
|
+
return `${seconds}s`;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Manager — multi-user identity resolution.
|
|
3
|
+
*
|
|
4
|
+
* Maps incoming messages to known users based on their channels.
|
|
5
|
+
* Same agent, same repo, different relationship per user.
|
|
6
|
+
*/
|
|
7
|
+
import type { UserProfile, UserChannel, Message } from '../core/types.js';
|
|
8
|
+
export declare class UserManager {
|
|
9
|
+
private users;
|
|
10
|
+
private channelIndex;
|
|
11
|
+
private usersFile;
|
|
12
|
+
constructor(stateDir: string, initialUsers?: UserProfile[]);
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a user from an incoming message.
|
|
15
|
+
* Returns the user profile if the sender is recognized.
|
|
16
|
+
*/
|
|
17
|
+
resolveFromMessage(message: Message): UserProfile | null;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a user from a channel identifier.
|
|
20
|
+
*/
|
|
21
|
+
resolveFromChannel(channel: UserChannel): UserProfile | null;
|
|
22
|
+
/**
|
|
23
|
+
* Get a user by ID.
|
|
24
|
+
*/
|
|
25
|
+
getUser(userId: string): UserProfile | null;
|
|
26
|
+
/**
|
|
27
|
+
* List all registered users.
|
|
28
|
+
*/
|
|
29
|
+
listUsers(): UserProfile[];
|
|
30
|
+
/**
|
|
31
|
+
* Add or update a user.
|
|
32
|
+
*/
|
|
33
|
+
upsertUser(profile: UserProfile): void;
|
|
34
|
+
/**
|
|
35
|
+
* Remove a user.
|
|
36
|
+
*/
|
|
37
|
+
removeUser(userId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Check if a user has a specific permission.
|
|
40
|
+
*/
|
|
41
|
+
hasPermission(userId: string, permission: string): boolean;
|
|
42
|
+
private loadUsers;
|
|
43
|
+
private persistUsers;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=UserManager.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Manager — multi-user identity resolution.
|
|
3
|
+
*
|
|
4
|
+
* Maps incoming messages to known users based on their channels.
|
|
5
|
+
* Same agent, same repo, different relationship per user.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
export class UserManager {
|
|
10
|
+
users = new Map();
|
|
11
|
+
channelIndex = new Map(); // "type:identifier" -> userId
|
|
12
|
+
usersFile;
|
|
13
|
+
constructor(stateDir, initialUsers) {
|
|
14
|
+
this.usersFile = path.join(stateDir, 'users.json');
|
|
15
|
+
this.loadUsers(initialUsers);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a user from an incoming message.
|
|
19
|
+
* Returns the user profile if the sender is recognized.
|
|
20
|
+
*/
|
|
21
|
+
resolveFromMessage(message) {
|
|
22
|
+
return this.resolveFromChannel(message.channel);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a user from a channel identifier.
|
|
26
|
+
*/
|
|
27
|
+
resolveFromChannel(channel) {
|
|
28
|
+
const key = `${channel.type}:${channel.identifier}`;
|
|
29
|
+
const userId = this.channelIndex.get(key);
|
|
30
|
+
if (!userId)
|
|
31
|
+
return null;
|
|
32
|
+
return this.users.get(userId) || null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get a user by ID.
|
|
36
|
+
*/
|
|
37
|
+
getUser(userId) {
|
|
38
|
+
return this.users.get(userId) || null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* List all registered users.
|
|
42
|
+
*/
|
|
43
|
+
listUsers() {
|
|
44
|
+
return Array.from(this.users.values());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Add or update a user.
|
|
48
|
+
*/
|
|
49
|
+
upsertUser(profile) {
|
|
50
|
+
// Remove old channel index entries
|
|
51
|
+
const existing = this.users.get(profile.id);
|
|
52
|
+
if (existing) {
|
|
53
|
+
for (const channel of existing.channels) {
|
|
54
|
+
this.channelIndex.delete(`${channel.type}:${channel.identifier}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Add new entries
|
|
58
|
+
this.users.set(profile.id, profile);
|
|
59
|
+
for (const channel of profile.channels) {
|
|
60
|
+
this.channelIndex.set(`${channel.type}:${channel.identifier}`, profile.id);
|
|
61
|
+
}
|
|
62
|
+
this.persistUsers();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Remove a user.
|
|
66
|
+
*/
|
|
67
|
+
removeUser(userId) {
|
|
68
|
+
const user = this.users.get(userId);
|
|
69
|
+
if (!user)
|
|
70
|
+
return false;
|
|
71
|
+
for (const channel of user.channels) {
|
|
72
|
+
this.channelIndex.delete(`${channel.type}:${channel.identifier}`);
|
|
73
|
+
}
|
|
74
|
+
this.users.delete(userId);
|
|
75
|
+
this.persistUsers();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a user has a specific permission.
|
|
80
|
+
*/
|
|
81
|
+
hasPermission(userId, permission) {
|
|
82
|
+
const user = this.users.get(userId);
|
|
83
|
+
if (!user)
|
|
84
|
+
return false;
|
|
85
|
+
return user.permissions.includes(permission) || user.permissions.includes('admin');
|
|
86
|
+
}
|
|
87
|
+
loadUsers(initialUsers) {
|
|
88
|
+
// Load from file if exists
|
|
89
|
+
if (fs.existsSync(this.usersFile)) {
|
|
90
|
+
const data = JSON.parse(fs.readFileSync(this.usersFile, 'utf-8'));
|
|
91
|
+
for (const user of data) {
|
|
92
|
+
this.users.set(user.id, user);
|
|
93
|
+
for (const channel of user.channels) {
|
|
94
|
+
this.channelIndex.set(`${channel.type}:${channel.identifier}`, user.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Merge initial users (config takes precedence for initial setup)
|
|
99
|
+
if (initialUsers) {
|
|
100
|
+
for (const user of initialUsers) {
|
|
101
|
+
if (!this.users.has(user.id)) {
|
|
102
|
+
this.upsertUser(user);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
persistUsers() {
|
|
108
|
+
const dir = path.dirname(this.usersFile);
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(this.usersFile, JSON.stringify(Array.from(this.users.values()), null, 2));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=UserManager.js.map
|