@selleragent/sa-admin 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ const help_1 = require("./help");
10
10
  const provisioning_1 = require("./provisioning");
11
11
  const rollout_1 = require("./rollout");
12
12
  const project_1 = require("./project");
13
+ const conversations_1 = require("./conversations");
13
14
  const render_1 = require("./render");
14
15
  function flagValue(args, name) {
15
16
  const index = args.indexOf(name);
@@ -57,11 +58,16 @@ function normalizeCommandTokens(tokens) {
57
58
  }
58
59
  return tokens;
59
60
  }
61
+ function resolveProfileFlag(args) {
62
+ return (flagValue(args, '--profile') ??
63
+ flagValue(args, '--business-profile-slug') ??
64
+ flagValue(args, '--business-profile'));
65
+ }
60
66
  function resolveProfileSlug(args, manifestBusinessSlug) {
61
- return flagValue(args, '--business-profile-slug') ?? manifestBusinessSlug;
67
+ return resolveProfileFlag(args) ?? manifestBusinessSlug;
62
68
  }
63
69
  function resolveScopedBusinessSlug(args, manifestBusinessSlug, options = {}) {
64
- const explicit = flagValue(args, '--business-profile-slug');
70
+ const explicit = resolveProfileFlag(args);
65
71
  if (explicit) {
66
72
  return explicit;
67
73
  }
@@ -169,6 +175,152 @@ function summarizeWorkspaceCapabilities(accessContext, workspaceSlug) {
169
175
  canManageTeam: isSystemAdmin || roleRank(role) >= roleRank('workspace_admin')
170
176
  };
171
177
  }
