@openacp/cli 2026.410.3 → 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/{channel-CFMUPzvH.d.ts → channel-Dg1nGCYa.d.ts} +29 -1
- package/dist/cli.js +1258 -90
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +250 -22
- package/dist/index.js +322 -34
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
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: '
|
|
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
|
|
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
|
|
5206
|
+
var NotificationService;
|
|
4489
5207
|
var init_notification = __esm({
|
|
4490
5208
|
"src/plugins/notifications/notification.ts"() {
|
|
4491
5209
|
"use strict";
|
|
4492
|
-
|
|
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
|
-
|
|
4544
|
-
|
|
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
|
|
4569
|
-
ctx.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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_${
|
|
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 =
|
|
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
|
|
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();
|
|
@@ -8708,8 +9514,17 @@ async function sessionRoutes(app, deps) {
|
|
|
8708
9514
|
}
|
|
8709
9515
|
attachments = await resolveAttachments(fileService, sessionId, body.attachments);
|
|
8710
9516
|
}
|
|
8711
|
-
const sourceAdapterId = body.sourceAdapterId ?? "
|
|
8712
|
-
const turnId = body.turnId ??
|
|
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
|
+
}
|
|
8713
9528
|
deps.core.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
|
|
8714
9529
|
sessionId,
|
|
8715
9530
|
turnId,
|
|
@@ -8722,7 +9537,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8722
9537
|
await session.enqueuePrompt(body.prompt, attachments, {
|
|
8723
9538
|
sourceAdapterId,
|
|
8724
9539
|
responseAdapterId: body.responseAdapterId
|
|
8725
|
-
}, turnId);
|
|
9540
|
+
}, turnId, meta);
|
|
8726
9541
|
return {
|
|
8727
9542
|
ok: true,
|
|
8728
9543
|
sessionId,
|
|
@@ -9180,7 +9995,7 @@ import { z as z4 } from "zod";
|
|
|
9180
9995
|
import * as fs21 from "fs";
|
|
9181
9996
|
import * as path24 from "path";
|
|
9182
9997
|
import * as os5 from "os";
|
|
9183
|
-
import { randomBytes as
|
|
9998
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9184
9999
|
import { EventEmitter } from "events";
|
|
9185
10000
|
function expandHome2(p2) {
|
|
9186
10001
|
if (p2.startsWith("~")) {
|
|
@@ -9310,7 +10125,7 @@ var init_config2 = __esm({
|
|
|
9310
10125
|
log14.error({ errors: result.error.issues }, "Config validation failed, not saving");
|
|
9311
10126
|
return;
|
|
9312
10127
|
}
|
|
9313
|
-
const tmpPath = this.configPath + `.tmp.${
|
|
10128
|
+
const tmpPath = this.configPath + `.tmp.${randomBytes3(4).toString("hex")}`;
|
|
9314
10129
|
fs21.writeFileSync(tmpPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
9315
10130
|
fs21.renameSync(tmpPath, this.configPath);
|
|
9316
10131
|
this.config = result.data;
|
|
@@ -9879,8 +10694,8 @@ async function commandRoutes(app, deps) {
|
|
|
9879
10694
|
const result = await deps.commandRegistry.execute(commandString, {
|
|
9880
10695
|
raw: "",
|
|
9881
10696
|
sessionId: body.sessionId ?? null,
|
|
9882
|
-
channelId: "
|
|
9883
|
-
userId: "api",
|
|
10697
|
+
channelId: "sse",
|
|
10698
|
+
userId: request.auth?.tokenId ?? "api",
|
|
9884
10699
|
reply: async () => {
|
|
9885
10700
|
}
|
|
9886
10701
|
});
|
|
@@ -10040,11 +10855,23 @@ async function authRoutes(app, deps) {
|
|
|
10040
10855
|
});
|
|
10041
10856
|
app.get("/me", async (request) => {
|
|
10042
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
|
+
}
|
|
10043
10867
|
return {
|
|
10044
10868
|
type: auth.type,
|
|
10045
10869
|
tokenId: auth.tokenId,
|
|
10046
10870
|
role: auth.role,
|
|
10047
|
-
scopes: auth.scopes
|
|
10871
|
+
scopes: auth.scopes,
|
|
10872
|
+
userId: userId ?? null,
|
|
10873
|
+
displayName,
|
|
10874
|
+
claimed: !!userId
|
|
10048
10875
|
};
|
|
10049
10876
|
});
|
|
10050
10877
|
app.post("/codes", {
|
|
@@ -10624,6 +11451,7 @@ function createApiServerPlugin() {
|
|
|
10624
11451
|
const tokenStore = new TokenStore2(tokensFilePath);
|
|
10625
11452
|
await tokenStore.load();
|
|
10626
11453
|
tokenStoreRef = tokenStore;
|
|
11454
|
+
ctx.registerService("token-store", tokenStore);
|
|
10627
11455
|
const { createApiServer: createApiServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
10628
11456
|
const { SSEManager: SSEManager2 } = await Promise.resolve().then(() => (init_sse_manager(), sse_manager_exports));
|
|
10629
11457
|
const { StaticServer: StaticServer2 } = await Promise.resolve().then(() => (init_static_server(), static_server_exports));
|
|
@@ -10677,7 +11505,12 @@ function createApiServerPlugin() {
|
|
|
10677
11505
|
server.registerPlugin("/api/v1/tunnel", async (app) => tunnelRoutes2(app, deps));
|
|
10678
11506
|
server.registerPlugin("/api/v1/notify", async (app) => notifyRoutes2(app, deps));
|
|
10679
11507
|
server.registerPlugin("/api/v1/commands", async (app) => commandRoutes2(app, deps));
|
|
10680
|
-
server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, {
|
|
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
|
+
}));
|
|
10681
11514
|
server.registerPlugin("/api/v1/plugins", async (app) => pluginRoutes2(app, deps));
|
|
10682
11515
|
const appConfig = core.configManager.get();
|
|
10683
11516
|
const workspaceName = appConfig.instanceName ?? "Main";
|
|
@@ -10826,7 +11659,7 @@ var init_api_server = __esm({
|
|
|
10826
11659
|
});
|
|
10827
11660
|
|
|
10828
11661
|
// src/plugins/sse-adapter/connection-manager.ts
|
|
10829
|
-
import { randomBytes as
|
|
11662
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
10830
11663
|
var ConnectionManager;
|
|
10831
11664
|
var init_connection_manager = __esm({
|
|
10832
11665
|
"src/plugins/sse-adapter/connection-manager.ts"() {
|
|
@@ -10835,6 +11668,8 @@ var init_connection_manager = __esm({
|
|
|
10835
11668
|
connections = /* @__PURE__ */ new Map();
|
|
10836
11669
|
// Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
|
|
10837
11670
|
sessionIndex = /* @__PURE__ */ new Map();
|
|
11671
|
+
// Secondary index: userId → Set of connection IDs for user-level event delivery
|
|
11672
|
+
userIndex = /* @__PURE__ */ new Map();
|
|
10838
11673
|
maxConnectionsPerSession;
|
|
10839
11674
|
maxTotalConnections;
|
|
10840
11675
|
constructor(opts) {
|
|
@@ -10857,7 +11692,7 @@ var init_connection_manager = __esm({
|
|
|
10857
11692
|
if (sessionConns && sessionConns.size >= this.maxConnectionsPerSession) {
|
|
10858
11693
|
throw new Error("Maximum connections per session reached");
|
|
10859
11694
|
}
|
|
10860
|
-
const id = `conn_${
|
|
11695
|
+
const id = `conn_${randomBytes5(8).toString("hex")}`;
|
|
10861
11696
|
const connection = { id, sessionId, tokenId, response, connectedAt: /* @__PURE__ */ new Date() };
|
|
10862
11697
|
this.connections.set(id, connection);
|
|
10863
11698
|
let sessionConnsSet = this.sessionIndex.get(sessionId);
|
|
@@ -10869,7 +11704,66 @@ var init_connection_manager = __esm({
|
|
|
10869
11704
|
response.on("close", () => this.removeConnection(id));
|
|
10870
11705
|
return connection;
|
|
10871
11706
|
}
|
|
10872
|
-
/**
|
|
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. */
|
|
10873
11767
|
removeConnection(connectionId) {
|
|
10874
11768
|
const conn = this.connections.get(connectionId);
|
|
10875
11769
|
if (!conn) return;
|
|
@@ -10879,6 +11773,13 @@ var init_connection_manager = __esm({
|
|
|
10879
11773
|
sessionConns.delete(connectionId);
|
|
10880
11774
|
if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
|
|
10881
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
|
+
}
|
|
10882
11783
|
}
|
|
10883
11784
|
/** Returns all active connections for a session. */
|
|
10884
11785
|
getConnectionsBySession(sessionId) {
|
|
@@ -10938,6 +11839,7 @@ var init_connection_manager = __esm({
|
|
|
10938
11839
|
}
|
|
10939
11840
|
this.connections.clear();
|
|
10940
11841
|
this.sessionIndex.clear();
|
|
11842
|
+
this.userIndex.clear();
|
|
10941
11843
|
}
|
|
10942
11844
|
};
|
|
10943
11845
|
}
|
|
@@ -11131,6 +12033,22 @@ var init_adapter = __esm({
|
|
|
11131
12033
|
this.connectionManager.broadcast(notification.sessionId, serialized);
|
|
11132
12034
|
}
|
|
11133
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
|
+
}
|
|
11134
12052
|
/** SSE has no concept of threads — return sessionId as the threadId */
|
|
11135
12053
|
async createSessionThread(sessionId, _name) {
|
|
11136
12054
|
return sessionId;
|
|
@@ -11221,8 +12139,14 @@ async function sseRoutes(app, deps) {
|
|
|
11221
12139
|
}
|
|
11222
12140
|
attachments = await resolveAttachments(fileService, sessionId, body.attachments);
|
|
11223
12141
|
}
|
|
11224
|
-
|
|
11225
|
-
|
|
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 };
|
|
11226
12150
|
}
|
|
11227
12151
|
);
|
|
11228
12152
|
app.post(
|
|
@@ -11300,6 +12224,45 @@ async function sseRoutes(app, deps) {
|
|
|
11300
12224
|
total: connections.length
|
|
11301
12225
|
};
|
|
11302
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
|
+
});
|
|
11303
12266
|
}
|
|
11304
12267
|
var init_routes = __esm({
|
|
11305
12268
|
"src/plugins/sse-adapter/routes.ts"() {
|
|
@@ -11353,6 +12316,7 @@ var init_sse_adapter = __esm({
|
|
|
11353
12316
|
_connectionManager = connectionManager;
|
|
11354
12317
|
ctx.registerService("adapter:sse", adapter);
|
|
11355
12318
|
const commandRegistry = ctx.getService("command-registry");
|
|
12319
|
+
const tokenStore = ctx.getService("token-store");
|
|
11356
12320
|
ctx.on(BusEvent.SESSION_DELETED, (data) => {
|
|
11357
12321
|
const { sessionId } = data;
|
|
11358
12322
|
eventBuffer.cleanup(sessionId);
|
|
@@ -11366,7 +12330,8 @@ var init_sse_adapter = __esm({
|
|
|
11366
12330
|
core,
|
|
11367
12331
|
connectionManager,
|
|
11368
12332
|
eventBuffer,
|
|
11369
|
-
commandRegistry: commandRegistry ?? void 0
|
|
12333
|
+
commandRegistry: commandRegistry ?? void 0,
|
|
12334
|
+
getUserId: tokenStore ? (id) => tokenStore.getUserId(id) : void 0
|
|
11370
12335
|
});
|
|
11371
12336
|
}, { auth: true });
|
|
11372
12337
|
ctx.log.info("SSE adapter registered");
|
|
@@ -15398,7 +16363,7 @@ var init_commands3 = __esm({
|
|
|
15398
16363
|
|
|
15399
16364
|
// src/plugins/telegram/permissions.ts
|
|
15400
16365
|
import { InlineKeyboard as InlineKeyboard11 } from "grammy";
|
|
15401
|
-
import { nanoid as
|
|
16366
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
15402
16367
|
var log26, PermissionHandler;
|
|
15403
16368
|
var init_permissions = __esm({
|
|
15404
16369
|
"src/plugins/telegram/permissions.ts"() {
|
|
@@ -15425,7 +16390,7 @@ var init_permissions = __esm({
|
|
|
15425
16390
|
*/
|
|
15426
16391
|
async sendPermissionRequest(session, request) {
|
|
15427
16392
|
const threadId = Number(session.threadId);
|
|
15428
|
-
const callbackKey =
|
|
16393
|
+
const callbackKey = nanoid4(8);
|
|
15429
16394
|
this.pending.set(callbackKey, {
|
|
15430
16395
|
sessionId: session.id,
|
|
15431
16396
|
requestId: request.id,
|
|
@@ -17560,10 +18525,19 @@ var init_adapter2 = __esm({
|
|
|
17560
18525
|
});
|
|
17561
18526
|
} catch {
|
|
17562
18527
|
}
|
|
17563
|
-
} else if (response.type === "text" || response.type === "error") {
|
|
17564
|
-
|
|
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
|
+
}
|
|
17565
18539
|
try {
|
|
17566
|
-
await ctx.editMessageText(text5, { parse_mode:
|
|
18540
|
+
await ctx.editMessageText(text5, { ...parseMode && { parse_mode: parseMode } });
|
|
17567
18541
|
} catch {
|
|
17568
18542
|
}
|
|
17569
18543
|
}
|
|
@@ -17845,6 +18819,15 @@ OpenACP will automatically retry until this is resolved.`;
|
|
|
17845
18819
|
message_thread_id: topicId
|
|
17846
18820
|
});
|
|
17847
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
|
+
}
|
|
17848
18831
|
case "error":
|
|
17849
18832
|
await this.bot.api.sendMessage(
|
|
17850
18833
|
chatId,
|
|
@@ -17953,12 +18936,25 @@ ${lines.join("\n")}`;
|
|
|
17953
18936
|
}
|
|
17954
18937
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
17955
18938
|
});
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
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"));
|
|
17962
18958
|
});
|
|
17963
18959
|
this.bot.on("message:photo", async (ctx) => {
|
|
17964
18960
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -18922,6 +19918,7 @@ var init_core_plugins = __esm({
|
|
|
18922
19918
|
"src/plugins/core-plugins.ts"() {
|
|
18923
19919
|
"use strict";
|
|
18924
19920
|
init_security();
|
|
19921
|
+
init_identity();
|
|
18925
19922
|
init_file_service2();
|
|
18926
19923
|
init_context();
|
|
18927
19924
|
init_speech();
|
|
@@ -18933,6 +19930,8 @@ var init_core_plugins = __esm({
|
|
|
18933
19930
|
corePlugins = [
|
|
18934
19931
|
// Service plugins (no adapter dependencies)
|
|
18935
19932
|
security_default,
|
|
19933
|
+
identity_default,
|
|
19934
|
+
// Must boot after security (blocked users rejected before identity records are created)
|
|
18936
19935
|
file_service_default,
|
|
18937
19936
|
context_default,
|
|
18938
19937
|
speech_default,
|
|
@@ -21162,16 +22161,16 @@ var init_prompt_queue = __esm({
|
|
|
21162
22161
|
* immediately. Otherwise, it's buffered and the returned promise resolves
|
|
21163
22162
|
* only after the prompt finishes processing.
|
|
21164
22163
|
*/
|
|
21165
|
-
async enqueue(text5, attachments, routing, turnId) {
|
|
22164
|
+
async enqueue(text5, attachments, routing, turnId, meta) {
|
|
21166
22165
|
if (this.processing) {
|
|
21167
22166
|
return new Promise((resolve9) => {
|
|
21168
|
-
this.queue.push({ text: text5, attachments, routing, turnId, resolve: resolve9 });
|
|
22167
|
+
this.queue.push({ text: text5, attachments, routing, turnId, meta, resolve: resolve9 });
|
|
21169
22168
|
});
|
|
21170
22169
|
}
|
|
21171
|
-
await this.process(text5, attachments, routing, turnId);
|
|
22170
|
+
await this.process(text5, attachments, routing, turnId, meta);
|
|
21172
22171
|
}
|
|
21173
22172
|
/** Run a single prompt through the processor, then drain the next queued item. */
|
|
21174
|
-
async process(text5, attachments, routing, turnId) {
|
|
22173
|
+
async process(text5, attachments, routing, turnId, meta) {
|
|
21175
22174
|
this.processing = true;
|
|
21176
22175
|
this.abortController = new AbortController();
|
|
21177
22176
|
const { signal } = this.abortController;
|
|
@@ -21181,7 +22180,7 @@ var init_prompt_queue = __esm({
|
|
|
21181
22180
|
});
|
|
21182
22181
|
try {
|
|
21183
22182
|
await Promise.race([
|
|
21184
|
-
this.processor(text5, attachments, routing, turnId),
|
|
22183
|
+
this.processor(text5, attachments, routing, turnId, meta),
|
|
21185
22184
|
new Promise((_, reject) => {
|
|
21186
22185
|
signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
|
|
21187
22186
|
})
|
|
@@ -21202,7 +22201,7 @@ var init_prompt_queue = __esm({
|
|
|
21202
22201
|
drainNext() {
|
|
21203
22202
|
const next = this.queue.shift();
|
|
21204
22203
|
if (next) {
|
|
21205
|
-
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);
|
|
21206
22205
|
}
|
|
21207
22206
|
}
|
|
21208
22207
|
/**
|
|
@@ -21308,10 +22307,10 @@ var init_permission_gate = __esm({
|
|
|
21308
22307
|
});
|
|
21309
22308
|
|
|
21310
22309
|
// src/core/sessions/turn-context.ts
|
|
21311
|
-
import { nanoid as
|
|
22310
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
21312
22311
|
function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
|
|
21313
22312
|
return {
|
|
21314
|
-
turnId: turnId ??
|
|
22313
|
+
turnId: turnId ?? nanoid5(8),
|
|
21315
22314
|
sourceAdapterId,
|
|
21316
22315
|
responseAdapterId
|
|
21317
22316
|
};
|
|
@@ -21339,7 +22338,7 @@ var init_turn_context = __esm({
|
|
|
21339
22338
|
});
|
|
21340
22339
|
|
|
21341
22340
|
// src/core/sessions/session.ts
|
|
21342
|
-
import { nanoid as
|
|
22341
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
21343
22342
|
import * as fs41 from "fs";
|
|
21344
22343
|
var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
|
|
21345
22344
|
var init_session2 = __esm({
|
|
@@ -21419,7 +22418,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21419
22418
|
pendingContext = null;
|
|
21420
22419
|
constructor(opts) {
|
|
21421
22420
|
super();
|
|
21422
|
-
this.id = opts.id ||
|
|
22421
|
+
this.id = opts.id || nanoid6(12);
|
|
21423
22422
|
this.channelId = opts.channelId;
|
|
21424
22423
|
this.attachedAdapters = [opts.channelId];
|
|
21425
22424
|
this.agentName = opts.agentName;
|
|
@@ -21431,7 +22430,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21431
22430
|
this.log = createSessionLogger(this.id, moduleLog);
|
|
21432
22431
|
this.log.info({ agentName: this.agentName }, "Session created");
|
|
21433
22432
|
this.queue = new PromptQueue(
|
|
21434
|
-
(text5, attachments, routing, turnId) => this.processPrompt(text5, attachments, routing, turnId),
|
|
22433
|
+
(text5, attachments, routing, turnId, meta) => this.processPrompt(text5, attachments, routing, turnId, meta),
|
|
21435
22434
|
(err) => {
|
|
21436
22435
|
this.log.error({ err }, "Prompt execution failed");
|
|
21437
22436
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -21530,19 +22529,20 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21530
22529
|
* then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
|
|
21531
22530
|
* queued/processing events before the prompt actually runs.
|
|
21532
22531
|
*/
|
|
21533
|
-
async enqueuePrompt(text5, attachments, routing, externalTurnId) {
|
|
21534
|
-
const turnId = externalTurnId ??
|
|
22532
|
+
async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
|
|
22533
|
+
const turnId = externalTurnId ?? nanoid6(8);
|
|
22534
|
+
const turnMeta = meta ?? { turnId };
|
|
21535
22535
|
if (this.middlewareChain) {
|
|
21536
|
-
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 };
|
|
21537
22537
|
const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
|
|
21538
22538
|
if (!result) return turnId;
|
|
21539
22539
|
text5 = result.text;
|
|
21540
22540
|
attachments = result.attachments;
|
|
21541
22541
|
}
|
|
21542
|
-
await this.queue.enqueue(text5, attachments, routing, turnId);
|
|
22542
|
+
await this.queue.enqueue(text5, attachments, routing, turnId, turnMeta);
|
|
21543
22543
|
return turnId;
|
|
21544
22544
|
}
|
|
21545
|
-
async processPrompt(text5, attachments, routing, turnId) {
|
|
22545
|
+
async processPrompt(text5, attachments, routing, turnId, meta) {
|
|
21546
22546
|
if (this._status === "finished") return;
|
|
21547
22547
|
this.activeTurnContext = createTurnContext(
|
|
21548
22548
|
routing?.sourceAdapterId ?? this.channelId,
|
|
@@ -21582,6 +22582,13 @@ ${text5}`;
|
|
|
21582
22582
|
if (accumulatorListener) {
|
|
21583
22583
|
this.on(SessionEv.AGENT_EVENT, accumulatorListener);
|
|
21584
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);
|
|
21585
22592
|
const mw = this.middlewareChain;
|
|
21586
22593
|
const afterEventListener = mw ? (event) => {
|
|
21587
22594
|
mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
|
|
@@ -21591,7 +22598,7 @@ ${text5}`;
|
|
|
21591
22598
|
this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
|
|
21592
22599
|
}
|
|
21593
22600
|
if (this.middlewareChain) {
|
|
21594
|
-
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(() => {
|
|
21595
22602
|
});
|
|
21596
22603
|
}
|
|
21597
22604
|
let stopReason = "end_turn";
|
|
@@ -21617,8 +22624,20 @@ ${text5}`;
|
|
|
21617
22624
|
if (afterEventListener) {
|
|
21618
22625
|
this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
|
|
21619
22626
|
}
|
|
22627
|
+
this.off(SessionEv.AGENT_EVENT, turnTextListener);
|
|
22628
|
+
const finalTurnId = this.activeTurnContext?.turnId ?? turnId ?? "";
|
|
22629
|
+
if (this.middlewareChain) {
|
|
22630
|
+
this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart, turnId: finalTurnId, meta }, async (p2) => p2).catch(() => {
|
|
22631
|
+
});
|
|
22632
|
+
}
|
|
21620
22633
|
if (this.middlewareChain) {
|
|
21621
|
-
this.middlewareChain.execute(Hook.
|
|
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(() => {
|
|
21622
22641
|
});
|
|
21623
22642
|
}
|
|
21624
22643
|
this.activeTurnContext = null;
|
|
@@ -23315,8 +24334,7 @@ var init_session_factory = __esm({
|
|
|
23315
24334
|
const payload = {
|
|
23316
24335
|
agentName: params.agentName,
|
|
23317
24336
|
workingDir: params.workingDirectory,
|
|
23318
|
-
userId: "",
|
|
23319
|
-
// userId is not part of SessionCreateParams — resolved upstream
|
|
24337
|
+
userId: params.userId ?? "",
|
|
23320
24338
|
channelId: params.channelId,
|
|
23321
24339
|
threadId: ""
|
|
23322
24340
|
// threadId is assigned after session creation
|
|
@@ -23418,7 +24436,8 @@ var init_session_factory = __esm({
|
|
|
23418
24436
|
this.eventBus.emit(BusEvent.SESSION_CREATED, {
|
|
23419
24437
|
sessionId: session.id,
|
|
23420
24438
|
agent: session.agentName,
|
|
23421
|
-
status: session.status
|
|
24439
|
+
status: session.status,
|
|
24440
|
+
userId: createParams.userId
|
|
23422
24441
|
});
|
|
23423
24442
|
}
|
|
23424
24443
|
return session;
|
|
@@ -24663,11 +25682,55 @@ var init_plugin_storage = __esm({
|
|
|
24663
25682
|
async list() {
|
|
24664
25683
|
return Object.keys(this.readKv());
|
|
24665
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
|
+
}
|
|
24666
25695
|
/** Returns the plugin's data directory, creating it lazily on first access. */
|
|
24667
25696
|
getDataDir() {
|
|
24668
25697
|
fs45.mkdirSync(this.dataDir, { recursive: true });
|
|
24669
25698
|
return this.dataDir;
|
|
24670
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
|
+
}
|
|
24671
25734
|
};
|
|
24672
25735
|
}
|
|
24673
25736
|
});
|
|
@@ -24733,9 +25796,52 @@ function createPluginContext(opts) {
|
|
|
24733
25796
|
requirePermission(permissions, "storage:read", "storage.list");
|
|
24734
25797
|
return storageImpl.list();
|
|
24735
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
|
+
},
|
|
24736
25807
|
getDataDir() {
|
|
24737
25808
|
requirePermission(permissions, "storage:read", "storage.getDataDir");
|
|
24738
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
|
+
};
|
|
24739
25845
|
}
|
|
24740
25846
|
};
|
|
24741
25847
|
const ctx = {
|
|
@@ -24786,6 +25892,26 @@ function createPluginContext(opts) {
|
|
|
24786
25892
|
await router.send(_sessionId, _content);
|
|
24787
25893
|
}
|
|
24788
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
|
+
},
|
|
24789
25915
|
registerMenuItem(item) {
|
|
24790
25916
|
requirePermission(permissions, "commands:register", "registerMenuItem()");
|
|
24791
25917
|
const menuRegistry = serviceRegistry.get("menu-registry");
|
|
@@ -25643,7 +26769,7 @@ var init_core_items = __esm({
|
|
|
25643
26769
|
|
|
25644
26770
|
// src/core/core.ts
|
|
25645
26771
|
import path51 from "path";
|
|
25646
|
-
import { nanoid as
|
|
26772
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
25647
26773
|
var log44, OpenACPCore;
|
|
25648
26774
|
var init_core = __esm({
|
|
25649
26775
|
"src/core/core.ts"() {
|
|
@@ -25945,7 +27071,7 @@ var init_core = __esm({
|
|
|
25945
27071
|
*
|
|
25946
27072
|
* If no session is found, the user is told to start one with /new.
|
|
25947
27073
|
*/
|
|
25948
|
-
async handleMessage(message) {
|
|
27074
|
+
async handleMessage(message, initialMeta) {
|
|
25949
27075
|
log44.debug(
|
|
25950
27076
|
{
|
|
25951
27077
|
channelId: message.channelId,
|
|
@@ -25954,10 +27080,12 @@ var init_core = __esm({
|
|
|
25954
27080
|
},
|
|
25955
27081
|
"Incoming message"
|
|
25956
27082
|
);
|
|
27083
|
+
const turnId = nanoid7(8);
|
|
27084
|
+
const meta = { turnId, ...initialMeta };
|
|
25957
27085
|
if (this.lifecycleManager?.middlewareChain) {
|
|
25958
27086
|
const result = await this.lifecycleManager.middlewareChain.execute(
|
|
25959
27087
|
Hook.MESSAGE_INCOMING,
|
|
25960
|
-
message,
|
|
27088
|
+
{ ...message, meta },
|
|
25961
27089
|
async (msg) => msg
|
|
25962
27090
|
);
|
|
25963
27091
|
if (!result) return;
|
|
@@ -26009,8 +27137,8 @@ ${text5}`;
|
|
|
26009
27137
|
}
|
|
26010
27138
|
const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
|
|
26011
27139
|
const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
|
|
27140
|
+
const enrichedMeta = message.meta ?? meta;
|
|
26012
27141
|
if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
|
|
26013
|
-
const turnId = nanoid6(8);
|
|
26014
27142
|
this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
|
|
26015
27143
|
sessionId: session.id,
|
|
26016
27144
|
turnId,
|
|
@@ -26020,10 +27148,50 @@ ${text5}`;
|
|
|
26020
27148
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26021
27149
|
queueDepth: session.queueDepth
|
|
26022
27150
|
});
|
|
26023
|
-
await session.enqueuePrompt(text5, message.attachments, routing, turnId);
|
|
27151
|
+
await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
|
|
26024
27152
|
} else {
|
|
26025
|
-
await session.enqueuePrompt(text5, message.attachments, routing);
|
|
27153
|
+
await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
|
|
27154
|
+
}
|
|
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;
|
|
26026
27192
|
}
|
|
27193
|
+
const routing = { sourceAdapterId: message.channelId };
|
|
27194
|
+
await session.enqueuePrompt(text5, attachments, routing, turnId, enrichedMeta);
|
|
26027
27195
|
}
|
|
26028
27196
|
// --- Unified Session Creation Pipeline ---
|
|
26029
27197
|
/**
|
|
@@ -26584,7 +27752,7 @@ function registerSessionCommands(registry, _core) {
|
|
|
26584
27752
|
await assistant.enqueuePrompt(prompt);
|
|
26585
27753
|
return { type: "delegated" };
|
|
26586
27754
|
}
|
|
26587
|
-
return { type: "text", text: "Usage: /new
|
|
27755
|
+
return { type: "text", text: "Usage: /new [agent] [workspace]\nOr use the Assistant topic for guided setup." };
|
|
26588
27756
|
}
|
|
26589
27757
|
});
|
|
26590
27758
|
registry.register({
|
|
@@ -26656,7 +27824,7 @@ Prompts: ${session.promptCount}` };
|
|
|
26656
27824
|
registry.register({
|
|
26657
27825
|
name: "resume",
|
|
26658
27826
|
description: "Resume a previous session",
|
|
26659
|
-
usage: "
|
|
27827
|
+
usage: "[session-number]",
|
|
26660
27828
|
category: "system",
|
|
26661
27829
|
handler: async (args2) => {
|
|
26662
27830
|
const assistant = core.assistantManager?.get(args2.channelId);
|
|
@@ -26670,7 +27838,7 @@ Prompts: ${session.promptCount}` };
|
|
|
26670
27838
|
registry.register({
|
|
26671
27839
|
name: "handoff",
|
|
26672
27840
|
description: "Hand off session to another agent",
|
|
26673
|
-
usage: "
|
|
27841
|
+
usage: "[agent-name]",
|
|
26674
27842
|
category: "system",
|
|
26675
27843
|
handler: async (args2) => {
|
|
26676
27844
|
if (!args2.sessionId) return { type: "text", text: "Use /handoff inside a session topic." };
|
|
@@ -26856,7 +28024,7 @@ function registerAgentCommands(registry, _core) {
|
|
|
26856
28024
|
registry.register({
|
|
26857
28025
|
name: "install",
|
|
26858
28026
|
description: "Install an agent",
|
|
26859
|
-
usage: "
|
|
28027
|
+
usage: "[agent-name]",
|
|
26860
28028
|
category: "system",
|
|
26861
28029
|
handler: async (args2) => {
|
|
26862
28030
|
const agentName = args2.raw.trim();
|
|
@@ -26931,7 +28099,7 @@ function registerAdminCommands(registry, _core) {
|
|
|
26931
28099
|
registry.register({
|
|
26932
28100
|
name: "integrate",
|
|
26933
28101
|
description: "Set up a new channel integration",
|
|
26934
|
-
usage: "
|
|
28102
|
+
usage: "[channel]",
|
|
26935
28103
|
category: "system",
|
|
26936
28104
|
handler: async (args2) => {
|
|
26937
28105
|
const channel = args2.raw.trim();
|
|
@@ -27334,7 +28502,7 @@ var init_plugin_field_registry = __esm({
|
|
|
27334
28502
|
|
|
27335
28503
|
// src/core/setup/types.ts
|
|
27336
28504
|
var ONBOARD_SECTION_OPTIONS, CHANNEL_META;
|
|
27337
|
-
var
|
|
28505
|
+
var init_types2 = __esm({
|
|
27338
28506
|
"src/core/setup/types.ts"() {
|
|
27339
28507
|
"use strict";
|
|
27340
28508
|
ONBOARD_SECTION_OPTIONS = [
|
|
@@ -27843,7 +29011,7 @@ var CHANNEL_PLUGIN_NAME;
|
|
|
27843
29011
|
var init_setup_channels = __esm({
|
|
27844
29012
|
"src/core/setup/setup-channels.ts"() {
|
|
27845
29013
|
"use strict";
|
|
27846
|
-
|
|
29014
|
+
init_types2();
|
|
27847
29015
|
init_helpers2();
|
|
27848
29016
|
CHANNEL_PLUGIN_NAME = {
|
|
27849
29017
|
discord: "@openacp/discord-adapter"
|
|
@@ -28501,7 +29669,7 @@ async function runReconfigure(configManager, settingsManager) {
|
|
|
28501
29669
|
var init_wizard = __esm({
|
|
28502
29670
|
"src/core/setup/wizard.ts"() {
|
|
28503
29671
|
"use strict";
|
|
28504
|
-
|
|
29672
|
+
init_types2();
|
|
28505
29673
|
init_helpers2();
|
|
28506
29674
|
init_setup_agents();
|
|
28507
29675
|
init_setup_run_mode();
|
|
@@ -28516,8 +29684,8 @@ var init_wizard = __esm({
|
|
|
28516
29684
|
});
|
|
28517
29685
|
|
|
28518
29686
|
// src/core/setup/index.ts
|
|
28519
|
-
var
|
|
28520
|
-
__export(
|
|
29687
|
+
var setup_exports2 = {};
|
|
29688
|
+
__export(setup_exports2, {
|
|
28521
29689
|
detectAgents: () => detectAgents,
|
|
28522
29690
|
printStartBanner: () => printStartBanner,
|
|
28523
29691
|
runReconfigure: () => runReconfigure,
|
|
@@ -28529,7 +29697,7 @@ __export(setup_exports, {
|
|
|
28529
29697
|
validateBotToken: () => validateBotToken,
|
|
28530
29698
|
validateChatId: () => validateChatId
|
|
28531
29699
|
});
|
|
28532
|
-
var
|
|
29700
|
+
var init_setup2 = __esm({
|
|
28533
29701
|
"src/core/setup/index.ts"() {
|
|
28534
29702
|
"use strict";
|
|
28535
29703
|
init_wizard();
|
|
@@ -28778,7 +29946,7 @@ async function startServer(opts) {
|
|
|
28778
29946
|
const configManager = new ConfigManager(ctx.paths.config);
|
|
28779
29947
|
const configExists = await configManager.exists();
|
|
28780
29948
|
if (!configExists) {
|
|
28781
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
29949
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
28782
29950
|
const shouldStart = await runSetup2(configManager, { settingsManager, pluginRegistry });
|
|
28783
29951
|
if (!shouldStart) process.exit(0);
|
|
28784
29952
|
}
|
|
@@ -28792,7 +29960,7 @@ async function startServer(opts) {
|
|
|
28792
29960
|
}
|
|
28793
29961
|
const isForegroundTTY = !!(process.stdout.isTTY && !process.env.NO_COLOR && config.runMode !== "daemon");
|
|
28794
29962
|
if (isForegroundTTY) {
|
|
28795
|
-
const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (
|
|
29963
|
+
const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
28796
29964
|
await printStartBanner2();
|
|
28797
29965
|
}
|
|
28798
29966
|
let spinner4;
|
|
@@ -33307,10 +34475,10 @@ async function cmdOnboard(instanceRoot) {
|
|
|
33307
34475
|
const pluginRegistry = new PluginRegistry2(REGISTRY_PATH);
|
|
33308
34476
|
await pluginRegistry.load();
|
|
33309
34477
|
if (await cm.exists()) {
|
|
33310
|
-
const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (
|
|
34478
|
+
const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33311
34479
|
await runReconfigure2(cm, settingsManager);
|
|
33312
34480
|
} else {
|
|
33313
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
34481
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33314
34482
|
await runSetup2(cm, { skipRunMode: true, settingsManager, pluginRegistry, instanceRoot: OPENACP_DIR });
|
|
33315
34483
|
}
|
|
33316
34484
|
}
|
|
@@ -33370,7 +34538,7 @@ async function cmdDefault(command2, instanceRoot) {
|
|
|
33370
34538
|
const settingsManager = new SettingsManager2(pluginsDataDir);
|
|
33371
34539
|
const pluginRegistry = new PluginRegistry2(registryPath);
|
|
33372
34540
|
await pluginRegistry.load();
|
|
33373
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
34541
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33374
34542
|
const shouldStart = await runSetup2(cm, { settingsManager, pluginRegistry, instanceRoot: root });
|
|
33375
34543
|
if (!shouldStart) process.exit(0);
|
|
33376
34544
|
}
|