optimal-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/.env.example +17 -0
  4. package/CLAUDE.md +67 -0
  5. package/COMMANDS.md +264 -0
  6. package/PUBLISH.md +70 -0
  7. package/agents/content-ops.md +2 -2
  8. package/agents/financial-ops.md +2 -2
  9. package/agents/infra-ops.md +2 -2
  10. package/apps/.gitkeep +0 -0
  11. package/bin/optimal.ts +278 -591
  12. package/docs/MIGRATION_NEEDED.md +37 -0
  13. package/docs/plans/.gitkeep +0 -0
  14. package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
  15. package/hooks/.gitkeep +0 -0
  16. package/lib/config/registry.ts +5 -4
  17. package/lib/kanban-obsidian.ts +232 -0
  18. package/lib/kanban-sync.ts +258 -0
  19. package/lib/kanban.ts +239 -0
  20. package/lib/obsidian-tasks.ts +231 -0
  21. package/package.json +5 -19
  22. package/pnpm-workspace.yaml +3 -0
  23. package/scripts/check-table.ts +24 -0
  24. package/scripts/create-tables.ts +94 -0
  25. package/scripts/migrate-kanban.sh +28 -0
  26. package/scripts/migrate-v2.ts +78 -0
  27. package/scripts/migrate.ts +79 -0
  28. package/scripts/run-migration.ts +59 -0
  29. package/scripts/seed-board.ts +203 -0
  30. package/scripts/test-kanban.ts +21 -0
  31. package/skills/audit-financials/SKILL.md +33 -0
  32. package/skills/board-create/SKILL.md +28 -0
  33. package/skills/board-update/SKILL.md +27 -0
  34. package/skills/board-view/SKILL.md +27 -0
  35. package/skills/delete-batch/SKILL.md +77 -0
  36. package/skills/deploy/SKILL.md +40 -0
  37. package/skills/diagnose-months/SKILL.md +68 -0
  38. package/skills/distribute-newsletter/SKILL.md +58 -0
  39. package/skills/export-budget/SKILL.md +44 -0
  40. package/skills/export-kpis/SKILL.md +52 -0
  41. package/skills/generate-netsuite-template/SKILL.md +51 -0
  42. package/skills/generate-newsletter/SKILL.md +53 -0
  43. package/skills/generate-newsletter-insurance/SKILL.md +59 -0
  44. package/skills/generate-social-posts/SKILL.md +67 -0
  45. package/skills/health-check/SKILL.md +42 -0
  46. package/skills/ingest-transactions/SKILL.md +51 -0
  47. package/skills/manage-cms/SKILL.md +50 -0
  48. package/skills/manage-scenarios/SKILL.md +83 -0
  49. package/skills/migrate-db/SKILL.md +79 -0
  50. package/skills/preview-newsletter/SKILL.md +50 -0
  51. package/skills/project-budget/SKILL.md +60 -0
  52. package/skills/publish-blog/SKILL.md +70 -0
  53. package/skills/publish-social-posts/SKILL.md +70 -0
  54. package/skills/rate-anomalies/SKILL.md +62 -0
  55. package/skills/scrape-ads/SKILL.md +49 -0
  56. package/skills/stamp-transactions/SKILL.md +62 -0
  57. package/skills/upload-income-statements/SKILL.md +54 -0
  58. package/skills/upload-netsuite/SKILL.md +56 -0
  59. package/skills/upload-r1/SKILL.md +45 -0
  60. package/supabase/.temp/cli-latest +1 -0
  61. package/supabase/migrations/.gitkeep +0 -0
  62. package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
  63. package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
  64. package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
  65. package/tests/config-command-smoke.test.ts +395 -0
  66. package/tests/config-registry.test.ts +173 -0
  67. package/tsconfig.json +19 -0
  68. package/agents/profiles.json +0 -5
  69. package/docs/CLI-REFERENCE.md +0 -361
  70. package/lib/assets/index.ts +0 -225
  71. package/lib/assets.ts +0 -124
  72. package/lib/auth/index.ts +0 -189
  73. package/lib/board/index.ts +0 -309
  74. package/lib/board/types.ts +0 -124
  75. package/lib/bot/claim.ts +0 -43
  76. package/lib/bot/coordinator.ts +0 -254
  77. package/lib/bot/heartbeat.ts +0 -37
  78. package/lib/bot/index.ts +0 -9
  79. package/lib/bot/protocol.ts +0 -99
  80. package/lib/bot/reporter.ts +0 -42
  81. package/lib/bot/skills.ts +0 -81
  82. package/lib/errors.ts +0 -129
  83. package/lib/format.ts +0 -120
  84. package/lib/returnpro/validate.ts +0 -154
  85. package/lib/social/meta.ts +0 -228
