@arbidocs/cli 0.3.47 → 0.3.49
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/CHANGELOG.md +26 -0
- package/dist/index.js +16 -31
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/scripts/time-workspace-open.mjs +245 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arbidocs/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.49",
|
|
4
4
|
"description": "CLI tool for interacting with ARBI — login, manage workspaces, upload documents, query the RAG assistant",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"preuninstall": "node scripts/preuninstall.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@arbidocs/sdk": "0.3.
|
|
25
|
-
"@arbidocs/client": "0.3.
|
|
26
|
-
"@arbidocs/tui": "0.3.
|
|
24
|
+
"@arbidocs/sdk": "0.3.49",
|
|
25
|
+
"@arbidocs/client": "0.3.49",
|
|
26
|
+
"@arbidocs/tui": "0.3.49",
|
|
27
27
|
"@inquirer/prompts": "^8.2.0",
|
|
28
28
|
"chalk": "^5.6.2",
|
|
29
29
|
"commander": "^13.1.0"
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* time-workspace-open.mjs
|
|
4
|
+
*
|
|
5
|
+
* Times the POST /v1/workspace/{id}/open call with a detailed breakdown:
|
|
6
|
+
*
|
|
7
|
+
* - setup — auth + crypto (workspace key unseal/seal). Excluded from "open" time.
|
|
8
|
+
* - ttfb — POST request start → first byte. ≈ network RTT + backend processing
|
|
9
|
+
* (DB query, decrypt, serialize JSON, begin streaming).
|
|
10
|
+
* - download — first byte → last byte. ≈ wire transfer time for the payload.
|
|
11
|
+
* - parse — JSON.parse on the raw body.
|
|
12
|
+
* - total — ttfb + download + parse.
|
|
13
|
+
*
|
|
14
|
+
* Also reports:
|
|
15
|
+
* - bytes — raw response body size
|
|
16
|
+
* - docs — documents[] count (if present)
|
|
17
|
+
* - per-doc — bytes / docs
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node packages/arbi-cli/scripts/time-workspace-open.mjs # uses selected workspace
|
|
21
|
+
* node packages/arbi-cli/scripts/time-workspace-open.mjs wrk-820e4c6d
|
|
22
|
+
* node packages/arbi-cli/scripts/time-workspace-open.mjs wrk-820e4c6d --runs=3
|
|
23
|
+
*
|
|
24
|
+
* Requires:
|
|
25
|
+
* - `arbi login` has been run with a valid cached session
|
|
26
|
+
* - the SDK is built (it's an internal workspace dep so fresh-from-source works too)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Stub IndexedDB + structuredClone before anything else — @arbidocs/client's login
|
|
30
|
+
// flow calls saveSession() which writes to IndexedDB; in Node we use fake-indexeddb.
|
|
31
|
+
import 'fake-indexeddb/auto'
|
|
32
|
+
|
|
33
|
+
import { performance } from 'node:perf_hooks'
|
|
34
|
+
import { readFileSync } from 'node:fs'
|
|
35
|
+
import { homedir } from 'node:os'
|
|
36
|
+
import path from 'node:path'
|
|
37
|
+
|
|
38
|
+
// ─── arg parsing ─────────────────────────────────────────────────────────
|
|
39
|
+
const rawArgs = process.argv.slice(2)
|
|
40
|
+
const positional = []
|
|
41
|
+
const flags = {}
|
|
42
|
+
for (const raw of rawArgs) {
|
|
43
|
+
if (raw.startsWith('--')) {
|
|
44
|
+
const m = raw.match(/^--([^=]+)(?:=(.*))?$/)
|
|
45
|
+
if (m) flags[m[1]] = m[2] ?? 'true'
|
|
46
|
+
} else {
|
|
47
|
+
positional.push(raw)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const RUNS = Number(flags.runs ?? 1)
|
|
51
|
+
const WORKSPACE_ARG = positional[0]
|
|
52
|
+
|
|
53
|
+
// ─── load CLI config/credentials from ~/.arbi ────────────────────────────
|
|
54
|
+
const arbiDir = path.join(homedir(), '.arbi')
|
|
55
|
+
const config = JSON.parse(readFileSync(path.join(arbiDir, 'config.json'), 'utf8'))
|
|
56
|
+
const credentials = JSON.parse(readFileSync(path.join(arbiDir, 'credentials.json'), 'utf8'))
|
|
57
|
+
|
|
58
|
+
const workspaceId = WORKSPACE_ARG ?? config.selectedWorkspaceId
|
|
59
|
+
if (!workspaceId) {
|
|
60
|
+
console.error('No workspace ID given and none selected.')
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`server: ${config.baseUrl}`)
|
|
65
|
+
console.log(`workspace: ${workspaceId}`)
|
|
66
|
+
console.log(`runs: ${RUNS}`)
|
|
67
|
+
console.log('')
|
|
68
|
+
|
|
69
|
+
// ─── import SDK + client ─────────────────────────────────────────────────
|
|
70
|
+
const { createArbiClient, base64ToBytes } = await import('@arbidocs/client')
|
|
71
|
+
const { generateEncryptedWorkspaceKey } = await import('@arbidocs/sdk')
|
|
72
|
+
|
|
73
|
+
// ─── per-run timing ──────────────────────────────────────────────────────
|
|
74
|
+
async function timeOneRun() {
|
|
75
|
+
const t0 = performance.now()
|
|
76
|
+
|
|
77
|
+
// ── setup: fresh arbi client + login to get a session ──────────────────
|
|
78
|
+
// Minimal in-memory sessionStorage shim so @arbidocs/client doesn't fall back
|
|
79
|
+
// to IndexedDB (which doesn't exist in Node). Only used by the auto-relogin
|
|
80
|
+
// middleware — we never actually trigger it since we log in fresh here.
|
|
81
|
+
const memorySessionStorage = {
|
|
82
|
+
async getSession() {
|
|
83
|
+
return {
|
|
84
|
+
signingPrivateKey: base64ToBytes(credentials.signingPrivateKeyBase64),
|
|
85
|
+
serverSessionKey: credentials.serverSessionKeyBase64
|
|
86
|
+
? base64ToBytes(credentials.serverSessionKeyBase64)
|
|
87
|
+
: undefined,
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async saveSession() {},
|
|
91
|
+
async clearSession() {},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const arbi = createArbiClient({
|
|
95
|
+
baseUrl: config.baseUrl,
|
|
96
|
+
deploymentDomain: config.deploymentDomain,
|
|
97
|
+
credentials: 'omit',
|
|
98
|
+
sessionStorage: memorySessionStorage,
|
|
99
|
+
})
|
|
100
|
+
await arbi.crypto.initSodium()
|
|
101
|
+
|
|
102
|
+
const signingPrivateKey = base64ToBytes(credentials.signingPrivateKeyBase64)
|
|
103
|
+
const loginResult = await arbi.auth.loginWithKey({
|
|
104
|
+
email: credentials.email,
|
|
105
|
+
signingPrivateKey,
|
|
106
|
+
ssoToken: credentials.ssoToken,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Fetch workspace list to get the wrapped key
|
|
110
|
+
const { data: workspaces, error: wsError } = await arbi.fetch.GET('/v1/user/workspaces')
|
|
111
|
+
if (wsError || !workspaces) throw new Error('Failed to fetch workspaces')
|
|
112
|
+
const ws = workspaces.find((w) => w.external_id === workspaceId)
|
|
113
|
+
if (!ws) throw new Error(`workspace ${workspaceId} not found`)
|
|
114
|
+
if (!ws.wrapped_key) throw new Error(`workspace ${workspaceId} has no wrapped_key`)
|
|
115
|
+
|
|
116
|
+
const encryptedWorkspaceKey = await generateEncryptedWorkspaceKey(
|
|
117
|
+
arbi,
|
|
118
|
+
ws.wrapped_key,
|
|
119
|
+
loginResult.serverSessionKey,
|
|
120
|
+
credentials.signingPrivateKeyBase64
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const setupMs = performance.now() - t0
|
|
124
|
+
const docCountHint = (ws.shared_document_count ?? 0) + (ws.private_document_count ?? 0)
|
|
125
|
+
|
|
126
|
+
// ── the actual /open POST with streaming fetch for TTFB measurement ────
|
|
127
|
+
const qs = flags['no-documents'] ? '?include_documents=false' : ''
|
|
128
|
+
const url = `${config.baseUrl}/v1/workspace/${workspaceId}/open${qs}`
|
|
129
|
+
const requestBody = JSON.stringify({ workspace_key: encryptedWorkspaceKey })
|
|
130
|
+
|
|
131
|
+
const tPostStart = performance.now()
|
|
132
|
+
const res = await fetch(url, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'content-type': 'application/json',
|
|
136
|
+
authorization: `Bearer ${loginResult.accessToken}`,
|
|
137
|
+
},
|
|
138
|
+
body: requestBody,
|
|
139
|
+
})
|
|
140
|
+
const tHeaders = performance.now() // headers received ≈ TTFB
|
|
141
|
+
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const text = await res.text()
|
|
144
|
+
throw new Error(`POST /open failed ${res.status}: ${text.slice(0, 500)}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Stream the body to measure download time separately from TTFB
|
|
148
|
+
const reader = res.body.getReader()
|
|
149
|
+
const chunks = []
|
|
150
|
+
let bytes = 0
|
|
151
|
+
// eslint-disable-next-line no-constant-condition
|
|
152
|
+
while (true) {
|
|
153
|
+
const { done, value } = await reader.read()
|
|
154
|
+
if (done) break
|
|
155
|
+
chunks.push(value)
|
|
156
|
+
bytes += value.byteLength
|
|
157
|
+
}
|
|
158
|
+
const tDownload = performance.now()
|
|
159
|
+
|
|
160
|
+
// Assemble + parse
|
|
161
|
+
const body = new Uint8Array(bytes)
|
|
162
|
+
let offset = 0
|
|
163
|
+
for (const chunk of chunks) {
|
|
164
|
+
body.set(chunk, offset)
|
|
165
|
+
offset += chunk.byteLength
|
|
166
|
+
}
|
|
167
|
+
const text = new TextDecoder().decode(body)
|
|
168
|
+
const json = JSON.parse(text)
|
|
169
|
+
const tParse = performance.now()
|
|
170
|
+
|
|
171
|
+
const ttfbMs = tHeaders - tPostStart
|
|
172
|
+
const downloadMs = tDownload - tHeaders
|
|
173
|
+
const parseMs = tParse - tDownload
|
|
174
|
+
const totalMs = tParse - tPostStart
|
|
175
|
+
|
|
176
|
+
const docs = Array.isArray(json.documents) ? json.documents.length : 0
|
|
177
|
+
const conversations = Array.isArray(json.conversations) ? json.conversations.length : 0
|
|
178
|
+
const tags = Array.isArray(json.tags) ? json.tags.length : 0
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
setupMs,
|
|
182
|
+
ttfbMs,
|
|
183
|
+
downloadMs,
|
|
184
|
+
parseMs,
|
|
185
|
+
totalMs,
|
|
186
|
+
bytes,
|
|
187
|
+
docs,
|
|
188
|
+
conversations,
|
|
189
|
+
tags,
|
|
190
|
+
docCountHint,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function fmt(ms) {
|
|
195
|
+
return `${ms.toFixed(1).padStart(8)} ms`
|
|
196
|
+
}
|
|
197
|
+
function fmtBytes(n) {
|
|
198
|
+
if (n < 1024) return `${n} B`
|
|
199
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
200
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── run it ──────────────────────────────────────────────────────────────
|
|
204
|
+
const results = []
|
|
205
|
+
for (let i = 1; i <= RUNS; i++) {
|
|
206
|
+
process.stdout.write(`run ${i}/${RUNS} ... `)
|
|
207
|
+
try {
|
|
208
|
+
const r = await timeOneRun()
|
|
209
|
+
results.push(r)
|
|
210
|
+
console.log(
|
|
211
|
+
`total=${r.totalMs.toFixed(0)}ms ttfb=${r.ttfbMs.toFixed(0)}ms dl=${r.downloadMs.toFixed(0)}ms parse=${r.parseMs.toFixed(0)}ms size=${fmtBytes(r.bytes)} docs=${r.docs}`
|
|
212
|
+
)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.log(`FAILED: ${err.message}`)
|
|
215
|
+
process.exit(1)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── summary ─────────────────────────────────────────────────────────────
|
|
220
|
+
console.log('')
|
|
221
|
+
console.log('─── summary ──────────────────────────────────────────')
|
|
222
|
+
const avg = (key) => results.reduce((s, r) => s + r[key], 0) / results.length
|
|
223
|
+
const last = results[results.length - 1]
|
|
224
|
+
|
|
225
|
+
console.log(`setup ${fmt(avg('setupMs'))} (auth + key prep, excluded from /open total)`)
|
|
226
|
+
console.log(`ttfb ${fmt(avg('ttfbMs'))} (network RTT + backend: DB + decrypt + serialize)`)
|
|
227
|
+
console.log(`download ${fmt(avg('downloadMs'))} (wire transfer of JSON body)`)
|
|
228
|
+
console.log(`parse ${fmt(avg('parseMs'))} (JSON.parse on client)`)
|
|
229
|
+
console.log(`─────────────────────────────────────`)
|
|
230
|
+
console.log(`total ${fmt(avg('totalMs'))} (ttfb + download + parse)`)
|
|
231
|
+
console.log('')
|
|
232
|
+
console.log(`payload ${fmtBytes(last.bytes)} (${last.bytes.toLocaleString()} bytes)`)
|
|
233
|
+
console.log(`documents ${last.docs}`)
|
|
234
|
+
console.log(`conversat. ${last.conversations}`)
|
|
235
|
+
console.log(`tags ${last.tags}`)
|
|
236
|
+
if (last.docs > 0) {
|
|
237
|
+
const perDoc = last.bytes / last.docs
|
|
238
|
+
console.log(`per-doc ${fmtBytes(perDoc)} ${perDoc.toFixed(0)} B/doc`)
|
|
239
|
+
console.log('')
|
|
240
|
+
console.log('─── extrapolation ────────────────────────────────────')
|
|
241
|
+
for (const n of [500, 2000, 10000, 40000]) {
|
|
242
|
+
const projBytes = perDoc * n
|
|
243
|
+
console.log(` ${String(n).padStart(6)} docs → ${fmtBytes(projBytes).padStart(10)}`)
|
|
244
|
+
}
|
|
245
|
+
}
|