@openacp/cli 2026.410.2 → 2026.413.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/cli.js CHANGED
@@ -1244,11 +1244,11 @@ mockServices.context(overrides?) // buildContext, registerProvider
1244
1244
  ctx.registerCommand({
1245
1245
  name: 'mycommand',
1246
1246
  description: 'Does something useful',
1247
- usage: '<arg>',
1247
+ usage: '[arg]',
1248
1248
  category: 'plugin',
1249
1249
  async handler(args) {
1250
1250
  const input = args.raw.trim()
1251
- if (!input) return { type: 'error', message: 'Usage: /mycommand <arg>' }
1251
+ if (!input) return { type: 'error', message: 'Usage: /mycommand [arg]' }
1252
1252
  return { type: 'text', text: \\\`Result: \\\${input}\\\` }
1253
1253
  },
1254
1254
  })
@@ -1811,6 +1811,8 @@ var init_events = __esm({
1811
1811
  TURN_START: "turn:start",
1812
1812
  /** Turn ended (always fires, even on error) — read-only, fire-and-forget. */
1813
1813
  TURN_END: "turn:end",
1814
+ /** After a turn completes — full assembled agent text, read-only, fire-and-forget. */
1815
+ AGENT_AFTER_TURN: "agent:afterTurn",
1814
1816
  // --- Session lifecycle ---
1815
1817
  /** Before a new session is created — modifiable, can block. */
1816
1818
  SESSION_BEFORE_CREATE: "session:beforeCreate",
@@ -1888,7 +1890,22 @@ var init_events = __esm({
1888
1890
  PLUGIN_UNLOADED: "plugin:unloaded",
1889
1891
  // --- Usage ---
1890
1892
  /** Fired when a token usage record is captured (consumed by usage plugin). */
1891
- USAGE_RECORDED: "usage:recorded"
1893
+ USAGE_RECORDED: "usage:recorded",
1894
+ // --- Identity lifecycle ---
1895
+ /** Fired when a new user+identity record is created. */
1896
+ IDENTITY_CREATED: "identity:created",
1897
+ /** Fired when user profile fields change. */
1898
+ IDENTITY_UPDATED: "identity:updated",
1899
+ /** Fired when two identities are linked (same person). */
1900
+ IDENTITY_LINKED: "identity:linked",
1901
+ /** Fired when an identity is unlinked into a new user. */
1902
+ IDENTITY_UNLINKED: "identity:unlinked",
1903
+ /** Fired when two user records are merged during a link operation. */
1904
+ IDENTITY_USER_MERGED: "identity:userMerged",
1905
+ /** Fired when a user's role changes. */
1906
+ IDENTITY_ROLE_CHANGED: "identity:roleChanged",
1907
+ /** Fired when a user is seen (throttled). */
1908
+ IDENTITY_SEEN: "identity:seen"
1892
1909
  };
1893
1910
  SessionEv = {
1894
1911
  /** Agent produced an event (text, tool_call, etc.) during a turn. */
@@ -2035,6 +2052,704 @@ var init_security = __esm({
2035
2052
  }
2036
2053
  });
2037
2054
 
2055
+ // src/plugins/identity/types.ts
2056
+ function formatIdentityId(source, platformId) {
2057
+ return `${source}:${platformId}`;
2058
+ }
2059
+ var init_types = __esm({
2060
+ "src/plugins/identity/types.ts"() {
2061
+ "use strict";
2062
+ }
2063
+ });
2064
+
2065
+ // src/plugins/identity/identity-service.ts
2066
+ import { nanoid } from "nanoid";
2067
+ var IdentityServiceImpl;
2068
+ var init_identity_service = __esm({
2069
+ "src/plugins/identity/identity-service.ts"() {
2070
+ "use strict";
2071
+ init_types();
2072
+ IdentityServiceImpl = class {
2073
+ /**
2074
+ * @param store - Persistence layer for user/identity records and indexes.
2075
+ * @param emitEvent - Callback to publish events on the EventBus.
2076
+ * @param getSessionsForUser - Optional function to look up sessions for a userId.
2077
+ */
2078
+ constructor(store, emitEvent, getSessionsForUser) {
2079
+ this.store = store;
2080
+ this.emitEvent = emitEvent;
2081
+ this.getSessionsForUser = getSessionsForUser;
2082
+ }
2083
+ registeredSources = /* @__PURE__ */ new Set();
2084
+ // ─── Lookups ───
2085
+ async getUser(userId) {
2086
+ return this.store.getUser(userId);
2087
+ }
2088
+ async getUserByUsername(username) {
2089
+ const userId = await this.store.getUserIdByUsername(username);
2090
+ if (!userId) return void 0;
2091
+ return this.store.getUser(userId);
2092
+ }
2093
+ async getIdentity(identityId) {
2094
+ return this.store.getIdentity(identityId);
2095
+ }
2096
+ async getUserByIdentity(identityId) {
2097
+ const identity = await this.store.getIdentity(identityId);
2098
+ if (!identity) return void 0;
2099
+ return this.store.getUser(identity.userId);
2100
+ }
2101
+ async getIdentitiesFor(userId) {
2102
+ return this.store.getIdentitiesForUser(userId);
2103
+ }
2104
+ async listUsers(filter) {
2105
+ return this.store.listUsers(filter);
2106
+ }
2107
+ /**
2108
+ * Case-insensitive substring search across displayName, username, and platform
2109
+ * usernames. Designed for admin tooling, not high-frequency user-facing paths.
2110
+ */
2111
+ async searchUsers(query) {
2112
+ const all = await this.store.listUsers();
2113
+ const q = query.toLowerCase();
2114
+ const matched = [];
2115
+ for (const user of all) {
2116
+ const nameMatch = user.displayName.toLowerCase().includes(q) || user.username && user.username.toLowerCase().includes(q);
2117
+ if (nameMatch) {
2118
+ matched.push(user);
2119
+ continue;
2120
+ }
2121
+ const identities = await this.store.getIdentitiesForUser(user.userId);
2122
+ const platformMatch = identities.some(
2123
+ (id) => id.platformUsername && id.platformUsername.toLowerCase().includes(q)
2124
+ );
2125
+ if (platformMatch) matched.push(user);
2126
+ }
2127
+ return matched;
2128
+ }
2129
+ async getSessionsFor(userId) {
2130
+ if (!this.getSessionsForUser) return [];
2131
+ return this.getSessionsForUser(userId);
2132
+ }
2133
+ // ─── Mutations ───
2134
+ /**
2135
+ * Creates a user + identity pair atomically.
2136
+ * The first ever user in the system is auto-promoted to admin — this ensures
2137
+ * there is always at least one admin when bootstrapping a fresh instance.
2138
+ */
2139
+ async createUserWithIdentity(data) {
2140
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2141
+ const userId = `u_${nanoid(12)}`;
2142
+ const identityId = formatIdentityId(data.source, data.platformId);
2143
+ const count = await this.store.getUserCount();
2144
+ const role = count === 0 ? "admin" : data.role ?? "member";
2145
+ const user = {
2146
+ userId,
2147
+ displayName: data.displayName,
2148
+ username: data.username,
2149
+ role,
2150
+ identities: [identityId],
2151
+ pluginData: {},
2152
+ createdAt: now,
2153
+ updatedAt: now,
2154
+ lastSeenAt: now
2155
+ };
2156
+ const identity = {
2157
+ identityId,
2158
+ userId,
2159
+ source: data.source,
2160
+ platformId: data.platformId,
2161
+ platformUsername: data.platformUsername,
2162
+ platformDisplayName: data.platformDisplayName,
2163
+ createdAt: now,
2164
+ updatedAt: now
2165
+ };
2166
+ await this.store.putUser(user);
2167
+ await this.store.putIdentity(identity);
2168
+ await this.store.setSourceIndex(data.source, data.platformId, identityId);
2169
+ if (data.username) {
2170
+ await this.store.setUsernameIndex(data.username, userId);
2171
+ }
2172
+ this.emitEvent("identity:created", { userId, identityId, source: data.source, displayName: data.displayName });
2173
+ return { user, identity };
2174
+ }
2175
+ async updateUser(userId, changes) {
2176
+ const user = await this.store.getUser(userId);
2177
+ if (!user) throw new Error(`User not found: ${userId}`);
2178
+ if (changes.username !== void 0 && changes.username !== user.username) {
2179
+ if (changes.username) {
2180
+ const existingId = await this.store.getUserIdByUsername(changes.username);
2181
+ if (existingId && existingId !== userId) {
2182
+ throw new Error(`Username already taken: ${changes.username}`);
2183
+ }
2184
+ await this.store.setUsernameIndex(changes.username, userId);
2185
+ }
2186
+ if (user.username) {
2187
+ await this.store.deleteUsernameIndex(user.username);
2188
+ }
2189
+ }
2190
+ const updated = {
2191
+ ...user,
2192
+ ...changes,
2193
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2194
+ };
2195
+ await this.store.putUser(updated);
2196
+ this.emitEvent("identity:updated", { userId, changes: Object.keys(changes) });
2197
+ return updated;
2198
+ }
2199
+ async setRole(userId, role) {
2200
+ const user = await this.store.getUser(userId);
2201
+ if (!user) throw new Error(`User not found: ${userId}`);
2202
+ const oldRole = user.role;
2203
+ await this.store.putUser({ ...user, role, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2204
+ this.emitEvent("identity:roleChanged", { userId, oldRole, newRole: role });
2205
+ }
2206
+ async createIdentity(userId, identity) {
2207
+ const user = await this.store.getUser(userId);
2208
+ if (!user) throw new Error(`User not found: ${userId}`);
2209
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2210
+ const identityId = formatIdentityId(identity.source, identity.platformId);
2211
+ const record = {
2212
+ identityId,
2213
+ userId,
2214
+ source: identity.source,
2215
+ platformId: identity.platformId,
2216
+ platformUsername: identity.platformUsername,
2217
+ platformDisplayName: identity.platformDisplayName,
2218
+ createdAt: now,
2219
+ updatedAt: now
2220
+ };
2221
+ await this.store.putIdentity(record);
2222
+ await this.store.setSourceIndex(identity.source, identity.platformId, identityId);
2223
+ const updatedUser = {
2224
+ ...user,
2225
+ identities: [...user.identities, identityId],
2226
+ updatedAt: now
2227
+ };
2228
+ await this.store.putUser(updatedUser);
2229
+ return record;
2230
+ }
2231
+ /**
2232
+ * Links two identities into a single user.
2233
+ *
2234
+ * When identities belong to different users, the younger (more recently created)
2235
+ * user is merged into the older one. We keep the older user as the canonical
2236
+ * record because it likely has more history, sessions, and plugin data.
2237
+ *
2238
+ * Merge strategy for pluginData: per-namespace, the winning user's data takes
2239
+ * precedence. The younger user's data only fills in missing namespaces.
2240
+ */
2241
+ async link(identityIdA, identityIdB) {
2242
+ const identityA = await this.store.getIdentity(identityIdA);
2243
+ const identityB = await this.store.getIdentity(identityIdB);
2244
+ if (!identityA) throw new Error(`Identity not found: ${identityIdA}`);
2245
+ if (!identityB) throw new Error(`Identity not found: ${identityIdB}`);
2246
+ if (identityA.userId === identityB.userId) return;
2247
+ const userA = await this.store.getUser(identityA.userId);
2248
+ const userB = await this.store.getUser(identityB.userId);
2249
+ if (!userA) throw new Error(`User not found: ${identityA.userId}`);
2250
+ if (!userB) throw new Error(`User not found: ${identityB.userId}`);
2251
+ const [keep, merge] = userA.createdAt <= userB.createdAt ? [userA, userB] : [userB, userA];
2252
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2253
+ for (const identityId of merge.identities) {
2254
+ const identity = await this.store.getIdentity(identityId);
2255
+ if (!identity) continue;
2256
+ const updated = { ...identity, userId: keep.userId, updatedAt: now };
2257
+ await this.store.putIdentity(updated);
2258
+ }
2259
+ const mergedPluginData = { ...merge.pluginData };
2260
+ for (const [ns, nsData] of Object.entries(keep.pluginData)) {
2261
+ mergedPluginData[ns] = nsData;
2262
+ }
2263
+ if (merge.username) {
2264
+ await this.store.deleteUsernameIndex(merge.username);
2265
+ }
2266
+ const updatedKeep = {
2267
+ ...keep,
2268
+ identities: [.../* @__PURE__ */ new Set([...keep.identities, ...merge.identities])],
2269
+ pluginData: mergedPluginData,
2270
+ updatedAt: now
2271
+ };
2272
+ await this.store.putUser(updatedKeep);
2273
+ await this.store.deleteUser(merge.userId);
2274
+ const linkedIdentityId = identityA.userId === merge.userId ? identityIdA : identityIdB;
2275
+ this.emitEvent("identity:linked", { userId: keep.userId, identityId: linkedIdentityId, linkedFrom: merge.userId });
2276
+ this.emitEvent("identity:userMerged", {
2277
+ keptUserId: keep.userId,
2278
+ mergedUserId: merge.userId,
2279
+ movedIdentities: merge.identities
2280
+ });
2281
+ }
2282
+ /**
2283
+ * Separates an identity from its user into a new standalone account.
2284
+ * Throws if it's the user's last identity — unlinking would produce a
2285
+ * ghost user with no way to authenticate.
2286
+ */
2287
+ async unlink(identityId) {
2288
+ const identity = await this.store.getIdentity(identityId);
2289
+ if (!identity) throw new Error(`Identity not found: ${identityId}`);
2290
+ const user = await this.store.getUser(identity.userId);
2291
+ if (!user) throw new Error(`User not found: ${identity.userId}`);
2292
+ if (user.identities.length <= 1) {
2293
+ throw new Error(`Cannot unlink the last identity from user ${identity.userId}`);
2294
+ }
2295
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2296
+ const newUserId = `u_${nanoid(12)}`;
2297
+ const newUser = {
2298
+ userId: newUserId,
2299
+ displayName: identity.platformDisplayName ?? identity.platformUsername ?? "User",
2300
+ role: "member",
2301
+ identities: [identityId],
2302
+ pluginData: {},
2303
+ createdAt: now,
2304
+ updatedAt: now,
2305
+ lastSeenAt: now
2306
+ };
2307
+ await this.store.putUser(newUser);
2308
+ await this.store.putIdentity({ ...identity, userId: newUserId, updatedAt: now });
2309
+ const updatedUser = {
2310
+ ...user,
2311
+ identities: user.identities.filter((id) => id !== identityId),
2312
+ updatedAt: now
2313
+ };
2314
+ await this.store.putUser(updatedUser);
2315
+ this.emitEvent("identity:unlinked", {
2316
+ userId: user.userId,
2317
+ identityId,
2318
+ newUserId
2319
+ });
2320
+ }
2321
+ // ─── Plugin data ───
2322
+ async setPluginData(userId, pluginName, key, value) {
2323
+ const user = await this.store.getUser(userId);
2324
+ if (!user) throw new Error(`User not found: ${userId}`);
2325
+ const pluginData = { ...user.pluginData };
2326
+ pluginData[pluginName] = { ...pluginData[pluginName] ?? {}, [key]: value };
2327
+ await this.store.putUser({ ...user, pluginData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2328
+ }
2329
+ async getPluginData(userId, pluginName, key) {
2330
+ const user = await this.store.getUser(userId);
2331
+ if (!user) return void 0;
2332
+ return user.pluginData[pluginName]?.[key];
2333
+ }
2334
+ // ─── Source registry ───
2335
+ registerSource(source) {
2336
+ this.registeredSources.add(source);
2337
+ }
2338
+ /**
2339
+ * Resolves a username mention to platform-specific info for the given source.
2340
+ * Finds the user by username, then scans their identities for the matching source.
2341
+ * Returns found=false when no user or no identity for that source exists.
2342
+ */
2343
+ async resolveCanonicalMention(username, source) {
2344
+ const user = await this.getUserByUsername(username);
2345
+ if (!user) return { found: false };
2346
+ const identities = await this.store.getIdentitiesForUser(user.userId);
2347
+ const sourceIdentity = identities.find((id) => id.source === source);
2348
+ if (!sourceIdentity) return { found: false };
2349
+ return {
2350
+ found: true,
2351
+ platformId: sourceIdentity.platformId,
2352
+ platformUsername: sourceIdentity.platformUsername
2353
+ };
2354
+ }
2355
+ async getUserCount() {
2356
+ return this.store.getUserCount();
2357
+ }
2358
+ };
2359
+ }
2360
+ });
2361
+
2362
+ // src/plugins/identity/store/kv-identity-store.ts
2363
+ var KvIdentityStore;
2364
+ var init_kv_identity_store = __esm({
2365
+ "src/plugins/identity/store/kv-identity-store.ts"() {
2366
+ "use strict";
2367
+ KvIdentityStore = class {
2368
+ constructor(storage) {
2369
+ this.storage = storage;
2370
+ }
2371
+ // === User CRUD ===
2372
+ async getUser(userId) {
2373
+ return this.storage.get(`users/${userId}`);
2374
+ }
2375
+ async putUser(record) {
2376
+ await this.storage.set(`users/${record.userId}`, record);
2377
+ }
2378
+ async deleteUser(userId) {
2379
+ await this.storage.delete(`users/${userId}`);
2380
+ }
2381
+ /**
2382
+ * Lists all users, optionally filtered by role or source.
2383
+ * Filtering by source requires scanning all identity records for the user,
2384
+ * which is acceptable given the expected user count (hundreds, not millions).
2385
+ */
2386
+ async listUsers(filter) {
2387
+ const keys = await this.storage.keys("users/");
2388
+ const users = [];
2389
+ for (const key of keys) {
2390
+ const user = await this.storage.get(key);
2391
+ if (!user) continue;
2392
+ if (filter?.role && user.role !== filter.role) continue;
2393
+ if (filter?.source) {
2394
+ const hasSource = user.identities.some((id) => id.startsWith(`${filter.source}:`));
2395
+ if (!hasSource) continue;
2396
+ }
2397
+ users.push(user);
2398
+ }
2399
+ return users;
2400
+ }
2401
+ // === Identity CRUD ===
2402
+ async getIdentity(identityId) {
2403
+ return this.storage.get(`identities/${identityId}`);
2404
+ }
2405
+ async putIdentity(record) {
2406
+ await this.storage.set(`identities/${record.identityId}`, record);
2407
+ }
2408
+ async deleteIdentity(identityId) {
2409
+ await this.storage.delete(`identities/${identityId}`);
2410
+ }
2411
+ /**
2412
+ * Fetches all identity records for a user by scanning their identities array.
2413
+ * Avoids a full table scan by leveraging the user record as a secondary index.
2414
+ */
2415
+ async getIdentitiesForUser(userId) {
2416
+ const user = await this.getUser(userId);
2417
+ if (!user) return [];
2418
+ const records = [];
2419
+ for (const identityId of user.identities) {
2420
+ const record = await this.getIdentity(identityId);
2421
+ if (record) records.push(record);
2422
+ }
2423
+ return records;
2424
+ }
2425
+ // === Secondary indexes ===
2426
+ async getUserIdByUsername(username) {
2427
+ return this.storage.get(`idx/usernames/${username.toLowerCase()}`);
2428
+ }
2429
+ async getIdentityIdBySource(source, platformId) {
2430
+ return this.storage.get(`idx/sources/${source}/${platformId}`);
2431
+ }
2432
+ // === Index mutations ===
2433
+ async setUsernameIndex(username, userId) {
2434
+ await this.storage.set(`idx/usernames/${username.toLowerCase()}`, userId);
2435
+ }
2436
+ async deleteUsernameIndex(username) {
2437
+ await this.storage.delete(`idx/usernames/${username.toLowerCase()}`);
2438
+ }
2439
+ async setSourceIndex(source, platformId, identityId) {
2440
+ await this.storage.set(`idx/sources/${source}/${platformId}`, identityId);
2441
+ }
2442
+ async deleteSourceIndex(source, platformId) {
2443
+ await this.storage.delete(`idx/sources/${source}/${platformId}`);
2444
+ }
2445
+ async getUserCount() {
2446
+ const keys = await this.storage.keys("users/");
2447
+ return keys.length;
2448
+ }
2449
+ };
2450
+ }
2451
+ });
2452
+
2453
+ // src/plugins/identity/middleware/auto-register.ts
2454
+ function createAutoRegisterHandler(service, store) {
2455
+ const lastSeenThrottle = /* @__PURE__ */ new Map();
2456
+ return async (payload, next) => {
2457
+ const { channelId, userId, meta } = payload;
2458
+ const identityId = formatIdentityId(channelId, userId);
2459
+ const channelUser = meta?.channelUser;
2460
+ let identity = await store.getIdentity(identityId);
2461
+ let user;
2462
+ if (!identity) {
2463
+ const result = await service.createUserWithIdentity({
2464
+ displayName: channelUser?.displayName ?? userId,
2465
+ username: channelUser?.username,
2466
+ source: channelId,
2467
+ platformId: userId,
2468
+ platformUsername: channelUser?.username,
2469
+ platformDisplayName: channelUser?.displayName
2470
+ });
2471
+ user = result.user;
2472
+ identity = result.identity;
2473
+ } else {
2474
+ user = await service.getUser(identity.userId);
2475
+ if (!user) return next();
2476
+ const now = Date.now();
2477
+ const lastSeen = lastSeenThrottle.get(user.userId);
2478
+ if (!lastSeen || now - lastSeen > LAST_SEEN_THROTTLE_MS) {
2479
+ lastSeenThrottle.set(user.userId, now);
2480
+ await store.putUser({ ...user, lastSeenAt: new Date(now).toISOString() });
2481
+ }
2482
+ if (channelUser) {
2483
+ const needsUpdate = channelUser.displayName !== void 0 && channelUser.displayName !== identity.platformDisplayName || channelUser.username !== void 0 && channelUser.username !== identity.platformUsername;
2484
+ if (needsUpdate) {
2485
+ await store.putIdentity({
2486
+ ...identity,
2487
+ platformDisplayName: channelUser.displayName ?? identity.platformDisplayName,
2488
+ platformUsername: channelUser.username ?? identity.platformUsername,
2489
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2490
+ });
2491
+ }
2492
+ }
2493
+ }
2494
+ if (meta) {
2495
+ meta.identity = {
2496
+ userId: user.userId,
2497
+ identityId: identity.identityId,
2498
+ displayName: user.displayName,
2499
+ username: user.username,
2500
+ role: user.role
2501
+ };
2502
+ }
2503
+ return next();
2504
+ };
2505
+ }
2506
+ var LAST_SEEN_THROTTLE_MS;
2507
+ var init_auto_register = __esm({
2508
+ "src/plugins/identity/middleware/auto-register.ts"() {
2509
+ "use strict";
2510
+ init_types();
2511
+ LAST_SEEN_THROTTLE_MS = 5 * 60 * 1e3;
2512
+ }
2513
+ });
2514
+
2515
+ // src/plugins/identity/routes/users.ts
2516
+ var users_exports = {};
2517
+ __export(users_exports, {
2518
+ registerIdentityRoutes: () => registerIdentityRoutes
2519
+ });
2520
+ function registerIdentityRoutes(app, deps) {
2521
+ const { service, tokenStore } = deps;
2522
+ function resolveUserId(request) {
2523
+ return tokenStore?.getUserId?.(request.auth?.tokenId);
2524
+ }
2525
+ app.get("/users", async (request) => {
2526
+ const { source, role, q } = request.query;
2527
+ if (q) return service.searchUsers(q);
2528
+ return service.listUsers({ source, role });
2529
+ });
2530
+ app.get("/users/me", async (request, reply) => {
2531
+ const userId = resolveUserId(request);
2532
+ if (!userId) return reply.status(403).send({ error: "Identity not set up" });
2533
+ const user = await service.getUser(userId);
2534
+ if (!user) return reply.status(404).send({ error: "User not found" });
2535
+ return user;
2536
+ });
2537
+ app.put("/users/me", async (request, reply) => {
2538
+ const userId = resolveUserId(request);
2539
+ if (!userId) {
2540
+ return reply.status(403).send({ error: "Identity not set up. Call POST /identity/setup first." });
2541
+ }
2542
+ const body = request.body;
2543
+ return service.updateUser(userId, {
2544
+ displayName: body.displayName,
2545
+ username: body.username,
2546
+ avatarUrl: body.avatarUrl,
2547
+ timezone: body.timezone,
2548
+ locale: body.locale
2549
+ });
2550
+ });
2551
+ app.get("/users/:userId", async (request, reply) => {
2552
+ const { userId } = request.params;
2553
+ const user = await service.getUser(userId);
2554
+ if (!user) return reply.status(404).send({ error: "User not found" });
2555
+ return user;
2556
+ });
2557
+ app.put("/users/:userId/role", async (request, reply) => {
2558
+ const callerUserId = resolveUserId(request);
2559
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2560
+ const caller = await service.getUser(callerUserId);
2561
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2562
+ const { userId } = request.params;
2563
+ const { role } = request.body;
2564
+ await service.setRole(userId, role);
2565
+ return { ok: true };
2566
+ });
2567
+ app.get("/users/:userId/identities", async (request) => {
2568
+ const { userId } = request.params;
2569
+ return service.getIdentitiesFor(userId);
2570
+ });
2571
+ app.get("/resolve/:identityId", async (request, reply) => {
2572
+ const { identityId } = request.params;
2573
+ const user = await service.getUserByIdentity(identityId);
2574
+ if (!user) return reply.status(404).send({ error: "Identity not found" });
2575
+ const identity = await service.getIdentity(identityId);
2576
+ return { user, identity };
2577
+ });
2578
+ app.post("/link", async (request, reply) => {
2579
+ const callerUserId = resolveUserId(request);
2580
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2581
+ const caller = await service.getUser(callerUserId);
2582
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2583
+ const { identityIdA, identityIdB } = request.body;
2584
+ await service.link(identityIdA, identityIdB);
2585
+ return { ok: true };
2586
+ });
2587
+ app.post("/unlink", async (request, reply) => {
2588
+ const callerUserId = resolveUserId(request);
2589
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2590
+ const caller = await service.getUser(callerUserId);
2591
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2592
+ const { identityId } = request.body;
2593
+ await service.unlink(identityId);
2594
+ return { ok: true };
2595
+ });
2596
+ app.get("/search", async (request) => {
2597
+ const { q } = request.query;
2598
+ if (!q) return [];
2599
+ return service.searchUsers(q);
2600
+ });
2601
+ }
2602
+ var init_users = __esm({
2603
+ "src/plugins/identity/routes/users.ts"() {
2604
+ "use strict";
2605
+ }
2606
+ });
2607
+
2608
+ // src/plugins/identity/routes/setup.ts
2609
+ var setup_exports = {};
2610
+ __export(setup_exports, {
2611
+ registerSetupRoutes: () => registerSetupRoutes
2612
+ });
2613
+ import { randomBytes } from "crypto";
2614
+ function registerSetupRoutes(app, deps) {
2615
+ const { service, tokenStore } = deps;
2616
+ app.post("/setup", async (request, reply) => {
2617
+ const auth = request.auth;
2618
+ if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
2619
+ const existingUserId = tokenStore?.getUserId?.(auth.tokenId);
2620
+ if (existingUserId) {
2621
+ const user2 = await service.getUser(existingUserId);
2622
+ if (user2) return user2;
2623
+ }
2624
+ const body = request.body;
2625
+ if (body?.linkCode) {
2626
+ const entry = linkCodes.get(body.linkCode);
2627
+ if (!entry || entry.expiresAt < Date.now()) {
2628
+ return reply.status(401).send({ error: "Invalid or expired link code" });
2629
+ }
2630
+ linkCodes.delete(body.linkCode);
2631
+ await service.createIdentity(entry.userId, {
2632
+ source: "api",
2633
+ platformId: auth.tokenId
2634
+ });
2635
+ tokenStore?.setUserId?.(auth.tokenId, entry.userId);
2636
+ return service.getUser(entry.userId);
2637
+ }
2638
+ if (!body?.displayName) return reply.status(400).send({ error: "displayName is required" });
2639
+ const { user } = await service.createUserWithIdentity({
2640
+ displayName: body.displayName,
2641
+ username: body.username,
2642
+ source: "api",
2643
+ platformId: auth.tokenId
2644
+ });
2645
+ tokenStore?.setUserId?.(auth.tokenId, user.userId);
2646
+ return user;
2647
+ });
2648
+ app.post("/link-code", async (request, reply) => {
2649
+ const auth = request.auth;
2650
+ if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
2651
+ const userId = tokenStore?.getUserId?.(auth.tokenId);
2652
+ if (!userId) return reply.status(403).send({ error: "Identity not set up" });
2653
+ const code = randomBytes(16).toString("hex");
2654
+ const expiresAt = Date.now() + 5 * 60 * 1e3;
2655
+ for (const [k, v] of linkCodes) {
2656
+ if (v.expiresAt < Date.now()) linkCodes.delete(k);
2657
+ }
2658
+ linkCodes.set(code, { userId, expiresAt });
2659
+ return { linkCode: code, expiresAt: new Date(expiresAt).toISOString() };
2660
+ });
2661
+ }
2662
+ var linkCodes;
2663
+ var init_setup = __esm({
2664
+ "src/plugins/identity/routes/setup.ts"() {
2665
+ "use strict";
2666
+ linkCodes = /* @__PURE__ */ new Map();
2667
+ }
2668
+ });
2669
+
2670
+ // src/plugins/identity/index.ts
2671
+ function createIdentityPlugin() {
2672
+ return {
2673
+ name: "@openacp/identity",
2674
+ version: "1.0.0",
2675
+ description: "User identity, cross-platform linking, and role-based access",
2676
+ essential: false,
2677
+ permissions: [
2678
+ "storage:read",
2679
+ "storage:write",
2680
+ "middleware:register",
2681
+ "services:register",
2682
+ "services:use",
2683
+ "events:emit",
2684
+ "events:read",
2685
+ "commands:register",
2686
+ "kernel:access"
2687
+ ],
2688
+ optionalPluginDependencies: {
2689
+ "@openacp/api-server": ">=1.0.0"
2690
+ },
2691
+ async setup(ctx) {
2692
+ const store = new KvIdentityStore(ctx.storage);
2693
+ const service = new IdentityServiceImpl(store, (event, data) => {
2694
+ ctx.emit(event, data);
2695
+ });
2696
+ ctx.registerService("identity", service);
2697
+ ctx.registerMiddleware(Hook.MESSAGE_INCOMING, {
2698
+ priority: 110,
2699
+ handler: createAutoRegisterHandler(service, store)
2700
+ });
2701
+ ctx.registerCommand({
2702
+ name: "whoami",
2703
+ description: "Set your display name and username",
2704
+ usage: "[name]",
2705
+ category: "plugin",
2706
+ async handler(args2) {
2707
+ const name = args2.raw.trim();
2708
+ if (!name) {
2709
+ return { type: "text", text: "Usage: /whoami <name>" };
2710
+ }
2711
+ const identityId = formatIdentityId(args2.channelId, args2.userId);
2712
+ const user = await service.getUserByIdentity(identityId);
2713
+ if (!user) {
2714
+ return { type: "error", message: "User not found \u2014 send a message first." };
2715
+ }
2716
+ try {
2717
+ const username = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
2718
+ await service.updateUser(user.userId, { displayName: name, username });
2719
+ return { type: "text", text: `Display name set to "${name}", username: @${username}` };
2720
+ } catch (err) {
2721
+ const message = err instanceof Error ? err.message : String(err);
2722
+ return { type: "error", message };
2723
+ }
2724
+ }
2725
+ });
2726
+ const apiServer = ctx.getService("api-server");
2727
+ if (apiServer) {
2728
+ const tokenStore = ctx.getService("token-store");
2729
+ const { registerIdentityRoutes: registerIdentityRoutes2 } = await Promise.resolve().then(() => (init_users(), users_exports));
2730
+ const { registerSetupRoutes: registerSetupRoutes2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
2731
+ apiServer.registerPlugin("/api/v1/identity", async (app) => {
2732
+ registerIdentityRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
2733
+ registerSetupRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
2734
+ }, { auth: true });
2735
+ }
2736
+ ctx.log.info(`Identity service ready (${await service.getUserCount()} users)`);
2737
+ }
2738
+ };
2739
+ }
2740
+ var identity_default;
2741
+ var init_identity = __esm({
2742
+ "src/plugins/identity/index.ts"() {
2743
+ "use strict";
2744
+ init_identity_service();
2745
+ init_kv_identity_store();
2746
+ init_auto_register();
2747
+ init_types();
2748
+ init_events();
2749
+ identity_default = createIdentityPlugin();
2750
+ }
2751
+ });
2752
+
2038
2753
  // src/core/utils/read-text-file.ts
2039
2754
  var read_text_file_exports = {};
2040
2755
  __export(read_text_file_exports, {
@@ -3631,7 +4346,7 @@ var init_history_recorder = __esm({
3631
4346
  }
3632
4347
  states = /* @__PURE__ */ new Map();
3633
4348
  debounceTimers = /* @__PURE__ */ new Map();
3634
- onBeforePrompt(sessionId, text5, attachments, sourceAdapterId) {
4349
+ onBeforePrompt(sessionId, text5, attachments, sourceAdapterId, meta) {
3635
4350
  let state = this.states.get(sessionId);
3636
4351
  if (!state) {
3637
4352
  state = {
@@ -3652,6 +4367,9 @@ var init_history_recorder = __esm({
3652
4367
  if (sourceAdapterId) {
3653
4368
  userTurn.sourceAdapterId = sourceAdapterId;
3654
4369
  }
4370
+ if (meta && Object.keys(meta).length > 0) {
4371
+ userTurn.meta = meta;
4372
+ }
3655
4373
  state.history.turns.push(userTurn);
3656
4374
  const assistantTurn = {
3657
4375
  index: state.history.turns.length,
@@ -3986,7 +4704,7 @@ var init_context = __esm({
3986
4704
  ctx.registerMiddleware(Hook.AGENT_BEFORE_PROMPT, {
3987
4705
  priority: 200,
3988
4706
  handler: async (payload, next) => {
3989
- recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId);
4707
+ recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId, payload.meta);
3990
4708
  return next();
3991
4709
  }
3992
4710
  });
@@ -4485,14 +5203,20 @@ var init_speech = __esm({
4485
5203
  });
4486
5204
 
4487
5205
  // src/plugins/notifications/notification.ts
4488
- var NotificationManager;
5206
+ var NotificationService;
4489
5207
  var init_notification = __esm({
4490
5208
  "src/plugins/notifications/notification.ts"() {
4491
5209
  "use strict";
4492
- NotificationManager = class {
5210
+ NotificationService = class {
4493
5211
  constructor(adapters) {
4494
5212
  this.adapters = adapters;
4495
5213
  }
5214
+ identityResolver;
5215
+ /** Inject identity resolver for user-targeted notifications. */
5216
+ setIdentityResolver(resolver) {
5217
+ this.identityResolver = resolver;
5218
+ }
5219
+ // --- Legacy API (backward compat with NotificationManager) ---
4496
5220
  /**
4497
5221
  * Send a notification to a specific channel adapter.
4498
5222
  *
@@ -4521,6 +5245,62 @@ var init_notification = __esm({
4521
5245
  }
4522
5246
  }
4523
5247
  }
5248
+ // --- New user-targeted API ---
5249
+ /**
5250
+ * Send a notification to a user across all their linked platforms.
5251
+ * Fire-and-forget — never throws, swallows all errors.
5252
+ */
5253
+ async notifyUser(target, message, options) {
5254
+ try {
5255
+ await this._resolveAndDeliver(target, message, options);
5256
+ } catch {
5257
+ }
5258
+ }
5259
+ async _resolveAndDeliver(target, message, options) {
5260
+ if ("channelId" in target && "platformId" in target) {
5261
+ const adapter = this.adapters.get(target.channelId);
5262
+ if (!adapter?.sendUserNotification) return;
5263
+ await adapter.sendUserNotification(target.platformId, message, {
5264
+ via: options?.via,
5265
+ topicId: options?.topicId,
5266
+ sessionId: options?.sessionId
5267
+ });
5268
+ return;
5269
+ }
5270
+ if (!this.identityResolver) return;
5271
+ let identities = [];
5272
+ if ("identityId" in target) {
5273
+ const identity = await this.identityResolver.getIdentity(target.identityId);
5274
+ if (!identity) return;
5275
+ const user = await this.identityResolver.getUser(identity.userId);
5276
+ if (!user) return;
5277
+ identities = await this.identityResolver.getIdentitiesFor(user.userId);
5278
+ } else if ("userId" in target) {
5279
+ identities = await this.identityResolver.getIdentitiesFor(target.userId);
5280
+ }
5281
+ if (options?.onlyPlatforms) {
5282
+ identities = identities.filter((i) => options.onlyPlatforms.includes(i.source));
5283
+ }
5284
+ if (options?.excludePlatforms) {
5285
+ identities = identities.filter((i) => !options.excludePlatforms.includes(i.source));
5286
+ }
5287
+ for (const identity of identities) {
5288
+ const adapter = this.adapters.get(identity.source);
5289
+ if (!adapter?.sendUserNotification) continue;
5290
+ try {
5291
+ await adapter.sendUserNotification(identity.platformId, message, {
5292
+ via: options?.via,
5293
+ topicId: options?.topicId,
5294
+ sessionId: options?.sessionId,
5295
+ platformMention: {
5296
+ platformUsername: identity.platformUsername,
5297
+ platformId: identity.platformId
5298
+ }
5299
+ });
5300
+ } catch {
5301
+ }
5302
+ }
5303
+ }
4524
5304
  };
4525
5305
  }
4526
5306
  });
@@ -4538,11 +5318,10 @@ function createNotificationsPlugin() {
4538
5318
  essential: false,
4539
5319
  // Depends on security so the notification service is only active for authorized sessions
4540
5320
  pluginDependencies: { "@openacp/security": "^1.0.0" },
4541
- permissions: ["services:register", "kernel:access"],
5321
+ permissions: ["services:register", "services:use", "kernel:access", "events:read"],
4542
5322
  async install(ctx) {
4543
- const { settings, terminal } = ctx;
4544
- await settings.setAll({ enabled: true });
4545
- terminal.log.success("Notifications defaults saved");
5323
+ await ctx.settings.setAll({ enabled: true });
5324
+ ctx.terminal.log.success("Notifications defaults saved");
4546
5325
  },
4547
5326
  async configure(ctx) {
4548
5327
  const { terminal, settings } = ctx;
@@ -4565,8 +5344,16 @@ function createNotificationsPlugin() {
4565
5344
  },
4566
5345
  async setup(ctx) {
4567
5346
  const core = ctx.core;
4568
- const manager = new NotificationManager(core.adapters);
4569
- ctx.registerService("notifications", manager);
5347
+ const service = new NotificationService(core.adapters);
5348
+ const identity = ctx.getService("identity");
5349
+ if (identity) service.setIdentityResolver(identity);
5350
+ ctx.on("plugin:loaded", (data) => {
5351
+ if (data?.name === "@openacp/identity") {
5352
+ const id = ctx.getService("identity");
5353
+ if (id) service.setIdentityResolver(id);
5354
+ }
5355
+ });
5356
+ ctx.registerService("notifications", service);
4570
5357
  ctx.log.info("Notifications service ready");
4571
5358
  }
4572
5359
  };
@@ -6866,7 +7653,7 @@ var init_viewer_routes = __esm({
6866
7653
  // src/plugins/tunnel/viewer-store.ts
6867
7654
  import * as fs17 from "fs";
6868
7655
  import * as path20 from "path";
6869
- import { nanoid } from "nanoid";
7656
+ import { nanoid as nanoid2 } from "nanoid";
6870
7657
  var log10, MAX_CONTENT_SIZE, EXTENSION_LANGUAGE, ViewerStore;
6871
7658
  var init_viewer_store = __esm({
6872
7659
  "src/plugins/tunnel/viewer-store.ts"() {
@@ -6927,7 +7714,7 @@ var init_viewer_store = __esm({
6927
7714
  log10.debug({ filePath, size: content.length }, "File too large for viewer");
6928
7715
  return null;
6929
7716
  }
6930
- const id = nanoid(12);
7717
+ const id = nanoid2(12);
6931
7718
  const now = Date.now();
6932
7719
  this.entries.set(id, {
6933
7720
  id,
@@ -6953,7 +7740,7 @@ var init_viewer_store = __esm({
6953
7740
  log10.debug({ filePath, size: combined }, "Diff content too large for viewer");
6954
7741
  return null;
6955
7742
  }
6956
- const id = nanoid(12);
7743
+ const id = nanoid2(12);
6957
7744
  const now = Date.now();
6958
7745
  this.entries.set(id, {
6959
7746
  id,
@@ -6975,7 +7762,7 @@ var init_viewer_store = __esm({
6975
7762
  log10.debug({ label, size: output.length }, "Output too large for viewer");
6976
7763
  return null;
6977
7764
  }
6978
- const id = nanoid(12);
7765
+ const id = nanoid2(12);
6979
7766
  const now = Date.now();
6980
7767
  this.entries.set(id, {
6981
7768
  id,
@@ -7424,9 +8211,9 @@ __export(token_store_exports, {
7424
8211
  parseDuration: () => parseDuration
7425
8212
  });
7426
8213
  import { readFile as readFile2, writeFile } from "fs/promises";
7427
- import { randomBytes } from "crypto";
8214
+ import { randomBytes as randomBytes2 } from "crypto";
7428
8215
  function generateTokenId() {
7429
- return `tok_${randomBytes(12).toString("hex")}`;
8216
+ return `tok_${randomBytes2(12).toString("hex")}`;
7430
8217
  }
7431
8218
  function parseDuration(duration) {
7432
8219
  const match = duration.match(/^(\d+)(h|d|m)$/);
@@ -7573,6 +8360,18 @@ var init_token_store = __esm({
7573
8360
  this.lastUsedSaveTimer = null;
7574
8361
  }
7575
8362
  }
8363
+ /** Associate a user ID with a token. Called by identity plugin after /identity/setup. */
8364
+ setUserId(tokenId, userId) {
8365
+ const token = this.tokens.get(tokenId);
8366
+ if (token) {
8367
+ token.userId = userId;
8368
+ this.scheduleSave();
8369
+ }
8370
+ }
8371
+ /** Get the user ID associated with a token. */
8372
+ getUserId(tokenId) {
8373
+ return this.tokens.get(tokenId)?.userId;
8374
+ }
7576
8375
  /**
7577
8376
  * Generates a one-time authorization code that can be exchanged for a JWT.
7578
8377
  *
@@ -7580,7 +8379,7 @@ var init_token_store = __esm({
7580
8379
  * the App, which exchanges it for a proper JWT without ever exposing the raw API secret.
7581
8380
  */
7582
8381
  createCode(opts) {
7583
- const code = randomBytes(16).toString("hex");
8382
+ const code = randomBytes2(16).toString("hex");
7584
8383
  const now = /* @__PURE__ */ new Date();
7585
8384
  const ttl = opts.codeTtlMs ?? 30 * 60 * 1e3;
7586
8385
  const stored = {
@@ -8100,9 +8899,16 @@ async function createApiServer(options) {
8100
8899
  });
8101
8900
  const authPreHandler = createAuthPreHandler(options.getSecret, options.getJwtSecret, options.tokenStore);
8102
8901
  app.decorateRequest("auth", null, []);
8902
+ let booted = false;
8903
+ app.addHook("onReady", async () => {
8904
+ booted = true;
8905
+ });
8103
8906
  return {
8104
8907
  app,
8105
8908
  registerPlugin(prefix, plugin2, opts) {
8909
+ if (booted) {
8910
+ return;
8911
+ }
8106
8912
  const wrappedPlugin = async (pluginApp, pluginOpts) => {
8107
8913
  if (opts?.auth !== false) {
8108
8914
  pluginApp.addHook("onRequest", authPreHandler);
@@ -8533,7 +9339,7 @@ var sessions_exports = {};
8533
9339
  __export(sessions_exports, {
8534
9340
  sessionRoutes: () => sessionRoutes
8535
9341
  });
8536
- import { nanoid as nanoid2 } from "nanoid";
9342
+ import { nanoid as nanoid3 } from "nanoid";
8537
9343
  async function sessionRoutes(app, deps) {
8538
9344
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
8539
9345
  const summaries = deps.core.sessionManager.listAllSessions();
@@ -8561,32 +9367,48 @@ async function sessionRoutes(app, deps) {
8561
9367
  { preHandler: requireScopes("sessions:read") },
8562
9368
  async (request) => {
8563
9369
  const { sessionId } = SessionIdParamSchema.parse(request.params);
8564
- const session = deps.core.sessionManager.getSession(
8565
- decodeURIComponent(sessionId)
8566
- );
8567
- if (!session) {
8568
- throw new NotFoundError(
8569
- "SESSION_NOT_FOUND",
8570
- `Session "${sessionId}" not found`
8571
- );
9370
+ const id = decodeURIComponent(sessionId);
9371
+ const session = deps.core.sessionManager.getSession(id);
9372
+ if (session) {
9373
+ return {
9374
+ session: {
9375
+ id: session.id,
9376
+ agent: session.agentName,
9377
+ status: session.status,
9378
+ name: session.name ?? null,
9379
+ workspace: session.workingDirectory,
9380
+ createdAt: session.createdAt.toISOString(),
9381
+ dangerousMode: session.clientOverrides.bypassPermissions ?? false,
9382
+ queueDepth: session.queueDepth,
9383
+ promptRunning: session.promptRunning,
9384
+ threadId: session.threadId,
9385
+ channelId: session.channelId,
9386
+ agentSessionId: session.agentSessionId,
9387
+ configOptions: session.configOptions?.length ? session.configOptions : void 0,
9388
+ capabilities: session.agentCapabilities ?? null
9389
+ }
9390
+ };
9391
+ }
9392
+ const record = deps.core.sessionManager.getSessionRecord(id);
9393
+ if (!record) {
9394
+ throw new NotFoundError("SESSION_NOT_FOUND", `Session "${id}" not found`);
8572
9395
  }
8573
9396
  return {
8574
9397
  session: {
8575
- id: session.id,
8576
- agent: session.agentName,
8577
- status: session.status,
8578
- name: session.name ?? null,
8579
- workspace: session.workingDirectory,
8580
- createdAt: session.createdAt.toISOString(),
8581
- dangerousMode: session.clientOverrides.bypassPermissions ?? false,
8582
- queueDepth: session.queueDepth,
8583
- promptRunning: session.promptRunning,
8584
- threadId: session.threadId,
8585
- channelId: session.channelId,
8586
- agentSessionId: session.agentSessionId,
8587
- // ACP state
8588
- configOptions: session.configOptions?.length ? session.configOptions : void 0,
8589
- capabilities: session.agentCapabilities ?? null
9398
+ id: record.sessionId,
9399
+ agent: record.agentName,
9400
+ status: record.status,
9401
+ name: record.name ?? null,
9402
+ workspace: record.workingDir,
9403
+ createdAt: record.createdAt,
9404
+ dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
9405
+ queueDepth: 0,
9406
+ promptRunning: false,
9407
+ threadId: null,
9408
+ channelId: record.channelId,
9409
+ agentSessionId: record.agentSessionId,
9410
+ configOptions: record.acpState?.configOptions?.length ? record.acpState.configOptions : void 0,
9411
+ capabilities: record.acpState?.agentCapabilities ?? null
8590
9412
  }
8591
9413
  };
8592
9414
  }
@@ -8692,8 +9514,17 @@ async function sessionRoutes(app, deps) {
8692
9514
  }
8693
9515
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
8694
9516
  }
8695
- const sourceAdapterId = body.sourceAdapterId ?? "api";
8696
- const turnId = body.turnId ?? nanoid2(8);
9517
+ const sourceAdapterId = body.sourceAdapterId ?? "sse";
9518
+ const turnId = body.turnId ?? nanoid3(8);
9519
+ const userId = request.auth?.tokenId ?? "api";
9520
+ const meta = { turnId, channelUser: { channelId: "sse", userId } };
9521
+ if (deps.lifecycleManager?.middlewareChain) {
9522
+ await deps.lifecycleManager.middlewareChain.execute(
9523
+ Hook.MESSAGE_INCOMING,
9524
+ { channelId: sourceAdapterId, threadId: session.id, userId, text: body.prompt, attachments, meta },
9525
+ async (p2) => p2
9526
+ );
9527
+ }
8697
9528
  deps.core.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
8698
9529
  sessionId,
8699
9530
  turnId,
@@ -8706,7 +9537,7 @@ async function sessionRoutes(app, deps) {
8706
9537
  await session.enqueuePrompt(body.prompt, attachments, {
8707
9538
  sourceAdapterId,
8708
9539
  responseAdapterId: body.responseAdapterId
8709
- }, turnId);
9540
+ }, turnId, meta);
8710
9541
  return {
8711
9542
  ok: true,
8712
9543
  sessionId,
@@ -8721,7 +9552,7 @@ async function sessionRoutes(app, deps) {
8721
9552
  async (request, reply) => {
8722
9553
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8723
9554
  const sessionId = decodeURIComponent(rawId);
8724
- const session = deps.core.sessionManager.getSession(sessionId);
9555
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8725
9556
  if (!session) {
8726
9557
  throw new NotFoundError(
8727
9558
  "SESSION_NOT_FOUND",
@@ -8750,7 +9581,7 @@ async function sessionRoutes(app, deps) {
8750
9581
  async (request) => {
8751
9582
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8752
9583
  const sessionId = decodeURIComponent(rawId);
8753
- const session = deps.core.sessionManager.getSession(sessionId);
9584
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8754
9585
  if (!session) {
8755
9586
  throw new NotFoundError(
8756
9587
  "SESSION_NOT_FOUND",
@@ -8787,7 +9618,7 @@ async function sessionRoutes(app, deps) {
8787
9618
  async (request) => {
8788
9619
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8789
9620
  const sessionId = decodeURIComponent(rawId);
8790
- const session = deps.core.sessionManager.getSession(sessionId);
9621
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8791
9622
  if (!session) {
8792
9623
  throw new NotFoundError(
8793
9624
  "SESSION_NOT_FOUND",
@@ -8809,15 +9640,19 @@ async function sessionRoutes(app, deps) {
8809
9640
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8810
9641
  const sessionId = decodeURIComponent(rawId);
8811
9642
  const session = deps.core.sessionManager.getSession(sessionId);
8812
- if (!session) {
8813
- throw new NotFoundError(
8814
- "SESSION_NOT_FOUND",
8815
- `Session "${sessionId}" not found`
8816
- );
9643
+ if (session) {
9644
+ return {
9645
+ configOptions: session.configOptions,
9646
+ clientOverrides: session.clientOverrides
9647
+ };
9648
+ }
9649
+ const record = deps.core.sessionManager.getSessionRecord(sessionId);
9650
+ if (!record) {
9651
+ throw new NotFoundError("SESSION_NOT_FOUND", `Session "${sessionId}" not found`);
8817
9652
  }
8818
9653
  return {
8819
- configOptions: session.configOptions,
8820
- clientOverrides: session.clientOverrides
9654
+ configOptions: record.acpState?.configOptions,
9655
+ clientOverrides: record.clientOverrides ?? {}
8821
9656
  };
8822
9657
  }
8823
9658
  );
@@ -8827,7 +9662,7 @@ async function sessionRoutes(app, deps) {
8827
9662
  async (request) => {
8828
9663
  const { sessionId: rawId, configId } = ConfigIdParamSchema.parse(request.params);
8829
9664
  const sessionId = decodeURIComponent(rawId);
8830
- const session = deps.core.sessionManager.getSession(sessionId);
9665
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8831
9666
  if (!session) {
8832
9667
  throw new NotFoundError(
8833
9668
  "SESSION_NOT_FOUND",
@@ -8852,13 +9687,14 @@ async function sessionRoutes(app, deps) {
8852
9687
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8853
9688
  const sessionId = decodeURIComponent(rawId);
8854
9689
  const session = deps.core.sessionManager.getSession(sessionId);
8855
- if (!session) {
8856
- throw new NotFoundError(
8857
- "SESSION_NOT_FOUND",
8858
- `Session "${sessionId}" not found`
8859
- );
9690
+ if (session) {
9691
+ return { clientOverrides: session.clientOverrides };
8860
9692
  }
8861
- return { clientOverrides: session.clientOverrides };
9693
+ const record = deps.core.sessionManager.getSessionRecord(sessionId);
9694
+ if (!record) {
9695
+ throw new NotFoundError("SESSION_NOT_FOUND", `Session "${sessionId}" not found`);
9696
+ }
9697
+ return { clientOverrides: record.clientOverrides ?? {} };
8862
9698
  }
8863
9699
  );
8864
9700
  app.put(
@@ -8867,7 +9703,7 @@ async function sessionRoutes(app, deps) {
8867
9703
  async (request) => {
8868
9704
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8869
9705
  const sessionId = decodeURIComponent(rawId);
8870
- const session = deps.core.sessionManager.getSession(sessionId);
9706
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8871
9707
  if (!session) {
8872
9708
  throw new NotFoundError(
8873
9709
  "SESSION_NOT_FOUND",
@@ -8933,8 +9769,8 @@ async function sessionRoutes(app, deps) {
8933
9769
  { preHandler: requireScopes("sessions:read") },
8934
9770
  async (request, reply) => {
8935
9771
  const { sessionId } = SessionIdParamSchema.parse(request.params);
8936
- const session = deps.core.sessionManager.getSession(sessionId);
8937
- if (!session) {
9772
+ const isKnown = deps.core.sessionManager.getSession(sessionId) ?? deps.core.sessionManager.getSessionRecord(sessionId);
9773
+ if (!isKnown) {
8938
9774
  throw new NotFoundError(
8939
9775
  "SESSION_NOT_FOUND",
8940
9776
  `Session "${sessionId}" not found`
@@ -8956,8 +9792,8 @@ async function sessionRoutes(app, deps) {
8956
9792
  async (request) => {
8957
9793
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8958
9794
  const sessionId = decodeURIComponent(rawId);
8959
- const session = deps.core.sessionManager.getSession(sessionId);
8960
- if (!session) {
9795
+ const isKnown = deps.core.sessionManager.getSession(sessionId) ?? deps.core.sessionManager.getSessionRecord(sessionId);
9796
+ if (!isKnown) {
8961
9797
  throw new NotFoundError(
8962
9798
  "SESSION_NOT_FOUND",
8963
9799
  `Session "${sessionId}" not found`
@@ -9159,7 +9995,7 @@ import { z as z4 } from "zod";
9159
9995
  import * as fs21 from "fs";
9160
9996
  import * as path24 from "path";
9161
9997
  import * as os5 from "os";
9162
- import { randomBytes as randomBytes2 } from "crypto";
9998
+ import { randomBytes as randomBytes3 } from "crypto";
9163
9999
  import { EventEmitter } from "events";
9164
10000
  function expandHome2(p2) {
9165
10001
  if (p2.startsWith("~")) {
@@ -9289,7 +10125,7 @@ var init_config2 = __esm({
9289
10125
  log14.error({ errors: result.error.issues }, "Config validation failed, not saving");
9290
10126
  return;
9291
10127
  }
9292
- const tmpPath = this.configPath + `.tmp.${randomBytes2(4).toString("hex")}`;
10128
+ const tmpPath = this.configPath + `.tmp.${randomBytes3(4).toString("hex")}`;
9293
10129
  fs21.writeFileSync(tmpPath, JSON.stringify(raw, null, 2), "utf-8");
9294
10130
  fs21.renameSync(tmpPath, this.configPath);
9295
10131
  this.config = result.data;
@@ -9858,8 +10694,8 @@ async function commandRoutes(app, deps) {
9858
10694
  const result = await deps.commandRegistry.execute(commandString, {
9859
10695
  raw: "",
9860
10696
  sessionId: body.sessionId ?? null,
9861
- channelId: "api",
9862
- userId: "api",
10697
+ channelId: "sse",
10698
+ userId: request.auth?.tokenId ?? "api",
9863
10699
  reply: async () => {
9864
10700
  }
9865
10701
  });
@@ -10019,11 +10855,23 @@ async function authRoutes(app, deps) {
10019
10855
  });
10020
10856
  app.get("/me", async (request) => {
10021
10857
  const { auth } = request;
10858
+ const userId = auth.tokenId ? deps.tokenStore.getUserId(auth.tokenId) : void 0;
10859
+ let displayName = null;
10860
+ if (userId && deps.getIdentityService) {
10861
+ const identityService = deps.getIdentityService();
10862
+ if (identityService) {
10863
+ const user = await identityService.getUser(userId);
10864
+ displayName = user?.displayName ?? null;
10865
+ }
10866
+ }
10022
10867
  return {
10023
10868
  type: auth.type,
10024
10869
  tokenId: auth.tokenId,
10025
10870
  role: auth.role,
10026
- scopes: auth.scopes
10871
+ scopes: auth.scopes,
10872
+ userId: userId ?? null,
10873
+ displayName,
10874
+ claimed: !!userId
10027
10875
  };
10028
10876
  });
10029
10877
  app.post("/codes", {
@@ -10603,6 +11451,7 @@ function createApiServerPlugin() {
10603
11451
  const tokenStore = new TokenStore2(tokensFilePath);
10604
11452
  await tokenStore.load();
10605
11453
  tokenStoreRef = tokenStore;
11454
+ ctx.registerService("token-store", tokenStore);
10606
11455
  const { createApiServer: createApiServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
10607
11456
  const { SSEManager: SSEManager2 } = await Promise.resolve().then(() => (init_sse_manager(), sse_manager_exports));
10608
11457
  const { StaticServer: StaticServer2 } = await Promise.resolve().then(() => (init_static_server(), static_server_exports));
@@ -10656,7 +11505,12 @@ function createApiServerPlugin() {
10656
11505
  server.registerPlugin("/api/v1/tunnel", async (app) => tunnelRoutes2(app, deps));
10657
11506
  server.registerPlugin("/api/v1/notify", async (app) => notifyRoutes2(app, deps));
10658
11507
  server.registerPlugin("/api/v1/commands", async (app) => commandRoutes2(app, deps));
10659
- server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, { tokenStore, getJwtSecret: () => jwtSecret }));
11508
+ server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, {
11509
+ tokenStore,
11510
+ getJwtSecret: () => jwtSecret,
11511
+ // Lazy resolver: identity plugin may not be loaded, so we fetch it on demand
11512
+ getIdentityService: () => ctx.getService("identity") ?? void 0
11513
+ }));
10660
11514
  server.registerPlugin("/api/v1/plugins", async (app) => pluginRoutes2(app, deps));
10661
11515
  const appConfig = core.configManager.get();
10662
11516
  const workspaceName = appConfig.instanceName ?? "Main";
@@ -10805,7 +11659,7 @@ var init_api_server = __esm({
10805
11659
  });
10806
11660
 
10807
11661
  // src/plugins/sse-adapter/connection-manager.ts
10808
- import { randomBytes as randomBytes4 } from "crypto";
11662
+ import { randomBytes as randomBytes5 } from "crypto";
10809
11663
  var ConnectionManager;
10810
11664
  var init_connection_manager = __esm({
10811
11665
  "src/plugins/sse-adapter/connection-manager.ts"() {
@@ -10814,6 +11668,8 @@ var init_connection_manager = __esm({
10814
11668
  connections = /* @__PURE__ */ new Map();
10815
11669
  // Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
10816
11670
  sessionIndex = /* @__PURE__ */ new Map();
11671
+ // Secondary index: userId → Set of connection IDs for user-level event delivery
11672
+ userIndex = /* @__PURE__ */ new Map();
10817
11673
  maxConnectionsPerSession;
10818
11674
  maxTotalConnections;
10819
11675
  constructor(opts) {
@@ -10836,7 +11692,7 @@ var init_connection_manager = __esm({
10836
11692
  if (sessionConns && sessionConns.size >= this.maxConnectionsPerSession) {
10837
11693
  throw new Error("Maximum connections per session reached");
10838
11694
  }
10839
- const id = `conn_${randomBytes4(8).toString("hex")}`;
11695
+ const id = `conn_${randomBytes5(8).toString("hex")}`;
10840
11696
  const connection = { id, sessionId, tokenId, response, connectedAt: /* @__PURE__ */ new Date() };
10841
11697
  this.connections.set(id, connection);
10842
11698
  let sessionConnsSet = this.sessionIndex.get(sessionId);
@@ -10848,7 +11704,66 @@ var init_connection_manager = __esm({
10848
11704
  response.on("close", () => this.removeConnection(id));
10849
11705
  return connection;
10850
11706
  }
10851
- /** Remove a connection from both indexes. Called automatically on client disconnect. */
11707
+ /**
11708
+ * Registers a user-level SSE connection (not tied to a specific session).
11709
+ * Used for notifications and system events delivered to a user.
11710
+ *
11711
+ * @throws if the global connection limit is reached.
11712
+ */
11713
+ addUserConnection(userId, tokenId, response) {
11714
+ if (this.connections.size >= this.maxTotalConnections) {
11715
+ throw new Error("Maximum total connections reached");
11716
+ }
11717
+ const id = `conn_${randomBytes5(8).toString("hex")}`;
11718
+ const connection = {
11719
+ id,
11720
+ sessionId: "",
11721
+ tokenId,
11722
+ userId,
11723
+ response,
11724
+ connectedAt: /* @__PURE__ */ new Date()
11725
+ };
11726
+ this.connections.set(id, connection);
11727
+ let userConns = this.userIndex.get(userId);
11728
+ if (!userConns) {
11729
+ userConns = /* @__PURE__ */ new Set();
11730
+ this.userIndex.set(userId, userConns);
11731
+ }
11732
+ userConns.add(id);
11733
+ response.on("close", () => this.removeConnection(id));
11734
+ return connection;
11735
+ }
11736
+ /**
11737
+ * Writes a serialized SSE event to all connections for a given user.
11738
+ *
11739
+ * Uses the same backpressure strategy as `broadcast`: flag on first overflow,
11740
+ * forcibly close if still backpressured on the next write.
11741
+ */
11742
+ pushToUser(userId, serializedEvent) {
11743
+ const connIds = this.userIndex.get(userId);
11744
+ if (!connIds) return;
11745
+ for (const connId of connIds) {
11746
+ const conn = this.connections.get(connId);
11747
+ if (!conn || conn.response.writableEnded) continue;
11748
+ try {
11749
+ const ok3 = conn.response.write(serializedEvent);
11750
+ if (!ok3) {
11751
+ if (conn.backpressured) {
11752
+ conn.response.end();
11753
+ this.removeConnection(conn.id);
11754
+ } else {
11755
+ conn.backpressured = true;
11756
+ conn.response.once("drain", () => {
11757
+ conn.backpressured = false;
11758
+ });
11759
+ }
11760
+ }
11761
+ } catch {
11762
+ this.removeConnection(conn.id);
11763
+ }
11764
+ }
11765
+ }
11766
+ /** Remove a connection from all indexes. Called automatically on client disconnect. */
10852
11767
  removeConnection(connectionId) {
10853
11768
  const conn = this.connections.get(connectionId);
10854
11769
  if (!conn) return;
@@ -10858,6 +11773,13 @@ var init_connection_manager = __esm({
10858
11773
  sessionConns.delete(connectionId);
10859
11774
  if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
10860
11775
  }
11776
+ if (conn.userId) {
11777
+ const userConns = this.userIndex.get(conn.userId);
11778
+ if (userConns) {
11779
+ userConns.delete(connectionId);
11780
+ if (userConns.size === 0) this.userIndex.delete(conn.userId);
11781
+ }
11782
+ }
10861
11783
  }
10862
11784
  /** Returns all active connections for a session. */
10863
11785
  getConnectionsBySession(sessionId) {
@@ -10917,6 +11839,7 @@ var init_connection_manager = __esm({
10917
11839
  }
10918
11840
  this.connections.clear();
10919
11841
  this.sessionIndex.clear();
11842
+ this.userIndex.clear();
10920
11843
  }
10921
11844
  };
10922
11845
  }
@@ -11110,6 +12033,22 @@ var init_adapter = __esm({
11110
12033
  this.connectionManager.broadcast(notification.sessionId, serialized);
11111
12034
  }
11112
12035
  }
12036
+ /**
12037
+ * Delivers a push notification to a specific user's SSE connections.
12038
+ *
12039
+ * `platformId` is the userId for the SSE adapter — SSE has no concept of
12040
+ * platform-specific user handles, so we use the internal userId directly.
12041
+ */
12042
+ async sendUserNotification(platformId, message, options) {
12043
+ const serialized = `event: notification:text
12044
+ data: ${JSON.stringify({
12045
+ text: message.text ?? message.summary ?? "",
12046
+ ...options ?? {}
12047
+ })}
12048
+
12049
+ `;
12050
+ this.connectionManager.pushToUser(platformId, serialized);
12051
+ }
11113
12052
  /** SSE has no concept of threads — return sessionId as the threadId */
11114
12053
  async createSessionThread(sessionId, _name) {
11115
12054
  return sessionId;
@@ -11200,8 +12139,14 @@ async function sseRoutes(app, deps) {
11200
12139
  }
11201
12140
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
11202
12141
  }
11203
- await session.enqueuePrompt(body.prompt, attachments, { sourceAdapterId: "sse" });
11204
- return { ok: true, sessionId, queueDepth: session.queueDepth };
12142
+ const queueDepth = session.queueDepth + 1;
12143
+ const userId = request.auth?.tokenId ?? "api";
12144
+ await deps.core.handleMessageInSession(
12145
+ session,
12146
+ { channelId: "sse", userId, text: body.prompt, attachments },
12147
+ { channelUser: { channelId: "sse", userId } }
12148
+ );
12149
+ return { ok: true, sessionId, queueDepth };
11205
12150
  }
11206
12151
  );
11207
12152
  app.post(
@@ -11279,6 +12224,45 @@ async function sseRoutes(app, deps) {
11279
12224
  total: connections.length
11280
12225
  };
11281
12226
  });
12227
+ app.get("/events", async (request, reply) => {
12228
+ const auth = request.auth;
12229
+ if (!auth?.tokenId) {
12230
+ return reply.status(401).send({ error: "Unauthorized" });
12231
+ }
12232
+ const userId = deps.getUserId?.(auth.tokenId);
12233
+ if (!userId) {
12234
+ return reply.status(403).send({ error: "Identity not set up. Complete /identity/setup first." });
12235
+ }
12236
+ try {
12237
+ deps.connectionManager.addUserConnection(userId, auth.tokenId, reply.raw);
12238
+ } catch (err) {
12239
+ return reply.status(503).send({ error: err.message });
12240
+ }
12241
+ reply.hijack();
12242
+ const raw = reply.raw;
12243
+ raw.writeHead(200, {
12244
+ "Content-Type": "text/event-stream",
12245
+ "Cache-Control": "no-cache",
12246
+ "Connection": "keep-alive",
12247
+ // Disable buffering in Nginx/Cloudflare so events arrive without delay
12248
+ "X-Accel-Buffering": "no"
12249
+ });
12250
+ raw.write(`event: heartbeat
12251
+ data: ${JSON.stringify({ ts: Date.now() })}
12252
+
12253
+ `);
12254
+ const heartbeat = setInterval(() => {
12255
+ if (raw.writableEnded) {
12256
+ clearInterval(heartbeat);
12257
+ return;
12258
+ }
12259
+ raw.write(`event: heartbeat
12260
+ data: ${JSON.stringify({ ts: Date.now() })}
12261
+
12262
+ `);
12263
+ }, 3e4);
12264
+ raw.on("close", () => clearInterval(heartbeat));
12265
+ });
11282
12266
  }
11283
12267
  var init_routes = __esm({
11284
12268
  "src/plugins/sse-adapter/routes.ts"() {
@@ -11332,6 +12316,7 @@ var init_sse_adapter = __esm({
11332
12316
  _connectionManager = connectionManager;
11333
12317
  ctx.registerService("adapter:sse", adapter);
11334
12318
  const commandRegistry = ctx.getService("command-registry");
12319
+ const tokenStore = ctx.getService("token-store");
11335
12320
  ctx.on(BusEvent.SESSION_DELETED, (data) => {
11336
12321
  const { sessionId } = data;
11337
12322
  eventBuffer.cleanup(sessionId);
@@ -11345,7 +12330,8 @@ var init_sse_adapter = __esm({
11345
12330
  core,
11346
12331
  connectionManager,
11347
12332
  eventBuffer,
11348
- commandRegistry: commandRegistry ?? void 0
12333
+ commandRegistry: commandRegistry ?? void 0,
12334
+ getUserId: tokenStore ? (id) => tokenStore.getUserId(id) : void 0
11349
12335
  });
11350
12336
  }, { auth: true });
11351
12337
  ctx.log.info("SSE adapter registered");
@@ -15377,7 +16363,7 @@ var init_commands3 = __esm({
15377
16363
 
15378
16364
  // src/plugins/telegram/permissions.ts
15379
16365
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
15380
- import { nanoid as nanoid3 } from "nanoid";
16366
+ import { nanoid as nanoid4 } from "nanoid";
15381
16367
  var log26, PermissionHandler;
15382
16368
  var init_permissions = __esm({
15383
16369
  "src/plugins/telegram/permissions.ts"() {
@@ -15404,7 +16390,7 @@ var init_permissions = __esm({
15404
16390
  */
15405
16391
  async sendPermissionRequest(session, request) {
15406
16392
  const threadId = Number(session.threadId);
15407
- const callbackKey = nanoid3(8);
16393
+ const callbackKey = nanoid4(8);
15408
16394
  this.pending.set(callbackKey, {
15409
16395
  sessionId: session.id,
15410
16396
  requestId: request.id,
@@ -17539,10 +18525,19 @@ var init_adapter2 = __esm({
17539
18525
  });
17540
18526
  } catch {
17541
18527
  }
17542
- } else if (response.type === "text" || response.type === "error") {
17543
- const text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
18528
+ } else if (response.type === "text" || response.type === "error" || response.type === "adaptive") {
18529
+ let text5;
18530
+ let parseMode;
18531
+ if (response.type === "adaptive") {
18532
+ const variant = response.variants?.["telegram"];
18533
+ text5 = variant?.text ?? response.fallback;
18534
+ parseMode = variant?.parse_mode;
18535
+ } else {
18536
+ text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
18537
+ parseMode = "Markdown";
18538
+ }
17544
18539
  try {
17545
- await ctx.editMessageText(text5, { parse_mode: "Markdown" });
18540
+ await ctx.editMessageText(text5, { ...parseMode && { parse_mode: parseMode } });
17546
18541
  } catch {
17547
18542
  }
17548
18543
  }
@@ -17824,6 +18819,15 @@ OpenACP will automatically retry until this is resolved.`;
17824
18819
  message_thread_id: topicId
17825
18820
  });
17826
18821
  break;
18822
+ case "adaptive": {
18823
+ const variant = response.variants?.["telegram"];
18824
+ const text5 = variant?.text ?? response.fallback;
18825
+ await this.bot.api.sendMessage(chatId, text5, {
18826
+ message_thread_id: topicId,
18827
+ ...variant?.parse_mode && { parse_mode: variant.parse_mode }
18828
+ });
18829
+ break;
18830
+ }
17827
18831
  case "error":
17828
18832
  await this.bot.api.sendMessage(
17829
18833
  chatId,
@@ -17932,12 +18936,25 @@ ${lines.join("\n")}`;
17932
18936
  }
17933
18937
  ctx.replyWithChatAction("typing").catch(() => {
17934
18938
  });
17935
- this.core.handleMessage({
17936
- channelId: "telegram",
17937
- threadId: String(threadId),
17938
- userId: String(ctx.from.id),
17939
- text: forwardText
17940
- }).catch((err) => log29.error({ err }, "handleMessage error"));
18939
+ const fromName = [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(" ") || void 0;
18940
+ this.core.handleMessage(
18941
+ {
18942
+ channelId: "telegram",
18943
+ threadId: String(threadId),
18944
+ userId: String(ctx.from.id),
18945
+ text: forwardText
18946
+ },
18947
+ // Inject structured channel user info into TurnMeta so plugins can identify
18948
+ // the sender by name without adapter-specific fields on IncomingMessage.
18949
+ {
18950
+ channelUser: {
18951
+ channelId: "telegram",
18952
+ userId: String(ctx.from.id),
18953
+ displayName: fromName,
18954
+ username: ctx.from.username
18955
+ }
18956
+ }
18957
+ ).catch((err) => log29.error({ err }, "handleMessage error"));
17941
18958
  });
17942
18959
  this.bot.on("message:photo", async (ctx) => {
17943
18960
  const threadId = ctx.message.message_thread_id;
@@ -18901,6 +19918,7 @@ var init_core_plugins = __esm({
18901
19918
  "src/plugins/core-plugins.ts"() {
18902
19919
  "use strict";
18903
19920
  init_security();
19921
+ init_identity();
18904
19922
  init_file_service2();
18905
19923
  init_context();
18906
19924
  init_speech();
@@ -18912,6 +19930,8 @@ var init_core_plugins = __esm({
18912
19930
  corePlugins = [
18913
19931
  // Service plugins (no adapter dependencies)
18914
19932
  security_default,
19933
+ identity_default,
19934
+ // Must boot after security (blocked users rejected before identity records are created)
18915
19935
  file_service_default,
18916
19936
  context_default,
18917
19937
  speech_default,
@@ -21141,16 +22161,16 @@ var init_prompt_queue = __esm({
21141
22161
  * immediately. Otherwise, it's buffered and the returned promise resolves
21142
22162
  * only after the prompt finishes processing.
21143
22163
  */
21144
- async enqueue(text5, attachments, routing, turnId) {
22164
+ async enqueue(text5, attachments, routing, turnId, meta) {
21145
22165
  if (this.processing) {
21146
22166
  return new Promise((resolve9) => {
21147
- this.queue.push({ text: text5, attachments, routing, turnId, resolve: resolve9 });
22167
+ this.queue.push({ text: text5, attachments, routing, turnId, meta, resolve: resolve9 });
21148
22168
  });
21149
22169
  }
21150
- await this.process(text5, attachments, routing, turnId);
22170
+ await this.process(text5, attachments, routing, turnId, meta);
21151
22171
  }
21152
22172
  /** Run a single prompt through the processor, then drain the next queued item. */
21153
- async process(text5, attachments, routing, turnId) {
22173
+ async process(text5, attachments, routing, turnId, meta) {
21154
22174
  this.processing = true;
21155
22175
  this.abortController = new AbortController();
21156
22176
  const { signal } = this.abortController;
@@ -21160,7 +22180,7 @@ var init_prompt_queue = __esm({
21160
22180
  });
21161
22181
  try {
21162
22182
  await Promise.race([
21163
- this.processor(text5, attachments, routing, turnId),
22183
+ this.processor(text5, attachments, routing, turnId, meta),
21164
22184
  new Promise((_, reject) => {
21165
22185
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
21166
22186
  })
@@ -21181,7 +22201,7 @@ var init_prompt_queue = __esm({
21181
22201
  drainNext() {
21182
22202
  const next = this.queue.shift();
21183
22203
  if (next) {
21184
- this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
22204
+ this.process(next.text, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
21185
22205
  }
21186
22206
  }
21187
22207
  /**
@@ -21287,10 +22307,10 @@ var init_permission_gate = __esm({
21287
22307
  });
21288
22308
 
21289
22309
  // src/core/sessions/turn-context.ts
21290
- import { nanoid as nanoid4 } from "nanoid";
22310
+ import { nanoid as nanoid5 } from "nanoid";
21291
22311
  function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
21292
22312
  return {
21293
- turnId: turnId ?? nanoid4(8),
22313
+ turnId: turnId ?? nanoid5(8),
21294
22314
  sourceAdapterId,
21295
22315
  responseAdapterId
21296
22316
  };
@@ -21318,7 +22338,7 @@ var init_turn_context = __esm({
21318
22338
  });
21319
22339
 
21320
22340
  // src/core/sessions/session.ts
21321
- import { nanoid as nanoid5 } from "nanoid";
22341
+ import { nanoid as nanoid6 } from "nanoid";
21322
22342
  import * as fs41 from "fs";
21323
22343
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
21324
22344
  var init_session2 = __esm({
@@ -21398,7 +22418,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21398
22418
  pendingContext = null;
21399
22419
  constructor(opts) {
21400
22420
  super();
21401
- this.id = opts.id || nanoid5(12);
22421
+ this.id = opts.id || nanoid6(12);
21402
22422
  this.channelId = opts.channelId;
21403
22423
  this.attachedAdapters = [opts.channelId];
21404
22424
  this.agentName = opts.agentName;
@@ -21410,7 +22430,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21410
22430
  this.log = createSessionLogger(this.id, moduleLog);
21411
22431
  this.log.info({ agentName: this.agentName }, "Session created");
21412
22432
  this.queue = new PromptQueue(
21413
- (text5, attachments, routing, turnId) => this.processPrompt(text5, attachments, routing, turnId),
22433
+ (text5, attachments, routing, turnId, meta) => this.processPrompt(text5, attachments, routing, turnId, meta),
21414
22434
  (err) => {
21415
22435
  this.log.error({ err }, "Prompt execution failed");
21416
22436
  const message = err instanceof Error ? err.message : String(err);
@@ -21509,19 +22529,20 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21509
22529
  * then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
21510
22530
  * queued/processing events before the prompt actually runs.
21511
22531
  */
21512
- async enqueuePrompt(text5, attachments, routing, externalTurnId) {
21513
- const turnId = externalTurnId ?? nanoid5(8);
22532
+ async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
22533
+ const turnId = externalTurnId ?? nanoid6(8);
22534
+ const turnMeta = meta ?? { turnId };
21514
22535
  if (this.middlewareChain) {
21515
- const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId };
22536
+ const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
21516
22537
  const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
21517
22538
  if (!result) return turnId;
21518
22539
  text5 = result.text;
21519
22540
  attachments = result.attachments;
21520
22541
  }
21521
- await this.queue.enqueue(text5, attachments, routing, turnId);
22542
+ await this.queue.enqueue(text5, attachments, routing, turnId, turnMeta);
21522
22543
  return turnId;
21523
22544
  }
21524
- async processPrompt(text5, attachments, routing, turnId) {
22545
+ async processPrompt(text5, attachments, routing, turnId, meta) {
21525
22546
  if (this._status === "finished") return;
21526
22547
  this.activeTurnContext = createTurnContext(
21527
22548
  routing?.sourceAdapterId ?? this.channelId,
@@ -21561,6 +22582,13 @@ ${text5}`;
21561
22582
  if (accumulatorListener) {
21562
22583
  this.on(SessionEv.AGENT_EVENT, accumulatorListener);
21563
22584
  }
22585
+ const turnTextBuffer = [];
22586
+ const turnTextListener = (event) => {
22587
+ if (event.type === "text" && typeof event.content === "string") {
22588
+ turnTextBuffer.push(event.content);
22589
+ }
22590
+ };
22591
+ this.on(SessionEv.AGENT_EVENT, turnTextListener);
21564
22592
  const mw = this.middlewareChain;
21565
22593
  const afterEventListener = mw ? (event) => {
21566
22594
  mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
@@ -21570,7 +22598,7 @@ ${text5}`;
21570
22598
  this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
21571
22599
  }
21572
22600
  if (this.middlewareChain) {
21573
- this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p2) => p2).catch(() => {
22601
+ this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount, turnId: this.activeTurnContext?.turnId ?? turnId ?? "", meta }, async (p2) => p2).catch(() => {
21574
22602
  });
21575
22603
  }
21576
22604
  let stopReason = "end_turn";
@@ -21596,8 +22624,20 @@ ${text5}`;
21596
22624
  if (afterEventListener) {
21597
22625
  this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
21598
22626
  }
22627
+ this.off(SessionEv.AGENT_EVENT, turnTextListener);
22628
+ const finalTurnId = this.activeTurnContext?.turnId ?? turnId ?? "";
21599
22629
  if (this.middlewareChain) {
21600
- this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p2) => p2).catch(() => {
22630
+ this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart, turnId: finalTurnId, meta }, async (p2) => p2).catch(() => {
22631
+ });
22632
+ }
22633
+ if (this.middlewareChain) {
22634
+ this.middlewareChain.execute(Hook.AGENT_AFTER_TURN, {
22635
+ sessionId: this.id,
22636
+ turnId: finalTurnId,
22637
+ fullText: turnTextBuffer.join(""),
22638
+ stopReason,
22639
+ meta
22640
+ }, async (p2) => p2).catch(() => {
21601
22641
  });
21602
22642
  }
21603
22643
  this.activeTurnContext = null;
@@ -23294,8 +24334,7 @@ var init_session_factory = __esm({
23294
24334
  const payload = {
23295
24335
  agentName: params.agentName,
23296
24336
  workingDir: params.workingDirectory,
23297
- userId: "",
23298
- // userId is not part of SessionCreateParams — resolved upstream
24337
+ userId: params.userId ?? "",
23299
24338
  channelId: params.channelId,
23300
24339
  threadId: ""
23301
24340
  // threadId is assigned after session creation
@@ -23397,7 +24436,8 @@ var init_session_factory = __esm({
23397
24436
  this.eventBus.emit(BusEvent.SESSION_CREATED, {
23398
24437
  sessionId: session.id,
23399
24438
  agent: session.agentName,
23400
- status: session.status
24439
+ status: session.status,
24440
+ userId: createParams.userId
23401
24441
  });
23402
24442
  }
23403
24443
  return session;
@@ -24642,11 +25682,55 @@ var init_plugin_storage = __esm({
24642
25682
  async list() {
24643
25683
  return Object.keys(this.readKv());
24644
25684
  }
25685
+ async keys(prefix) {
25686
+ const all = Object.keys(this.readKv());
25687
+ return prefix ? all.filter((k) => k.startsWith(prefix)) : all;
25688
+ }
25689
+ async clear() {
25690
+ this.writeChain = this.writeChain.then(() => {
25691
+ this.writeKv({});
25692
+ });
25693
+ return this.writeChain;
25694
+ }
24645
25695
  /** Returns the plugin's data directory, creating it lazily on first access. */
24646
25696
  getDataDir() {
24647
25697
  fs45.mkdirSync(this.dataDir, { recursive: true });
24648
25698
  return this.dataDir;
24649
25699
  }
25700
+ /**
25701
+ * Creates a namespaced storage instance scoped to a session.
25702
+ * Keys are prefixed with `session:{sessionId}:` to isolate session data
25703
+ * from global plugin storage in the same backing file.
25704
+ */
25705
+ forSession(sessionId) {
25706
+ const prefix = `session:${sessionId}:`;
25707
+ return {
25708
+ get: (key) => this.get(`${prefix}${key}`),
25709
+ set: (key, value) => this.set(`${prefix}${key}`, value),
25710
+ delete: (key) => this.delete(`${prefix}${key}`),
25711
+ list: async () => {
25712
+ const all = await this.keys(prefix);
25713
+ return all.map((k) => k.slice(prefix.length));
25714
+ },
25715
+ keys: async (p2) => {
25716
+ const full = p2 ? `${prefix}${p2}` : prefix;
25717
+ const all = await this.keys(full);
25718
+ return all.map((k) => k.slice(prefix.length));
25719
+ },
25720
+ clear: async () => {
25721
+ this.writeChain = this.writeChain.then(() => {
25722
+ const data = this.readKv();
25723
+ for (const key of Object.keys(data)) {
25724
+ if (key.startsWith(prefix)) delete data[key];
25725
+ }
25726
+ this.writeKv(data);
25727
+ });
25728
+ return this.writeChain;
25729
+ },
25730
+ getDataDir: () => this.getDataDir(),
25731
+ forSession: (nestedId) => this.forSession(`${sessionId}:${nestedId}`)
25732
+ };
25733
+ }
24650
25734
  };
24651
25735
  }
24652
25736
  });
@@ -24712,9 +25796,52 @@ function createPluginContext(opts) {
24712
25796
  requirePermission(permissions, "storage:read", "storage.list");
24713
25797
  return storageImpl.list();
24714
25798
  },
25799
+ async keys(prefix) {
25800
+ requirePermission(permissions, "storage:read", "storage.keys");
25801
+ return storageImpl.keys(prefix);
25802
+ },
25803
+ async clear() {
25804
+ requirePermission(permissions, "storage:write", "storage.clear");
25805
+ return storageImpl.clear();
25806
+ },
24715
25807
  getDataDir() {
24716
25808
  requirePermission(permissions, "storage:read", "storage.getDataDir");
24717
25809
  return storageImpl.getDataDir();
25810
+ },
25811
+ forSession(sessionId) {
25812
+ requirePermission(permissions, "storage:read", "storage.forSession");
25813
+ const scoped = storageImpl.forSession(sessionId);
25814
+ return {
25815
+ get: (key) => {
25816
+ requirePermission(permissions, "storage:read", "storage.get");
25817
+ return scoped.get(key);
25818
+ },
25819
+ set: (key, value) => {
25820
+ requirePermission(permissions, "storage:write", "storage.set");
25821
+ return scoped.set(key, value);
25822
+ },
25823
+ delete: (key) => {
25824
+ requirePermission(permissions, "storage:write", "storage.delete");
25825
+ return scoped.delete(key);
25826
+ },
25827
+ list: () => {
25828
+ requirePermission(permissions, "storage:read", "storage.list");
25829
+ return scoped.list();
25830
+ },
25831
+ keys: (prefix) => {
25832
+ requirePermission(permissions, "storage:read", "storage.keys");
25833
+ return scoped.keys(prefix);
25834
+ },
25835
+ clear: () => {
25836
+ requirePermission(permissions, "storage:write", "storage.clear");
25837
+ return scoped.clear();
25838
+ },
25839
+ getDataDir: () => {
25840
+ requirePermission(permissions, "storage:read", "storage.getDataDir");
25841
+ return scoped.getDataDir();
25842
+ },
25843
+ forSession: (nestedId) => storage.forSession(`${sessionId}:${nestedId}`)
25844
+ };
24718
25845
  }
24719
25846
  };
24720
25847
  const ctx = {
@@ -24765,6 +25892,26 @@ function createPluginContext(opts) {
24765
25892
  await router.send(_sessionId, _content);
24766
25893
  }
24767
25894
  },
25895
+ notify(target, message, options) {
25896
+ requirePermission(permissions, "notifications:send", "notify()");
25897
+ const svc = serviceRegistry.get("notifications");
25898
+ if (svc?.notifyUser) {
25899
+ svc.notifyUser(target, message, options).catch(() => {
25900
+ });
25901
+ }
25902
+ },
25903
+ defineHook(_name) {
25904
+ },
25905
+ async emitHook(name, payload) {
25906
+ const qualifiedName = `plugin:${pluginName}:${name}`;
25907
+ return middlewareChain.execute(qualifiedName, payload, (p2) => p2);
25908
+ },
25909
+ async getSessionInfo(sessionId) {
25910
+ requirePermission(permissions, "sessions:read", "getSessionInfo()");
25911
+ const sessionMgr = serviceRegistry.get("session-info");
25912
+ if (!sessionMgr) return void 0;
25913
+ return sessionMgr.getSessionInfo(sessionId);
25914
+ },
24768
25915
  registerMenuItem(item) {
24769
25916
  requirePermission(permissions, "commands:register", "registerMenuItem()");
24770
25917
  const menuRegistry = serviceRegistry.get("menu-registry");
@@ -25622,7 +26769,7 @@ var init_core_items = __esm({
25622
26769
 
25623
26770
  // src/core/core.ts
25624
26771
  import path51 from "path";
25625
- import { nanoid as nanoid6 } from "nanoid";
26772
+ import { nanoid as nanoid7 } from "nanoid";
25626
26773
  var log44, OpenACPCore;
25627
26774
  var init_core = __esm({
25628
26775
  "src/core/core.ts"() {
@@ -25924,7 +27071,7 @@ var init_core = __esm({
25924
27071
  *
25925
27072
  * If no session is found, the user is told to start one with /new.
25926
27073
  */
25927
- async handleMessage(message) {
27074
+ async handleMessage(message, initialMeta) {
25928
27075
  log44.debug(
25929
27076
  {
25930
27077
  channelId: message.channelId,
@@ -25933,10 +27080,12 @@ var init_core = __esm({
25933
27080
  },
25934
27081
  "Incoming message"
25935
27082
  );
27083
+ const turnId = nanoid7(8);
27084
+ const meta = { turnId, ...initialMeta };
25936
27085
  if (this.lifecycleManager?.middlewareChain) {
25937
27086
  const result = await this.lifecycleManager.middlewareChain.execute(
25938
27087
  Hook.MESSAGE_INCOMING,
25939
- message,
27088
+ { ...message, meta },
25940
27089
  async (msg) => msg
25941
27090
  );
25942
27091
  if (!result) return;
@@ -25988,8 +27137,8 @@ ${text5}`;
25988
27137
  }
25989
27138
  const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
25990
27139
  const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
27140
+ const enrichedMeta = message.meta ?? meta;
25991
27141
  if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
25992
- const turnId = nanoid6(8);
25993
27142
  this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
25994
27143
  sessionId: session.id,
25995
27144
  turnId,
@@ -25999,11 +27148,51 @@ ${text5}`;
25999
27148
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26000
27149
  queueDepth: session.queueDepth
26001
27150
  });
26002
- await session.enqueuePrompt(text5, message.attachments, routing, turnId);
27151
+ await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
26003
27152
  } else {
26004
- await session.enqueuePrompt(text5, message.attachments, routing);
27153
+ await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
26005
27154
  }
26006
27155
  }
27156
+ /**
27157
+ * Send a message to a known session, running the full message:incoming → agent:beforePrompt
27158
+ * middleware chain (same as handleMessage) but without the threadId-based session lookup.
27159
+ *
27160
+ * Used by channels that already hold a direct session reference (e.g. SSE adapter), where
27161
+ * looking up by channelId+threadId is unreliable (API sessions may have no threadId).
27162
+ *
27163
+ * @param session The target session — caller is responsible for validating its status.
27164
+ * @param message Sender context and message content.
27165
+ * @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
27166
+ * (e.g. channelUser with display name/username).
27167
+ */
27168
+ async handleMessageInSession(session, message, initialMeta) {
27169
+ const turnId = nanoid7(8);
27170
+ const meta = { turnId, ...initialMeta };
27171
+ let text5 = message.text;
27172
+ let { attachments } = message;
27173
+ let enrichedMeta = meta;
27174
+ if (this.lifecycleManager?.middlewareChain) {
27175
+ const payload = {
27176
+ channelId: message.channelId,
27177
+ threadId: session.id,
27178
+ userId: message.userId,
27179
+ text: text5,
27180
+ attachments,
27181
+ meta
27182
+ };
27183
+ const result = await this.lifecycleManager.middlewareChain.execute(
27184
+ Hook.MESSAGE_INCOMING,
27185
+ payload,
27186
+ async (p2) => p2
27187
+ );
27188
+ if (!result) return;
27189
+ text5 = result.text;
27190
+ attachments = result.attachments;
27191
+ enrichedMeta = result.meta ?? meta;
27192
+ }
27193
+ const routing = { sourceAdapterId: message.channelId };
27194
+ await session.enqueuePrompt(text5, attachments, routing, turnId, enrichedMeta);
27195
+ }
26007
27196
  // --- Unified Session Creation Pipeline ---
26008
27197
  /**
26009
27198
  * Create (or resume) a session with full wiring: agent, adapter thread, bridge, persistence.
@@ -26563,7 +27752,7 @@ function registerSessionCommands(registry, _core) {
26563
27752
  await assistant.enqueuePrompt(prompt);
26564
27753
  return { type: "delegated" };
26565
27754
  }
26566
- return { type: "text", text: "Usage: /new <agent> <workspace>\nOr use the Assistant topic for guided setup." };
27755
+ return { type: "text", text: "Usage: /new [agent] [workspace]\nOr use the Assistant topic for guided setup." };
26567
27756
  }
26568
27757
  });
26569
27758
  registry.register({
@@ -26635,7 +27824,7 @@ Prompts: ${session.promptCount}` };
26635
27824
  registry.register({
26636
27825
  name: "resume",
26637
27826
  description: "Resume a previous session",
26638
- usage: "<session-number>",
27827
+ usage: "[session-number]",
26639
27828
  category: "system",
26640
27829
  handler: async (args2) => {
26641
27830
  const assistant = core.assistantManager?.get(args2.channelId);
@@ -26649,7 +27838,7 @@ Prompts: ${session.promptCount}` };
26649
27838
  registry.register({
26650
27839
  name: "handoff",
26651
27840
  description: "Hand off session to another agent",
26652
- usage: "<agent-name>",
27841
+ usage: "[agent-name]",
26653
27842
  category: "system",
26654
27843
  handler: async (args2) => {
26655
27844
  if (!args2.sessionId) return { type: "text", text: "Use /handoff inside a session topic." };
@@ -26835,7 +28024,7 @@ function registerAgentCommands(registry, _core) {
26835
28024
  registry.register({
26836
28025
  name: "install",
26837
28026
  description: "Install an agent",
26838
- usage: "<agent-name>",
28027
+ usage: "[agent-name]",
26839
28028
  category: "system",
26840
28029
  handler: async (args2) => {
26841
28030
  const agentName = args2.raw.trim();
@@ -26910,7 +28099,7 @@ function registerAdminCommands(registry, _core) {
26910
28099
  registry.register({
26911
28100
  name: "integrate",
26912
28101
  description: "Set up a new channel integration",
26913
- usage: "<channel>",
28102
+ usage: "[channel]",
26914
28103
  category: "system",
26915
28104
  handler: async (args2) => {
26916
28105
  const channel = args2.raw.trim();
@@ -27313,7 +28502,7 @@ var init_plugin_field_registry = __esm({
27313
28502
 
27314
28503
  // src/core/setup/types.ts
27315
28504
  var ONBOARD_SECTION_OPTIONS, CHANNEL_META;
27316
- var init_types = __esm({
28505
+ var init_types2 = __esm({
27317
28506
  "src/core/setup/types.ts"() {
27318
28507
  "use strict";
27319
28508
  ONBOARD_SECTION_OPTIONS = [
@@ -27822,7 +29011,7 @@ var CHANNEL_PLUGIN_NAME;
27822
29011
  var init_setup_channels = __esm({
27823
29012
  "src/core/setup/setup-channels.ts"() {
27824
29013
  "use strict";
27825
- init_types();
29014
+ init_types2();
27826
29015
  init_helpers2();
27827
29016
  CHANNEL_PLUGIN_NAME = {
27828
29017
  discord: "@openacp/discord-adapter"
@@ -28480,7 +29669,7 @@ async function runReconfigure(configManager, settingsManager) {
28480
29669
  var init_wizard = __esm({
28481
29670
  "src/core/setup/wizard.ts"() {
28482
29671
  "use strict";
28483
- init_types();
29672
+ init_types2();
28484
29673
  init_helpers2();
28485
29674
  init_setup_agents();
28486
29675
  init_setup_run_mode();
@@ -28495,8 +29684,8 @@ var init_wizard = __esm({
28495
29684
  });
28496
29685
 
28497
29686
  // src/core/setup/index.ts
28498
- var setup_exports = {};
28499
- __export(setup_exports, {
29687
+ var setup_exports2 = {};
29688
+ __export(setup_exports2, {
28500
29689
  detectAgents: () => detectAgents,
28501
29690
  printStartBanner: () => printStartBanner,
28502
29691
  runReconfigure: () => runReconfigure,
@@ -28508,7 +29697,7 @@ __export(setup_exports, {
28508
29697
  validateBotToken: () => validateBotToken,
28509
29698
  validateChatId: () => validateChatId
28510
29699
  });
28511
- var init_setup = __esm({
29700
+ var init_setup2 = __esm({
28512
29701
  "src/core/setup/index.ts"() {
28513
29702
  "use strict";
28514
29703
  init_wizard();
@@ -28757,7 +29946,7 @@ async function startServer(opts) {
28757
29946
  const configManager = new ConfigManager(ctx.paths.config);
28758
29947
  const configExists = await configManager.exists();
28759
29948
  if (!configExists) {
28760
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
29949
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
28761
29950
  const shouldStart = await runSetup2(configManager, { settingsManager, pluginRegistry });
28762
29951
  if (!shouldStart) process.exit(0);
28763
29952
  }
@@ -28771,7 +29960,7 @@ async function startServer(opts) {
28771
29960
  }
28772
29961
  const isForegroundTTY = !!(process.stdout.isTTY && !process.env.NO_COLOR && config.runMode !== "daemon");
28773
29962
  if (isForegroundTTY) {
28774
- const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
29963
+ const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
28775
29964
  await printStartBanner2();
28776
29965
  }
28777
29966
  let spinner4;
@@ -33286,10 +34475,10 @@ async function cmdOnboard(instanceRoot) {
33286
34475
  const pluginRegistry = new PluginRegistry2(REGISTRY_PATH);
33287
34476
  await pluginRegistry.load();
33288
34477
  if (await cm.exists()) {
33289
- const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34478
+ const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33290
34479
  await runReconfigure2(cm, settingsManager);
33291
34480
  } else {
33292
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34481
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33293
34482
  await runSetup2(cm, { skipRunMode: true, settingsManager, pluginRegistry, instanceRoot: OPENACP_DIR });
33294
34483
  }
33295
34484
  }
@@ -33349,7 +34538,7 @@ async function cmdDefault(command2, instanceRoot) {
33349
34538
  const settingsManager = new SettingsManager2(pluginsDataDir);
33350
34539
  const pluginRegistry = new PluginRegistry2(registryPath);
33351
34540
  await pluginRegistry.load();
33352
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34541
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33353
34542
  const shouldStart = await runSetup2(cm, { settingsManager, pluginRegistry, instanceRoot: root });
33354
34543
  if (!shouldStart) process.exit(0);
33355
34544
  }