@@ -1,225 +0,0 @@
1
- /**
2
- * Asset tracking — manage digital infrastructure items
3
- * (domains, servers, API keys, services, repos).
4
- * Stores in the OptimalOS Supabase instance.
5
- */
6
-
7
- import { getSupabase } from '../supabase.js'
8
-
9
- // ── Types ────────────────────────────────────────────────────────────
10
-
11
- export type AssetType = 'domain' | 'server' | 'api_key' | 'service' | 'repo' | 'other'
12
- export type AssetStatus = 'active' | 'inactive' | 'expired' | 'pending'
13
-
14
- export interface Asset {
15
- id: string
16
- name: string
17
- type: AssetType
18
- status: AssetStatus
19
- metadata: Record<string, unknown>
20
- owner: string | null
21
- expires_at: string | null
22
- created_at: string
23
- updated_at: string
24
- }
25
-
26
- export interface CreateAssetInput {
27
- name: string
28
- type: AssetType
29
- status?: AssetStatus
30
- metadata?: Record<string, unknown>
31
- owner?: string
32
- expires_at?: string
33
- }
34
-
35
- export interface UpdateAssetInput {
36
- name?: string
37
- type?: AssetType
38
- status?: AssetStatus
39
- metadata?: Record<string, unknown>
40
- owner?: string
41
- expires_at?: string | null
42
- }
43
-
44
- export interface AssetFilters {
45
- type?: AssetType
46
- status?: AssetStatus
47
- owner?: string
48
- }
49
-
50
- export interface AssetUsageEvent {
51
- id: string
52
- asset_id: string
53
- event: string
54
- actor: string | null
55
- metadata: Record<string, unknown>
56
- created_at: string
57
- }
58
-
59
- // ── Supabase accessor ────────────────────────────────────────────────
60
-
61
- const sb = () => getSupabase('optimal')
62
-
63
- // ── CRUD operations ──────────────────────────────────────────────────
64
-
65
- /**
66
- * List assets, optionally filtered by type, status, or owner.
67
- */
68
- export async function listAssets(filters?: AssetFilters): Promise<Asset[]> {
69
- let query = sb().from('assets').select('*')
70
-
71
- if (filters?.type) query = query.eq('type', filters.type)
72
- if (filters?.status) query = query.eq('status', filters.status)
73
- if (filters?.owner) query = query.eq('owner', filters.owner)
74
-
75
- const { data, error } = await query.order('updated_at', { ascending: false })
76
- if (error) throw new Error(`Failed to list assets: ${error.message}`)
77
- return (data ?? []) as Asset[]
78
- }
79
-
80
- /**
81
- * Create a new asset.
82
- */
83
- export async function createAsset(input: CreateAssetInput): Promise<Asset> {
84
- const { data, error } = await sb()
85
- .from('assets')
86
- .insert({
87
- name: input.name,
88
- type: input.type,
89
- status: input.status ?? 'active',
90
- metadata: input.metadata ?? {},
91
- owner: input.owner ?? null,
92
- expires_at: input.expires_at ?? null,
93
- })
94
- .select()
95
- .single()
96
-
97
- if (error) throw new Error(`Failed to create asset: ${error.message}`)
98
- return data as Asset
99
- }
100
-
101
- /**
102
- * Update an existing asset by ID.
103
- */
104
- export async function updateAsset(id: string, updates: UpdateAssetInput): Promise<Asset> {
105
- const { data, error } = await sb()
106
- .from('assets')
107
- .update(updates)
108
- .eq('id', id)
109
- .select()
110
- .single()
111
-
112
- if (error) throw new Error(`Failed to update asset: ${error.message}`)
113
- return data as Asset
114
- }
115
-
116
- /**
117
- * Get a single asset by ID.
118
- */
119
- export async function getAsset(id: string): Promise<Asset> {
120
- const { data, error } = await sb()
121
- .from('assets')
122
- .select('*')
123
- .eq('id', id)
124
- .single()
125
-
126
- if (error) throw new Error(`Asset not found: ${error.message}`)
127
- return data as Asset
128
- }
129
-
130
- /**
131
- * Delete an asset by ID.
132
- */
133
- export async function deleteAsset(id: string): Promise<void> {
134
- const { error } = await sb()
135
- .from('assets')
136
- .delete()
137
- .eq('id', id)
138
-
139
- if (error) throw new Error(`Failed to delete asset: ${error.message}`)
140
- }
141
-
142
- // ── Usage tracking ───────────────────────────────────────────────────
143
-
144
- /**
145
- * Log a usage event against an asset.
146
- */
147
- export async function trackAssetUsage(
148
- assetId: string,
149
- event: string,
150
- actor?: string,
151
- metadata?: Record<string, unknown>,
152
- ): Promise<AssetUsageEvent> {
153
- const { data, error } = await sb()
154
- .from('asset_usage_log')
155
- .insert({
156
- asset_id: assetId,
157
- event,
158
- actor: actor ?? null,
159
- metadata: metadata ?? {},
160
- })
161
- .select()
162
- .single()
163
-
164
- if (error) throw new Error(`Failed to track usage: ${error.message}`)
165
- return data as AssetUsageEvent
166
- }
167
-
168
- /**
169
- * List usage events for a given asset.
170
- */
171
- export async function listAssetUsage(assetId: string, limit = 50): Promise<AssetUsageEvent[]> {
172
- const { data, error } = await sb()
173
- .from('asset_usage_log')
174
- .select('*')
175
- .eq('asset_id', assetId)
176
- .order('created_at', { ascending: false })
177
- .limit(limit)
178
-
179
- if (error) throw new Error(`Failed to list usage: ${error.message}`)
180
- return (data ?? []) as AssetUsageEvent[]
181
- }
182
-
183
- // ── Formatting ───────────────────────────────────────────────────────
184
-
185
- const TYPE_LABELS: Record<AssetType, string> = {
186
- domain: 'Domain',
187
- server: 'Server',
188
- api_key: 'API Key',
189
- service: 'Service',
190
- repo: 'Repo',
191
- other: 'Other',
192
- }
193
-
194
- /**
195
- * Format assets into a table string for CLI display.
196
- */
197
- export function formatAssetTable(assets: Asset[]): string {
198
- if (assets.length === 0) return 'No assets found.'
199
-
200
- const headers = ['Type', 'Status', 'Name', 'Owner', 'Expires']
201
- const rows = assets.map(a => [
202
- TYPE_LABELS[a.type] ?? a.type,
203
- a.status,
204
- a.name.length > 35 ? a.name.slice(0, 32) + '...' : a.name,
205
- a.owner ?? '-',
206
- a.expires_at ? a.expires_at.slice(0, 10) : '-',
207
- ])
208
-
209
- // Compute column widths
210
- const widths = headers.map((h, i) => {
211
- let max = h.length
212
- for (const row of rows) {
213
- if ((row[i]?.length ?? 0) > max) max = row[i].length
214
- }
215
- return max
216
- })
217
-
218
- const sep = '+-' + widths.map(w => '-'.repeat(w)).join('-+-') + '-+'
219
- const headerRow = '| ' + headers.map((h, i) => h.padEnd(widths[i])).join(' | ') + ' |'
220
- const bodyRows = rows.map(row =>
221
- '| ' + row.map((cell, i) => (cell ?? '').padEnd(widths[i])).join(' | ') + ' |'
222
- )
223
-
224
- return [sep, headerRow, sep, ...bodyRows, sep, `\nTotal: ${assets.length} assets`].join('\n')
225
- }
package/lib/assets.ts DELETED
@@ -1,124 +0,0 @@
1
- import { createClient, SupabaseClient } from '@supabase/supabase-js'
2
- import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
3
- import { join, basename } from 'node:path'
4
- import { homedir } from 'node:os'
5
- import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto'
6
-
7
- const CONFIG_DIR = join(homedir(), '.openclaw')
8
- const SKILLS_DIR = join(CONFIG_DIR, 'skills')
9
- const PLUGINS_DIR = join(CONFIG_DIR, 'plugins')
10
- const WORKSPACE_DIR = join(homedir(), '.openclaw', 'workspace')
11
-
12
- const ALGORITHM = 'aes-256-gcm'
13
-
14
- function getSupabase(): SupabaseClient {
15
- const url = process.env.OPTIMAL_SUPABASE_URL
16
- const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY
17
- if (!url || !key) throw new Error('OPTIMAL_SUPABASE_URL and OPTIMAL_SUPABASE_SERVICE_KEY required')
18
- return createClient(url, key)
19
- }
20
-
21
- function hashContent(content: string): string {
22
- return createHash('sha256').update(content).digest('hex')
23
- }
24
-
25
- export interface ScannedAsset {
26
- type: 'skill' | 'cli' | 'cron' | 'repo' | 'env' | 'ssh_key' | 'plugin'
27
- name: string
28
- version?: string
29
- path: string
30
- hash: string
31
- content?: string
32
- metadata: Record<string, unknown>
33
- }
34
-
35
- export function scanSkills(): ScannedAsset[] {
36
- const assets: ScannedAsset[] = []
37
- if (!existsSync(SKILLS_DIR)) return assets
38
- for (const dir of readdirSync(SKILLS_DIR)) {
39
- const skillPath = join(SKILLS_DIR, dir)
40
- if (!statSync(skillPath).isDirectory()) continue
41
- const skillFile = join(skillPath, 'SKILL.md')
42
- if (existsSync(skillFile)) {
43
- const content = readFileSync(skillFile, 'utf-8')
44
- assets.push({ type: 'skill', name: dir, path: skillFile, hash: hashContent(content), content, metadata: {} })
45
- }
46
- }
47
- return assets
48
- }
49
-
50
- export function scanPlugins(): ScannedAsset[] {
51
- const assets: ScannedAsset[] = []
52
- if (!existsSync(PLUGINS_DIR)) return assets
53
- for (const dir of readdirSync(PLUGINS_DIR)) {
54
- const pluginPath = join(PLUGINS_DIR, dir)
55
- if (!statSync(pluginPath).isDirectory()) continue
56
- assets.push({ type: 'plugin', name: dir, path: pluginPath, hash: hashContent(dir), metadata: {} })
57
- }
58
- return assets
59
- }
60
-
61
- export function scanCLIs(): ScannedAsset[] {
62
- const assets: ScannedAsset[] = []
63
- const knownCLIs = ['vercel', 'supabase', 'gh', 'openclaw']
64
- for (const cli of knownCLIs) {
65
- try {
66
- const { execSync } = require('node:child_process')
67
- const version = execSync(`${cli} --version 2>/dev/null || echo ""`).toString().trim()
68
- if (version) {
69
- assets.push({ type: 'cli', name: cli, version: version.slice(0, 20), path: '', hash: hashContent(version), metadata: {} })
70
- }
71
- } catch {}
72
- }
73
- return assets
74
- }
75
-
76
- export function scanRepos(): ScannedAsset[] {
77
- const assets: ScannedAsset[] = []
78
- if (!existsSync(WORKSPACE_DIR)) return assets
79
- for (const dir of readdirSync(WORKSPACE_DIR)) {
80
- const repoPath = join(WORKSPACE_DIR, dir)
81
- if (!statSync(repoPath).isDirectory()) continue
82
- if (existsSync(join(repoPath, '.git'))) {
83
- assets.push({ type: 'repo', name: dir, path: repoPath, hash: '', metadata: {} })
84
- }
85
- }
86
- return assets
87
- }
88
-
89
- export function scanAllAssets(): ScannedAsset[] {
90
- return [...scanSkills(), ...scanPlugins(), ...scanCLIs(), ...scanRepos()]
91
- }
92
-
93
- export async function pushAssets(agentName: string): Promise<{ pushed: number; updated: number }> {
94
- const supabase = getSupabase()
95
- const assets = scanAllAssets()
96
- let pushed = 0, updated = 0
97
- for (const asset of assets) {
98
- const { data: existing } = await supabase.from('agent_assets').select('id, asset_hash').eq('agent_name', agentName).eq('asset_type', asset.type).eq('asset_name', asset.name).single()
99
- if (existing) {
100
- if (existing.asset_hash !== asset.hash) {
101
- await supabase.from('agent_assets').update({ asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata, updated_at: new Date().toISOString() }).eq('id', existing.id)
102
- updated++
103
- }
104
- } else {
105
- await supabase.from('agent_assets').insert({ agent_name: agentName, asset_type: asset.type, asset_name: asset.name, asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata })
106
- pushed++
107
- }
108
- }
109
- return { pushed, updated }
110
- }
111
-
112
- export async function listAssets(agentName?: string): Promise<any[]> {
113
- const supabase = getSupabase()
114
- let query = supabase.from('agent_assets').select('*')
115
- if (agentName) query = query.eq('agent_name', agentName)
116
- const { data } = await query.order('updated_at', { ascending: false })
117
- return data || []
118
- }
119
-
120
- export async function getInventory(): Promise<any[]> {
121
- const supabase = getSupabase()
122
- const { data } = await supabase.from('agent_inventory').select('*')
123
- return data || []
124
- }
package/lib/auth/index.ts DELETED
@@ -1,189 +0,0 @@
1
- /**
2
- * Auth module — ported from optimalOS Supabase auth patterns.
3
- *
4
- * OptimalOS uses three client tiers:
5
- * 1. Browser client (anon key + cookie session) — N/A for CLI
6
- * 2. Server client (anon key + SSR cookies) — N/A for CLI
7
- * 3. Admin client (service_role key, no session) — primary CLI path
8
- *
9
- * In a headless CLI context there are no cookies or browser sessions.
10
- * Auth reduces to two modes:
11
- * - Service-role access (bot / automation operations)
12
- * - User-scoped access (pass an access_token obtained externally)
13
- *
14
- * Environment variables (defined in .env):
15
- * OPTIMAL_SUPABASE_URL — Supabase project URL
16
- * OPTIMAL_SUPABASE_SERVICE_KEY — service_role secret
17
- */
18
-
19
- import { createClient, type SupabaseClient } from '@supabase/supabase-js'
20
- import 'dotenv/config'
21
-
22
- // ---------------------------------------------------------------------------
23
- // Types
24
- // ---------------------------------------------------------------------------
25
-
26
- /** Describes how the current invocation is authenticated. */
27
- export interface AuthContext {
28
- /** 'service' when using service_role key, 'user' when using a user JWT */
29
- mode: 'service' | 'user'
30
- /** The Supabase client for this context */
31
- client: SupabaseClient
32
- /** User ID (only set when mode === 'user') */
33
- userId?: string
34
- /** User email (only set when mode === 'user' and resolvable) */
35
- email?: string
36
- }
37
-
38
- /** Minimal session shape returned by getSession(). */
39
- export interface Session {
40
- accessToken: string
41
- user: {
42
- id: string
43
- email?: string
44
- }
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Internal helpers
49
- // ---------------------------------------------------------------------------
50
-
51
- function envOrThrow(name: string): string {
52
- const value = process.env[name]
53
- if (!value) {
54
- throw new Error(`Missing required environment variable: ${name}`)
55
- }
56
- return value
57
- }
58
-
59
- /** Singleton service-role client (matches optimalOS admin.ts pattern). */
60
- let _serviceClient: SupabaseClient | null = null
61
-
62
- // ---------------------------------------------------------------------------
63
- // Public API
64
- // ---------------------------------------------------------------------------
65
-
66
- /**
67
- * Return a service-role Supabase client.
68
- *
69
- * Mirrors optimalOS `createAdminClient()` from lib/supabase/admin.ts:
70
- * - Uses SUPABASE_SERVICE_ROLE_KEY
71
- * - persistSession: false, autoRefreshToken: false
72
- * - Singleton — safe to call repeatedly
73
- */
74
- export function getServiceClient(): SupabaseClient {
75
- if (_serviceClient) return _serviceClient
76
-
77
- const url = envOrThrow('OPTIMAL_SUPABASE_URL')
78
- const key = envOrThrow('OPTIMAL_SUPABASE_SERVICE_KEY')
79
-
80
- _serviceClient = createClient(url, key, {
81
- auth: {
82
- persistSession: false,
83
- autoRefreshToken: false,
84
- },
85
- })
86
-
87
- return _serviceClient
88
- }
89
-
90
- /**
91
- * Return a user-scoped Supabase client authenticated with the given JWT.
92
- *
93
- * This is the CLI equivalent of optimalOS browser/server clients that carry
94
- * a user session via cookies. The caller is responsible for obtaining the
95
- * access token (e.g., via `supabase login`, OAuth device flow, or env var).
96
- *
97
- * A new client is created on every call — callers should cache if needed.
98
- */
99
- export function getUserClient(accessToken: string): SupabaseClient {
100
- const url = envOrThrow('OPTIMAL_SUPABASE_URL')
101
-
102
- // Use service key as the initial key — the global auth header override
103
- // ensures all requests are scoped to the user's JWT instead.
104
- const anonOrServiceKey = process.env.OPTIMAL_SUPABASE_ANON_KEY
105
- ?? envOrThrow('OPTIMAL_SUPABASE_SERVICE_KEY')
106
-
107
- return createClient(url, anonOrServiceKey, {
108
- global: {
109
- headers: { Authorization: `Bearer ${accessToken}` },
110
- },
111
- auth: {
112
- persistSession: false,
113
- autoRefreshToken: false,
114
- },
115
- })
116
- }
117
-
118
- /**
119
- * Attempt to retrieve the current session.
120
- *
121
- * In the CLI there is no implicit cookie jar. A session exists only when:
122
- * 1. OPTIMAL_ACCESS_TOKEN env var is set (user JWT), or
123
- * 2. A future `optimal login` command has cached a token locally.
124
- *
125
- * Returns null if no user session is available (service-role only mode).
126
- */
127
- export async function getSession(): Promise<Session | null> {
128
- const token = process.env.OPTIMAL_ACCESS_TOKEN
129
- if (!token) return null
130
-
131
- try {
132
- const client = getUserClient(token)
133
- const { data: { user }, error } = await client.auth.getUser(token)
134
-
135
- if (error || !user) return null
136
-
137
- return {
138
- accessToken: token,
139
- user: {
140
- id: user.id,
141
- email: user.email,
142
- },
143
- }
144
- } catch {
145
- return null
146
- }
147
- }
148
-
149
- /**
150
- * Guard that throws if no user session is present.
151
- *
152
- * Use at the top of CLI commands that require a logged-in user:
153
- *
154
- * const session = await requireAuth()
155
- * // session.user.id is guaranteed
156
- */
157
- export async function requireAuth(): Promise<Session> {
158
- const session = await getSession()
159
- if (!session) {
160
- throw new Error(
161
- 'Authentication required. Set OPTIMAL_ACCESS_TOKEN or run `optimal login`.',
162
- )
163
- }
164
- return session
165
- }
166
-
167
- /**
168
- * Build an AuthContext describing the current invocation's auth state.
169
- *
170
- * Prefers user-scoped auth when OPTIMAL_ACCESS_TOKEN is set;
171
- * falls back to service-role.
172
- */
173
- export async function resolveAuthContext(): Promise<AuthContext> {
174
- const session = await getSession()
175
-
176
- if (session) {
177
- return {
178
- mode: 'user',
179
- client: getUserClient(session.accessToken),
180
- userId: session.user.id,
181
- email: session.user.email,
182
- }
183
- }
184
-
185
- return {
186
- mode: 'service',
187
- client: getServiceClient(),
188
- }
189
- }