dataiku-sdk 3.9.0 → 4.0.0

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 (56) hide show
  1. package/README.md +3 -3
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +4 -0
  4. package/dist/packages/types/src/index.js +4 -0
  5. package/dist/src/cli/coerce.d.ts +6 -0
  6. package/dist/src/cli/coerce.js +9 -0
  7. package/dist/src/cli/commands/app.js +2 -2
  8. package/dist/src/cli/commands/code-env.js +5 -6
  9. package/dist/src/cli/commands/code.js +16 -2
  10. package/dist/src/cli/commands/dashboard.js +1 -2
  11. package/dist/src/cli/commands/flow-zone.js +1 -1
  12. package/dist/src/cli/commands/job.js +67 -16
  13. package/dist/src/cli/commands/plugin.js +7 -7
  14. package/dist/src/cli/commands/project-library.js +2 -2
  15. package/dist/src/cli/commands/project.js +21 -4
  16. package/dist/src/cli/commands/recipe.js +11 -19
  17. package/dist/src/cli/commands/scenario.js +2 -4
  18. package/dist/src/cli/contract.js +27 -5
  19. package/dist/src/cli/env.js +2 -3
  20. package/dist/src/cli/helpers/app-permissions.d.ts +1 -1
  21. package/dist/src/cli/helpers/app-permissions.js +2 -2
  22. package/dist/src/cli/helpers/cleanup.d.ts +2 -0
  23. package/dist/src/cli/helpers/cleanup.js +122 -8
  24. package/dist/src/cli/helpers/flow-zone.d.ts +1 -1
  25. package/dist/src/cli/helpers/flow-zone.js +3 -4
  26. package/dist/src/cli/list-output.d.ts +5 -0
  27. package/dist/src/cli/list-output.js +11 -2
  28. package/dist/src/cli/list-shapes.d.ts +6 -0
  29. package/dist/src/cli/list-shapes.js +7 -1
  30. package/dist/src/cli/output-schemas.d.ts +1 -1
  31. package/dist/src/cli/output-schemas.js +1 -1
  32. package/dist/src/cli/output.js +7 -0
  33. package/dist/src/cli/version.d.ts +2 -2
  34. package/dist/src/cli/version.js +4 -5
  35. package/dist/src/cli.js +66 -17
  36. package/dist/src/client.d.ts +14 -2
  37. package/dist/src/client.js +18 -10
  38. package/dist/src/generated/action-output-schemas.json +18 -0
  39. package/dist/src/index.d.ts +1 -1
  40. package/dist/src/resources/datasets.d.ts +11 -1
  41. package/dist/src/resources/datasets.js +91 -30
  42. package/dist/src/resources/jobs.d.ts +35 -5
  43. package/dist/src/resources/jobs.js +68 -41
  44. package/dist/src/resources/projects.js +2 -2
  45. package/dist/src/resources/recipes.d.ts +12 -1
  46. package/dist/src/resources/recipes.js +39 -12
  47. package/dist/src/resources/scenarios.d.ts +8 -1
  48. package/dist/src/resources/scenarios.js +51 -12
  49. package/dist/src/resources/wiki.js +3 -7
  50. package/dist/src/skill.js +1 -2
  51. package/dist/src/utils/project-archive.d.ts +25 -0
  52. package/dist/src/utils/project-archive.js +216 -5
  53. package/dist/src/utils/response-file.js +15 -8
  54. package/package.json +11 -12
  55. package/plugin.json +1 -1
  56. package/skills/dataiku-dss/SKILL.md +1 -1
@@ -188,7 +188,7 @@ export class ProjectsResource extends BaseResource {
188
188
  && settings.targetProjectKey.trim() !== ""
189
189
  ? { targetProjectKey: settings.targetProjectKey, }
190
190
  : {}),
191
- });
191
+ }, { cause: error, });
192
192
  }
193
193
  throw error;
194
194
  }
