@trydreamstate/cli 1.3.2 → 1.3.3
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 +29 -16
- package/agent-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/hooks/approve-readonly.sh +15 -0
- package/agent-plugin/hooks/hooks.json +17 -0
- package/agent-plugin/skills/crm/SKILL.md +2 -2
- package/agent-plugin/skills/diagnostics/SKILL.md +1 -1
- package/agent-plugin/skills/dreamstate/SKILL.md +13 -5
- package/agent-plugin/skills/first-win/SKILL.md +2 -2
- package/agent-plugin/skills/inbox-replies/SKILL.md +1 -1
- package/agent-plugin/skills/knowledge/SKILL.md +1 -1
- package/agent-plugin/skills/linkedin-posts/SKILL.md +1 -1
- package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
- package/agent-plugin/skills/list-building/SKILL.md +4 -4
- package/agent-plugin/skills/outreach/SKILL.md +1 -1
- package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
- package/agent-plugin/skills/plays/SKILL.md +2 -2
- package/agent-plugin/skills/reddit-posts/SKILL.md +1 -1
- package/agent-plugin/skills/skill-authoring/SKILL.md +1 -1
- package/agent-plugin/skills/social/SKILL.md +2 -2
- package/agent-plugin/skills/visibility/SKILL.md +2 -2
- package/agent-plugin/skills/workflows/SKILL.md +3 -3
- package/agent-plugin/skills/worksheet-tidy/SKILL.md +1 -1
- package/agent-plugin/skills/writing/SKILL.md +1 -1
- package/agent-plugin/skills/x-posts/SKILL.md +1 -1
- package/dist/agentGuidance.generated.json +35 -0
- package/dist/client.d.ts +31 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +115 -33
- package/dist/commandTable.d.ts +26 -0
- package/dist/commandTable.d.ts.map +1 -0
- package/dist/commandTable.js +54 -0
- package/dist/commands/sequences.d.ts +1 -1
- package/dist/commands/sequences.d.ts.map +1 -1
- package/dist/commands/sequences.js +37 -36
- package/dist/commands/shared.d.ts +4 -26
- package/dist/commands/shared.d.ts.map +1 -1
- package/dist/commands/shared.js +129 -127
- package/dist/commands/workflows.d.ts +1 -1
- package/dist/commands/workflows.d.ts.map +1 -1
- package/dist/commands/workflows.js +35 -41
- package/dist/errors.d.ts +78 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +117 -0
- package/dist/gatedRun.d.ts +81 -0
- package/dist/gatedRun.d.ts.map +1 -0
- package/dist/gatedRun.js +370 -0
- package/dist/idempotency.d.ts +38 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +106 -0
- package/dist/index.d.ts +21 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +372 -366
- package/dist/runWait.d.ts +2 -1
- package/dist/runWait.d.ts.map +1 -1
- package/dist/runWait.js +3 -1
- package/dist/schemaCoercion.d.ts +20 -0
- package/dist/schemaCoercion.d.ts.map +1 -0
- package/dist/schemaCoercion.js +123 -0
- package/dist/skillInstaller.d.ts +40 -0
- package/dist/skillInstaller.d.ts.map +1 -1
- package/dist/skillInstaller.js +108 -9
- package/dist/tableFilePreview.d.ts +14 -0
- package/dist/tableFilePreview.d.ts.map +1 -1
- package/dist/tableFilePreview.js +19 -8
- package/dist/tableFileReader.d.ts +26 -0
- package/dist/tableFileReader.d.ts.map +1 -0
- package/dist/tableFileReader.js +108 -0
- package/dist/tablesImport.d.ts +57 -0
- package/dist/tablesImport.d.ts.map +1 -0
- package/dist/tablesImport.js +263 -0
- package/dist/turnCache.d.ts +14 -1
- package/dist/turnCache.d.ts.map +1 -1
- package/dist/turnCache.js +25 -8
- package/package.json +3 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_IMPORT_FILE_BYTES = void 0;
|
|
4
|
+
exports.stagingHeaderKeys = stagingHeaderKeys;
|
|
5
|
+
exports.readLocalTableFile = readLocalTableFile;
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const tableFilePreview_1 = require("./tableFilePreview");
|
|
8
|
+
/** Largest file an import reads into memory. The server caps one upload at 200,000 rows. */
|
|
9
|
+
exports.MAX_IMPORT_FILE_BYTES = 250_000_000;
|
|
10
|
+
/**
|
|
11
|
+
* Duplicate CSV labels cannot be object keys twice, so they get private
|
|
12
|
+
* staging keys; the column definitions keep the labels. Same rule as
|
|
13
|
+
* csvStagingHeaderKeys in packages/types (the server's own parser), pinned by
|
|
14
|
+
* tableFileReader.test.ts against that function.
|
|
15
|
+
*/
|
|
16
|
+
function stagingHeaderKeys(headers) {
|
|
17
|
+
const counts = new Map();
|
|
18
|
+
for (const header of headers)
|
|
19
|
+
counts.set(header, (counts.get(header) ?? 0) + 1);
|
|
20
|
+
const reserved = new Set(headers);
|
|
21
|
+
const used = new Set();
|
|
22
|
+
return headers.map((header, index) => {
|
|
23
|
+
let key = counts.get(header) === 1 ? header : `__csv_column_${index + 1}`;
|
|
24
|
+
while (used.has(key) || (key !== header && reserved.has(key)))
|
|
25
|
+
key = `${key}_`;
|
|
26
|
+
used.add(key);
|
|
27
|
+
return key;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function tabularString(value) {
|
|
31
|
+
if (value === null || value === undefined)
|
|
32
|
+
return '';
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
36
|
+
return String(value);
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
function fromGrid(kind, grid) {
|
|
40
|
+
const [rawHeaders, ...dataRows] = grid;
|
|
41
|
+
const headers = (rawHeaders ?? []).map((value) => value.trim());
|
|
42
|
+
if (headers.length === 0 || headers.every((header) => header.length === 0))
|
|
43
|
+
throw new Error('File has no header row');
|
|
44
|
+
const stagingHeaders = stagingHeaderKeys(headers);
|
|
45
|
+
return {
|
|
46
|
+
file_kind: kind,
|
|
47
|
+
headers,
|
|
48
|
+
rows: dataRows
|
|
49
|
+
.filter((values) => values.some(Boolean))
|
|
50
|
+
.map((values, index) => ({
|
|
51
|
+
source_row_number: index + 2,
|
|
52
|
+
data: Object.fromEntries(stagingHeaders.map((header, column) => [header, values[column] ?? ''])),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function fromObjects(kind, records) {
|
|
57
|
+
if (!records.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
|
|
58
|
+
throw new Error('JSON import must contain only object rows');
|
|
59
|
+
}
|
|
60
|
+
const headers = [...new Set(records.flatMap((row) => Object.keys(row)))];
|
|
61
|
+
return {
|
|
62
|
+
file_kind: kind,
|
|
63
|
+
headers,
|
|
64
|
+
rows: records.map((row, index) => ({
|
|
65
|
+
source_row_number: index + 1,
|
|
66
|
+
data: Object.fromEntries(headers.map((header) => [header, tabularString(row[header])])),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Every row of a local CSV, JSON, JSONL or XLSX file, shaped for csv_upload_stage. */
|
|
71
|
+
async function readLocalTableFile(filePath, format) {
|
|
72
|
+
const kind = (0, tableFilePreview_1.inferLocalTableFileKind)(filePath, format);
|
|
73
|
+
const bytes = await (0, promises_1.readFile)(filePath);
|
|
74
|
+
if (bytes.length > exports.MAX_IMPORT_FILE_BYTES)
|
|
75
|
+
throw new Error(`File is larger than ${exports.MAX_IMPORT_FILE_BYTES / 1_000_000}MB; split it before importing.`);
|
|
76
|
+
if (kind === 'xlsx')
|
|
77
|
+
return { file: fromGrid('xlsx', (0, tableFilePreview_1.readXlsxRows)(bytes, null).rows), bytes };
|
|
78
|
+
const text = bytes.toString('utf8').replace(/^/, '');
|
|
79
|
+
if (kind === 'json') {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(text);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error(`JSON import could not parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(parsed))
|
|
88
|
+
return { file: fromObjects('json', parsed), bytes };
|
|
89
|
+
if (parsed && typeof parsed === 'object')
|
|
90
|
+
return { file: fromObjects('json', [parsed]), bytes };
|
|
91
|
+
throw new Error('JSON import expects an object or an array of objects');
|
|
92
|
+
}
|
|
93
|
+
if (kind === 'jsonl') {
|
|
94
|
+
const records = text.split(/\r?\n/).filter((line) => line.trim()).map((line, index) => {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(`JSONL line ${index + 1} is not valid JSON`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return { file: fromObjects('jsonl', records), bytes };
|
|
103
|
+
}
|
|
104
|
+
const parsed = (0, tableFilePreview_1.parseCsv)(text);
|
|
105
|
+
if (parsed.unterminatedQuote)
|
|
106
|
+
throw new Error('CSV contains an unterminated quoted field');
|
|
107
|
+
return { file: fromGrid('csv', parsed.rows), bytes };
|
|
108
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type GatedRunContext } from './gatedRun';
|
|
2
|
+
/**
|
|
3
|
+
* `dreamstate tables import`: the real import. The old command ran
|
|
4
|
+
* tables.table_sources.csv_import_prepare, which only returns the text of a
|
|
5
|
+
* CLI command, and printed it: nothing was ever uploaded. This follows the
|
|
6
|
+
* lifecycle the web app uses (apps/frontend/src/components/worksheet/
|
|
7
|
+
* CsvSourceUploadPanel.tsx), one capability per step, through the same door:
|
|
8
|
+
*
|
|
9
|
+
* csv_upload_create open an upload receipt from the header row
|
|
10
|
+
* csv_upload_map choose which header feeds which field, and the identity column
|
|
11
|
+
* csv_upload_stage send the rows in chunks of at most 5,000
|
|
12
|
+
* csv_upload_attach turn the upload into a live source.csv source
|
|
13
|
+
* csv_upload_land_chunk land staged rows as worksheet rows until done
|
|
14
|
+
*
|
|
15
|
+
* Wire shapes come from packages/types/src/tableApi.ts (csvUploadCreate ...
|
|
16
|
+
* csvUploadLandChunkOutput) and the server dispatch in tableCanonicalApiService.ts.
|
|
17
|
+
*/
|
|
18
|
+
export declare const STAGE_CHUNK_ROWS = 5000;
|
|
19
|
+
/** One staged chunk stays under this size on the wire, whatever the row count. */
|
|
20
|
+
export declare const STAGE_CHUNK_BYTES = 8000000;
|
|
21
|
+
/** Matches DEFAULT_LAND_CHUNK in csvSourceUpload.ts and the web app. */
|
|
22
|
+
export declare const LAND_CHUNK_ROWS = 250;
|
|
23
|
+
/** csvSourceUpload.ts MAX_ROWS_PER_UPLOAD. Refused here before any request. */
|
|
24
|
+
export declare const MAX_ROWS_PER_UPLOAD = 200000;
|
|
25
|
+
export interface TablesImportOptions {
|
|
26
|
+
tableId: string;
|
|
27
|
+
file: string;
|
|
28
|
+
format?: 'csv' | 'json' | 'jsonl' | 'xlsx';
|
|
29
|
+
upsertKey?: string;
|
|
30
|
+
rowKind?: 'person' | 'company';
|
|
31
|
+
keepUnmappedColumns?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface TablesImportResult {
|
|
34
|
+
table_id: string;
|
|
35
|
+
upload_id: string;
|
|
36
|
+
source_id: string;
|
|
37
|
+
file_name: string;
|
|
38
|
+
file_kind: string;
|
|
39
|
+
row_kind: 'person' | 'company';
|
|
40
|
+
rows_total: number;
|
|
41
|
+
rows_staged: number;
|
|
42
|
+
rows_landed: number;
|
|
43
|
+
rows_skipped: number;
|
|
44
|
+
mapping: Record<string, string>;
|
|
45
|
+
identity_column: string;
|
|
46
|
+
land_calls: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The outputs of a capability run, whichever envelope nesting the door used. A
|
|
50
|
+
* run that ended failed answers HTTP 200 with the failure in the run, so it is
|
|
51
|
+
* raised here as an error: an import must never read a failed step as success.
|
|
52
|
+
*/
|
|
53
|
+
export declare function capabilityOutputs(result: unknown): Record<string, unknown>;
|
|
54
|
+
/** Header the rows are matched on across re-imports. An explicit --upsert-key wins; otherwise the strongest mapped field. */
|
|
55
|
+
export declare function chooseIdentityColumn(headers: readonly string[], mapping: Readonly<Record<string, string>>, rowKind: 'person' | 'company'): string | null;
|
|
56
|
+
export declare function importLocalTable(context: GatedRunContext, options: TablesImportOptions): Promise<TablesImportResult>;
|
|
57
|
+
//# sourceMappingURL=tablesImport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tablesImport.d.ts","sourceRoot":"","sources":["../src/tablesImport.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlE;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAC3C,wEAAwE;AACxE,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAY3C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoB1E;AAMD,6HAA6H;AAC7H,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACzC,OAAO,EAAE,QAAQ,GAAG,SAAS,GAC5B,MAAM,GAAG,IAAI,CAMf;AAkCD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiJ1H"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_ROWS_PER_UPLOAD = exports.LAND_CHUNK_ROWS = exports.STAGE_CHUNK_BYTES = exports.STAGE_CHUNK_ROWS = void 0;
|
|
4
|
+
exports.capabilityOutputs = capabilityOutputs;
|
|
5
|
+
exports.chooseIdentityColumn = chooseIdentityColumn;
|
|
6
|
+
exports.importLocalTable = importLocalTable;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const client_1 = require("./client");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const gatedRun_1 = require("./gatedRun");
|
|
12
|
+
const idempotency_1 = require("./idempotency");
|
|
13
|
+
const tableFileReader_1 = require("./tableFileReader");
|
|
14
|
+
/**
|
|
15
|
+
* `dreamstate tables import`: the real import. The old command ran
|
|
16
|
+
* tables.table_sources.csv_import_prepare, which only returns the text of a
|
|
17
|
+
* CLI command, and printed it: nothing was ever uploaded. This follows the
|
|
18
|
+
* lifecycle the web app uses (apps/frontend/src/components/worksheet/
|
|
19
|
+
* CsvSourceUploadPanel.tsx), one capability per step, through the same door:
|
|
20
|
+
*
|
|
21
|
+
* csv_upload_create open an upload receipt from the header row
|
|
22
|
+
* csv_upload_map choose which header feeds which field, and the identity column
|
|
23
|
+
* csv_upload_stage send the rows in chunks of at most 5,000
|
|
24
|
+
* csv_upload_attach turn the upload into a live source.csv source
|
|
25
|
+
* csv_upload_land_chunk land staged rows as worksheet rows until done
|
|
26
|
+
*
|
|
27
|
+
* Wire shapes come from packages/types/src/tableApi.ts (csvUploadCreate ...
|
|
28
|
+
* csvUploadLandChunkOutput) and the server dispatch in tableCanonicalApiService.ts.
|
|
29
|
+
*/
|
|
30
|
+
exports.STAGE_CHUNK_ROWS = 5_000;
|
|
31
|
+
/** One staged chunk stays under this size on the wire, whatever the row count. */
|
|
32
|
+
exports.STAGE_CHUNK_BYTES = 8_000_000;
|
|
33
|
+
/** Matches DEFAULT_LAND_CHUNK in csvSourceUpload.ts and the web app. */
|
|
34
|
+
exports.LAND_CHUNK_ROWS = 250;
|
|
35
|
+
/** csvSourceUpload.ts MAX_ROWS_PER_UPLOAD. Refused here before any request. */
|
|
36
|
+
exports.MAX_ROWS_PER_UPLOAD = 200_000;
|
|
37
|
+
const MAX_HEADERS = 200;
|
|
38
|
+
/** The server's turn lives 10 minutes; stay well inside it. */
|
|
39
|
+
const TURN_REUSE_WINDOW_MS = 8 * 60_000;
|
|
40
|
+
/** A land loop that never reports done is a server fault, not a file this size. */
|
|
41
|
+
const MAX_LAND_CALLS = 10_000;
|
|
42
|
+
const IDENTITY_PREFERENCE = {
|
|
43
|
+
person: ['email', 'linkedin_url', 'external_id'],
|
|
44
|
+
company: ['domain', 'linkedin_url', 'external_id'],
|
|
45
|
+
};
|
|
46
|
+
function record(value) {
|
|
47
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
48
|
+
}
|
|
49
|
+
const FAILED_STATUSES = new Set(['failed', 'cancelled', 'canceled', 'timed_out', 'rejected']);
|
|
50
|
+
/**
|
|
51
|
+
* The outputs of a capability run, whichever envelope nesting the door used. A
|
|
52
|
+
* run that ended failed answers HTTP 200 with the failure in the run, so it is
|
|
53
|
+
* raised here as an error: an import must never read a failed step as success.
|
|
54
|
+
*/
|
|
55
|
+
function capabilityOutputs(result) {
|
|
56
|
+
const data = record(record(result)?.data);
|
|
57
|
+
const run = record(data?.run);
|
|
58
|
+
if (run) {
|
|
59
|
+
const steps = Array.isArray(run.steps) ? run.steps : Array.isArray(run.completed) ? run.completed : [];
|
|
60
|
+
const stepError = steps.map((step) => record(record(step)?.user_error)).find(Boolean) ?? record(run.user_error);
|
|
61
|
+
if (typeof run.status === 'string' && FAILED_STATUSES.has(run.status)) {
|
|
62
|
+
throw new client_1.CliError(typeof stepError?.message === 'string' ? stepError.message : `The run ${run.status}.`, typeof stepError?.code === 'string' ? stepError.code : 'run_failed', 0, { details: stepError ? { user_error: stepError } : undefined, retryable: false });
|
|
63
|
+
}
|
|
64
|
+
const outputs = steps.map((step) => record(record(step)?.outputs)).find(Boolean);
|
|
65
|
+
if (outputs)
|
|
66
|
+
return outputs;
|
|
67
|
+
const direct = record(run.outputs);
|
|
68
|
+
if (direct)
|
|
69
|
+
return direct;
|
|
70
|
+
}
|
|
71
|
+
return record(data?.outputs) ?? data ?? {};
|
|
72
|
+
}
|
|
73
|
+
function importIdentity(parts) {
|
|
74
|
+
return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 24);
|
|
75
|
+
}
|
|
76
|
+
/** Header the rows are matched on across re-imports. An explicit --upsert-key wins; otherwise the strongest mapped field. */
|
|
77
|
+
function chooseIdentityColumn(headers, mapping, rowKind) {
|
|
78
|
+
for (const field of IDENTITY_PREFERENCE[rowKind]) {
|
|
79
|
+
const header = headers.find((candidate) => mapping[candidate] === field);
|
|
80
|
+
if (header)
|
|
81
|
+
return header;
|
|
82
|
+
}
|
|
83
|
+
return headers.find((header) => mapping[header]) ?? null;
|
|
84
|
+
}
|
|
85
|
+
function chunkRows(rows) {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let current = [];
|
|
88
|
+
let bytes = 0;
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const size = JSON.stringify(row).length;
|
|
91
|
+
if (current.length && (current.length >= exports.STAGE_CHUNK_ROWS || bytes + size > exports.STAGE_CHUNK_BYTES)) {
|
|
92
|
+
chunks.push(current);
|
|
93
|
+
current = [];
|
|
94
|
+
bytes = 0;
|
|
95
|
+
}
|
|
96
|
+
current.push(row);
|
|
97
|
+
bytes += size;
|
|
98
|
+
}
|
|
99
|
+
if (current.length)
|
|
100
|
+
chunks.push(current);
|
|
101
|
+
return chunks;
|
|
102
|
+
}
|
|
103
|
+
function withStep(error, step, uploadId) {
|
|
104
|
+
if (!(error instanceof client_1.CliError))
|
|
105
|
+
return error;
|
|
106
|
+
const details = record(error.details) ?? {};
|
|
107
|
+
return new client_1.CliError(error.message, error.code, error.status, {
|
|
108
|
+
details: { ...details, import_step: step, ...(uploadId ? { upload_id: uploadId } : {}) },
|
|
109
|
+
...(error.retryAfter === undefined ? {} : { retryAfter: error.retryAfter }),
|
|
110
|
+
...(error.traceId === undefined ? {} : { traceId: error.traceId }),
|
|
111
|
+
...(error.idempotencyKey === undefined ? {} : { idempotencyKey: error.idempotencyKey }),
|
|
112
|
+
...(error.retryable === undefined ? {} : { retryable: error.retryable }),
|
|
113
|
+
...(error.repair === undefined ? {} : { repair: error.repair }),
|
|
114
|
+
...(error.meta === undefined ? {} : { meta: error.meta }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function importLocalTable(context, options) {
|
|
118
|
+
const rowKind = options.rowKind ?? 'person';
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = await (0, tableFileReader_1.readLocalTableFile)(options.file, options.format);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new errors_1.CliUsageError(error instanceof Error ? error.message : String(error));
|
|
125
|
+
}
|
|
126
|
+
const { file, bytes } = parsed;
|
|
127
|
+
if (file.rows.length === 0)
|
|
128
|
+
throw new errors_1.CliUsageError('The file has a header row but no data rows; nothing to import.');
|
|
129
|
+
if (file.rows.length > exports.MAX_ROWS_PER_UPLOAD) {
|
|
130
|
+
throw new errors_1.CliUsageError(`The file has ${file.rows.length} rows; one upload takes at most ${exports.MAX_ROWS_PER_UPLOAD}. Split it and import the parts.`);
|
|
131
|
+
}
|
|
132
|
+
if (file.headers.length > MAX_HEADERS)
|
|
133
|
+
throw new errors_1.CliUsageError(`The file has ${file.headers.length} columns; one upload takes at most ${MAX_HEADERS}.`);
|
|
134
|
+
const blank = file.headers.findIndex((header) => !header);
|
|
135
|
+
if (blank >= 0)
|
|
136
|
+
throw new errors_1.CliUsageError(`Column ${blank + 1} has no header name. Name every column before importing.`);
|
|
137
|
+
const fileName = (0, node_path_1.basename)(options.file);
|
|
138
|
+
const keepUnmappedColumns = options.keepUnmappedColumns !== false;
|
|
139
|
+
const importId = importIdentity({
|
|
140
|
+
table_id: options.tableId,
|
|
141
|
+
sha256: (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex'),
|
|
142
|
+
file_name: fileName,
|
|
143
|
+
row_kind: rowKind,
|
|
144
|
+
keep_unmapped_columns: keepUnmappedColumns,
|
|
145
|
+
upsert_key: options.upsertKey ?? null,
|
|
146
|
+
});
|
|
147
|
+
const key = (...parts) => (0, idempotency_1.deterministicIdempotencyKey)({
|
|
148
|
+
scope: 'tables.import',
|
|
149
|
+
workspace: context.workspace,
|
|
150
|
+
body: { import_id: importId, step: parts },
|
|
151
|
+
});
|
|
152
|
+
// Landing makes hundreds of calls to one capability. One tools get per
|
|
153
|
+
// capability mints the turn; later calls reuse it inside the server's window.
|
|
154
|
+
const turns = new Map();
|
|
155
|
+
const step = async (capabilityId, params, idempotencyKey, stepName, uploadId) => {
|
|
156
|
+
const run = async (reuse) => {
|
|
157
|
+
const turn = turns.get(capabilityId);
|
|
158
|
+
return (0, gatedRun_1.runGated)(context, {
|
|
159
|
+
capabilityId,
|
|
160
|
+
body: { params, mode: 'live' },
|
|
161
|
+
idempotencyKey,
|
|
162
|
+
waitTimeout: 120,
|
|
163
|
+
...(reuse && turn && Date.now() - turn.at < TURN_REUSE_WINDOW_MS ? { turnToken: turn.token } : {}),
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
let outcome;
|
|
168
|
+
try {
|
|
169
|
+
outcome = await run(true);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if ((0, gatedRun_1.code)(error) !== 'tool_turn_expired' && (0, gatedRun_1.code)(error) !== 'capability_not_fetched')
|
|
173
|
+
throw error;
|
|
174
|
+
turns.delete(capabilityId);
|
|
175
|
+
outcome = await run(false);
|
|
176
|
+
}
|
|
177
|
+
if (outcome.turnToken && !turns.has(capabilityId))
|
|
178
|
+
turns.set(capabilityId, { token: outcome.turnToken, at: Date.now() });
|
|
179
|
+
return capabilityOutputs(outcome.value);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw withStep(error, stepName, uploadId);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const progress = (message) => context.stderr.write(`${message}\n`);
|
|
186
|
+
progress(`Importing ${file.rows.length} rows from ${fileName} into ${options.tableId}.`);
|
|
187
|
+
const created = await step('tables.table_sources.csv_upload_create', {
|
|
188
|
+
table_id: options.tableId,
|
|
189
|
+
file_name: fileName,
|
|
190
|
+
headers: file.headers,
|
|
191
|
+
row_kind: rowKind,
|
|
192
|
+
keep_unmapped_columns: keepUnmappedColumns,
|
|
193
|
+
}, key('create'), 'create', null);
|
|
194
|
+
const uploadId = typeof created.upload_id === 'string' ? created.upload_id : null;
|
|
195
|
+
if (!uploadId)
|
|
196
|
+
throw new client_1.CliError('csv_upload_create returned no upload_id.', 'invalid_response', 0, { details: { import_step: 'create' } });
|
|
197
|
+
const mapping = { ...(record(created.suggested_mapping) ?? {}) };
|
|
198
|
+
if (options.upsertKey) {
|
|
199
|
+
if (!file.headers.includes(options.upsertKey)) {
|
|
200
|
+
throw new errors_1.CliUsageError(`--upsert-key "${options.upsertKey}" is not a column of ${fileName}. Columns: ${file.headers.join(', ')}.`);
|
|
201
|
+
}
|
|
202
|
+
if (!mapping[options.upsertKey]) {
|
|
203
|
+
if (Object.values(mapping).includes('external_id')) {
|
|
204
|
+
throw new errors_1.CliUsageError(`--upsert-key "${options.upsertKey}" is not mapped to a field and external_id is already used by another column. Rename the key column to a mappable name (email, linkedin_url, external_id).`);
|
|
205
|
+
}
|
|
206
|
+
mapping[options.upsertKey] = 'external_id';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const identityColumn = options.upsertKey ?? chooseIdentityColumn(file.headers, mapping, rowKind);
|
|
210
|
+
if (!identityColumn || Object.keys(mapping).length === 0) {
|
|
211
|
+
throw new errors_1.CliUsageError(`No column of ${fileName} could be matched to an identity field (email, linkedin_url, external_id${rowKind === 'company' ? ', domain' : ''}). The upload ${uploadId} was opened and stays unattached. Pass --upsert-key <column> to choose the identity column.`);
|
|
212
|
+
}
|
|
213
|
+
await step('tables.table_sources.csv_upload_map', {
|
|
214
|
+
upload_id: uploadId, mapping, identity_column: identityColumn,
|
|
215
|
+
}, key(uploadId, 'map'), 'map', uploadId);
|
|
216
|
+
const chunks = chunkRows(file.rows);
|
|
217
|
+
let staged = 0;
|
|
218
|
+
for (const [index, rows] of chunks.entries()) {
|
|
219
|
+
const result = await step('tables.table_sources.csv_upload_stage', { upload_id: uploadId, rows }, key(uploadId, 'stage', index), 'stage', uploadId);
|
|
220
|
+
staged += typeof result.staged === 'number' ? result.staged : rows.length;
|
|
221
|
+
progress(`Staged ${staged} of ${file.rows.length} rows.`);
|
|
222
|
+
}
|
|
223
|
+
const attached = await step('tables.table_sources.csv_upload_attach', {
|
|
224
|
+
table_id: options.tableId, upload_id: uploadId, source_label: fileName,
|
|
225
|
+
}, key(uploadId, 'attach'), 'attach', uploadId);
|
|
226
|
+
const sourceId = typeof attached.source_id === 'string' ? attached.source_id : null;
|
|
227
|
+
if (!sourceId)
|
|
228
|
+
throw new client_1.CliError('csv_upload_attach returned no source_id.', 'invalid_response', 0, { details: { import_step: 'attach', upload_id: uploadId } });
|
|
229
|
+
let landed = 0;
|
|
230
|
+
let skipped = 0;
|
|
231
|
+
let calls = 0;
|
|
232
|
+
for (;;) {
|
|
233
|
+
if (calls >= MAX_LAND_CALLS) {
|
|
234
|
+
throw new client_1.CliError(`Landing did not finish after ${MAX_LAND_CALLS} calls.`, 'run_failed', 0, { details: { import_step: 'land', upload_id: uploadId, source_id: sourceId } });
|
|
235
|
+
}
|
|
236
|
+
const chunk = await step('tables.table_sources.csv_upload_land_chunk', {
|
|
237
|
+
table_id: options.tableId, upload_id: uploadId, limit: exports.LAND_CHUNK_ROWS,
|
|
238
|
+
}, key(uploadId, 'land', calls), 'land', uploadId);
|
|
239
|
+
calls += 1;
|
|
240
|
+
landed += typeof chunk.landed === 'number' ? chunk.landed : 0;
|
|
241
|
+
skipped += typeof chunk.skipped === 'number' ? chunk.skipped : 0;
|
|
242
|
+
progress(`Landed ${landed} rows, ${typeof chunk.remaining === 'number' ? chunk.remaining : '?'} remaining.`);
|
|
243
|
+
if (chunk.done === true)
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
if (skipped > 0)
|
|
247
|
+
progress(`${skipped} rows had no ${identityColumn} value and were skipped.`);
|
|
248
|
+
return {
|
|
249
|
+
table_id: options.tableId,
|
|
250
|
+
upload_id: uploadId,
|
|
251
|
+
source_id: sourceId,
|
|
252
|
+
file_name: fileName,
|
|
253
|
+
file_kind: file.file_kind,
|
|
254
|
+
row_kind: rowKind,
|
|
255
|
+
rows_total: file.rows.length,
|
|
256
|
+
rows_staged: staged,
|
|
257
|
+
rows_landed: landed,
|
|
258
|
+
rows_skipped: skipped,
|
|
259
|
+
mapping,
|
|
260
|
+
identity_column: identityColumn,
|
|
261
|
+
land_calls: calls,
|
|
262
|
+
};
|
|
263
|
+
}
|
package/dist/turnCache.d.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
export declare const TURN_REUSE_MS: number;
|
|
2
|
+
/** The quote a dry run returned, and the exact input it priced. */
|
|
3
|
+
export type CachedQuote = {
|
|
4
|
+
quote_hash: string;
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
};
|
|
2
7
|
type CacheWarning = (error: NodeJS.ErrnoException) => void;
|
|
3
8
|
export declare function turnCachePath(credentialsPath: string): string;
|
|
4
9
|
export declare function reusableTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, now?: number, onWarning?: CacheWarning): Promise<string | null>;
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The cached turn together with the quote its dry run returned. The quote is
|
|
12
|
+
* handed back only when `fingerprint` matches the input it was minted for.
|
|
13
|
+
*/
|
|
14
|
+
export declare function reusableTurn(path: string, baseUrl: string, apiKey: string, capabilityId: string, fingerprint: string, now?: number, onWarning?: CacheWarning): Promise<{
|
|
15
|
+
token: string;
|
|
16
|
+
quote_hash: string | null;
|
|
17
|
+
} | null>;
|
|
18
|
+
export declare function rememberTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, token: string, now?: number, onWarning?: CacheWarning, quote?: CachedQuote): Promise<void>;
|
|
6
19
|
export declare function forgetTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, onWarning?: CacheWarning): Promise<void>;
|
|
7
20
|
export {};
|
|
8
21
|
//# sourceMappingURL=turnCache.d.ts.map
|
package/dist/turnCache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turnCache.d.ts","sourceRoot":"","sources":["../src/turnCache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turnCache.d.ts","sourceRoot":"","sources":["../src/turnCache.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,aAAa,QAAa,CAAC;AAExC,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAGtE,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,KAAK,IAAI,CAAC;AAE3D,wBAAgB,aAAa,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE7D;AA2BD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,SAAS,CAAC,EAAE,YAAY,GAC9G,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,SAAS,CAAC,EAAE,YAAY,GACnI,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAAC,CAO9D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,SAAS,CAAC,EAAE,YAAY,EAC9H,KAAK,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,YAAY,GAC5F,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
package/dist/turnCache.js
CHANGED
|
@@ -3,17 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TURN_REUSE_MS = void 0;
|
|
4
4
|
exports.turnCachePath = turnCachePath;
|
|
5
5
|
exports.reusableTurnToken = reusableTurnToken;
|
|
6
|
+
exports.reusableTurn = reusableTurn;
|
|
6
7
|
exports.rememberTurnToken = rememberTurnToken;
|
|
7
8
|
exports.forgetTurnToken = forgetTurnToken;
|
|
8
9
|
const node_crypto_1 = require("node:crypto");
|
|
9
10
|
const promises_1 = require("node:fs/promises");
|
|
10
11
|
const node_path_1 = require("node:path");
|
|
11
|
-
// A paid capability's
|
|
12
|
-
//
|
|
13
|
-
// the SAME token can spend. Each CLI
|
|
14
|
-
// a fresh token per run made every paid
|
|
15
|
-
// (2026-09-23: tables.table_sources.run
|
|
16
|
-
//
|
|
12
|
+
// A paid capability's live call needs the quote_hash of a dry_run of the same
|
|
13
|
+
// input, and the quote is bound to the dry run's tool_turn_token: only a live
|
|
14
|
+
// call carrying the SAME token and that quote_hash can spend. Each CLI
|
|
15
|
+
// invocation is a new process, so fetching a fresh token per run made every paid
|
|
16
|
+
// capability unrunnable from the CLI (2026-09-23: tables.table_sources.run
|
|
17
|
+
// refused four times in a row). The cache therefore keeps the dry run's turn
|
|
18
|
+
// token together with its quote_hash and a fingerprint of the exact input that
|
|
19
|
+
// was priced, so a later `--live` of the same input can echo it and a changed
|
|
20
|
+
// input never inherits a price it was not shown.
|
|
17
21
|
// The server turn lives 10 minutes; reuse for 9 so a cached token never
|
|
18
22
|
// expires mid-request.
|
|
19
23
|
exports.TURN_REUSE_MS = 9 * 60_000;
|
|
@@ -49,13 +53,26 @@ async function reusableTurnToken(path, baseUrl, apiKey, capabilityId, now = Date
|
|
|
49
53
|
const entry = (await load(path, onWarning))[entryKey(baseUrl, apiKey, capabilityId)];
|
|
50
54
|
return entry && typeof entry.token === 'string' && now - entry.at < exports.TURN_REUSE_MS ? entry.token : null;
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The cached turn together with the quote its dry run returned. The quote is
|
|
58
|
+
* handed back only when `fingerprint` matches the input it was minted for.
|
|
59
|
+
*/
|
|
60
|
+
async function reusableTurn(path, baseUrl, apiKey, capabilityId, fingerprint, now = Date.now(), onWarning) {
|
|
61
|
+
const entry = (await load(path, onWarning))[entryKey(baseUrl, apiKey, capabilityId)];
|
|
62
|
+
if (!entry || typeof entry.token !== 'string' || now - entry.at >= exports.TURN_REUSE_MS)
|
|
63
|
+
return null;
|
|
64
|
+
const quote = entry.quote && entry.quote.fingerprint === fingerprint && typeof entry.quote.quote_hash === 'string'
|
|
65
|
+
? entry.quote.quote_hash
|
|
66
|
+
: null;
|
|
67
|
+
return { token: entry.token, quote_hash: quote };
|
|
68
|
+
}
|
|
69
|
+
async function rememberTurnToken(path, baseUrl, apiKey, capabilityId, token, now = Date.now(), onWarning, quote) {
|
|
53
70
|
const entries = await load(path, onWarning);
|
|
54
71
|
for (const [key, entry] of Object.entries(entries)) {
|
|
55
72
|
if (!entry || now - entry.at >= exports.TURN_REUSE_MS)
|
|
56
73
|
delete entries[key];
|
|
57
74
|
}
|
|
58
|
-
entries[entryKey(baseUrl, apiKey, capabilityId)] = { token, at: now };
|
|
75
|
+
entries[entryKey(baseUrl, apiKey, capabilityId)] = { token, at: now, ...(quote ? { quote } : {}) };
|
|
59
76
|
await persist(path, entries, onWarning);
|
|
60
77
|
}
|
|
61
78
|
async function forgetTurnToken(path, baseUrl, apiKey, capabilityId, onWarning) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trydreamstate/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Live command-line client for the Dreamstate v1 API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node scripts/build-agent-plugin.mjs",
|
|
23
|
-
"test": "npm run build && jest --config jest.config.cjs --runInBand",
|
|
23
|
+
"test": "node scripts/build-agent-plugin.mjs --check && npm run build && jest --config jest.config.cjs --runInBand",
|
|
24
24
|
"type-check": "tsc --noEmit",
|
|
25
25
|
"pack:check": "npm pack --dry-run",
|
|
26
26
|
"pack:smoke": "npm run build && node scripts/pack-smoke.mjs",
|
|
27
27
|
"prepublishOnly": "npm run build && npm run type-check && npm test",
|
|
28
|
+
"verify:guidance": "node scripts/build-agent-plugin.mjs --check",
|
|
28
29
|
"verify:test-collection": "jest --config jest.config.cjs --runInBand --testNamePattern='^(?!)'",
|
|
29
30
|
"plugin:publish": "bash scripts/publish-plugin-repo.sh"
|
|
30
31
|
},
|