178
+ function summarizeProfileCapabilities(input) {
179
+ const fallback = summarizeWorkspaceCapabilities(input.accessContext, input.workspaceSlug);
180
+ return {
181
+ ...fallback,
182
+ workspaceName: input.workspaceName ?? fallback.workspaceName,
183
+ membershipStatus: input.membershipStatus ?? fallback.membershipStatus,
184
+ role: input.role ?? fallback.role
185
+ };
186
+ }
187
+ function describeWorkspaceAccess(input) {
188
+ if (input.isSystemAdmin) {
189
+ return `${input.workspaceSlug} (${input.workspaceName ?? 'unknown workspace'}): full access as system admin`;
190
+ }
191
+ const capabilities = ['read'];
192
+ if (roleRank(input.role) >= roleRank('workspace_admin')) {
193
+ capabilities.push('upload', 'activate', 'manage team');
194
+ }
195
+ return `${input.workspaceSlug} (${input.workspaceName ?? 'unknown workspace'}): ${input.role ?? 'no role'} [${capabilities.join(', ')}]`;
196
+ }
197
+ function uniqueByWorkspaceSlug(entries) {
198
+ const seen = new Set();
199
+ const result = [];
200
+ for (const entry of entries) {
201
+ if (seen.has(entry.workspaceSlug)) {
202
+ continue;
203
+ }
204
+ seen.add(entry.workspaceSlug);
205
+ result.push(entry);
206
+ }
207
+ return result;
208
+ }
209
+ async function collectAccessibleProfiles(input) {
210
+ const explicitProfile = input.explicitProfile?.trim() || null;
211
+ if (input.accessContext?.isSystemAdmin) {
212
+ const listed = await input.auth.client.businessProfiles.list({});
213
+ const profiles = listed.profiles.map((profile) => ({
214
+ workspaceSlug: profile.businessProfileSlug,
215
+ workspaceName: profile.businessProfileName,
216
+ status: profile.status,
217
+ role: null,
218
+ membershipStatus: null
219
+ }));
220
+ const filtered = explicitProfile
221
+ ? profiles.filter((profile) => profile.workspaceSlug === explicitProfile)
222
+ : profiles;
223
+ if (explicitProfile && filtered.length === 0) {
224
+ throw new Error(`Business profile ${explicitProfile} was not found.`);
225
+ }
226
+ return filtered;
227
+ }
228
+ const memberships = uniqueByWorkspaceSlug((input.accessContext?.memberships ?? []).map((membership) => ({
229
+ workspaceSlug: membership.workspaceSlug,
230
+ workspaceName: membership.workspaceName,
231
+ status: 'active',
232
+ role: membership.role,
233
+ membershipStatus: membership.status
234
+ })));
235
+ const filtered = explicitProfile
236
+ ? memberships.filter((membership) => membership.workspaceSlug === explicitProfile)
237
+ : memberships;
238
+ if (explicitProfile && filtered.length === 0) {
239
+ throw new Error(`Current account does not have access to profile ${explicitProfile}.`);
240
+ }
241
+ return filtered;
242
+ }
243
+ async function renderProfileAccessView(input) {
244
+ const auth = await (0, auth_1.resolveAuthenticatedClient)({ context: input.context });
245
+ const whoami = await (0, auth_1.resolveWhoAmI)(input.context, {
246
+ preferActive: true,
247
+ allowAnyActiveFallback: !Boolean(flagValue(input.args, '--base-url') || flagValue(input.args, '--environment'))
248
+ });
249
+ const explicitProfile = resolveProfileFlag(input.args);
250
+ const profiles = await collectAccessibleProfiles({
251
+ auth,
252
+ accessContext: whoami.accessContext,
253
+ explicitProfile
254
+ });
255
+ (0, render_1.renderOutput)({
256
+ session: whoami.session,
257
+ authenticated: whoami.accessContext?.authenticated ?? false,
258
+ operator: whoami.accessContext?.operator ?? null,
259
+ isSystemAdmin: whoami.accessContext?.isSystemAdmin ?? false,
260
+ profiles: profiles.map((profile) => summarizeProfileCapabilities({
261
+ accessContext: whoami.accessContext,
262
+ workspaceSlug: profile.workspaceSlug,
263
+ workspaceName: profile.workspaceName,
264
+ membershipStatus: profile.membershipStatus,
265
+ role: profile.role
266
+ }))
267
+ }, input.outputMode);
268
+ }
269
+ async function renderProfileMembersList(input) {
270
+ const auth = await (0, auth_1.resolveAuthenticatedClient)({ context: input.context });
271
+ const whoami = await (0, auth_1.resolveWhoAmI)(input.context, {
272
+ preferActive: true,
273
+ allowAnyActiveFallback: !Boolean(flagValue(input.args, '--base-url') || flagValue(input.args, '--environment'))
274
+ });
275
+ const profiles = await collectAccessibleProfiles({
276
+ auth,
277
+ accessContext: whoami.accessContext,
278
+ explicitProfile: resolveProfileFlag(input.args)
279
+ });
280
+ const entries = await Promise.all(profiles.map(async (profile) => ({
281
+ ...(await auth.client.auth.listWorkspaceMembers({
282
+ workspaceSlug: profile.workspaceSlug
283
+ })),
284
+ profileAccess: summarizeProfileCapabilities({
285
+ accessContext: whoami.accessContext,
286
+ workspaceSlug: profile.workspaceSlug,
287
+ workspaceName: profile.workspaceName,
288
+ membershipStatus: profile.membershipStatus,
289
+ role: profile.role
290
+ })
291
+ })));
292
+ (0, render_1.renderOutput)({
293
+ authenticated: whoami.accessContext?.authenticated ?? false,
294
+ operator: whoami.accessContext?.operator ?? null,
295
+ isSystemAdmin: whoami.accessContext?.isSystemAdmin ?? false,
296
+ profiles: entries
297
+ }, input.outputMode);
298
+ }
299
+ function formatAuthLoginSuccess(input) {
300
+ const lines = [
301
+ `Authenticated as ${input.accessContext?.operator?.email ?? input.state.operatorEmail ?? 'unknown account'}`,
302
+ `Target: ${input.state.environment} (${input.state.baseUrl})`,
303
+ `Session store: ${input.context.authFilePath}`
304
+ ];
305
+ if (input.accessContext?.isSystemAdmin) {
306
+ lines.push('Platform access: system admin');
307
+ }
308
+ const memberships = input.accessContext?.memberships
309
+ .map((membership) => describeWorkspaceAccess({
310
+ workspaceSlug: membership.workspaceSlug,
311
+ workspaceName: membership.workspaceName,
312
+ role: membership.role,
313
+ isSystemAdmin: input.accessContext?.isSystemAdmin ?? false
314
+ })) ?? [];
315
+ if (memberships.length > 0) {
316
+ lines.push('Workspace access:');
317
+ lines.push(...memberships.map((entry) => `- ${entry}`));
318
+ }
319
+ else if (!(input.accessContext?.isSystemAdmin ?? false)) {
320
+ lines.push('Workspace access: none');
321
+ }
322
+ return lines.join('\n');
323
+ }
172
324
  function parseEmployeeRole(value, fallback = 'operator') {
173
325
  const normalized = value?.trim().toLowerCase();
174
326
  if (normalized === 'admin' || normalized === 'workspace_admin') {
@@ -183,7 +335,7 @@ function parseEmployeeRole(value, fallback = 'operator') {
183
335
  throw new Error(`Unsupported role ${value}. Use employee|admin|observer.`);
184
336
  }
185
337
  function resolveAuthBusinessProfileSlug(context, args) {
186
- const explicit = flagValue(args, '--business-profile-slug') ?? flagValue(args, '--business-profile');
338
+ const explicit = resolveProfileFlag(args);
187
339
  if (explicit) {
188
340
  return explicit;
189
341
  }
@@ -191,7 +343,7 @@ function resolveAuthBusinessProfileSlug(context, args) {
191
343
  if (manifestBusinessSlug) {
192
344
  return manifestBusinessSlug;
193
345
  }
194
- throw new Error('This command requires --business-profile-slug when you are not inside a business-profile repo.');
346
+ throw new Error('This command requires --profile when you are not inside a business-profile repo.');
195
347
  }
196
348
  function parseDeployActions(args) {
197
349
  const values = [
@@ -207,12 +359,19 @@ function parseDeployActions(args) {
207
359
  }
208
360
  async function handleProfileCommand(input) {
209
361
  const { args, subcommand, context, outputMode } = input;
362
+ const repoContext = 'manifest' in context ? context : null;
210
363
  if (subcommand === 'validate') {
211
- (0, render_1.renderOutput)(await (0, project_1.validateProfileProject)(context), outputMode);
364
+ if (!repoContext) {
365
+ throw new Error('profile validate requires a business-profile repo checkout.');
366
+ }
367
+ (0, render_1.renderOutput)(await (0, project_1.validateProfileProject)(repoContext), outputMode);
212
368
  return;
213
369
  }
214
370
  if (subcommand === 'upload') {
215
- const prepared = await (0, project_1.buildProfileBundleForUpload)(context);
371
+ if (!repoContext) {
372
+ throw new Error('profile upload requires a business-profile repo checkout.');
373
+ }
374
+ const prepared = await (0, project_1.buildProfileBundleForUpload)(repoContext);
216
375
  const auth = await (0, auth_1.resolveAuthenticatedClient)({
217
376
  context
218
377
  });
@@ -227,8 +386,8 @@ async function handleProfileCommand(input) {
227
386
  (0, render_1.renderOutput)({
228
387
  ...result,
229
388
  authMode: auth.authMode,
230
- projectRoot: context.root.rootDir,
231
- manifestPath: context.root.manifestPath,
389
+ projectRoot: repoContext.root.rootDir,
390
+ manifestPath: repoContext.root.manifestPath,
232
391
  sourceRepositoryUrl: prepared.sourceRepositoryUrl,
233
392
  sourceCommitSha: prepared.sourceCommitSha,
234
393
  resolvedBaseUrl: context.baseUrl,
@@ -238,15 +397,18 @@ async function handleProfileCommand(input) {
238
397
  return;
239
398
  }
240
399
  if (subcommand === 'download') {
400
+ if (!repoContext) {
401
+ throw new Error('profile download requires a business-profile repo checkout.');
402
+ }
241
403
  const auth = await (0, auth_1.resolveAuthenticatedClient)({
242
404
  context
243
405
  });
244
- const businessProfileSlug = resolveProfileSlug(args, context.manifest.business_slug);
406
+ const businessProfileSlug = resolveProfileSlug(args, repoContext.manifest.business_slug);
245
407
  const result = await auth.client.businessProfiles.downloadBundle({
246
408
  businessProfileSlug,
247
409
  versionId: flagValue(args, '--version-id')
248
410
  });
249
- const outputDir = (0, node_path_1.resolve)(context.root.rootDir, flagValue(args, '--output-dir') ?? '.');
411
+ const outputDir = (0, node_path_1.resolve)(repoContext.root.rootDir, flagValue(args, '--output-dir') ?? '.');
250
412
  await (0, project_1.writeDownloadedProfileBundle)({
251
413
  rootDir: outputDir,
252
414
  bundle: result.bundle
@@ -260,6 +422,9 @@ async function handleProfileCommand(input) {
260
422
  return;
261
423
  }
262
424
  if (subcommand === 'activate') {
425
+ if (!repoContext) {
426
+ throw new Error('profile activate requires a business-profile repo checkout.');
427
+ }
263
428
  const specifier = flagValue(args, '--version') ?? flagValue(args, '--version-id');
264
429
  if (!specifier) {
265
430
  throw new Error('profile activate requires --version <latest|active|version-id>.');
@@ -267,7 +432,7 @@ async function handleProfileCommand(input) {
267
432
  const auth = await (0, auth_1.resolveAuthenticatedClient)({
268
433
  context
269
434
  });
270
- const businessProfileSlug = resolveProfileSlug(args, context.manifest.business_slug);
435
+ const businessProfileSlug = resolveProfileSlug(args, repoContext.manifest.business_slug);
271
436
  const resolvedVersion = await resolveVersionId({
272
437
  client: auth.client,
273
438
  businessProfileSlug,
@@ -285,10 +450,13 @@ async function handleProfileCommand(input) {
285
450
  return;
286
451
  }
287
452
  if (subcommand === 'status') {
453
+ if (!repoContext) {
454
+ throw new Error('profile status requires a business-profile repo checkout.');
455
+ }
288
456
  const auth = await (0, auth_1.resolveAuthenticatedClient)({
289
457
  context
290
458
  });
291
- const businessProfileSlug = resolveProfileSlug(args, context.manifest.business_slug);
459
+ const businessProfileSlug = resolveProfileSlug(args, repoContext.manifest.business_slug);
292
460
  const [profile, versions, members] = await Promise.all([
293
461
  auth.client.businessProfiles.get({
294
462
  businessProfileSlug
@@ -310,10 +478,13 @@ async function handleProfileCommand(input) {
310
478
  return;
311
479
  }
312
480
  if (subcommand === 'versions') {
481
+ if (!repoContext) {
482
+ throw new Error('profile versions requires a business-profile repo checkout.');
483
+ }
313
484
  const auth = await (0, auth_1.resolveAuthenticatedClient)({
314
485
  context
315
486
  });
316
- const businessProfileSlug = resolveProfileSlug(args, context.manifest.business_slug);
487
+ const businessProfileSlug = resolveProfileSlug(args, repoContext.manifest.business_slug);
317
488
  const versions = await auth.client.businessProfiles.listVersions({
318
489
  businessProfileSlug
319
490
  });
@@ -324,15 +495,11 @@ async function handleProfileCommand(input) {
324
495
  return;
325
496
  }
326
497
  if (subcommand === 'access') {
327
- const whoami = await (0, auth_1.resolveWhoAmI)(context);
328
- (0, render_1.renderOutput)({
329
- session: whoami.session,
330
- authenticated: whoami.accessContext?.authenticated ?? false,
331
- operator: whoami.accessContext?.operator ?? null,
332
- isSystemAdmin: whoami.accessContext?.isSystemAdmin ?? false,
333
- businessProfileSlug: context.manifest.business_slug,
334
- access: summarizeWorkspaceCapabilities(whoami.accessContext, context.manifest.business_slug)
335
- }, outputMode);
498
+ await renderProfileAccessView({
499
+ args,
500
+ context,
501
+ outputMode
502
+ });
336
503
  return;
337
504
  }
338
505
  if (subcommand === 'members') {
@@ -340,15 +507,20 @@ async function handleProfileCommand(input) {
340
507
  context
341
508
  });
342
509
  const memberAction = args[2] ?? null;
343
- const workspaceSlug = context.manifest.business_slug;
344
510
  if (memberAction === 'list') {
345
- (0, render_1.renderOutput)(await auth.client.auth.listWorkspaceMembers({
346
- workspaceSlug
347
- }), outputMode);
511
+ await renderProfileMembersList({
512
+ args,
513
+ context,
514
+ outputMode
515
+ });
348
516
  return;
349
517
  }
350
518
  if (memberAction === 'add') {
519
+ const workspaceSlug = resolveProfileFlag(args);
351
520
  const email = flagValue(args, '--email');
521
+ if (!workspaceSlug) {
522
+ throw new Error('profile members add requires --profile.');
523
+ }
352
524
  if (!email) {
353
525
  throw new Error('profile members add requires --email.');
354
526
  }
@@ -361,7 +533,11 @@ async function handleProfileCommand(input) {
361
533
  return;
362
534
  }
363
535
  if (memberAction === 'promote') {
536
+ const workspaceSlug = resolveProfileFlag(args);
364
537
  const email = flagValue(args, '--email');
538
+ if (!workspaceSlug) {
539
+ throw new Error('profile members promote requires --profile.');
540
+ }
365
541
  if (!email) {
366
542
  throw new Error('profile members promote requires --email.');
367
543
  }
@@ -377,6 +553,49 @@ async function handleProfileCommand(input) {
377
553
  }
378
554
  throw new Error(`Unknown profile command: ${subcommand ?? '(missing)'}.`);
379
555
  }
556
+ async function handleConversationCommand(input) {
557
+ const { args, subcommand, context, outputMode } = input;
558
+ if (subcommand === 'export') {
559
+ const conversationId = flagValue(args, '--conversation-id');
560
+ const channelKind = flagValue(args, '--channel-kind');
561
+ const threadRef = flagValue(args, '--thread-ref');
562
+ const result = await (0, conversations_1.exportConversationTranscript)({
563
+ context,
564
+ lookup: {
565
+ ...(conversationId ? { conversationId } : {}),
566
+ ...(channelKind ? { channelKind } : {}),
567
+ ...(threadRef ? { threadRef } : {})
568
+ },
569
+ outputFile: flagValue(args, '--output-file'),
570
+ transcriptId: flagValue(args, '--transcript-id'),
571
+ familyId: flagValue(args, '--family-id')
572
+ });
573
+ (0, render_1.renderOutput)(result, outputMode);
574
+ return;
575
+ }
576
+ if (subcommand === 'replay') {
577
+ const transcriptFile = flagValue(args, '--file');
578
+ if (!transcriptFile) {
579
+ throw new Error('conversation replay requires --file <canonical-transcript.yaml>.');
580
+ }
581
+ const channelKind = flagValue(args, '--channel-kind');
582
+ const result = await (0, conversations_1.replayConversationTranscript)({
583
+ context,
584
+ transcriptFile,
585
+ outputFile: flagValue(args, '--output-file'),
586
+ transcriptId: flagValue(args, '--transcript-id'),
587
+ familyId: flagValue(args, '--family-id'),
588
+ channelKind,
589
+ channelAccountId: flagValue(args, '--channel-account-id'),
590
+ threadRef: flagValue(args, '--thread-ref'),
591
+ externalUserId: flagValue(args, '--external-user-id'),
592
+ displayName: flagValue(args, '--display-name')
593
+ });
594
+ (0, render_1.renderOutput)(result, outputMode);
595
+ return;
596
+ }
597
+ throw new Error(`Unknown conversation command: ${subcommand ?? '(missing)'}.`);
598
+ }
380
599
  async function handleTelegramIntegrationsCommand(input) {
381
600
  const { args, action, context, outputMode } = input;
382
601
  if (action === 'upload') {
@@ -1185,18 +1404,30 @@ async function handleAuthCommand(input) {
1185
1404
  allowAnyActiveFallback: !explicitTarget
1186
1405
  };
1187
1406
  if (subcommand === 'login') {
1407
+ const positionalEmail = (() => {
1408
+ const candidate = args[2] ?? null;
1409
+ return candidate && !candidate.startsWith('-') ? candidate : null;
1410
+ })();
1188
1411
  const result = await (0, auth_1.loginAdminSession)({
1189
1412
  context,
1190
- email: flagValue(args, '--email'),
1413
+ email: flagValue(args, '--email') ?? positionalEmail,
1191
1414
  nextPath: flagValue(args, '--next-path'),
1192
1415
  timeoutSeconds: Number(flagValue(args, '--timeout-seconds') ?? '600'),
1193
- openBrowser: !hasFlag(args, '--no-browser'),
1416
+ openBrowser: positionalEmail || flagValue(args, '--email') ? false : !hasFlag(args, '--no-browser'),
1194
1417
  onStatus: outputMode === 'pretty'
1195
1418
  ? (message) => {
1196
1419
  process.stdout.write(`${message}\n`);
1197
1420
  }
1198
1421
  : null
1199
1422
  });
1423
+ if (outputMode === 'pretty') {
1424
+ (0, render_1.renderOutput)(formatAuthLoginSuccess({
1425
+ context,
1426
+ state: result.state,
1427
+ accessContext: result.accessContext
1428
+ }), outputMode);
1429
+ return;
1430
+ }
1200
1431
  (0, render_1.renderOutput)({
1201
1432
  authMode: 'employee_login',
1202
1433
  state: result.state,
@@ -1260,9 +1491,7 @@ async function handleAuthCommand(input) {
1260
1491
  authMode: auth.authMode,
1261
1492
  ...(await auth.client.auth.listSessions({
1262
1493
  source: flagValue(args, '--source') ?? null,
1263
- businessProfileSlug: flagValue(args, '--business-profile-slug') ??
1264
- flagValue(args, '--business-profile') ??
1265
- null,
1494
+ businessProfileSlug: resolveProfileFlag(args),
1266
1495
  includeRevoked: hasFlag(args, '--include-revoked')
1267
1496
  }))
1268
1497
  }, outputMode);
@@ -1339,14 +1568,47 @@ async function main() {
1339
1568
  await handleAuthCommand({ args, subcommand, context: authContext, outputMode });
1340
1569
  return;
1341
1570
  }
1571
+ if (command === 'profile') {
1572
+ const authOnlyProfileCommand = subcommand === 'access' ||
1573
+ (subcommand === 'members' &&
1574
+ (action === 'list' || action === 'add' || action === 'promote'));
1575
+ if (authOnlyProfileCommand) {
1576
+ try {
1577
+ const repoAwareContext = await (0, context_1.resolveSaAdminContext)({
1578
+ ...(explicitCwd ? { cwd: explicitCwd } : {}),
1579
+ baseUrl: flagValue(args, '--base-url'),
1580
+ environment: flagValue(args, '--environment'),
1581
+ envFiles: flagValues(args, '--env-file')
1582
+ });
1583
+ await handleProfileCommand({ args, subcommand, context: repoAwareContext, outputMode });
1584
+ return;
1585
+ }
1586
+ catch {
1587
+ const authContext = await (0, context_1.resolveSaAdminAuthContext)({
1588
+ baseUrl: flagValue(args, '--base-url'),
1589
+ environment: flagValue(args, '--environment')
1590
+ });
1591
+ await handleProfileCommand({ args, subcommand, context: authContext, outputMode });
1592
+ return;
1593
+ }
1594
+ }
1595
+ const context = await (0, context_1.resolveSaAdminContext)({
1596
+ ...(explicitCwd ? { cwd: explicitCwd } : {}),
1597
+ baseUrl: flagValue(args, '--base-url'),
1598
+ environment: flagValue(args, '--environment'),
1599
+ envFiles: flagValues(args, '--env-file')
1600
+ });
1601
+ await handleProfileCommand({ args, subcommand, context, outputMode });
1602
+ return;
1603
+ }
1342
1604
  const context = await (0, context_1.resolveSaAdminContext)({
1343
1605
  ...(explicitCwd ? { cwd: explicitCwd } : {}),
1344
1606
  baseUrl: flagValue(args, '--base-url'),
1345
1607
  environment: flagValue(args, '--environment'),
1346
1608
  envFiles: flagValues(args, '--env-file')
1347
1609
  });
1348
- if (command === 'profile') {
1349
- await handleProfileCommand({ args, subcommand, context, outputMode });
1610
+ if (command === 'conversation') {
1611
+ await handleConversationCommand({ args, subcommand, context, outputMode });
1350
1612
  return;
1351
1613
  }
1352
1614
  if (command === 'db') {