deepline 0.0.1 → 0.1.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/README.md +324 -0
- package/dist/cli/index.js +6750 -503
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6735 -512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +2349 -32
- package/dist/index.d.ts +2349 -32
- package/dist/index.js +1631 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1617 -83
- package/dist/index.mjs.map +1 -1
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
- package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
- package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
- package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
- package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
- package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
- package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
- package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
- package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
- package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
- package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
- package/dist/repo/sdk/src/cli/index.ts +138 -0
- package/dist/repo/sdk/src/cli/progress.ts +135 -0
- package/dist/repo/sdk/src/cli/trace.ts +61 -0
- package/dist/repo/sdk/src/cli/utils.ts +145 -0
- package/dist/repo/sdk/src/client.ts +1188 -0
- package/dist/repo/sdk/src/compat.ts +77 -0
- package/dist/repo/sdk/src/config.ts +285 -0
- package/dist/repo/sdk/src/errors.ts +125 -0
- package/dist/repo/sdk/src/http.ts +391 -0
- package/dist/repo/sdk/src/index.ts +139 -0
- package/dist/repo/sdk/src/play.ts +1330 -0
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
- package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
- package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
- package/dist/repo/sdk/src/tool-output.ts +489 -0
- package/dist/repo/sdk/src/types.ts +669 -0
- package/dist/repo/sdk/src/version.ts +2 -0
- package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
- package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
- package/dist/repo/shared_libs/observability/tracing.ts +98 -0
- package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
- package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
- package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
- package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
- package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
- package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
- package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
- package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
- package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
- package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
- package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
- package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
- package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
- package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
- package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
- package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
- package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
- package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
- package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
- package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
- package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
- package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
- package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
- package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
- package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
- package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
- package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
- package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
- package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
- package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
- package/dist/repo/shared_libs/plays/contracts.ts +51 -0
- package/dist/repo/shared_libs/plays/dataset.ts +308 -0
- package/dist/repo/shared_libs/plays/definition.ts +264 -0
- package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
- package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
- package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
- package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
- package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
- package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
- package/dist/repo/shared_libs/temporal/constants.ts +39 -0
- package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
- package/package.json +14 -12
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { csvStringFromRows } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export type DatasetStats = {
|
|
6
|
+
total_rows: number;
|
|
7
|
+
columnStats: Record<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
non_empty: string;
|
|
11
|
+
unique: number;
|
|
12
|
+
sample_value?: unknown;
|
|
13
|
+
sample_type?: string;
|
|
14
|
+
top_values?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
>;
|
|
17
|
+
miss_reasons?: Record<string, Record<string, number>>;
|
|
18
|
+
miss_details?: Record<
|
|
19
|
+
string,
|
|
20
|
+
Record<
|
|
21
|
+
string,
|
|
22
|
+
{
|
|
23
|
+
count: number;
|
|
24
|
+
examples: Array<{ row: number; detail: string }>;
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CanonicalRowsInfo = {
|
|
31
|
+
rows: Array<Record<string, unknown>>;
|
|
32
|
+
totalRows: number;
|
|
33
|
+
columns: string[];
|
|
34
|
+
complete: boolean;
|
|
35
|
+
source: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSerializedDataset(value: unknown): value is {
|
|
43
|
+
kind: 'dataset';
|
|
44
|
+
count: number;
|
|
45
|
+
preview: unknown[];
|
|
46
|
+
columns?: unknown[];
|
|
47
|
+
tableNamespace?: unknown;
|
|
48
|
+
} {
|
|
49
|
+
return (
|
|
50
|
+
isRecord(value) &&
|
|
51
|
+
value.kind === 'dataset' &&
|
|
52
|
+
typeof value.count === 'number' &&
|
|
53
|
+
Array.isArray(value.preview)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rowArray(value: unknown): Array<Record<string, unknown>> | null {
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const rows = value.filter(
|
|
62
|
+
(row): row is Record<string, unknown> => isRecord(row),
|
|
63
|
+
);
|
|
64
|
+
return rows.length === value.length ? rows : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readNumber(value: unknown): number | null {
|
|
68
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
69
|
+
? Math.trunc(value)
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inferColumns(rows: Array<Record<string, unknown>>): string[] {
|
|
74
|
+
const columns: string[] = [];
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
for (const key of Object.keys(row)) {
|
|
78
|
+
if (key.startsWith('_')) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!seen.has(key)) {
|
|
82
|
+
seen.add(key);
|
|
83
|
+
columns.push(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return columns;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function extractCanonicalRowsInfo(statusOrResult: unknown): CanonicalRowsInfo | null {
|
|
91
|
+
const root = isRecord(statusOrResult) ? statusOrResult : null;
|
|
92
|
+
const result = isRecord(root?.result) ? root.result : root;
|
|
93
|
+
if (!result) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const metadata = isRecord(result._metadata) ? result._metadata : null;
|
|
97
|
+
const totalFromMetadata = metadata?.totalRows ?? metadata?.rowCount ?? metadata?.count;
|
|
98
|
+
|
|
99
|
+
const candidates: Array<{ source: string; value: unknown; total?: unknown }> = [
|
|
100
|
+
{ source: 'result.contacts', value: result.contacts, total: totalFromMetadata ?? result.totalRows ?? result.rowCount ?? result.count },
|
|
101
|
+
{ source: 'result.previewRows', value: result.previewRows, total: totalFromMetadata ?? result.totalRows ?? result.rowCount ?? result.count },
|
|
102
|
+
{ source: 'result.rows', value: result.rows, total: totalFromMetadata ?? result.totalRows ?? result.rowCount ?? result.count },
|
|
103
|
+
{ source: 'result.results', value: result.results, total: totalFromMetadata ?? result.totalRows ?? result.rowCount ?? result.count },
|
|
104
|
+
];
|
|
105
|
+
if (isRecord(result.output)) {
|
|
106
|
+
const outputMetadata = isRecord(result.output._metadata)
|
|
107
|
+
? result.output._metadata
|
|
108
|
+
: null;
|
|
109
|
+
const outputTotalFromMetadata =
|
|
110
|
+
outputMetadata?.totalRows ?? outputMetadata?.rowCount ?? outputMetadata?.count;
|
|
111
|
+
candidates.push(
|
|
112
|
+
{ source: 'result.output.contacts', value: result.output.contacts, total: outputTotalFromMetadata ?? result.output.totalRows ?? result.output.rowCount ?? result.output.count },
|
|
113
|
+
{ source: 'result.output.previewRows', value: result.output.previewRows, total: outputTotalFromMetadata ?? result.output.totalRows ?? result.output.rowCount ?? result.output.count },
|
|
114
|
+
{ source: 'result.output.rows', value: result.output.rows, total: outputTotalFromMetadata ?? result.output.totalRows ?? result.output.rowCount ?? result.output.count },
|
|
115
|
+
{ source: 'result.output.results', value: result.output.results, total: outputTotalFromMetadata ?? result.output.totalRows ?? result.output.rowCount ?? result.output.count },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const candidate of candidates) {
|
|
120
|
+
if (isSerializedDataset(candidate.value)) {
|
|
121
|
+
const rows = rowArray(candidate.value.preview) ?? [];
|
|
122
|
+
const totalRows = readNumber(candidate.value.count) ?? rows.length;
|
|
123
|
+
return {
|
|
124
|
+
rows,
|
|
125
|
+
totalRows,
|
|
126
|
+
columns:
|
|
127
|
+
Array.isArray(candidate.value.columns) &&
|
|
128
|
+
candidate.value.columns.every((column) => typeof column === 'string')
|
|
129
|
+
? (candidate.value.columns as string[])
|
|
130
|
+
: inferColumns(rows),
|
|
131
|
+
complete: rows.length === totalRows,
|
|
132
|
+
source: candidate.source,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rows = rowArray(candidate.value);
|
|
137
|
+
if (!rows) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const totalRows = readNumber(candidate.total) ?? rows.length;
|
|
141
|
+
return {
|
|
142
|
+
rows,
|
|
143
|
+
totalRows,
|
|
144
|
+
columns: inferColumns(rows),
|
|
145
|
+
complete: rows.length === totalRows,
|
|
146
|
+
source: candidate.source,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function percentText(numerator: number, denominator: number): string {
|
|
154
|
+
return denominator > 0
|
|
155
|
+
? `${numerator}/${denominator} (${Math.round((100 * numerator) / denominator)}%)`
|
|
156
|
+
: '0/0 (0%)';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function countPercentText(count: number, denominator: number): string {
|
|
160
|
+
return denominator > 0
|
|
161
|
+
? `${count} (${Math.round((100 * count) / denominator)}%)`
|
|
162
|
+
: '0 (0%)';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseJsonLike(value: unknown): unknown {
|
|
166
|
+
if (typeof value !== 'string') {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
const text = value.trim();
|
|
170
|
+
if (!text || !['{', '['].includes(text[0] ?? '')) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(text);
|
|
175
|
+
} catch {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sampleType(value: unknown): string {
|
|
181
|
+
const parsed = parseJsonLike(value);
|
|
182
|
+
if (parsed === null || parsed === undefined) return 'null';
|
|
183
|
+
if (typeof parsed === 'boolean') return 'boolean';
|
|
184
|
+
if (Number.isInteger(parsed)) return 'integer';
|
|
185
|
+
if (typeof parsed === 'number') return 'number';
|
|
186
|
+
if (Array.isArray(parsed)) return 'array';
|
|
187
|
+
if (typeof parsed === 'object') return 'object';
|
|
188
|
+
return 'string';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function compactScalar(value: unknown, maxLength = 200): string {
|
|
192
|
+
const text = String(value).replace(/\s+/g, ' ').trim();
|
|
193
|
+
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3)}...`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizeSampleValue(value: unknown, depth = 0): unknown {
|
|
197
|
+
const parsed = parseJsonLike(value);
|
|
198
|
+
if (parsed === null || parsed === undefined) return '';
|
|
199
|
+
if (typeof parsed === 'string') return compactScalar(parsed);
|
|
200
|
+
if (typeof parsed === 'number' || typeof parsed === 'boolean') return parsed;
|
|
201
|
+
if (depth >= 3) {
|
|
202
|
+
if (Array.isArray(parsed)) return [];
|
|
203
|
+
if (isRecord(parsed)) return {};
|
|
204
|
+
return compactScalar(parsed);
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(parsed)) {
|
|
207
|
+
return parsed.slice(0, 3).map((item) => summarizeSampleValue(item, depth + 1));
|
|
208
|
+
}
|
|
209
|
+
if (isRecord(parsed)) {
|
|
210
|
+
const out: Record<string, unknown> = {};
|
|
211
|
+
for (const [key, nested] of Object.entries(parsed)) {
|
|
212
|
+
if (['__dl', 'meta', 'metadata'].includes(key)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (nested === null || nested === undefined || nested === '') {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
out[key] = summarizeSampleValue(nested, depth + 1);
|
|
219
|
+
if (Object.keys(out).length >= 6) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
return compactScalar(parsed);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function compactCell(value: unknown): string {
|
|
229
|
+
const parsed = parseJsonLike(value);
|
|
230
|
+
if (parsed === null || parsed === undefined) return '';
|
|
231
|
+
if (typeof parsed === 'string') return compactScalar(parsed, 120);
|
|
232
|
+
if (typeof parsed === 'number' || typeof parsed === 'boolean') return String(parsed);
|
|
233
|
+
if (Array.isArray(parsed)) {
|
|
234
|
+
if (parsed.length === 0) return '';
|
|
235
|
+
if (parsed.slice(0, 3).every((item) => ['string', 'number', 'boolean'].includes(typeof item))) {
|
|
236
|
+
return compactScalar(parsed.slice(0, 3).join(' | '), 120);
|
|
237
|
+
}
|
|
238
|
+
return `[${parsed.length} items]`;
|
|
239
|
+
}
|
|
240
|
+
if (isRecord(parsed)) {
|
|
241
|
+
for (const key of ['matched_result', 'output']) {
|
|
242
|
+
if (parsed[key] !== null && parsed[key] !== undefined && parsed[key] !== '') {
|
|
243
|
+
return compactCell(parsed[key]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const preferred = ['email', 'status', 'name', 'full_name', 'title', 'domain', 'linkedin_url'];
|
|
247
|
+
const parts: string[] = [];
|
|
248
|
+
for (const key of preferred) {
|
|
249
|
+
if (parsed[key] !== null && parsed[key] !== undefined && parsed[key] !== '') {
|
|
250
|
+
parts.push(`${key}=${compactCell(parsed[key])}`);
|
|
251
|
+
}
|
|
252
|
+
if (parts.length >= 4) break;
|
|
253
|
+
}
|
|
254
|
+
return parts.length > 0 ? compactScalar(parts.join(' | '), 160) : '{}';
|
|
255
|
+
}
|
|
256
|
+
return compactScalar(parsed, 120);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildDatasetStats(
|
|
260
|
+
rows: Array<Record<string, unknown>>,
|
|
261
|
+
totalRows = rows.length,
|
|
262
|
+
columns = inferColumns(rows),
|
|
263
|
+
): DatasetStats {
|
|
264
|
+
const columnStats: DatasetStats['columnStats'] = {};
|
|
265
|
+
for (const column of columns) {
|
|
266
|
+
let nonEmpty = 0;
|
|
267
|
+
let empty = 0;
|
|
268
|
+
let sampleValue: unknown;
|
|
269
|
+
let sampleValueType: string | null = null;
|
|
270
|
+
const valueCounts = new Map<string, number>();
|
|
271
|
+
|
|
272
|
+
for (const row of rows) {
|
|
273
|
+
const raw = row[column];
|
|
274
|
+
const value = compactCell(raw);
|
|
275
|
+
if (value) {
|
|
276
|
+
nonEmpty += 1;
|
|
277
|
+
valueCounts.set(value, (valueCounts.get(value) ?? 0) + 1);
|
|
278
|
+
if (sampleValue === undefined) {
|
|
279
|
+
sampleValue = summarizeSampleValue(raw);
|
|
280
|
+
sampleValueType = sampleType(raw);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
empty += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const denominator = nonEmpty + empty;
|
|
288
|
+
const stat: DatasetStats['columnStats'][string] = {
|
|
289
|
+
non_empty: percentText(nonEmpty, denominator),
|
|
290
|
+
unique: valueCounts.size,
|
|
291
|
+
};
|
|
292
|
+
if (sampleValue !== undefined && sampleValueType) {
|
|
293
|
+
stat.sample_value = sampleValue;
|
|
294
|
+
stat.sample_type = sampleValueType;
|
|
295
|
+
}
|
|
296
|
+
if (valueCounts.size > 0 && valueCounts.size < nonEmpty) {
|
|
297
|
+
const top = [...valueCounts.entries()]
|
|
298
|
+
.sort((left, right) => right[1] - left[1])
|
|
299
|
+
.slice(0, 3);
|
|
300
|
+
const topKeys = new Set(top.map(([key]) => key));
|
|
301
|
+
const otherCount = [...valueCounts.entries()]
|
|
302
|
+
.filter(([key]) => !topKeys.has(key))
|
|
303
|
+
.reduce((sum, [, count]) => sum + count, 0);
|
|
304
|
+
stat.top_values = Object.fromEntries(
|
|
305
|
+
top.map(([key, count]) => [key, countPercentText(count, denominator)]),
|
|
306
|
+
);
|
|
307
|
+
if (otherCount > 0) {
|
|
308
|
+
stat.top_values['(other)'] = countPercentText(otherCount, denominator);
|
|
309
|
+
}
|
|
310
|
+
if (empty > 0) {
|
|
311
|
+
stat.top_values['(null)'] = countPercentText(empty, denominator);
|
|
312
|
+
}
|
|
313
|
+
} else if (empty > 0 && nonEmpty > 0) {
|
|
314
|
+
stat.top_values = { '(null)': countPercentText(empty, denominator) };
|
|
315
|
+
}
|
|
316
|
+
columnStats[column] = stat;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
total_rows: totalRows,
|
|
321
|
+
columnStats,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function writeCanonicalRowsCsv(
|
|
326
|
+
rowsInfo: CanonicalRowsInfo,
|
|
327
|
+
outPath: string,
|
|
328
|
+
): string {
|
|
329
|
+
if (!rowsInfo.complete) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Run output only includes ${rowsInfo.rows.length} preview row(s) of ${rowsInfo.totalRows}; cannot export a complete CSV from this status payload yet.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const resolved = resolve(outPath);
|
|
335
|
+
writeFileSync(
|
|
336
|
+
resolved,
|
|
337
|
+
csvStringFromRows(rowsInfo.rows, rowsInfo.columns),
|
|
338
|
+
'utf-8',
|
|
339
|
+
);
|
|
340
|
+
return resolved;
|
|
341
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { DeeplineClient } from '../client.js';
|
|
5
|
+
import { autoDetectBaseUrl } from '../config.js';
|
|
6
|
+
import { enforceSdkCompatibility } from '../compat.js';
|
|
7
|
+
import { SDK_VERSION } from '../version.js';
|
|
8
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
9
|
+
import { registerBillingCommands } from './commands/billing.js';
|
|
10
|
+
import { registerCsvCommands } from './commands/csv.js';
|
|
11
|
+
import { registerDbCommands } from './commands/db.js';
|
|
12
|
+
import { registerFeedbackCommands } from './commands/feedback.js';
|
|
13
|
+
import { registerOrgCommands } from './commands/org.js';
|
|
14
|
+
import { registerPlayCommands } from './commands/play.js';
|
|
15
|
+
import { registerToolsCommands } from './commands/tools.js';
|
|
16
|
+
import { createCliProgress } from './progress.js';
|
|
17
|
+
import { recordCliTrace, traceCliSpan } from './trace.js';
|
|
18
|
+
import { printJsonError } from './utils.js';
|
|
19
|
+
|
|
20
|
+
function shouldPrintStartupPhase(): boolean {
|
|
21
|
+
if (process.argv.includes('--json')) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const command = args[0];
|
|
26
|
+
const subcommand = args[1];
|
|
27
|
+
return (command === 'play' || command === 'plays') && subcommand === 'run';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main(): Promise<void> {
|
|
31
|
+
const mainStartedAt = Date.now();
|
|
32
|
+
recordCliTrace({
|
|
33
|
+
phase: 'cli.main_start',
|
|
34
|
+
argv: process.argv.slice(2),
|
|
35
|
+
});
|
|
36
|
+
const printStartupPhase = shouldPrintStartupPhase();
|
|
37
|
+
const progress = printStartupPhase ? createCliProgress(true) : null;
|
|
38
|
+
if (printStartupPhase) {
|
|
39
|
+
progress?.phase('loading deepline cli');
|
|
40
|
+
}
|
|
41
|
+
const program = new Command();
|
|
42
|
+
program
|
|
43
|
+
.name('deepline')
|
|
44
|
+
.description('Deepline CLI (TypeScript SDK)')
|
|
45
|
+
.version(SDK_VERSION, '-v, --version', 'Show version')
|
|
46
|
+
.showHelpAfterError()
|
|
47
|
+
.showSuggestionAfterError(true)
|
|
48
|
+
.addHelpText(
|
|
49
|
+
'after',
|
|
50
|
+
`
|
|
51
|
+
Common commands:
|
|
52
|
+
deepline health
|
|
53
|
+
deepline auth status --json
|
|
54
|
+
deepline plays search email --json
|
|
55
|
+
deepline plays describe person-linkedin-to-email --json
|
|
56
|
+
deepline plays run my.play.ts --input '{"domain":"stripe.com"}' --watch
|
|
57
|
+
deepline tools call hunter_email_verifier --input '{"email":"a@b.com"}'
|
|
58
|
+
|
|
59
|
+
Output:
|
|
60
|
+
Structured commands print human-readable output in a terminal and JSON when stdout is piped.
|
|
61
|
+
Use --json to force JSON in an interactive terminal.
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
66
|
+
if (actionCommand.name() === 'version') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (printStartupPhase) {
|
|
70
|
+
progress?.phase('checking sdk compatibility');
|
|
71
|
+
}
|
|
72
|
+
const baseUrl = autoDetectBaseUrl().replace(/\/$/, '');
|
|
73
|
+
await traceCliSpan(
|
|
74
|
+
'cli.sdk_compatibility',
|
|
75
|
+
{ baseUrl },
|
|
76
|
+
() => enforceSdkCompatibility(baseUrl),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
registerAuthCommands(program);
|
|
81
|
+
registerToolsCommands(program);
|
|
82
|
+
registerPlayCommands(program);
|
|
83
|
+
registerBillingCommands(program);
|
|
84
|
+
registerOrgCommands(program);
|
|
85
|
+
registerCsvCommands(program);
|
|
86
|
+
registerDbCommands(program);
|
|
87
|
+
registerFeedbackCommands(program);
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('health')
|
|
91
|
+
.description('Check server health.')
|
|
92
|
+
.action(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const client = new DeeplineClient();
|
|
95
|
+
const data = await client.health();
|
|
96
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Cannot reach Deepline API: ${error instanceof Error ? error.message : String(error)}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command('version')
|
|
106
|
+
.description('Show version.')
|
|
107
|
+
.action(() => {
|
|
108
|
+
process.stdout.write(`deepline ${SDK_VERSION}\n`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await program.parseAsync(process.argv);
|
|
113
|
+
recordCliTrace({
|
|
114
|
+
phase: 'cli.main_total',
|
|
115
|
+
ms: Date.now() - mainStartedAt,
|
|
116
|
+
ok: true,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
recordCliTrace({
|
|
120
|
+
phase: 'cli.main_total',
|
|
121
|
+
ms: Date.now() - mainStartedAt,
|
|
122
|
+
ok: false,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
if (process.argv.includes('--json')) {
|
|
126
|
+
printJsonError(error);
|
|
127
|
+
} else if (error instanceof Error) {
|
|
128
|
+
console.error(`Error: ${error.message}`);
|
|
129
|
+
} else {
|
|
130
|
+
console.error(`Error: ${String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.exit(process.exitCode ?? 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
export class CliProgress {
|
|
4
|
+
private lastMessage: string | null = null;
|
|
5
|
+
private readonly interactive = Boolean(process.stderr.isTTY);
|
|
6
|
+
private readonly grey = '\x1b[90m';
|
|
7
|
+
private readonly cyan = '\x1b[36m';
|
|
8
|
+
private readonly dim = '\x1b[2m';
|
|
9
|
+
private readonly reset = '\x1b[0m';
|
|
10
|
+
private worker: Worker | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(private readonly enabled: boolean) {}
|
|
13
|
+
|
|
14
|
+
phase(message: string): void {
|
|
15
|
+
if (!this.enabled || this.lastMessage === message) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (this.interactive && this.lastMessage) {
|
|
19
|
+
this.settle('✓');
|
|
20
|
+
}
|
|
21
|
+
this.lastMessage = message;
|
|
22
|
+
if (!this.interactive) {
|
|
23
|
+
process.stderr.write(`${message}\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.ensureWorker();
|
|
27
|
+
this.worker?.postMessage({ type: 'phase', message });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
complete(): void {
|
|
31
|
+
this.settle('✓');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fail(): void {
|
|
35
|
+
this.settle('✗');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
writeLogLine(line: string): void {
|
|
39
|
+
if (!this.enabled) {
|
|
40
|
+
process.stderr.write(`${line}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const message = this.interactive
|
|
44
|
+
? `${this.cyan}[logs]${this.reset} ${this.dim}${line}${this.reset}`
|
|
45
|
+
: `[logs] ${line}`;
|
|
46
|
+
if (!this.interactive) {
|
|
47
|
+
process.stderr.write(`${message}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const activeMessage = this.lastMessage;
|
|
51
|
+
this.worker?.terminate().catch(() => undefined);
|
|
52
|
+
this.worker = null;
|
|
53
|
+
process.stderr.write(`\r\x1b[2K${message}\n`);
|
|
54
|
+
if (activeMessage) {
|
|
55
|
+
this.startWorker().postMessage({ type: 'phase', message: activeMessage });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private settle(mark: '✓' | '✗'): void {
|
|
60
|
+
if (!this.enabled || !this.lastMessage) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.interactive) {
|
|
64
|
+
this.worker?.terminate().catch(() => undefined);
|
|
65
|
+
this.worker = null;
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`\r\x1b[2K${this.grey}${mark} ${this.lastMessage}${this.reset}\n`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
this.lastMessage = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private ensureWorker(): void {
|
|
74
|
+
this.startWorker();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private startWorker(): Worker {
|
|
78
|
+
if (this.worker) {
|
|
79
|
+
return this.worker;
|
|
80
|
+
}
|
|
81
|
+
this.worker = new Worker(
|
|
82
|
+
`
|
|
83
|
+
const { parentPort } = require('node:worker_threads');
|
|
84
|
+
const { writeSync } = require('node:fs');
|
|
85
|
+
const frames = ['—', '\\\\', '|', '/'];
|
|
86
|
+
const grey = '\\x1b[90m';
|
|
87
|
+
const reset = '\\x1b[0m';
|
|
88
|
+
let message = '';
|
|
89
|
+
let frameIndex = 0;
|
|
90
|
+
let timer = null;
|
|
91
|
+
function render() {
|
|
92
|
+
if (!message) return;
|
|
93
|
+
const frame = frames[frameIndex % frames.length] || '—';
|
|
94
|
+
frameIndex += 1;
|
|
95
|
+
writeSync(2, '\\r\\x1b[2K' + grey + frame + ' ' + message + reset);
|
|
96
|
+
}
|
|
97
|
+
parentPort.on('message', (event) => {
|
|
98
|
+
if (event.type === 'phase') {
|
|
99
|
+
message = String(event.message || '');
|
|
100
|
+
frameIndex = 0;
|
|
101
|
+
render();
|
|
102
|
+
if (!timer) {
|
|
103
|
+
timer = setInterval(render, 120);
|
|
104
|
+
timer.unref && timer.unref();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (event.type === 'stop') {
|
|
108
|
+
if (timer) clearInterval(timer);
|
|
109
|
+
timer = null;
|
|
110
|
+
writeSync(2, '\\r\\x1b[2K');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
`,
|
|
114
|
+
{ eval: true },
|
|
115
|
+
);
|
|
116
|
+
this.worker.unref();
|
|
117
|
+
return this.worker;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let activeProgress: CliProgress | null = null;
|
|
122
|
+
|
|
123
|
+
export function setActiveCliProgress(progress: CliProgress | null): void {
|
|
124
|
+
activeProgress = progress;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getActiveCliProgress(): CliProgress | null {
|
|
128
|
+
return activeProgress;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createCliProgress(enabled: boolean): CliProgress {
|
|
132
|
+
const progress = new CliProgress(enabled);
|
|
133
|
+
setActiveCliProgress(progress);
|
|
134
|
+
return progress;
|
|
135
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type CliTraceFields = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
type CliTraceEvent = CliTraceFields & {
|
|
4
|
+
phase: string;
|
|
5
|
+
ms?: number;
|
|
6
|
+
ok?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const cliTraceStartedAt = Date.now();
|
|
10
|
+
|
|
11
|
+
function isTruthyEnv(value: string | undefined): boolean {
|
|
12
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isCliTraceEnabled(): boolean {
|
|
16
|
+
return isTruthyEnv(process.env.DEEPLINE_CLI_TRACE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function recordCliTrace(event: CliTraceEvent): void {
|
|
20
|
+
if (!isCliTraceEnabled()) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const payload = {
|
|
25
|
+
ts: now,
|
|
26
|
+
source: 'cli',
|
|
27
|
+
sinceStartMs: now - cliTraceStartedAt,
|
|
28
|
+
...event,
|
|
29
|
+
};
|
|
30
|
+
process.stderr.write(`[cli-trace] ${JSON.stringify(payload)}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function traceCliSpan<T>(
|
|
34
|
+
phase: string,
|
|
35
|
+
fields: CliTraceFields,
|
|
36
|
+
run: () => Promise<T>,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
if (!isCliTraceEnabled()) {
|
|
39
|
+
return run();
|
|
40
|
+
}
|
|
41
|
+
const startedAt = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
const result = await run();
|
|
44
|
+
recordCliTrace({
|
|
45
|
+
phase,
|
|
46
|
+
ms: Date.now() - startedAt,
|
|
47
|
+
ok: true,
|
|
48
|
+
...fields,
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
recordCliTrace({
|
|
53
|
+
phase,
|
|
54
|
+
ms: Date.now() - startedAt,
|
|
55
|
+
ok: false,
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
...fields,
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|