dsh-bailinghub 0.2.0 → 0.3.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/lib/runtime.js CHANGED
@@ -31,6 +31,7 @@ const INVOCATION_STATES = new Set([
31
31
  const DEFAULT_RESUME_POLL_INTERVAL_MILLISECONDS = 2_000
32
32
  const DEFAULT_RESUME_MAX_WAIT_MILLISECONDS = 120_000
33
33
  const DEFAULT_RESUME_MAX_ATTEMPTS = 60
34
+ export const TESTED_DSH_HOST_RELEASES = Object.freeze(['0.1.0-rc.7', '0.1.1-rc.2'])
34
35
  const SENSITIVE_TEXT = [
35
36
  /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
36
37
  /([?&](?:access_token|code|credential|secret|token)=)[^&#\s]+/gi,
@@ -63,6 +64,116 @@ function commandText(value) {
63
64
  return text || '(no additional details)'
64
65
  }
65
66
 
67
+ function loginCommandText(value) {
68
+ if (
69
+ value === null ||
70
+ typeof value !== 'object' ||
71
+ Array.isArray(value) ||
72
+ value.state !== 'authorized' ||
73
+ value.cleanupRequired !== true
74
+ ) {
75
+ return commandText(value)
76
+ }
77
+
78
+ const cleanupNames = Array.isArray(value.cleanupConnections)
79
+ ? value.cleanupConnections
80
+ .map((connection) => redactText(
81
+ connection?.connectionName ?? connection?.connectionKey ?? '',
82
+ ).replace(/\s+/gu, ' ').trim())
83
+ .filter(Boolean)
84
+ : []
85
+ const pending = cleanupNames.length > 0
86
+ ? ` Pending cleanup: ${cleanupNames.join(', ')}.`
87
+ : ''
88
+ return [
89
+ 'Authorization succeeded. The selected connection remains authorized.',
90
+ `WARNING: an existing connection still requires cleanup.${pending}`,
91
+ 'Do not authorize again. Use /bailinghub connections list, then /bailinghub connections remove <name-or-key> to retry cleanup.',
92
+ commandText(value),
93
+ ].join('\n')
94
+ }
95
+
96
+ export function parseCommandArguments(value) {
97
+ const tokens = []
98
+ let current = ''
99
+ let quote = ''
100
+ let escaped = false
101
+ let started = false
102
+ for (const character of String(value ?? '')) {
103
+ if (escaped) {
104
+ current += character
105
+ escaped = false
106
+ started = true
107
+ continue
108
+ }
109
+ if (character === '\\') {
110
+ escaped = true
111
+ started = true
112
+ continue
113
+ }
114
+ if (quote) {
115
+ if (character === quote) quote = ''
116
+ else current += character
117
+ continue
118
+ }
119
+ if (character === '"' || character === "'") {
120
+ quote = character
121
+ started = true
122
+ continue
123
+ }
124
+ if (/\s/u.test(character)) {
125
+ if (started) {
126
+ tokens.push(current)
127
+ current = ''
128
+ started = false
129
+ }
130
+ continue
131
+ }
132
+ current += character
133
+ started = true
134
+ }
135
+ if (quote || escaped) throw new TypeError('BailingHub command contains an unfinished quote or escape')
136
+ if (started) tokens.push(current)
137
+ return tokens
138
+ }
139
+
140
+ function inspectHostContract(ctx) {
141
+ const capabilities = {
142
+ events: typeof ctx?.on === 'function',
143
+ commands: typeof ctx?.commands?.register === 'function',
144
+ effects: typeof ctx?.effect === 'function',
145
+ services: typeof ctx?.provide === 'function',
146
+ }
147
+ return {
148
+ ok: Object.values(capabilities).every(Boolean),
149
+ capabilities,
150
+ host_contract_tested_releases: [...TESTED_DSH_HOST_RELEASES],
151
+ }
152
+ }
153
+
154
+ function doctorText(report) {
155
+ const label = (check) => (check?.ok ? 'PASS' : check?.skipped ? 'SKIP' : 'FAIL')
156
+ const configuration = report.checks.configuration
157
+ const authorization = report.checks.agent_session
158
+ const workspace = report.checks.workspace
159
+ const lines = [
160
+ 'BailingHub Agent Client doctor',
161
+ `Overall: ${report.ok ? 'PASS' : 'FAIL'}`,
162
+ `- Configuration: ${label(configuration)}`,
163
+ `- DSH host contract: ${label(report.checks.dsh_host)} (host-contract tests: ${TESTED_DSH_HOST_RELEASES.join(', ')})`,
164
+ `- Agent Client SDK: ${label(report.checks.sdk)}`,
165
+ '- Connection selector: connectionName is a local user-selected label, not a business identity claim',
166
+ '- Business identity: the single business authorization page handles sign-in, account switching, and tenant selection',
167
+ `- Authorization: ${label(authorization)} (${authorization.state})`,
168
+ `- Workspace: ${label(workspace)}${Number.isSafeInteger(workspace.authorized_count) ? ` (${workspace.authorized_count} authorized)` : ''}`,
169
+ ]
170
+ if (configuration.invalid?.length) {
171
+ lines.push(`- Invalid configuration fields: ${configuration.invalid.join(', ')}`)
172
+ }
173
+ if (report.next_action) lines.push(`Next action: ${report.next_action}`)
174
+ return lines.join('\n')
175
+ }
176
+
66
177
  function redactText(value) {
67
178
  let text = String(value ?? '')
68
179
  for (const pattern of SENSITIVE_TEXT) {
@@ -249,6 +360,46 @@ export function normalizeConfig(config = {}) {
249
360
  return { ...normalized, valid: invalid.length === 0, invalid }
250
361
  }
251
362
 
363
+ function currentConnectionDefaults(value) {
364
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
365
+
366
+ const currentConnectionKey = typeof value.currentConnectionKey === 'string'
367
+ ? value.currentConnectionKey.trim()
368
+ : ''
369
+ if (!currentConnectionKey || currentConnectionKey.length > 128 || !Array.isArray(value.connections)) {
370
+ return undefined
371
+ }
372
+
373
+ const selected = value.connections.find((entry) =>
374
+ entry !== null &&
375
+ typeof entry === 'object' &&
376
+ !Array.isArray(entry) &&
377
+ typeof entry.connectionKey === 'string' &&
378
+ entry.connectionKey.trim() === currentConnectionKey,
379
+ )
380
+ if (!selected) return undefined
381
+
382
+ const connectionName = typeof selected.connectionName === 'string'
383
+ ? selected.connectionName.trim()
384
+ : ''
385
+ const normalized = normalizeConfig({
386
+ hubUrl: selected.hubUrl,
387
+ clientAppId: selected.clientAppId,
388
+ workspace: selected.workspace,
389
+ connectionName: connectionName || currentConnectionKey,
390
+ })
391
+ return normalized.valid ? normalized : undefined
392
+ }
393
+
394
+ function connectionRegistryIsEmpty(value) {
395
+ return value !== null &&
396
+ typeof value === 'object' &&
397
+ !Array.isArray(value) &&
398
+ Array.isArray(value.connections) &&
399
+ value.connections.length === 0 &&
400
+ (value.currentConnectionKey === null || value.currentConnectionKey === undefined)
401
+ }
402
+
252
403
  function visibleText(message) {
253
404
  if (!Array.isArray(message?.content)) return ''
254
405
  return message.content
@@ -591,7 +742,14 @@ export class BailingHubAgentClientRuntime {
591
742
  this.config = normalizeConfig(config)
592
743
  this.getTransport = getTransport
593
744
  this.recovery = normalizeRecoveryOptions(recoveryOptions)
745
+ this.hostContract = inspectHostContract(ctx)
594
746
  this.defaultWorkspace = this.config.workspace
747
+ this.defaultConnectionName = this.config.connectionName
748
+ this.defaultHubUrl = this.config.hubUrl
749
+ this.defaultClientAppId = this.config.clientAppId
750
+ this.connectionReady = this.config.valid
751
+ this.connectionBootstrap = undefined
752
+ this.pendingClaimsByAgent = new WeakMap()
595
753
  this.states = new Set()
596
754
  this.statesByAgent = new WeakMap()
597
755
  this.statesBySessionId = new Map()
@@ -607,8 +765,8 @@ export class BailingHubAgentClientRuntime {
607
765
  this.ctx.on('session/disposed', (session) => void this.disposeSession(session.id))
608
766
  const registerCommand = () => this.ctx.commands.register({
609
767
  name: 'bailinghub',
610
- description: 'authorize, inspect, or resync the BailingHub Agent Client connection',
611
- input: { hint: 'login|status|logout|workspaces|use <workspace>|sync' },
768
+ description: 'authorize, diagnose, inspect, switch, or resync BailingHub Agent Client connections',
769
+ input: { hint: 'doctor|connections list|add|use|remove|login|status|logout|workspaces|use <workspace>|sync' },
612
770
  handler: (invocation) => this.handleCommand(invocation),
613
771
  })
614
772
  if (typeof this.ctx.effect === 'function') this.ctx.effect(registerCommand)
@@ -618,12 +776,47 @@ export class BailingHubAgentClientRuntime {
618
776
  }
619
777
 
620
778
  async handleCommand(invocation) {
779
+ await this.restoreCurrentConnection()
621
780
  const input = invocation.rawInput.trim()
622
- const [action, ...rest] = input ? input.split(/\s+/u) : ['status']
623
781
  try {
782
+ const [action = 'status', ...rest] = parseCommandArguments(input)
624
783
  switch (action) {
784
+ case 'connections': {
785
+ const [operation = 'list', ...parameters] = rest
786
+ if (operation === 'list' && parameters.length === 0) {
787
+ return { kind: 'success', text: commandText(await this.connectionsList()) }
788
+ }
789
+ if (operation === 'add' && parameters.length === 4) {
790
+ const [connectionName, hubUrl, clientAppId, workspace] = parameters
791
+ const result = await this.connectionsAdd({ connectionName, hubUrl, clientAppId, workspace })
792
+ const action = result?.state === 'selected' ? 'selected' : 'registered and selected'
793
+ return {
794
+ kind: 'success',
795
+ text: `Connection instance ${action} for new sessions: ${connectionName}\n${commandText(result)}`,
796
+ }
797
+ }
798
+ if (operation === 'use' && parameters.length === 1) {
799
+ const result = await this.connectionsUse(parameters[0])
800
+ return {
801
+ kind: 'success',
802
+ text: `Connection selected for new sessions: ${parameters[0]}\n${commandText(result)}`,
803
+ }
804
+ }
805
+ if (operation === 'remove' && parameters.length === 1) {
806
+ const result = await this.connectionsRemove(parameters[0])
807
+ return { kind: 'success', text: commandText(result) }
808
+ }
809
+ return {
810
+ kind: 'error',
811
+ text: 'Usage: /bailinghub connections list|add <name> <hub-url> <client-app-id> <workspace>|use <name-or-key>|remove <name-or-key>',
812
+ }
813
+ }
814
+ case 'doctor': {
815
+ const result = await this.doctor()
816
+ return { kind: result.ok ? 'success' : 'error', text: doctorText(result) }
817
+ }
625
818
  case 'login':
626
- return { kind: 'success', text: commandText(await this.login()) }
819
+ return { kind: 'success', text: loginCommandText(await this.login()) }
627
820
  case 'status':
628
821
  return { kind: 'success', text: commandText(await this.status()) }
629
822
  case 'logout':
@@ -648,7 +841,7 @@ export class BailingHubAgentClientRuntime {
648
841
  default:
649
842
  return {
650
843
  kind: 'error',
651
- text: 'Usage: /bailinghub login|status|logout|workspaces|use <workspace>|sync',
844
+ text: 'Usage: /bailinghub doctor|connections list|add|use|remove|login|status|logout|workspaces|use <workspace>|sync',
652
845
  }
653
846
  }
654
847
  } catch (error) {
@@ -663,8 +856,13 @@ export class BailingHubAgentClientRuntime {
663
856
  let state = this.statesByAgent.get(agent)
664
857
  if (state) return state
665
858
 
666
- state = createState(agent, this.defaultWorkspace, this.config.connectionName)
667
- if (!this.config.valid) {
859
+ state = createState(agent, this.defaultWorkspace, this.defaultConnectionName)
860
+ const pendingClaims = this.pendingClaimsByAgent.get(agent)
861
+ if (pendingClaims) {
862
+ state.pendingClaims = pendingClaims
863
+ this.pendingClaimsByAgent.delete(agent)
864
+ }
865
+ if (!this.connectionReady) {
668
866
  state.status = {
669
867
  state: 'unconfigured',
670
868
  missing: [...this.config.invalid],
@@ -678,17 +876,23 @@ export class BailingHubAgentClientRuntime {
678
876
 
679
877
  onInboxClaimed({ agent, message, turn }) {
680
878
  if (message?.source?.kind !== 'user') return
681
- const state = this.stateFor(agent)
879
+ const state = this.statesByAgent.get(agent)
880
+ let pendingClaims = state?.pendingClaims ?? this.pendingClaimsByAgent.get(agent)
881
+ if (!pendingClaims) {
882
+ pendingClaims = new Map()
883
+ this.pendingClaimsByAgent.set(agent, pendingClaims)
884
+ }
885
+ const sessionId = String(agent.session.id)
682
886
  const rawMessageId = String(
683
- message.id ?? messageAlias('source', state.sessionId, String(turn), visibleText(message)),
887
+ message.id ?? messageAlias('source', sessionId, String(turn), visibleText(message)),
684
888
  )
685
889
  const claim = {
686
- id: messageAlias('user', state.sessionId, String(turn), rawMessageId),
890
+ id: messageAlias('user', sessionId, String(turn), rawMessageId),
687
891
  text: visibleText(message),
688
892
  }
689
- const claims = state.pendingClaims.get(turn) ?? []
893
+ const claims = pendingClaims.get(turn) ?? []
690
894
  claims.push(claim)
691
- state.pendingClaims.set(turn, claims)
895
+ pendingClaims.set(turn, claims)
692
896
  }
693
897
 
694
898
  async onAssemble(assembly, context, next) {
@@ -696,6 +900,7 @@ export class BailingHubAgentClientRuntime {
696
900
  const agent = context?.agent
697
901
  if (!agent) return base
698
902
 
903
+ await this.restoreCurrentConnection()
699
904
  const state = this.stateFor(agent)
700
905
  const turn = Math.max(...state.pendingClaims.keys(), -1)
701
906
  let removedNames = new Set()
@@ -712,7 +917,7 @@ export class BailingHubAgentClientRuntime {
712
917
  await this.clearActiveTools(state)
713
918
  if (state.currentRun?.status === 'active') {
714
919
  state.currentRun.status = 'degraded'
715
- state.currentRun.degradedReason = 'DSH Code Mode is not supported by this candidate'
920
+ state.currentRun.degradedReason = 'DSH Code Mode is not supported by this plugin version'
716
921
  }
717
922
  state.status = { state: 'degraded', reason: 'unsupported DSH tool presentation mode' }
718
923
  }
@@ -738,7 +943,7 @@ export class BailingHubAgentClientRuntime {
738
943
  const removedNames = new Set(state.knownToolNames)
739
944
  await this.clearActiveTools(state)
740
945
 
741
- if (!this.config.valid) {
946
+ if (!this.connectionReady) {
742
947
  const run = { turn, status: 'unconfigured', degradedReason: 'configuration required' }
743
948
  state.runs.set(turn, run)
744
949
  state.currentRun = run
@@ -1101,7 +1306,7 @@ export class BailingHubAgentClientRuntime {
1101
1306
 
1102
1307
  if (!run || run.status !== 'active') {
1103
1308
  tools = tools.filter((tool) => !state.knownToolNames.has(tool.name))
1104
- const reason = run?.degradedReason ?? (this.config.valid ? 'no active user turn' : 'configuration required')
1309
+ const reason = run?.degradedReason ?? (this.connectionReady ? 'no active user turn' : 'configuration required')
1105
1310
  sections = replaceNamed(sections, degradedSection(reason))
1106
1311
  return { ...base, sections, contexts, tools }
1107
1312
  }
@@ -1234,34 +1439,162 @@ export class BailingHubAgentClientRuntime {
1234
1439
  return { synced, pending }
1235
1440
  }
1236
1441
 
1442
+ async restoreCurrentConnection() {
1443
+ if (!this.connectionBootstrap) {
1444
+ this.connectionBootstrap = this.restoreCurrentConnectionOnce()
1445
+ }
1446
+ return this.connectionBootstrap
1447
+ }
1448
+
1449
+ async restoreCurrentConnectionOnce() {
1450
+ if (!this.config.valid) return false
1451
+
1452
+ try {
1453
+ const selected = currentConnectionDefaults(await this.connectionsList())
1454
+ if (!selected) return false
1455
+ this.applyConnectionDefaults(selected)
1456
+ return true
1457
+ } catch {
1458
+ return false
1459
+ }
1460
+ }
1461
+
1462
+ applyConnectionDefaults(selected) {
1463
+ this.defaultConnectionName = selected.connectionName
1464
+ this.defaultWorkspace = selected.workspace
1465
+ this.defaultHubUrl = selected.hubUrl
1466
+ this.defaultClientAppId = selected.clientAppId
1467
+ this.connectionReady = true
1468
+ }
1469
+
1237
1470
  async login(options = {}) {
1238
- return this.callConnection('login', {
1239
- hubUrl: this.config.hubUrl,
1240
- clientAppId: this.config.clientAppId,
1471
+ const result = await this.callConnection('login', {
1472
+ hubUrl: this.defaultHubUrl,
1473
+ clientAppId: this.defaultClientAppId,
1241
1474
  workspace: options.workspace ?? this.defaultWorkspace,
1242
1475
  route: options.workspace ?? this.defaultWorkspace,
1243
- connectionName: options.connectionName ?? this.config.connectionName,
1476
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1244
1477
  })
1478
+ const selectedConnectionName = typeof result?.connectionName === 'string'
1479
+ ? result.connectionName.trim()
1480
+ : ''
1481
+ if (
1482
+ result?.state === 'authorized' &&
1483
+ selectedConnectionName &&
1484
+ selectedConnectionName.length <= 128
1485
+ ) {
1486
+ this.defaultConnectionName = selectedConnectionName
1487
+ if (typeof result.workspace === 'string' && WORKSPACE.test(result.workspace)) {
1488
+ this.defaultWorkspace = result.workspace
1489
+ }
1490
+ }
1491
+ return result
1492
+ }
1493
+
1494
+ async doctor(options = {}) {
1495
+ const report = {
1496
+ schema: 'bailing.agent-client-doctor.v1',
1497
+ ok: false,
1498
+ checks: {
1499
+ configuration: {
1500
+ ok: this.connectionReady,
1501
+ invalid: this.connectionReady ? [] : [...this.config.invalid, 'selectedConnection'],
1502
+ },
1503
+ dsh_host: cloneJson(this.hostContract),
1504
+ sdk: { ok: false },
1505
+ agent_session: { ok: false, state: 'not_checked', skipped: true },
1506
+ workspace: { ok: false, state: 'not_checked', skipped: true },
1507
+ },
1508
+ next_action: '',
1509
+ }
1510
+ const finish = (nextAction) => sanitizePublicValue({
1511
+ ...report,
1512
+ ok: Object.values(report.checks).every((check) => check.ok === true),
1513
+ next_action: nextAction,
1514
+ })
1515
+
1516
+ if (!this.hostContract.ok) {
1517
+ return finish('Use a DSH release whose host lifecycle matches the documented compatibility matrix.')
1518
+ }
1519
+ if (!this.connectionReady) {
1520
+ return finish('Set the four public connection fields, reload the DSH profile, and run doctor again.')
1521
+ }
1522
+
1523
+ let transport
1524
+ try {
1525
+ transport = await this.getTransport()
1526
+ report.checks.sdk = { ok: true }
1527
+ } catch {
1528
+ return finish('Reinstall this plugin so its exact Agent Client SDK dependency can resolve.')
1529
+ }
1530
+
1531
+ let status
1532
+ try {
1533
+ status = sanitizePublicValue(await transport.status({
1534
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1535
+ }))
1536
+ } catch {
1537
+ report.checks.agent_session = {
1538
+ ok: false,
1539
+ state: 'unreachable',
1540
+ }
1541
+ return finish('Check Hub reachability, then run doctor again; no business action was attempted.')
1542
+ }
1543
+
1544
+ const authorizationState = typeof status?.state === 'string' ? status.state : 'unknown'
1545
+ const authorized = authorizationState === 'authorized'
1546
+ report.checks.agent_session = {
1547
+ ok: authorized,
1548
+ state: authorizationState,
1549
+ }
1550
+ if (!authorized) {
1551
+ return finish('Run /bailinghub login to authorize this Hub/client/workspace connection.')
1552
+ }
1553
+
1554
+ try {
1555
+ const result = sanitizePublicValue(await transport.workspaces({
1556
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1557
+ }))
1558
+ const workspaces = Array.isArray(result?.workspaces) ? result.workspaces : []
1559
+ const configuredAuthorized = workspaces.some((entry) => entry?.route === this.defaultWorkspace)
1560
+ const selectedMatches = status?.workspace === this.defaultWorkspace
1561
+ report.checks.workspace = {
1562
+ ok: configuredAuthorized && selectedMatches,
1563
+ state: configuredAuthorized && selectedMatches ? 'authorized' : 'mismatch',
1564
+ authorized_count: workspaces.length,
1565
+ }
1566
+ } catch {
1567
+ report.checks.workspace = {
1568
+ ok: false,
1569
+ state: 'unreachable',
1570
+ }
1571
+ return finish('Check Agent API reachability and the current authorization, then run doctor again.')
1572
+ }
1573
+
1574
+ if (!report.checks.workspace.ok) {
1575
+ return finish('Authorize the configured workspace; use a dedicated client app or workspace when credential isolation is required.')
1576
+ }
1577
+ return finish('Open a new conversation in Native Tool Mode and run a read-only acceptance request.')
1245
1578
  }
1246
1579
 
1247
1580
  async status(options = {}) {
1248
- if (!this.config.valid) {
1249
- return { state: 'unconfigured', missing: [...this.config.invalid] }
1581
+ if (!this.connectionReady) {
1582
+ return { state: 'unconfigured', missing: [...this.config.invalid, 'selectedConnection'] }
1250
1583
  }
1251
1584
  return this.callConnection('status', {
1252
- connectionName: options.connectionName ?? this.config.connectionName,
1585
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1253
1586
  })
1254
1587
  }
1255
1588
 
1256
1589
  async logout(options = {}) {
1257
1590
  return this.callConnection('logout', {
1258
- connectionName: options.connectionName ?? this.config.connectionName,
1591
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1259
1592
  })
1260
1593
  }
1261
1594
 
1262
1595
  async workspaces(options = {}) {
1263
1596
  return this.callConnection('workspaces', {
1264
- connectionName: options.connectionName ?? this.config.connectionName,
1597
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1265
1598
  })
1266
1599
  }
1267
1600
 
@@ -1281,7 +1614,7 @@ export class BailingHubAgentClientRuntime {
1281
1614
  'Finish active BailingHub runs and sync pending completions before switching workspace',
1282
1615
  )
1283
1616
  }
1284
- const connectionName = options.connectionName ?? this.config.connectionName
1617
+ const connectionName = options.connectionName ?? this.defaultConnectionName
1285
1618
  const result = await this.callConnection('use', {
1286
1619
  workspace: selected,
1287
1620
  route: selected,
@@ -1291,6 +1624,69 @@ export class BailingHubAgentClientRuntime {
1291
1624
  return result
1292
1625
  }
1293
1626
 
1627
+ async connectionsList() {
1628
+ return this.callConnection('connectionsList', {})
1629
+ }
1630
+
1631
+ async connectionsAdd(input) {
1632
+ const result = await this.callConnection('connectionsAdd', input)
1633
+ const selected = result?.connection
1634
+ if (selected?.connectionName && selected?.workspace) {
1635
+ this.defaultConnectionName = selected.connectionName
1636
+ this.defaultWorkspace = selected.workspace
1637
+ this.defaultHubUrl = selected.hubUrl
1638
+ this.defaultClientAppId = selected.clientAppId
1639
+ this.connectionReady = true
1640
+ }
1641
+ return result
1642
+ }
1643
+
1644
+ async connectionsUse(selector) {
1645
+ const connectionName = String(selector ?? '').trim()
1646
+ if (!connectionName) throw new TypeError('connection name is required')
1647
+ const result = await this.callTransport('connectionsUse', [connectionName])
1648
+ const selected = result?.connection
1649
+ if (!selected?.connectionName || !selected?.hubUrl || !selected?.clientAppId || !selected?.workspace) {
1650
+ throw new TypeError('The selected BailingHub connection metadata is incomplete')
1651
+ }
1652
+ this.defaultConnectionName = selected.connectionName
1653
+ this.defaultWorkspace = selected.workspace
1654
+ this.defaultHubUrl = selected.hubUrl
1655
+ this.defaultClientAppId = selected.clientAppId
1656
+ this.connectionReady = true
1657
+ return result
1658
+ }
1659
+
1660
+ async connectionsRemove(selector) {
1661
+ const connectionName = String(selector ?? '').trim()
1662
+ if (!connectionName) throw new TypeError('connection name is required')
1663
+ const hasBlockingRun = [...this.states].some((state) =>
1664
+ [...state.runs.values()].some((run) =>
1665
+ run.status === 'active' ||
1666
+ run.status === 'completing' ||
1667
+ run.completion?.state === 'pending' ||
1668
+ run.completion?.state === 'syncing',
1669
+ ),
1670
+ )
1671
+ if (hasBlockingRun) {
1672
+ throw new Error(
1673
+ 'Finish active BailingHub runs and sync pending completions before removing a connection',
1674
+ )
1675
+ }
1676
+ const result = await this.callTransport('connectionsRemove', [connectionName])
1677
+ const removedDefault = [result?.connectionName, result?.connectionKey]
1678
+ .some((value) => typeof value === 'string' && value.trim() === this.defaultConnectionName)
1679
+ try {
1680
+ const registry = await this.connectionsList()
1681
+ const selected = currentConnectionDefaults(registry)
1682
+ if (selected) this.applyConnectionDefaults(selected)
1683
+ else if (connectionRegistryIsEmpty(registry) || removedDefault) this.connectionReady = false
1684
+ } catch {
1685
+ if (removedDefault || result?.currentConnectionKey === null) this.connectionReady = false
1686
+ }
1687
+ return result
1688
+ }
1689
+
1294
1690
  async searchCapabilities(query, options = {}) {
1295
1691
  return this.callTransport('searchCapabilities', [
1296
1692
  {
@@ -1300,7 +1696,7 @@ export class BailingHubAgentClientRuntime {
1300
1696
  },
1301
1697
  {
1302
1698
  workspace: options.workspace ?? this.defaultWorkspace,
1303
- connectionName: options.connectionName ?? this.config.connectionName,
1699
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1304
1700
  signal: options.signal,
1305
1701
  },
1306
1702
  ])
@@ -1319,7 +1715,7 @@ export class BailingHubAgentClientRuntime {
1319
1715
  {},
1320
1716
  {
1321
1717
  workspace: options.workspace ?? this.defaultWorkspace,
1322
- connectionName: options.connectionName ?? this.config.connectionName,
1718
+ connectionName: options.connectionName ?? this.defaultConnectionName,
1323
1719
  signal: options.signal,
1324
1720
  },
1325
1721
  ),
package/lib/transport.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const DEFAULT_SDK_MODULE = 'bailinghub-mcp-server/sdk'
2
2
 
3
3
  const REQUIRED_METHODS = [
4
+ 'connectionsList',
5
+ 'connectionsAdd',
6
+ 'connectionsUse',
7
+ 'connectionsRemove',
4
8
  'login',
5
9
  'status',
6
10
  'logout',
@@ -45,7 +49,7 @@ function resolveSdkFactory(module) {
45
49
  * Lazily resolves the generic BailingHub Agent Client SDK. The SDK, rather than
46
50
  * this DSH adapter, owns browser authorization, token storage, refresh, and the
47
51
  * HTTP endpoint mapping. A missing SDK therefore degrades only the first use;
48
- * it never prevents Cordis from loading the plugin candidate.
52
+ * it never prevents Cordis from loading the plugin.
49
53
  */
50
54
  export function createLazySdkTransport(config, options = {}) {
51
55
  const importModule = options.importModule ?? ((specifier) => import(specifier))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-bailinghub",
3
- "version": "0.2.0",
4
- "description": "Native DeepSeek Harness adapter for the BailingHub Agent Client Core.",
3
+ "version": "0.3.0",
4
+ "description": "Let a local DeepSeek Harness Agent use BailingHub to query data, update records, and run permitted actions in your online store, SaaS, or other admin system.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -39,6 +39,8 @@
39
39
  "cordis.agent-client.patch.yml",
40
40
  "cordis.patch.yml",
41
41
  "docs/AGENT_CLIENT_CONTRACT.md",
42
+ "docs/GETTING_STARTED.md",
43
+ "docs/GETTING_STARTED.zh-CN.md",
42
44
  "docs/MIGRATION_VNEXT.md",
43
45
  "docs/README.zh-CN.md",
44
46
  "SECURITY.md",
@@ -52,14 +54,22 @@
52
54
  },
53
55
  "scripts": {
54
56
  "check:project": "node scripts/check-project.mjs",
57
+ "check:marketplace": "node scripts/check-marketplace.mjs",
55
58
  "check:release-tag": "node scripts/check-release-tag.mjs",
56
59
  "test": "node --test tests/*.test.mjs",
57
- "verify": "npm run check:project && npm test",
60
+ "verify": "npm run check:project && npm run check:marketplace && npm test",
58
61
  "prepack": "npm run verify"
59
62
  },
60
63
  "dependencies": {
61
64
  "@deepseek-ai/schemastery": "^3.18.1",
62
- "bailinghub-mcp-server": "0.2.0"
65
+ "bailinghub-mcp-server": "0.3.0"
66
+ },
67
+ "devDependencies": {
68
+ "@deepseek-ai/cordis": "4.0.1",
69
+ "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-scope": "0.1.1-rc.2",
71
+ "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
72
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2"
63
73
  },
64
74
  "dsh": {
65
75
  "bundle": {