@trydreamstate/cli 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +36 -16
  2. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  4. package/agent-plugin/hooks/approve-readonly.sh +15 -0
  5. package/agent-plugin/hooks/hooks.json +17 -0
  6. package/agent-plugin/skills/crm/SKILL.md +2 -2
  7. package/agent-plugin/skills/diagnostics/SKILL.md +3 -1
  8. package/agent-plugin/skills/dreamstate/SKILL.md +15 -5
  9. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  10. package/agent-plugin/skills/inbox-replies/SKILL.md +3 -1
  11. package/agent-plugin/skills/knowledge/SKILL.md +3 -5
  12. package/agent-plugin/skills/linkedin-posts/SKILL.md +3 -1
  13. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
  14. package/agent-plugin/skills/list-building/SKILL.md +5 -10
  15. package/agent-plugin/skills/list-building/column-authoring.md +1 -1
  16. package/agent-plugin/skills/list-building/sourcing.md +2 -2
  17. package/agent-plugin/skills/outreach/SKILL.md +2 -2
  18. package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
  19. package/agent-plugin/skills/plays/SKILL.md +3 -3
  20. package/agent-plugin/skills/reddit-posts/SKILL.md +3 -1
  21. package/agent-plugin/skills/skill-authoring/SKILL.md +3 -1
  22. package/agent-plugin/skills/social/SKILL.md +4 -6
  23. package/agent-plugin/skills/visibility/SKILL.md +2 -6
  24. package/agent-plugin/skills/workflows/SKILL.md +3 -3
  25. package/agent-plugin/skills/workflows/lifecycle-and-enrollment.md +1 -1
  26. package/agent-plugin/skills/worksheet-tidy/SKILL.md +3 -1
  27. package/agent-plugin/skills/writing/SKILL.md +3 -5
  28. package/agent-plugin/skills/x-posts/SKILL.md +3 -1
  29. package/dist/agentGuidance.generated.json +35 -0
  30. package/dist/capabilityPosture.d.ts +12 -0
  31. package/dist/capabilityPosture.d.ts.map +1 -0
  32. package/dist/capabilityPosture.js +39 -0
  33. package/dist/client.d.ts +33 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +155 -64
  36. package/dist/commandTable.d.ts +26 -0
  37. package/dist/commandTable.d.ts.map +1 -0
  38. package/dist/commandTable.js +54 -0
  39. package/dist/commands/sequences.d.ts +3 -2
  40. package/dist/commands/sequences.d.ts.map +1 -1
  41. package/dist/commands/sequences.js +17 -34
  42. package/dist/commands/shared.d.ts +4 -26
  43. package/dist/commands/shared.d.ts.map +1 -1
  44. package/dist/commands/shared.js +136 -158
  45. package/dist/commands/workflows.d.ts +3 -2
  46. package/dist/commands/workflows.d.ts.map +1 -1
  47. package/dist/commands/workflows.js +17 -39
  48. package/dist/errors.d.ts +74 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +84 -0
  51. package/dist/gatedRun.d.ts +79 -0
  52. package/dist/gatedRun.d.ts.map +1 -0
  53. package/dist/gatedRun.js +346 -0
  54. package/dist/generated/packVerbs.generated.json +492 -0
  55. package/dist/generated/runContract.generated.d.ts +4 -0
  56. package/dist/generated/runContract.generated.d.ts.map +1 -0
  57. package/dist/generated/runContract.generated.js +7 -0
  58. package/dist/idempotency.d.ts +38 -0
  59. package/dist/idempotency.d.ts.map +1 -0
  60. package/dist/idempotency.js +106 -0
  61. package/dist/index.d.ts +22 -35
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +405 -378
  64. package/dist/registryManifest.d.ts +6 -0
  65. package/dist/registryManifest.d.ts.map +1 -0
  66. package/dist/registryManifest.js +66 -0
  67. package/dist/runContract.d.ts +10 -0
  68. package/dist/runContract.d.ts.map +1 -0
  69. package/dist/runContract.js +13 -0
  70. package/dist/runWait.d.ts +2 -2
  71. package/dist/runWait.d.ts.map +1 -1
  72. package/dist/runWait.js +3 -14
  73. package/dist/schemaCoercion.d.ts +20 -0
  74. package/dist/schemaCoercion.d.ts.map +1 -0
  75. package/dist/schemaCoercion.js +123 -0
  76. package/dist/skillInstaller.d.ts +40 -0
  77. package/dist/skillInstaller.d.ts.map +1 -1
  78. package/dist/skillInstaller.js +108 -9
  79. package/dist/tableFilePreview.d.ts +14 -0
  80. package/dist/tableFilePreview.d.ts.map +1 -1
  81. package/dist/tableFilePreview.js +19 -8
  82. package/dist/tableFileReader.d.ts +26 -0
  83. package/dist/tableFileReader.d.ts.map +1 -0
  84. package/dist/tableFileReader.js +108 -0
  85. package/dist/tablesImport.d.ts +57 -0
  86. package/dist/tablesImport.d.ts.map +1 -0
  87. package/dist/tablesImport.js +263 -0
  88. package/dist/turnCache.d.ts +14 -1
  89. package/dist/turnCache.d.ts.map +1 -1
  90. package/dist/turnCache.js +25 -8
  91. package/package.json +6 -3
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.inferLocalTableFileKind = inferLocalTableFileKind;
4
+ exports.parseCsv = parseCsv;
5
+ exports.readXlsxRows = readXlsxRows;
4
6
  exports.previewLocalTableFile = previewLocalTableFile;
