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,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output processing utilities.
|
|
3
|
+
*
|
|
4
|
+
* Tools return data in varied shapes — some return flat objects, others
|
|
5
|
+
* wrap results in `{ result: { data: [...] } }` envelopes, and list
|
|
6
|
+
* responses can be nested at different depths. This module provides
|
|
7
|
+
* utilities to normalize, extract, and persist tool outputs.
|
|
8
|
+
*
|
|
9
|
+
* ## Key functions
|
|
10
|
+
*
|
|
11
|
+
* - {@link createToolCallResult} — Wrap a tool response with ergonomic accessors.
|
|
12
|
+
*
|
|
13
|
+
* - {@link tryConvertToList} — Extract a list of records from any tool response.
|
|
14
|
+
* Uses configured `listExtractorPaths` first, then falls back to auto-detection.
|
|
15
|
+
*
|
|
16
|
+
* - {@link writeCsvOutputFile} — Write records to a CSV file with automatic
|
|
17
|
+
* column ordering, cell escaping, and preview generation.
|
|
18
|
+
*
|
|
19
|
+
* - {@link writeJsonOutputFile} — Write any payload to a pretty-printed JSON file.
|
|
20
|
+
*
|
|
21
|
+
* - {@link extractSummaryFields} — Pull out scalar fields (string, number, boolean)
|
|
22
|
+
* for a quick summary of a tool response.
|
|
23
|
+
*
|
|
24
|
+
* All file outputs go to `~/.local/share/deepline/data/` with timestamped filenames.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of converting a tool response to a list of records.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const conversion = tryConvertToList(toolResponse, {
|
|
38
|
+
* listExtractorPaths: ['people', 'result.data'],
|
|
39
|
+
* });
|
|
40
|
+
* if (conversion) {
|
|
41
|
+
* console.log(`Found ${conversion.rows.length} rows via ${conversion.strategy}`);
|
|
42
|
+
* console.log(`Source path: ${conversion.sourcePath}`);
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export type ListConversionResult = {
|
|
47
|
+
/** Normalized array of record objects. Scalars are wrapped as `{ value: <scalar> }`. */
|
|
48
|
+
rows: Array<Record<string, unknown>>;
|
|
49
|
+
/**
|
|
50
|
+
* How the list was found:
|
|
51
|
+
* - `'configured_paths'` — matched one of the `listExtractorPaths`
|
|
52
|
+
* - `'auto_detected'` — found via recursive DFS (longest array wins)
|
|
53
|
+
*/
|
|
54
|
+
strategy: 'configured_paths' | 'auto_detected';
|
|
55
|
+
/** Dotted path to where the list was found (e.g. `"result.data"`, `"people"`). */
|
|
56
|
+
sourcePath: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type Scalar = string | number | boolean | null;
|
|
60
|
+
|
|
61
|
+
/** Ergonomic wrapper returned by high-level SDK tool execution. */
|
|
62
|
+
export type ToolCallResult = {
|
|
63
|
+
/** Raw tool response. Use this when a provider-specific shape matters. */
|
|
64
|
+
readonly value: unknown;
|
|
65
|
+
/** Best-effort email extraction from common response shapes. */
|
|
66
|
+
getEmail(): string | null;
|
|
67
|
+
/** Best-effort phone extraction from common response shapes. */
|
|
68
|
+
getPhone(): string | null;
|
|
69
|
+
/** Best-effort list extraction. Returns rows only; use `tryConvertToList` for metadata. */
|
|
70
|
+
tryList(options?: { listExtractorPaths?: string[] }): Array<Record<string, unknown>> | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
74
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
78
|
+
const PHONE_KEY_PATTERN = /(^|[_-])(phone|mobile|cell|telephone|tel)([_-]|$)|phone|mobile|telephone/i;
|
|
79
|
+
|
|
80
|
+
function normalizeScalarString(value: unknown): string | null {
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function looksLikeEmail(value: unknown): string | null {
|
|
92
|
+
const candidate = normalizeScalarString(value);
|
|
93
|
+
if (!candidate || !EMAIL_PATTERN.test(candidate)) return null;
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function looksLikePhone(value: unknown): string | null {
|
|
98
|
+
const candidate = normalizeScalarString(value);
|
|
99
|
+
if (!candidate) return null;
|
|
100
|
+
const digits = candidate.replace(/\D/g, '');
|
|
101
|
+
if (digits.length < 7 || digits.length > 16) return null;
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findEmail(value: unknown, depth = 0): string | null {
|
|
106
|
+
if (depth > 6) return null;
|
|
107
|
+
|
|
108
|
+
const direct = looksLikeEmail(value);
|
|
109
|
+
if (direct) return direct;
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
for (const entry of value) {
|
|
113
|
+
const nested = findEmail(entry, depth + 1);
|
|
114
|
+
if (nested) return nested;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isPlainObject(value)) return null;
|
|
120
|
+
|
|
121
|
+
for (const [key, child] of Object.entries(value)) {
|
|
122
|
+
if (/email/i.test(key)) {
|
|
123
|
+
const keyed = looksLikeEmail(child);
|
|
124
|
+
if (keyed) return keyed;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const child of Object.values(value)) {
|
|
129
|
+
const nested = findEmail(child, depth + 1);
|
|
130
|
+
if (nested) return nested;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findPhone(value: unknown, depth = 0): string | null {
|
|
137
|
+
if (depth > 6) return null;
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
for (const entry of value) {
|
|
141
|
+
const nested = findPhone(entry, depth + 1);
|
|
142
|
+
if (nested) return nested;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isPlainObject(value)) return null;
|
|
148
|
+
|
|
149
|
+
for (const [key, child] of Object.entries(value)) {
|
|
150
|
+
if (PHONE_KEY_PATTERN.test(key)) {
|
|
151
|
+
const keyed = looksLikePhone(child);
|
|
152
|
+
if (keyed) return keyed;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const child of Object.values(value)) {
|
|
157
|
+
const nested = findPhone(child, depth + 1);
|
|
158
|
+
if (nested) return nested;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class DeeplineToolCallResult implements ToolCallResult {
|
|
165
|
+
constructor(readonly value: unknown) {}
|
|
166
|
+
|
|
167
|
+
getEmail(): string | null {
|
|
168
|
+
return findEmail(this.value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getPhone(): string | null {
|
|
172
|
+
return findPhone(this.value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tryList(options?: { listExtractorPaths?: string[] }): Array<Record<string, unknown>> | null {
|
|
176
|
+
return tryConvertToList(this.value, options)?.rows ?? null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Wrap a raw tool response with the high-level SDK result API. */
|
|
181
|
+
export function createToolCallResult(value: unknown): ToolCallResult {
|
|
182
|
+
return new DeeplineToolCallResult(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Traverse a nested object by a dotted path string.
|
|
187
|
+
*
|
|
188
|
+
* @param root - Object to traverse
|
|
189
|
+
* @param dottedPath - Path like `"result.data.items"`
|
|
190
|
+
* @returns Value at the path, or `null` if not found
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* getByDottedPath({ a: { b: { c: 42 } } }, 'a.b.c') // 42
|
|
195
|
+
* getByDottedPath({ a: 1 }, 'a.b.c') // null
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
function getByDottedPath(root: unknown, dottedPath: string): unknown {
|
|
199
|
+
let current = root;
|
|
200
|
+
for (const segment of String(dottedPath || '').split('.').filter(Boolean)) {
|
|
201
|
+
if (!isPlainObject(current) || !(segment in current)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
current = current[segment];
|
|
205
|
+
}
|
|
206
|
+
return current;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Normalize an array value to an array of record objects.
|
|
211
|
+
* Non-object entries are wrapped as `{ value: <entry> }`.
|
|
212
|
+
*/
|
|
213
|
+
function normalizeRows(value: unknown): Array<Record<string, unknown>> | null {
|
|
214
|
+
if (!Array.isArray(value)) return null;
|
|
215
|
+
return value.map((entry) => {
|
|
216
|
+
if (isPlainObject(entry)) return entry;
|
|
217
|
+
return { value: entry };
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate candidate root objects to search for lists.
|
|
223
|
+
* Tries: raw payload → payload.result → payload.result.data.
|
|
224
|
+
*/
|
|
225
|
+
function candidateRoots(payload: unknown): Array<{ path: string | null; value: unknown }> {
|
|
226
|
+
const roots: Array<{ path: string | null; value: unknown }> = [{ path: null, value: payload }];
|
|
227
|
+
if (isPlainObject(payload) && isPlainObject(payload.result)) {
|
|
228
|
+
roots.push({ path: 'result', value: payload.result });
|
|
229
|
+
if (isPlainObject(payload.result.data)) {
|
|
230
|
+
roots.push({ path: 'result.data', value: payload.result.data });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return roots;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Recursively search for the largest array of objects in a nested structure.
|
|
238
|
+
* Depth-limited to 5 levels. Prefers arrays with real object entries
|
|
239
|
+
* (not just `{ value: ... }` wrappers).
|
|
240
|
+
*/
|
|
241
|
+
function findBestArrayCandidate(
|
|
242
|
+
value: unknown,
|
|
243
|
+
pathPrefix = '',
|
|
244
|
+
depth = 0,
|
|
245
|
+
): { path: string; rows: Array<Record<string, unknown>> } | null {
|
|
246
|
+
if (depth > 5) return null;
|
|
247
|
+
|
|
248
|
+
const directRows = normalizeRows(value);
|
|
249
|
+
const hasObjectRow = directRows?.some((row) => Object.keys(row).some((key) => key !== 'value')) ?? false;
|
|
250
|
+
let best: { path: string; rows: Array<Record<string, unknown>> } | null =
|
|
251
|
+
directRows && directRows.length > 0 && hasObjectRow
|
|
252
|
+
? { path: pathPrefix, rows: directRows }
|
|
253
|
+
: null;
|
|
254
|
+
|
|
255
|
+
if (!isPlainObject(value)) {
|
|
256
|
+
return best;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const [key, child] of Object.entries(value)) {
|
|
260
|
+
const childPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
261
|
+
const candidate = findBestArrayCandidate(child, childPath, depth + 1);
|
|
262
|
+
if (!candidate) continue;
|
|
263
|
+
if (!best || candidate.rows.length > best.rows.length) {
|
|
264
|
+
best = candidate;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return best;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Extract a list of records from a tool response.
|
|
273
|
+
*
|
|
274
|
+
* Handles the common problem of tools returning data in varied shapes.
|
|
275
|
+
* First tries configured `listExtractorPaths` (from tool metadata), then
|
|
276
|
+
* falls back to automatic detection via recursive DFS.
|
|
277
|
+
*
|
|
278
|
+
* ## Extraction strategy
|
|
279
|
+
*
|
|
280
|
+
* 1. **Configured paths** — If `listExtractorPaths` is provided, each path is
|
|
281
|
+
* tried against multiple candidate roots (raw payload, `.result`, `.result.data`).
|
|
282
|
+
* First match wins.
|
|
283
|
+
*
|
|
284
|
+
* 2. **Auto-detection** — If no configured path matches, recursively searches
|
|
285
|
+
* the response for the largest array of objects (up to depth 5).
|
|
286
|
+
*
|
|
287
|
+
* @param payload - Raw tool response
|
|
288
|
+
* @param options - Optional extraction configuration
|
|
289
|
+
* @returns Extracted list with metadata, or `null` if no list found
|
|
290
|
+
*
|
|
291
|
+
* @example Using configured paths (from tool metadata)
|
|
292
|
+
* ```typescript
|
|
293
|
+
* const meta = await client.getTool('apollo_people_search');
|
|
294
|
+
* const result = await client.executeTool('apollo_people_search', { query: 'cto' });
|
|
295
|
+
*
|
|
296
|
+
* const list = tryConvertToList(result, {
|
|
297
|
+
* listExtractorPaths: meta.listExtractorPaths,
|
|
298
|
+
* });
|
|
299
|
+
* if (list) {
|
|
300
|
+
* console.log(`${list.rows.length} people found via ${list.strategy}`);
|
|
301
|
+
* // Write to CSV
|
|
302
|
+
* const csv = writeCsvOutputFile(list.rows, 'apollo-people');
|
|
303
|
+
* console.log(`Saved to ${csv.path}`);
|
|
304
|
+
* }
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* @example Auto-detection (no configured paths)
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const result = await client.executeTool('some_tool', { query: 'test' });
|
|
310
|
+
* const list = tryConvertToList(result);
|
|
311
|
+
* // Finds the largest array of objects anywhere in the response
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export function tryConvertToList(
|
|
315
|
+
payload: unknown,
|
|
316
|
+
options?: { listExtractorPaths?: string[] },
|
|
317
|
+
): ListConversionResult | null {
|
|
318
|
+
const listExtractorPaths = Array.isArray(options?.listExtractorPaths)
|
|
319
|
+
? options?.listExtractorPaths.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
320
|
+
: [];
|
|
321
|
+
|
|
322
|
+
if (listExtractorPaths.length > 0) {
|
|
323
|
+
for (const root of candidateRoots(payload)) {
|
|
324
|
+
for (const extractorPath of listExtractorPaths) {
|
|
325
|
+
const resolved = getByDottedPath(root.value, extractorPath);
|
|
326
|
+
const rows = normalizeRows(resolved);
|
|
327
|
+
if (rows && rows.length > 0) {
|
|
328
|
+
const sourcePath = root.path ? `${root.path}.${extractorPath}` : extractorPath;
|
|
329
|
+
return { rows, strategy: 'configured_paths', sourcePath };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const root of candidateRoots(payload)) {
|
|
336
|
+
const candidate = findBestArrayCandidate(root.value, root.path ?? '');
|
|
337
|
+
if (!candidate || candidate.rows.length === 0) continue;
|
|
338
|
+
return {
|
|
339
|
+
rows: candidate.rows,
|
|
340
|
+
strategy: 'auto_detected',
|
|
341
|
+
sourcePath: candidate.path || root.path,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Ensure the shared output directory exists. Returns its path. */
|
|
349
|
+
function ensureOutputDir(): string {
|
|
350
|
+
const outputDir = join(homedir(), '.local', 'share', 'deepline', 'data');
|
|
351
|
+
mkdirSync(outputDir, { recursive: true });
|
|
352
|
+
return outputDir;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Write a JSON payload to a timestamped file.
|
|
357
|
+
*
|
|
358
|
+
* Output location: `~/.local/share/deepline/data/{stem}_{timestamp}.json`
|
|
359
|
+
*
|
|
360
|
+
* @param payload - Any JSON-serializable value
|
|
361
|
+
* @param stem - Filename prefix (e.g. tool ID or play name)
|
|
362
|
+
* @returns Absolute path to the written file
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* const result = await client.executeTool('test_company_search', { domain: 'stripe.com' });
|
|
367
|
+
* const path = writeJsonOutputFile(result, 'test_company_search');
|
|
368
|
+
* console.log(`Saved to ${path}`);
|
|
369
|
+
* // ~/.local/share/deepline/data/test_company_search_1713456789000.json
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
export function writeJsonOutputFile(payload: unknown, stem: string): string {
|
|
373
|
+
const outputDir = ensureOutputDir();
|
|
374
|
+
const outputPath = join(outputDir, `${stem}_${Date.now()}.json`);
|
|
375
|
+
writeFileSync(outputPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
376
|
+
return outputPath;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Write an array of records to a CSV file.
|
|
381
|
+
*
|
|
382
|
+
* Columns are ordered by first appearance across all rows. Cells containing
|
|
383
|
+
* commas, quotes, or newlines are properly escaped. Objects and arrays are
|
|
384
|
+
* JSON-serialized.
|
|
385
|
+
*
|
|
386
|
+
* Output location: `~/.local/share/deepline/data/{stem}_{timestamp}.csv`
|
|
387
|
+
*
|
|
388
|
+
* @param rows - Array of record objects
|
|
389
|
+
* @param stem - Filename prefix
|
|
390
|
+
* @returns File metadata including path, row count, columns, and a 5×5 preview
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* const list = tryConvertToList(toolResponse);
|
|
395
|
+
* if (list) {
|
|
396
|
+
* const csv = writeCsvOutputFile(list.rows, 'search-results');
|
|
397
|
+
* console.log(`Wrote ${csv.rowCount} rows, ${csv.columns.length} columns`);
|
|
398
|
+
* console.log(`File: ${csv.path}`);
|
|
399
|
+
* console.log(`Preview:\n${csv.preview}`);
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
export function writeCsvOutputFile(
|
|
404
|
+
rows: Array<Record<string, unknown>>,
|
|
405
|
+
stem: string,
|
|
406
|
+
): { path: string; rowCount: number; columns: string[]; preview: string } {
|
|
407
|
+
const outputDir = ensureOutputDir();
|
|
408
|
+
const outputPath = join(outputDir, `${stem}_${Date.now()}.csv`);
|
|
409
|
+
const seen = new Set<string>();
|
|
410
|
+
const columns: string[] = [];
|
|
411
|
+
for (const row of rows) {
|
|
412
|
+
for (const key of Object.keys(row)) {
|
|
413
|
+
if (!seen.has(key)) {
|
|
414
|
+
seen.add(key);
|
|
415
|
+
columns.push(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const escapeCell = (value: unknown): string => {
|
|
421
|
+
const normalized = value == null
|
|
422
|
+
? ''
|
|
423
|
+
: typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
424
|
+
? String(value)
|
|
425
|
+
: JSON.stringify(value);
|
|
426
|
+
if (/[",\n]/.test(normalized)) {
|
|
427
|
+
return `"${normalized.replace(/"/g, '""')}"`;
|
|
428
|
+
}
|
|
429
|
+
return normalized;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const lines: string[] = [];
|
|
433
|
+
lines.push(columns.map(escapeCell).join(','));
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
lines.push(columns.map((column) => escapeCell(row[column])).join(','));
|
|
436
|
+
}
|
|
437
|
+
writeFileSync(outputPath, `${lines.join('\n')}\n`, 'utf-8');
|
|
438
|
+
|
|
439
|
+
const previewRows = rows.slice(0, 5);
|
|
440
|
+
const previewColumns = columns.slice(0, 5);
|
|
441
|
+
const preview = [
|
|
442
|
+
previewColumns.join(','),
|
|
443
|
+
...previewRows.map((row) =>
|
|
444
|
+
previewColumns.map((column) => escapeCell(row[column])).join(',')),
|
|
445
|
+
].join('\n');
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
path: outputPath,
|
|
449
|
+
rowCount: rows.length,
|
|
450
|
+
columns,
|
|
451
|
+
preview,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Extract scalar (non-nested) fields from a tool response for summary display.
|
|
457
|
+
*
|
|
458
|
+
* Searches through candidate roots (raw → `.result` → `.result.data`) and
|
|
459
|
+
* returns the first set of scalar fields found. Useful for displaying a
|
|
460
|
+
* quick summary of single-record responses.
|
|
461
|
+
*
|
|
462
|
+
* @param payload - Raw tool response
|
|
463
|
+
* @returns Object containing only scalar fields (string, number, boolean, null)
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```typescript
|
|
467
|
+
* const result = await client.executeTool('test_company_search', { domain: 'stripe.com' });
|
|
468
|
+
* const summary = extractSummaryFields(result);
|
|
469
|
+
* // { name: "Stripe", industry: "Financial Services", employeeCount: 8000 }
|
|
470
|
+
* // (nested objects and arrays are excluded)
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
export function extractSummaryFields(payload: unknown): Record<string, Scalar> {
|
|
474
|
+
const candidates = candidateRoots(payload);
|
|
475
|
+
for (const candidate of candidates) {
|
|
476
|
+
if (!isPlainObject(candidate.value)) continue;
|
|
477
|
+
const summaryEntries = Object.entries(candidate.value).filter(([, value]) => {
|
|
478
|
+
return (
|
|
479
|
+
value == null ||
|
|
480
|
+
typeof value === 'string' ||
|
|
481
|
+
typeof value === 'number' ||
|
|
482
|
+
typeof value === 'boolean'
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
if (summaryEntries.length === 0) continue;
|
|
486
|
+
return Object.fromEntries(summaryEntries) as Record<string, Scalar>;
|
|
487
|
+
}
|
|
488
|
+
return {};
|
|
489
|
+
}
|