@@ -246,7 +246,7 @@ export class ProjectsResource extends BaseResource {
246
246
  catch (error) {
247
247
  if (error instanceof DataikuError
248
248
  && (error.status === 404 || error.status === 0 || error.status >= 500)) {
249
- throw new ClientValidationError(`DSS reported import success for project ${usedProjectKey}, but the project could not be re-read for verification.`, "ambiguous_outcome", PROJECT_IMPORT_AMBIGUOUS_REMEDIATION, { importId, usedProjectKey, });
249
+ throw new ClientValidationError(`DSS reported import success for project ${usedProjectKey}, but the project could not be re-read for verification.`, "ambiguous_outcome", PROJECT_IMPORT_AMBIGUOUS_REMEDIATION, { importId, usedProjectKey, }, { cause: error, });
250
250
  }
251
251
  throw error;
252
252
  }
@@ -65,6 +65,14 @@ export interface RecipeCloneResult {
65
65
  payloadTextRewrites: Record<string, string>;
66
66
  copiedOutputDatasets: string[];
67
67
  }
68
+ /**
69
+ * Validate an update patch against the current recipe document and return the
70
+ * document to PUT. `recipe` is deep-merged, except `recipe.params.envSelection`,
71
+ * which the patch replaces wholesale so no stale `envName` survives an
72
+ * EXPLICIT_ENV to INHERIT switch. `envSelection` outside `recipe.params` is
73
+ * rejected: neither DSS nor the CLI reads it there.
74
+ */
75
+ export declare function buildRecipeUpdate(current: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
68
76
  export declare class RecipesResource extends BaseResource {
69
77
  /** List all recipes in a project. */
70
78
  list(projectKey?: string): Promise<RecipeSummary[]>;
@@ -136,7 +144,10 @@ export declare class RecipesResource extends BaseResource {
136
144
  }): Promise<RecipeUpdateSchemaResult>;
137
145
  /**
138
146
  * Update a recipe by merging the patch into the current definition.
139
- * The `recipe` sub-object is deep-merged to preserve nested fields.
147
+ * The `recipe` sub-object is deep-merged to preserve nested fields, except
148
+ * `recipe.params.envSelection`: when the patch provides it, the whole object
149
+ * is replaced (not merged), so switching EXPLICIT_ENV to INHERIT cannot leave
150
+ * a stale `envName` behind. `envSelection` outside `recipe.params` is rejected.
140
151
  */
141
152
  update(recipeName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
142
153
  /** Replace a full recipe API document. */
@@ -1,4 +1,3 @@
1
- import { writeFile, } from "node:fs/promises";
2
1
  import { resolve, } from "node:path";
3
2
  import { ClientValidationError, DataikuError, nonJsonResponseBody, unexpectedResponseError, } from "../errors.js";
4
3
  import { ProjectMetadataSchema, RecipeSchemaUpdatesSchema, RecipeSummaryArraySchema, } from "../schemas.js";
@@ -40,6 +39,37 @@ const RECIPE_DEFINITION_FIELDS = new Set(["params", "inputs", "outputs", "script
40
39
  function rootRecipeDefinitionFields(data) {
41
40
  return Object.keys(data).filter((key) => RECIPE_DEFINITION_FIELDS.has(key));
42
41
  }
42
+ /**
43
+ * Validate an update patch against the current recipe document and return the
44
+ * document to PUT. `recipe` is deep-merged, except `recipe.params.envSelection`,
45
+ * which the patch replaces wholesale so no stale `envName` survives an
46
+ * EXPLICIT_ENV to INHERIT switch. `envSelection` outside `recipe.params` is
47
+ * rejected: neither DSS nor the CLI reads it there.
48
+ */
49
+ export function buildRecipeUpdate(current, data) {
50
+ const currentRecipe = asRecord(current.recipe) ?? {};
51
+ const recipePatch = asRecord(data.recipe) ?? {};
52
+ const misplacedEnvSelection = [
53
+ ...("envSelection" in data ? ["envSelection",] : []),
54
+ ...("envSelection" in recipePatch ? ["recipe.envSelection",] : []),
55
+ ];
56
+ if (misplacedEnvSelection.length > 0) {
57
+ throw new ClientValidationError(`envSelection at ${misplacedEnvSelection.join(" and ")} is ignored; it only applies under recipe.params. Example: {"recipe":{"params":{"envSelection":{"envMode":"EXPLICIT_ENV","envName":"py311_env"}}}}`, "validation_failed");
58
+ }
59
+ const misplacedRecipeFields = rootRecipeDefinitionFields(data);
60
+ if (misplacedRecipeFields.length > 0) {
61
+ throw new ClientValidationError(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`, "validation_failed");
62
+ }
63
+ const mergedRecipe = deepMerge(currentRecipe, recipePatch);
64
+ const paramsPatch = asRecord(recipePatch.params);
65
+ if (paramsPatch && "envSelection" in paramsPatch) {
66
+ mergedRecipe.params = {
67
+ ...asRecord(mergedRecipe.params),
68
+ envSelection: paramsPatch.envSelection,
69
+ };
70
+ }
71
+ return { ...current, ...data, recipe: mergedRecipe, };
72
+ }
43
73
  function normalizeRecipeOutputType(value) {
44
74
  if (typeof value !== "string")
45
75
  return undefined;
@@ -823,22 +853,19 @@ export class RecipesResource extends BaseResource {
823
853
  }
824
854
  /**
825
855
  * Update a recipe by merging the patch into the current definition.
826
- * The `recipe` sub-object is deep-merged to preserve nested fields.
856
+ * The `recipe` sub-object is deep-merged to preserve nested fields, except
857
+ * `recipe.params.envSelection`: when the patch provides it, the whole object
858
+ * is replaced (not merged), so switching EXPLICIT_ENV to INHERIT cannot leave
859
+ * a stale `envName` behind. `envSelection` outside `recipe.params` is rejected.
827
860
  */
828
861
  async update(recipeName, data, projectKey) {
829
862
  const enc = this.enc(projectKey);
830
863
  const rnEnc = encodeURIComponent(recipeName);
831
864
  const current = await this.client.get(`/public/api/projects/${enc}/recipes/${rnEnc}`);
832
- const currentRecipe = asRecord(current.recipe);
833
- if (!currentRecipe) {
865
+ if (!asRecord(current.recipe)) {
834
866
  throw new ClientValidationError(`Recipe "${recipeName}" was not found or returned an empty definition.`, "validation_failed");
835
867
  }
836
- const misplacedRecipeFields = rootRecipeDefinitionFields(data);
837
- if (misplacedRecipeFields.length > 0) {
838
- throw new ClientValidationError(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`, "validation_failed");
839
- }
840
- const mergedRecipe = deepMerge(currentRecipe, asRecord(data.recipe) ?? {});
841
- const merged = { ...current, ...data, recipe: mergedRecipe, };
868
+ const merged = buildRecipeUpdate(current, data);
842
869
  await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, merged);
843
870
  }
844
871
  /** Replace a full recipe API document. */
@@ -922,7 +949,7 @@ export class RecipesResource extends BaseResource {
922
949
  }
923
950
  const safeRecipeName = sanitizeFileName(recipeName, "recipe");
924
951
  const filePath = opts?.outputPath ?? resolve(process.cwd(), `${safeRecipeName}${inferRecipeCodeExtension(result.recipe.type)}`);
925
- await writeFile(filePath, result.payload, "utf-8");
952
+ await Bun.write(filePath, result.payload);
926
953
  return filePath;
927
954
  }
928
955
  /** Get only the code payload of a recipe as a raw string. */
@@ -965,7 +992,7 @@ export class RecipesResource extends BaseResource {
965
992
  const recipe = await this.client.get(`/public/api/projects/${enc}/recipes/${rnEnc}`);
966
993
  const safeRecipeName = sanitizeFileName(recipeName, "recipe");
967
994
  const filePath = opts?.outputPath ?? resolve(process.cwd(), `${safeRecipeName}.json`);
968
- await writeFile(filePath, JSON.stringify(recipe, null, 2), "utf-8");
995
+ await Bun.write(filePath, JSON.stringify(recipe, null, 2));
969
996
  return filePath;
970
997
  }
971
998
  }
@@ -107,6 +107,10 @@ export interface ScenarioScriptRunResult {
107
107
  elapsedMs: number;
108
108
  pollCount: number;
109
109
  output?: string;
110
+ /** True when the captured script output was cut (wrapper byte budget or log read cap); `output` then holds the last part only. */
111
+ outputTruncated?: boolean;
112
+ /** Bytes of script output the wrapper dropped from the front; absent when the cut happened in the log read instead. */
113
+ outputOmittedBytes?: number;
110
114
  log: string;
111
115
  logTruncated: boolean;
112
116
  maxLogBytes: number;
@@ -186,12 +190,15 @@ export declare class ScenariosResource extends BaseResource {
186
190
  * Get the log of a scenario run (GET /scenarios/{id}/{runId}/log), optionally
187
191
  * scoped to a single step via `stepId`. The text is byte-bounded: at most
188
192
  * `maxLogBytes` (default 1 MiB) of the beginning of the log is retained and
189
- * `truncated` reports whether the cap cut the body.
193
+ * `truncated` reports whether the cap cut the body. With `tail: true` the
194
+ * last `maxLogBytes` are retained instead (used by code runs, whose output
195
+ * markers sit at the end of the log).
190
196
  */
191
197
  getRunLog(scenarioId: string, runId: string, opts?: {
192
198
  stepId?: string;
193
199
  maxLogBytes?: number;
194
200
  projectKey?: string;
201
+ tail?: boolean;
195
202
  }): Promise<ScenarioRunLog>;
196
203
  /** Get the payload of a (custom) scenario (GET /scenarios/{id}/payload). */
197
204
  getPayload(scenarioId: string, opts?: {
@@ -32,6 +32,14 @@ export class ScenarioScriptRunWithCleanupFailureError extends Error {
32
32
  const DEFAULT_CODE_RUN_MAX_LOG_BYTES = 1_048_576;
33
33
  const CODE_RUN_OUTPUT_START = "<<<DSS_CODE_RUN_OUTPUT_b7e3a1>>>";
34
34
  const CODE_RUN_OUTPUT_END = "<<<DSS_CODE_RUN_OUTPUT_END_b7e3a1>>>";
35
+ /** Written right after the END marker when the wrapper cut the captured output; carries the omitted byte count. */
36
+ const CODE_RUN_OUTPUT_TRUNCATED_RE = /^<<<DSS_CODE_RUN_OUTPUT_TRUNCATED_b7e3a1:(\d+)>>>$/;
37
+ /**
38
+ * The wrapper keeps at most `maxLogBytes / CODE_RUN_OUTPUT_BUDGET_DIVISOR` bytes
39
+ * of script output. The remainder of the read cap is headroom for the per-line
40
+ * DSS log prefix, so the markers still fit in what `runScript` reads back.
41
+ */
42
+ const CODE_RUN_OUTPUT_BUDGET_DIVISOR = 4;
35
43
  /** Prefer the current runId field, with the legacy id field as fallback. */
36
44
  function triggerRunIdOf(trigger, scenarioId) {
37
45
  const runId = trigger?.runId ?? trigger?.id;
@@ -80,8 +88,10 @@ function scenarioLightStatusFromRaw(raw) {
80
88
  * wrapper noise. The script is base64-encoded to avoid quoting/escaping issues and
81
89
  * exec'd as `__main__`. A failing script re-raises SystemExit(1) so the scenario
82
90
  * outcome is FAILED while the captured traceback still lands between the markers.
91
+ * Only the last `maxOutputBytes` UTF-8 bytes of output are re-emitted; when output
92
+ * was cut, a truncation marker with the omitted byte count follows the END marker.
83
93
  */
84
- function buildCodeRunScript(script) {
94
+ function buildCodeRunScript(script, maxOutputBytes) {
85
95
  const encoded = Buffer.from(script, "utf-8").toString("base64");
86
96
  return [
87
97
  "import base64 as _dku_b64, sys as _dku_sys, io as _dku_io, traceback as _dku_tb",
@@ -99,9 +109,14 @@ function buildCodeRunScript(script) {
99
109
  "\t_dku_tb.print_exc()",
100
110
  "finally:",
101
111
  "\t_dku_sys.stdout, _dku_sys.stderr = _dku_out, _dku_err",
112
+ '\t_dku_bytes = _dku_buf.getvalue().encode("utf-8", "replace")',
113
+ `\t_dku_omitted = max(0, len(_dku_bytes) - ${maxOutputBytes})`,
114
+ '\t_dku_text = _dku_bytes[_dku_omitted:].decode("utf-8", "ignore")',
102
115
  `\t_dku_out.write("${CODE_RUN_OUTPUT_START}\\n")`,
103
- "\t_dku_out.write(_dku_buf.getvalue())",
116
+ "\t_dku_out.write(_dku_text)",
104
117
  `\t_dku_out.write("\\n${CODE_RUN_OUTPUT_END}\\n")`,
118
+ "\tif _dku_omitted:",
119
+ '\t\t_dku_out.write("<<<DSS_CODE_RUN_OUTPUT_TRUNCATED_b7e3a1:%d>>>\\n" % _dku_omitted)',
105
120
  "\t_dku_out.flush()",
106
121
  "if _dku_code:",
107
122
  "\traise SystemExit(_dku_code)",
@@ -112,10 +127,13 @@ function buildCodeRunScript(script) {
112
127
  * Pull the script's own stdout/stderr back out of the full DSS run log by slicing
113
128
  * the `[process]` lines between the markers emitted by {@link buildCodeRunScript}.
114
129
  * Returns undefined if the markers are absent (e.g. the harness never ran), in which
115
- * case callers should fall back to the full log.
130
+ * case callers should fall back to the full log. When the log read itself was cut
131
+ * (`logTruncated`) the START marker may be gone while END survives; the output is
132
+ * then whatever precedes END, reported as truncated.
116
133
  */
117
- function extractCodeRunOutput(log) {
118
- const messageRe = /^\[[^\]]*\] \[[^\]]*\] \[[^\]]*\] \[process\] - (.*)$/;
134
+ function extractCodeRunOutput(log, logTruncated) {
135
+ // `[\s\S]` (not `.`): CR, U+2028 and U+2029 inside a printed line must not drop it.
136
+ const messageRe = /^\[[^\]]*\] \[[^\]]*\] \[[^\]]*\] \[process\] - ([\s\S]*)$/;
119
137
  const contents = [];
120
138
  for (const rawLine of log.split("\n")) {
121
139
  const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
@@ -125,7 +143,7 @@ function extractCodeRunOutput(log) {
125
143
  }
126
144
  // First start + last end, so a script that prints a marker string stays body content.
127
145
  const start = contents.indexOf(CODE_RUN_OUTPUT_START);
128
- if (start < 0)
146
+ if (start < 0 && !logTruncated)
129
147
  return undefined;
130
148
  let end = -1;
131
149
  for (let i = contents.length - 1; i > start; i--) {
@@ -140,7 +158,13 @@ function extractCodeRunOutput(log) {
140
158
  // Drop only the single trailing separator the harness writes before the end marker.
141
159
  if (body.length > 0 && body[body.length - 1] === "")
142
160
  body.pop();
143
- return body.join("\n");
161
+ const omitted = CODE_RUN_OUTPUT_TRUNCATED_RE.exec(contents[end + 1] ?? "");
162
+ const omittedBytes = omitted ? Number(omitted[1]) : undefined;
163
+ return {
164
+ output: body.join("\n"),
165
+ truncated: omitted !== null || start < 0,
166
+ ...(omittedBytes !== undefined ? { omittedBytes, } : {}),
167
+ };
144
168
  }
145
169
  function scenarioStepWarningSummary(stepResult) {
146
170
  const container = stepResult?.warnings;
@@ -441,7 +465,10 @@ export class ScenariosResource extends BaseResource {
441
465
  type: "custom_python",
442
466
  params: { envSelection, },
443
467
  });
444
- await this.client.putVoid(`${base}/payload`, { script: buildCodeRunScript(script), extension: "py", });
468
+ await this.client.putVoid(`${base}/payload`, {
469
+ script: buildCodeRunScript(script, Math.floor(maxLogBytes / CODE_RUN_OUTPUT_BUDGET_DIVISOR)),
470
+ extension: "py",
471
+ });
445
472
  const trigger = await this.client.post(`${base}/run/`, {});
446
473
  const tracked = await this.trackTriggerRun(scenarioId, base, trigger, {
447
474
  startedAt,
@@ -454,9 +481,12 @@ export class ScenariosResource extends BaseResource {
454
481
  pollCount = tracked.pollCount;
455
482
  timedOut = tracked.timedOut;
456
483
  if (runId && outcome !== "TIMEOUT") {
484
+ // Tail read: the markers are printed last, so a log over the cap loses
485
+ // DSS start-up noise instead of the END marker.
457
486
  const limitedLog = await this.getRunLog(scenarioId, runId, {
458
487
  projectKey: opts?.projectKey,
459
488
  maxLogBytes,
489
+ tail: true,
460
490
  });
461
491
  log = limitedLog.text;
462
492
  logTruncated = limitedLog.truncated;
@@ -475,7 +505,7 @@ export class ScenariosResource extends BaseResource {
475
505
  }
476
506
  throw primaryError;
477
507
  }
478
- const output = extractCodeRunOutput(log);
508
+ const extracted = extractCodeRunOutput(log, logTruncated);
479
509
  return {
480
510
  scenarioId,
481
511
  runId,
@@ -483,7 +513,11 @@ export class ScenariosResource extends BaseResource {
483
513
  success: outcome === "SUCCESS",
484
514
  elapsedMs: Date.now() - startedAt,
485
515
  pollCount,
486
- output,
516
+ output: extracted?.output,
517
+ ...(extracted?.truncated ? { outputTruncated: true, } : {}),
518
+ ...(extracted?.omittedBytes !== undefined
519
+ ? { outputOmittedBytes: extracted.omittedBytes, }
520
+ : {}),
487
521
  log,
488
522
  logTruncated,
489
523
  maxLogBytes,
@@ -519,7 +553,9 @@ export class ScenariosResource extends BaseResource {
519
553
  * Get the log of a scenario run (GET /scenarios/{id}/{runId}/log), optionally
520
554
  * scoped to a single step via `stepId`. The text is byte-bounded: at most
521
555
  * `maxLogBytes` (default 1 MiB) of the beginning of the log is retained and
522
- * `truncated` reports whether the cap cut the body.
556
+ * `truncated` reports whether the cap cut the body. With `tail: true` the
557
+ * last `maxLogBytes` are retained instead (used by code runs, whose output
558
+ * markers sit at the end of the log).
523
559
  */
524
560
  async getRunLog(scenarioId, runId, opts) {
525
561
  if (opts?.stepId !== undefined)
@@ -529,7 +565,10 @@ export class ScenariosResource extends BaseResource {
529
565
  const query = opts?.stepId !== undefined
530
566
  ? `?stepId=${encodeURIComponent(opts.stepId)}`
531
567
  : "";
532
- const { text, truncated, } = await this.client.getTextLimited(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/${encodeURIComponent(runId)}/log${query}`, maxLogBytes);
568
+ const readText = opts?.tail === true
569
+ ? this.client.getTextTailLimited.bind(this.client)
570
+ : this.client.getTextLimited.bind(this.client);
571
+ const { text, truncated, } = await readText(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/${encodeURIComponent(runId)}/log${query}`, maxLogBytes);
533
572
  return { text, truncated, maxLogBytes, };
534
573
  }
535
574
  /** Get the payload of a (custom) scenario (GET /scenarios/{id}/payload). */
@@ -1,4 +1,4 @@
1
- import fs from "node:fs";
1
+ import { stat as statFile, } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { ClientValidationError, } from "../errors.js";
4
4
  import { WikiArticleDataArraySchema, WikiArticleDataSchema, WikiSettingsSchema, } from "../schemas.js";
@@ -19,13 +19,9 @@ export function wikiAttachmentNotFound(data, smartId) {
19
19
  }
20
20
  /** Check that a local path is a readable regular file before any DSS call. */
21
21
  export async function statWikiAttachmentFile(localPath) {
22
- let stat;
23
- try {
24
- stat = await fs.promises.stat(localPath);
25
- }
26
- catch (error) {
22
+ const stat = await statFile(localPath).catch((error) => {
27
23
  throw new ClientValidationError(`Could not read attachment file ${localPath}: ${error instanceof Error ? error.message : String(error)}.`, "not_found", "Verify the file path and that it is readable.");
28
- }
24
+ });
29
25
  if (!stat.isFile()) {
30
26
  throw new ClientValidationError(`${localPath} is not a regular file.`);
31
27
  }
package/dist/src/skill.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { fileURLToPath, } from "node:url";
5
4
  import { ClientValidationError, } from "./errors.js";
6
5
  import { sha256Hex, } from "./utils/stable-hash.js";
7
6
  const SKILL_URLS = [
@@ -114,7 +113,7 @@ function skillState(target, expectedSha256) {
114
113
  }
115
114
  /** Load the packaged tree, keeping portable relative paths in the report. */
116
115
  function skillFiles() {
117
- const root = fileURLToPath(skillDirectory());
116
+ const root = Bun.fileURLToPath(skillDirectory());
118
117
  const files = new Map();
119
118
  function visit(relativeDir) {
120
119
  const dir = path.join(root, relativeDir);
@@ -35,6 +35,25 @@ export interface ProjectArchiveIssue {
35
35
  message: string;
36
36
  member?: string;
37
37
  }
38
+ /**
39
+ * One credential-bearing structure found in an archive member. Carries the
40
+ * member path and the JSON key path / field name only, never the value.
41
+ * - `member_name`: the member itself is a credential store (for example
42
+ * `project_config/apikeys.json`) and is not empty.
43
+ * - `json_key`: a credential-named JSON key holds a non-empty string value
44
+ * (or a container of them), e.g. `$.standard.DHL_API_KEY` or a nested
45
+ * web-app `apiKey`.
46
+ * - `list_variable`: a list-form `{name, value}` entry whose `name` is
47
+ * credential-named, e.g. `{name: "X_API_KEY", value: "..."}`.
48
+ */
49
+ export interface ProjectArchiveCredentialFinding {
50
+ member: string;
51
+ rule: "member_name" | "json_key" | "list_variable";
52
+ /** JSON path of the credential value inside the member (absent for `member_name`). */
53
+ keyPath?: string;
54
+ /** Credential-named key or list-form variable name (absent for `member_name`). */
55
+ field?: string;
56
+ }
38
57
  export interface ProjectArchiveInspection {
39
58
  filePath: string;
40
59
  sizeBytes: number;
@@ -42,6 +61,12 @@ export interface ProjectArchiveInspection {
42
61
  sourceProjectKey?: string;
43
62
  valid: boolean;
44
63
  issues: ProjectArchiveIssue[];
64
+ /**
65
+ * Credential inventory: member paths and key paths/field names of
66
+ * credential-bearing structures. Values are never included. Each finding
67
+ * is also summarized by a `credential_structure` warning issue.
68
+ */
69
+ credentialFindings: ProjectArchiveCredentialFinding[];
45
70
  }
46
71
  /**
47
72
  * Inspect a DSS project export ZIP archive without extracting anything to