5
7
  const promises_1 = require("node:fs/promises");
6
8
  const node_zlib_1 = require("node:zlib");
@@ -9,7 +11,7 @@ const TEXT_PREVIEW_BYTES = 1_000_000;
9
11
  const XLSX_PREVIEW_BYTES = 20_000_000;
10
12
  const MAPPABLE_FIELDS = [
11
13
  'external_id', 'name', 'first_name', 'last_name', 'company',
12
- 'title', 'headline', 'email', 'linkedin_url', 'profile_url', 'avatar_url',
14
+ 'title', 'headline', 'email', 'linkedin_url', 'profile_url', 'domain', 'avatar_url',
13
15
  ];
14
16
  function normalizeHeaderKey(value) {
15
17
  return value.trim().toLowerCase().replace(/[\s_-]+/g, '');
@@ -101,7 +103,7 @@ function parseCsv(text) {
101
103
  if (!(row.length === 1 && row[0] === ''))
102
104
  rows.push(row);
103
105
  }
104
- return { rows, endedClean };
106
+ return { rows, endedClean, unterminatedQuote: quoted };
105
107
  }
106
108
  function previewDelimited(kind, text, clipped) {
107
109
  const parsed = parseCsv(text);
@@ -234,13 +236,18 @@ function sharedStringTable(xml) {
234
236
  return [...xml.matchAll(/<(?:[\w-]+:)?si\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?si>/g)]
235
237
  .map((match) => xmlTexts(match[1] ?? ''));
236
238
  }
237
- function previewXlsx(buffer) {
239
+ /**
240
+ * The first worksheet of an xlsx file as rows of cell text. `rowLimit` stops at
241
+ * that many rows (the preview needs a header and a sample); null reads them all
242
+ * (an import needs every row).
243
+ */
244
+ function readXlsxRows(buffer, rowLimit) {
238
245
  const entries = unzipEntries(buffer);
239
246
  const sheetName = [...entries.keys()]
240
247
  .filter((name) => /xl\/worksheets\/sheet\d+\.xml$/.test(name))
241
248
  .sort()[0];
242
249
  if (!sheetName)
243
- throw new Error('xlsx preview could not find a worksheet');
250
+ throw new Error('xlsx file has no worksheet');
244
251
  const sheet = entries.get(sheetName).toString('utf8');
245
252
  const sharedName = [...entries.keys()].find((name) => name.endsWith('xl/sharedStrings.xml'));
246
253
  const shared = sharedName ? sharedStringTable(entries.get(sharedName).toString('utf8')) : [];
@@ -248,7 +255,7 @@ function previewXlsx(buffer) {
248
255
  for (const rowMatch of sheet.matchAll(/<(?:[\w-]+:)?row\b([^>]*)>([\s\S]*?)<\/(?:[\w-]+:)?row>/g)) {
249
256
  const rowRef = /\br="(\d+)"/.exec(rowMatch[1] ?? '');
250
257
  const rowNumber = rowRef ? Number(rowRef[1]) : grid.size + 1;
251
- if (rowNumber > SAMPLE_ROWS + 1)
258
+ if (rowLimit !== null && rowNumber > rowLimit)
252
259
  continue;
253
260
  const cells = new Map();
254
261
  for (const cellMatch of (rowMatch[2] ?? '').matchAll(/<(?:[\w-]+:)?c\b([^>]*)(?:\/>|>([\s\S]*?)<\/(?:[\w-]+:)?c>)/g)) {
@@ -277,9 +284,13 @@ function previewXlsx(buffer) {
277
284
  throw new Error('File has no header row');
278
285
  const width = Math.max(...[...grid.values()].map((cells) => Math.max(-1, ...cells.keys()))) + 1;
279
286
  const toRow = (cells) => Array.from({ length: width }, (_, index) => cells?.get(index) ?? '');
280
- const [headerNumber, ...dataNumbers] = rowNumbers;
281
- const rowCount = [...sheet.matchAll(/<(?:[\w-]+:)?row\b/g)].length;
282
- return finish('xlsx', toRow(grid.get(headerNumber)), dataNumbers.map((number) => toRow(grid.get(number))), rowCount > SAMPLE_ROWS + 1);
287
+ const totalRows = [...sheet.matchAll(/<(?:[\w-]+:)?row\b/g)].length;
288
+ return { rows: rowNumbers.map((number) => toRow(grid.get(number))), totalRows };
289
+ }
290
+ function previewXlsx(buffer) {
291
+ const { rows, totalRows } = readXlsxRows(buffer, SAMPLE_ROWS + 1);
292
+ const [header, ...dataRows] = rows;
293
+ return finish('xlsx', header ?? [], dataRows, totalRows > SAMPLE_ROWS + 1);
283
294
  }
284
295
  async function previewLocalTableFile(filePath, format) {
285
296
  const kind = inferLocalTableFileKind(filePath, format);
@@ -0,0 +1,26 @@
1
+ import { type LocalTableFileKind } from './tableFilePreview';
2
+ /** One staged row, the wire shape of tables.table_sources.csv_upload_stage.rows. */
3
+ export interface StagedRow {
4
+ source_row_number: number;
5
+ data: Record<string, string>;
6
+ }
7
+ export interface LocalTableFile {
8
+ file_kind: LocalTableFileKind;
9
+ headers: string[];
10
+ rows: StagedRow[];
11
+ }
12
+ /** Largest file an import reads into memory. The server caps one upload at 200,000 rows. */
13
+ export declare const MAX_IMPORT_FILE_BYTES = 250000000;
14
+ /**
15
+ * Duplicate CSV labels cannot be object keys twice, so they get private
16
+ * staging keys; the column definitions keep the labels. Same rule as
17
+ * csvStagingHeaderKeys in packages/types (the server's own parser), pinned by
18
+ * tableFileReader.test.ts against that function.
19
+ */
20
+ export declare function stagingHeaderKeys(headers: readonly string[]): string[];
21
+ /** Every row of a local CSV, JSON, JSONL or XLSX file, shaped for csv_upload_stage. */
22
+ export declare function readLocalTableFile(filePath: string, format?: string): Promise<{
23
+ file: LocalTableFile;
24
+ bytes: Buffer;
25
+ }>;
26
+ //# sourceMappingURL=tableFileReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tableFileReader.d.ts","sourceRoot":"","sources":["../src/tableFileReader.ts"],"names":[],"mappings":"AACA,OAAO,EAAmD,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE9G,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAED,4FAA4F;AAC5F,eAAO,MAAM,qBAAqB,YAAc,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAWtE;AAyCD,uFAAuF;AACvF,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAwB5H"}
@@ -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
+ }
@@ -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
- export declare function rememberTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, token: string, now?: number, onWarning?: CacheWarning): Promise<void>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"turnCache.d.ts","sourceRoot":"","sources":["../src/turnCache.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,aAAa,QAAa,CAAC;AAGxC,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,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,GAC7H,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"}
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 first live call is refused with quote_required and the
12
- // server binds a quote to that call's tool_turn_token. Only a rerun carrying
13
- // the SAME token can spend. Each CLI invocation is a new process, so fetching
14
- // a fresh token per run made every paid capability unrunnable from the CLI
15
- // (2026-09-23: tables.table_sources.run refused four times in a row), while
16
- // the refusal itself told the user the CLI reuses the turn for 10 minutes.
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
- async function rememberTurnToken(path, baseUrl, apiKey, capabilityId, token, now = Date.now(), onWarning) {
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.2",
3
+ "version": "1.3.4",
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",
@@ -19,12 +19,15 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
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",
22
+ "build": "node scripts/generate-pack-verbs.mjs --check && node scripts/generate-run-contract.mjs --check && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node scripts/build-agent-plugin.mjs",
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",
29
+ "verify:pack-verbs": "node scripts/generate-pack-verbs.mjs --check",
30
+ "verify:run-contract": "node scripts/generate-run-contract.mjs --check",
28
31
  "verify:test-collection": "jest --config jest.config.cjs --runInBand --testNamePattern='^(?!)'",
29
32
  "plugin:publish": "bash scripts/publish-plugin-repo.sh"
30
33
  },