dataiku-sdk 0.12.1 → 0.12.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 CHANGED
@@ -2,7 +2,23 @@
2
2
 
3
3
  Agent-only TypeScript SDK and `dss` CLI for Dataiku DSS automation.
4
4
 
5
- Examples below assume the installed `dss` binary. From this checkout, use `./bin/dss ...` or `bun --no-env-file src/cli.ts ...` with the same arguments; from another working directory, call `/path/to/dataiku-sdk/bin/dss ...`.
5
+ ## Platform support
6
+
7
+ The published `dss` CLI supports Linux, macOS, and Windows under Bun >= 1.3.14 or Node.js >= 22.15.0. The release gate runs Bun 1.3.14 with Node 24 on all three operating systems, plus a lightweight minimum-Node 22.15 package smoke on Linux. The runtime dependency is pure JavaScript, so the same package also runs on runtime-supported x64 and ARM64 systems.
8
+
9
+ Run directly with Bun:
10
+
11
+ ```text
12
+ bunx --bun dataiku-sdk version
13
+ ```
14
+
15
+ Or install the npm binary:
16
+
17
+ ```text
18
+ npm install --global dataiku-sdk
19
+ ```
20
+
21
+ Bun is the primary development runtime and package manager. Examples below assume an installed `dss` binary. From a checkout, use `bun --no-env-file src/cli.ts ...` or the cross-runtime `bun --no-env-file ./bin/dss.js ...` launcher. Node users can invoke the same launcher as `node ./bin/dss.js ...`; if `dist/` is absent, it delegates to Bun. From another working directory, pass the checkout's absolute `bin/dss.js` path to Bun or Node.
6
22
  `--no-env-file` disables Bun's automatic preloading only; the CLI still applies its documented `.env` handling unless `DATAIKU_DISABLE_ENV=1` is set.
7
23
 
8
24
  ## CLI contract
@@ -13,12 +29,12 @@ Examples below assume the installed `dss` binary. From this checkout, use `./bin
13
29
  - `--fields a,b,c` projects those fields from an object or array-of-objects result; dotted paths (`a.b.c`) drill into nested objects, and missing fields become `null`; string and scalar results pass through unchanged.
14
30
  - No prompts, help screens, tables, banners, or prose output are part of the contract.
15
31
  - Exit codes: `0` success, `1` usage/configuration error, `2` DSS/internal error, `3` transient DSS error, `4` completed command with failed long-running DSS work.
16
- - The exit code is the success signal: chain mutations with `&&` and never infer success from piped output that discards the exit code (e.g. `dss ... 2>&1 | helper; echo done` reports success even when the command failed).
32
+ - The exit code is the success signal. For portable multi-step mutations, prefer `dss batch`; shell chaining and pipeline exit semantics differ across POSIX shells, Windows PowerShell, PowerShell 7, and Command Prompt.
17
33
  - `--raw` is only for recipe payload commands. Without `--output`, stdout is raw bytes; with `--output PATH`, stdout is the JSON string equal to `PATH` and the file contains the exact raw bytes.
18
34
 
19
35
  Discover the agent protocol and complete machine-readable command surface:
20
36
 
21
- ```bash
37
+ ```text
22
38
  dss agent contract
23
39
  dss commands run
24
40
  ```
@@ -27,7 +43,7 @@ Agents should parse `agent contract` once for protocol/schema compatibility, the
27
43
 
28
44
  ## Agent skill installation
29
45
 
30
- ```bash
46
+ ```text
31
47
  dss install-skill --list-agents
32
48
  dss install-skill --agent omp --target .
33
49
  dss install-skill --agent omp --target . --dry-run
@@ -48,22 +64,41 @@ Global installs write under the agent's home config path, for example OMP: `~/.o
48
64
 
49
65
  ## Credentials
50
66
 
51
- Use environment variables for ephemeral runs:
52
- For disposable agent tests, set `DSS_CONFIG_DIR` to a temporary directory so saved credentials never touch your real profile.
67
+ Use environment variables for ephemeral runs. For disposable agent tests, set `DSS_CONFIG_DIR` to a temporary directory so saved credentials never touch your real profile.
53
68
  Credential precedence is flags first, then `DATAIKU_*` environment variables, then saved credentials in `DSS_CONFIG_DIR` or the platform config directory.
54
69
  Set `DATAIKU_DISABLE_ENV=1` when a test must ignore both `.env` files and `DATAIKU_*` environment variables.
55
70
  When `.env` loading is enabled, the CLI reads `.env` from the CLI build/root directory and from the command's current working directory; put test-specific `.env` files in the directory where you invoke `dss`.
56
71
 
57
- ```bash
58
- DATAIKU_URL=https://dss.example.com \
59
- DATAIKU_API_KEY=your-api-key \
60
- DATAIKU_PROJECT_KEY=MYPROJ \
72
+ POSIX shell:
73
+
74
+ ```sh
75
+ export DATAIKU_URL=https://dss.example.com
76
+ export DATAIKU_API_KEY=your-api-key
77
+ export DATAIKU_PROJECT_KEY=MYPROJ
78
+ dss project list
79
+ ```
80
+
81
+ PowerShell:
82
+
83
+ ```powershell
84
+ $env:DATAIKU_URL = "https://dss.example.com"
85
+ $env:DATAIKU_API_KEY = "your-api-key"
86
+ $env:DATAIKU_PROJECT_KEY = "MYPROJ"
87
+ dss project list
88
+ ```
89
+
90
+ Windows Command Prompt:
91
+
92
+ ```bat
93
+ set "DATAIKU_URL=https://dss.example.com"
94
+ set "DATAIKU_API_KEY=your-api-key"
95
+ set "DATAIKU_PROJECT_KEY=MYPROJ"
61
96
  dss project list
62
97
  ```
63
98
 
64
99
  Persist credentials when needed:
65
100
 
66
- ```bash
101
+ ```text
67
102
  dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ
68
103
  ```
69
104
 
package/bin/dss.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync, } from "node:child_process";
3
- import { existsSync, } from "node:fs";
4
- import { dirname, resolve, } from "node:path";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL, } from "node:url";
6
6
 
7
7
  const args = process.argv.slice(2,);
8
8
  const optionArgs = args.includes("--",) ? args.slice(0, args.indexOf("--",),) : args;
9
- const here = dirname(fileURLToPath(import.meta.url,),);
10
- const distCliPath = resolve(here, "../dist/src/cli.js",);
11
- const sourceCliPath = resolve(here, "../src/cli.ts",);
12
- const cliPath = existsSync(distCliPath,) ? distCliPath : sourceCliPath;
9
+ const here = path.dirname(fileURLToPath(import.meta.url,),);
10
+ const distCliPath = path.resolve(here, "../dist/src/cli.js",);
11
+ const sourceCliPath = path.resolve(here, "../src/cli.ts",);
12
+ const cliPath = fs.existsSync(distCliPath,) ? distCliPath : sourceCliPath;
13
13
  const cliUrl = pathToFileURL(cliPath,).href;
14
14
 
15
15
  function flagValue(names,) {
@@ -40,11 +40,6 @@ async function loadSavedTlsSettings() {
40
40
  }
41
41
  }
42
42
 
43
- function supportsSystemCa(nodeBin,) {
44
- const probe = spawnSync(nodeBin, ["--use-system-ca", "-e", "",], { stdio: "ignore", },);
45
- return !probe.error && probe.status === 0;
46
- }
47
-
48
43
  const savedTls = await loadSavedTlsSettings();
49
44
  const env = { ...process.env, };
50
45
  const explicitCaCert = flagValue(["--ca-cert", "--extra-ca-certs",],);
@@ -62,12 +57,13 @@ if (hasFlag(["--insecure", "--skip-tls-verify",],)) {
62
57
  env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
63
58
  }
64
59
 
65
- const nodeBin = process.versions.bun ? "node" : process.execPath;
60
+ const runningOnBun = Boolean(process.versions.bun,);
66
61
  const usesSourceCli = cliPath === sourceCliPath;
67
- const nodeArgs = !usesSourceCli && supportsSystemCa(nodeBin,) ? ["--use-system-ca",] : [];
62
+ const runtimeBin = runningOnBun ? process.execPath : usesSourceCli ? "bun" : process.execPath;
63
+ const runtimeArgs = runningOnBun || usesSourceCli ? ["--no-env-file",] : ["--use-system-ca",];
68
64
  const result = spawnSync(
69
- usesSourceCli ? "bun" : nodeBin,
70
- [...(usesSourceCli ? ["--no-env-file",] : nodeArgs), cliPath, ...args,],
65
+ runtimeBin,
66
+ [...runtimeArgs, cliPath, ...args,],
71
67
  {
72
68
  stdio: "inherit",
73
69
  env,
@@ -77,25 +73,24 @@ const result = spawnSync(
77
73
  if (result.error) {
78
74
  const message = usesSourceCli
79
75
  ? `Unable to start Bun runtime for source dss CLI (${result.error.message}).`
80
- : `Unable to start Node runtime for packaged dss CLI (${result.error.message}); falling back to current runtime without Node system CA bootstrap.`;
76
+ : `Unable to start ${
77
+ runningOnBun ? "Bun" : "Node"
78
+ } runtime for packaged dss CLI (${result.error.message}); falling back to the current runtime.`;
81
79
  if (usesSourceCli) {
82
80
  process.stderr.write(`${
83
- JSON.stringify(
84
- {
85
- ok: false,
86
- error: message,
87
- code: "internal_error",
88
- category: "internal",
89
- message,
90
- exitCode: 2,
91
- },
92
- null,
93
- 2,
94
- )
81
+ JSON.stringify({
82
+ type: "error",
83
+ ok: false,
84
+ error: message,
85
+ code: "internal_error",
86
+ category: "internal",
87
+ exitCode: 2,
88
+ },)
95
89
  }\n`,);
96
- process.exit(2,);
90
+ process.exitCode = 2;
91
+ } else {
92
+ await import(cliUrl);
97
93
  }
98
- await import(cliUrl);
99
94
  } else if (result.signal) {
100
95
  process.kill(process.pid, result.signal,);
101
96
  } else {
@@ -59,7 +59,6 @@ export const apiServiceCommands = {
59
59
  handler: async (c, a, f) => {
60
60
  requireArgs(a, 1, "dss api-service list-packages <serviceId> [--project-key KEY]");
61
61
  const projectKey = f["project-key"];
62
- await c.apiServices.getSettings(a[0], projectKey);
63
62
  return c.apiServices.listPackages(a[0], projectKey);
64
63
  },
65
64
  usage: "dss api-service list-packages <serviceId> [--project-key KEY]",
@@ -44,7 +44,11 @@ export const notebookCommands = {
44
44
  const current = await c.notebooks.getJupyter(a[0], pk);
45
45
  const next = {
46
46
  ...current,
47
- cells: current.cells.map((cell) => ({ ...cell, outputs: [], execution_count: null, })),
47
+ cells: current.cells.map((cell) => {
48
+ if (cell.cell_type !== "code")
49
+ return cell;
50
+ return { ...cell, outputs: [], execution_count: 0, };
51
+ }),
48
52
  };
49
53
  return {
50
54
  dryRun: true,
@@ -73,7 +73,7 @@ export function transientBodyWithTargetContext(body, target, elapsedMs) {
73
73
  }
74
74
  export function addTransientTargetContext(error, target, elapsedMs) {
75
75
  if (error instanceof DataikuError && error.category === "transient") {
76
- throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId);
76
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId, { target, elapsedMs, });
77
77
  }
78
78
  throw error;
79
79
  }
package/dist/src/cli.js CHANGED
@@ -12,7 +12,7 @@ import { COMMANDS_RUN_HINT, missingActionError, noCommandError, requireArgs, unk
12
12
  import { cliVersionResult, } from "./cli/version.js";
13
13
  import { DataikuClient, } from "./client.js";
14
14
  import { getCredentialsPath, } from "./config.js";
15
- import { ClientValidationError, DataikuError, dataikuErrorCode, } from "./errors.js";
15
+ import { canonicalStatusText, ClientValidationError, DataikuError, dataikuErrorCode, } from "./errors.js";
16
16
  import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, planSkillInstalls, } from "./skill.js";
17
17
  import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
18
18
  // ---------------------------------------------------------------------------
@@ -833,16 +833,41 @@ function rawCommandContext() {
833
833
  : undefined),
834
834
  };
835
835
  }
836
- function requestIdFromBody(body) {
836
+ const SAFE_RESPONSE_METADATA_KEYS = [
837
+ "requestId",
838
+ "request_id",
839
+ "errorId",
840
+ "elapsedMs",
841
+ ];
842
+ function safeResponseMetadata(body) {
837
843
  try {
838
844
  const parsed = JSON.parse(body);
839
- const value = parsed.requestId ?? parsed.request_id ?? parsed.errorId;
840
- return typeof value === "string" && value.length > 0 ? value : undefined;
845
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
846
+ return {};
847
+ const metadata = {};
848
+ for (const key of SAFE_RESPONSE_METADATA_KEYS) {
849
+ const value = parsed[key];
850
+ if ((typeof value === "string" && value.length > 0)
851
+ || (typeof value === "number" && Number.isFinite(value))
852
+ || typeof value === "boolean") {
853
+ metadata[key] = value;
854
+ }
855
+ }
856
+ return metadata;
841
857
  }
842
858
  catch {
843
- return undefined;
859
+ return {};
844
860
  }
845
861
  }
862
+ function requestIdFromBody(body) {
863
+ const metadata = safeResponseMetadata(body);
864
+ for (const key of ["requestId", "request_id", "errorId",]) {
865
+ const value = metadata[key];
866
+ if (typeof value === "string")
867
+ return value;
868
+ }
869
+ return undefined;
870
+ }
846
871
  const MISSING_PROJECT_KEY_ERROR_PREFIX = "projectKey is required";
847
872
  function errorExitCode(err) {
848
873
  if (err instanceof CommandResultFailure)
@@ -884,13 +909,18 @@ function buildErrorReport(err) {
884
909
  };
885
910
  }
886
911
  if (err instanceof DataikuError) {
887
- const errorMessage = err.category === "not_found"
888
- && err.message.includes("Dataiku instance not found")
889
- && context.resource
912
+ const errorMessage = err.category === "not_found" && context.resource
890
913
  ? `Not found: ${context.resource}${context.action ? ` ${context.action}` : ""}`
891
914
  + `${context.projectKey ? ` in project ${context.projectKey}` : ""}`
892
- + ` — verify the object identifier and project key (DSS: ${err.message.split("\n")[0]}).`
893
- : err.message;
915
+ + " — verify the object identifier and project key."
916
+ + `\nHint: ${err.retryHint}`
917
+ : err.safeMessage;
918
+ const safeMetadata = safeResponseMetadata(err.body);
919
+ if (err.trustedTarget)
920
+ safeMetadata.target = err.trustedTarget;
921
+ if (err.trustedElapsedMs !== undefined)
922
+ safeMetadata.elapsedMs = err.trustedElapsedMs;
923
+ const safeBody = JSON.stringify(safeMetadata);
894
924
  return {
895
925
  type: "error",
896
926
  ok: false,
@@ -904,8 +934,8 @@ function buildErrorReport(err) {
904
934
  requestId: err.requestId ?? requestIdFromBody(err.body),
905
935
  details: {
906
936
  dssCategory: err.category,
907
- statusText: err.statusText,
908
- body: err.body,
937
+ statusText: canonicalStatusText(err.status),
938
+ body: safeBody,
909
939
  ...(err.retry ? { retry: err.retry, } : {}),
910
940
  },
911
941
  ...context,
@@ -969,7 +999,7 @@ async function main() {
969
999
  if (metaCommand) {
970
1000
  writeCommandResult(metaCommand.result);
971
1001
  if (metaCommand.exitCode !== 0)
972
- process.exit(metaCommand.exitCode);
1002
+ process.exitCode = metaCommand.exitCode;
973
1003
  return;
974
1004
  }
975
1005
  if (!commands[resource])
@@ -1033,5 +1063,5 @@ async function main() {
1033
1063
  }
1034
1064
  main().catch((err) => {
1035
1065
  writeErrorReport(err);
1036
- process.exit(errorExitCode(err));
1066
+ process.exitCode = errorExitCode(err);
1037
1067
  });
@@ -15,6 +15,11 @@ export interface DataikuRetryMetadata {
15
15
  delaysMs: number[];
16
16
  timedOut: boolean;
17
17
  }
18
+ export interface DataikuErrorTrustedMetadata {
19
+ target?: string;
20
+ elapsedMs?: number;
21
+ }
22
+ export declare function canonicalStatusText(status: number): string;
18
23
  export declare class ClientValidationError extends Error {
19
24
  readonly code: StableErrorCode;
20
25
  readonly hint?: string;
@@ -31,7 +36,10 @@ export declare class DataikuError extends Error {
31
36
  retryHint: string;
32
37
  retry?: DataikuRetryMetadata;
33
38
  requestId?: string;
34
- constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string);
39
+ readonly trustedTarget?: string;
40
+ readonly trustedElapsedMs?: number;
41
+ constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string, trustedMetadata?: DataikuErrorTrustedMetadata);
42
+ get safeMessage(): string;
35
43
  private static extractSummary;
36
44
  private static formatRetryMetadata;
37
45
  private static buildDetails;
@@ -12,6 +12,25 @@ export function dataikuErrorCode(category) {
12
12
  return "internal_error";
13
13
  }
14
14
  }
15
+ const CANONICAL_STATUS_TEXT = {
16
+ 400: "Bad Request",
17
+ 401: "Unauthorized",
18
+ 403: "Forbidden",
19
+ 404: "Not Found",
20
+ 408: "Request Timeout",
21
+ 409: "Conflict",
22
+ 422: "Unprocessable Entity",
23
+ 425: "Too Early",
24
+ 429: "Too Many Requests",
25
+ 500: "Internal Server Error",
26
+ 501: "Not Implemented",
27
+ 502: "Bad Gateway",
28
+ 503: "Service Unavailable",
29
+ 504: "Gateway Timeout",
30
+ };
31
+ export function canonicalStatusText(status) {
32
+ return CANONICAL_STATUS_TEXT[status] ?? (status === 0 ? "Network Error" : "HTTP Error");
33
+ }
15
34
  export class ClientValidationError extends Error {
16
35
  code;
17
36
  hint;
@@ -276,8 +295,10 @@ export class DataikuError extends Error {
276
295
  retryHint;
277
296
  retry;
278
297
  requestId;
279
- constructor(status, statusText, body, retry, requestId) {
280
- const details = DataikuError.buildDetails(status, statusText, body, retry);
298
+ trustedTarget;
299
+ trustedElapsedMs;
300
+ constructor(status, statusText, body, retry, requestId, trustedMetadata) {
301
+ const details = DataikuError.buildDetails(status, body, retry);
281
302
  super(details.message);
282
303
  this.status = status;
283
304
  this.statusText = statusText;
@@ -288,8 +309,20 @@ export class DataikuError extends Error {
288
309
  this.retryHint = details.retryHint;
289
310
  this.retry = retry;
290
311
  this.requestId = requestId;
312
+ this.trustedTarget = trustedMetadata?.target;
313
+ this.trustedElapsedMs = trustedMetadata?.elapsedMs;
314
+ }
315
+ get safeMessage() {
316
+ const retrySummary = DataikuError.formatRetryMetadata(this.retry);
317
+ return [
318
+ `${this.status} ${canonicalStatusText(this.status)}`,
319
+ `Error type: ${this.category}`,
320
+ `Retryable: ${this.retryable ? "yes" : "no"}`,
321
+ `Hint: ${this.retryHint}`,
322
+ ...(retrySummary ? [retrySummary,] : []),
323
+ ].join("\n");
291
324
  }
292
- static extractSummary(_status, _statusText, body) {
325
+ static extractSummary(_status, body) {
293
326
  try {
294
327
  const parsed = JSON.parse(body);
295
328
  if (parsed.message)
@@ -316,14 +349,14 @@ export class DataikuError extends Error {
316
349
  `Timed out: ${retry.timedOut ? "yes" : "no"}`,
317
350
  ].join(" | ");
318
351
  }
319
- static buildDetails(status, statusText, body, retry) {
320
- const summary = DataikuError.extractSummary(status, statusText, body);
352
+ static buildDetails(status, body, retry) {
353
+ const summary = DataikuError.extractSummary(status, body);
321
354
  const taxonomy = classifyDataikuError(status, body);
322
355
  const retrySummary = DataikuError.formatRetryMetadata(retry);
323
356
  return {
324
357
  ...taxonomy,
325
358
  message: [
326
- `${status} ${statusText}: ${summary}`,
359
+ `${status} ${canonicalStatusText(status)}: ${summary}`,
327
360
  `Error type: ${taxonomy.category}`,
328
361
  `Retryable: ${taxonomy.retryable ? "yes" : "no"}`,
329
362
  `Hint: ${taxonomy.retryHint}`,
@@ -28,6 +28,7 @@ export class ApiServicesResource extends BaseResource {
28
28
  }
29
29
  /** List deployable packages for an API service. */
30
30
  async listPackages(serviceId, projectKey) {
31
+ await this.getSettings(serviceId, projectKey);
31
32
  return this.client.get(`${this.servicePath(serviceId, projectKey)}/packages`);
32
33
  }
33
34
  /** Get an API service package summary. */
@@ -36,11 +36,15 @@ export class NotebooksResource extends BaseResource {
36
36
  */
37
37
  async clearJupyterOutputs(name, projectKey) {
38
38
  const notebook = await this.getJupyter(name, projectKey);
39
- const clearedCells = notebook.cells.map((cell) => ({
40
- ...cell,
41
- outputs: [],
42
- execution_count: null,
43
- }));
39
+ const clearedCells = notebook.cells.map((cell) => {
40
+ if (cell.cell_type !== "code")
41
+ return cell;
42
+ return {
43
+ ...cell,
44
+ outputs: [],
45
+ execution_count: 0,
46
+ };
47
+ });
44
48
  await this.saveJupyter(name, { ...notebook, cells: clearedCells, }, projectKey);
45
49
  }
46
50
  /** List running kernel sessions for a Jupyter notebook. */
@@ -1,4 +1,4 @@
1
- import { DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { BaseResource, } from "./base.js";
3
3
  const UNSUPPORTED_SQL_DATASET_CONNECTION_DETAIL = "neither of sql nor hdfs type";
4
4
  function isUnsupportedSqlDatasetConnectionError(error) {
@@ -161,28 +161,23 @@ export class SqlResource extends BaseResource {
161
161
  const datasetFullName = opts.datasetFullName;
162
162
  if (!datasetFullName)
163
163
  return null;
164
- try {
165
- const identifier = splitDatasetIdentifier(datasetFullName, opts.projectKey);
166
- const projectKey = identifier.projectKey
167
- ? identifier.projectKey
168
- : this.resolveProjectKey(opts.projectKey);
169
- const dsEnc = encodeURIComponent(identifier.datasetName);
170
- const raw = await this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/datasets/${dsEnc}`);
171
- const params = asRecord(raw.params);
172
- const connection = asString(params?.connection);
173
- if (!connection)
174
- return null;
175
- return {
176
- ...opts,
177
- connection,
178
- datasetFullName: undefined,
179
- database: opts.database ?? asString(params?.schema) ?? asString(params?.catalog),
180
- projectKey,
181
- };
182
- }
183
- catch {
164
+ const identifier = splitDatasetIdentifier(datasetFullName, opts.projectKey);
165
+ const projectKey = identifier.projectKey
166
+ ? identifier.projectKey
167
+ : this.resolveProjectKey(opts.projectKey);
168
+ const dsEnc = encodeURIComponent(identifier.datasetName);
169
+ const raw = await this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/datasets/${dsEnc}`);
170
+ const params = asRecord(raw.params);
171
+ const connection = asString(params?.connection);
172
+ if (!connection)
184
173
  return null;
185
- }
174
+ return {
175
+ ...opts,
176
+ connection,
177
+ datasetFullName: undefined,
178
+ database: opts.database ?? asString(params?.schema) ?? asString(params?.catalog),
179
+ projectKey,
180
+ };
186
181
  }
187
182
  /**
188
183
  * Execute a SQL query end-to-end: start, stream all rows, verify, return combined result.
@@ -198,9 +193,7 @@ export class SqlResource extends BaseResource {
198
193
  withSqlErrorContext(error);
199
194
  const retryOpts = await this.resolveDatasetQueryFallback(queryOpts);
200
195
  if (!retryOpts) {
201
- throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
202
- cause: error,
203
- });
196
+ throw new ClientValidationError(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName));
204
197
  }
205
198
  try {
206
199
  return await this.executeQuery(retryOpts);
package/dist/src/skill.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { execFileSync, } from "node:child_process";
2
- import { existsSync, mkdirSync, writeFileSync, } from "node:fs";
3
- import { homedir, } from "node:os";
4
- import { dirname, join, } from "node:path";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
5
  const SKILL_BODY = `# Dataiku DSS agent CLI
6
6
 
7
7
  Use \`dss\` when an agent needs to inspect or change Dataiku DSS resources: projects, datasets, recipes, jobs, scenarios, folders, notebooks, SQL, variables, code envs, and connections.
8
- If the installed \`dss\` binary is unavailable but the repository checkout is the current workspace, use \`./bin/dss ...\` or \`bun --no-env-file src/cli.ts ...\` with the same arguments; from another working directory, call \`/path/to/dataiku-sdk/bin/dss ...\`.
8
+ If the installed \`dss\` binary is unavailable but the repository checkout is the current workspace, prefer \`bun --no-env-file src/cli.ts ...\` or \`bun --no-env-file ./bin/dss.js ...\`. Node users can invoke the same cross-runtime launcher as \`node ./bin/dss.js ...\`; when \`dist/\` has not been built, it delegates to Bun. From another working directory, pass the checkout's absolute \`bin/dss.js\` path to Bun or Node.
9
9
  \`--no-env-file\` disables Bun's automatic preloading only; the CLI still applies its documented \`.env\` handling unless \`DATAIKU_DISABLE_ENV=1\` is set.
10
10
 
11
11
  ## Contract
@@ -20,7 +20,7 @@ If the installed \`dss\` binary is unavailable but the repository checkout is th
20
20
 
21
21
  ## Discover commands
22
22
 
23
- \`\`\`bash
23
+ \`\`\`text
24
24
  dss agent contract
25
25
  dss commands run
26
26
  \`\`\`
@@ -31,30 +31,57 @@ Set \`DATAIKU_DISABLE_ENV=1\` when a test must ignore both \`.env\` files and \`
31
31
  When \`.env\` loading is enabled, the CLI reads \`.env\` from the command's current working directory first and then the CLI build/root directory; the invocation directory wins on conflicting keys. Put test-specific \`.env\` files in the directory where you invoke \`dss\`.
32
32
  For disposable agent tests, set \`DSS_CONFIG_DIR\` to a temporary directory so saved credentials never touch the real profile.
33
33
 
34
+ ## Planning, safety, and generic inputs
35
+
36
+ - Before any registry entry with \`sideEffect:"write"\`, run the exact argv with \`--plan\` first. Planning is local: it returns the derived operation without resolving credentials or calling DSS. Check \`destructive\`, \`idempotency\`, \`async\`, \`unsafeOutputs\`, and \`exitCodes\` before execution.
37
+ - Use \`--dry-run\` only when that action's registry entry has \`dryRun:true\`. \`--plan\` explains the operation; \`--dry-run\` exercises the action-specific simulation path. Never add an unsupported flag.
38
+ - When a create/upload action advertises \`--record-cleanup\`, pass \`--record-cleanup cleanup.jsonl\`. \`dss cleanup --file cleanup.jsonl\` previews the recorded cleanup steps in reverse order and does not mutate DSS; add \`--apply\` only after checking the preview.
39
+ - For JSON payload actions, follow \`inputContract\`, \`requiredFlags\`, and \`requiredOneOf\`. Prefer \`--data-file PATH\` or \`--stdin\` when advertised instead of inline \`--data\`; this preserves exact JSON across shells and keeps large or sensitive payloads out of process arguments.
40
+ - Authenticated actions advertise \`--request-timeout MS\` and \`--retries N\`. Long-running actions separately advertise controls such as \`--timeout MS\`, \`--poll-interval MS\`, and log limits; use only the flags in that action's registry entry.
41
+ - Before live mutation tests, use \`dss fixtures --json\` to discover compatible test resources instead of guessing project objects.
42
+
34
43
  ## Authentication
35
44
 
36
- Prefer environment variables for ephemeral agent runs:
45
+ Prefer environment variables for ephemeral agent runs. Use the syntax for the active shell:
37
46
 
38
- \`\`\`bash
47
+ POSIX shell:
48
+
49
+ \`\`\`sh
39
50
  export DATAIKU_URL=https://dss.example.com
40
51
  export DATAIKU_API_KEY=your-api-key
41
52
  export DATAIKU_PROJECT_KEY=MYPROJ
42
53
  \`\`\`
43
54
 
55
+ PowerShell:
56
+
57
+ \`\`\`powershell
58
+ $env:DATAIKU_URL = "https://dss.example.com"
59
+ $env:DATAIKU_API_KEY = "your-api-key"
60
+ $env:DATAIKU_PROJECT_KEY = "MYPROJ"
61
+ \`\`\`
62
+
63
+ Windows Command Prompt:
64
+
65
+ \`\`\`bat
66
+ set "DATAIKU_URL=https://dss.example.com"
67
+ set "DATAIKU_API_KEY=your-api-key"
68
+ set "DATAIKU_PROJECT_KEY=MYPROJ"
69
+ \`\`\`
70
+
44
71
  To persist credentials for later invocations:
45
72
 
46
73
  \`\`\`bash
47
74
  dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ
48
75
  \`\`\`
49
76
 
50
- The command saves credentials and returns \`{"saved":true,"path":"..."}\`. Credentials are saved to \`~/.config/dataiku/credentials.json\` unless \`DSS_CONFIG_DIR\` or platform config env vars redirect the path.
77
+ The command saves credentials and returns \`{"saved":true,"path":"..."}\`. \`DSS_CONFIG_DIR\` wins when set. Otherwise credentials use \`XDG_CONFIG_HOME/dataiku/credentials.json\`, the \`dataiku/credentials.json\` directory under \`APPDATA\` on Windows, or \`~/.config/dataiku/credentials.json\`.
51
78
  \`auth login\` validates by listing accessible projects before saving credentials, so the API key must be allowed to call DSS project-list APIs.
52
79
 
53
80
  TLS flags: \`--insecure\` disables certificate verification; \`--ca-cert PATH\` adds a PEM CA bundle. Environment equivalents: \`NODE_TLS_REJECT_UNAUTHORIZED\`, \`NODE_EXTRA_CA_CERTS\`.
54
81
 
55
82
  ## Common workflows
56
83
 
57
- \`\`\`bash
84
+ \`\`\`text
58
85
  dss version
59
86
  dss project list
60
87
  dss doctor --fast
@@ -77,10 +104,10 @@ For fake-DSS smoke tests, return project lists as JSON arrays such as \`[{"proje
77
104
 
78
105
  Mutations print a small JSON ack to stdout and exit 0 on success (e.g. \`{"updated":"NAME","resource":"recipe"}\`); on failure they print the error envelope to stderr and exit non-zero. The exit code is the source of truth.
79
106
 
80
- - Chain steps with \`&&\` so a failed step halts the sequence: \`dss recipe set-payload R --file r.py --project-key P && dss recipe update R --data-file env.json --project-key P\`.
81
- - Never pipe a mutation into a command that prints a fixed string or merges stderr (e.g. \`dss ... 2>&1 | helper; echo done\`): the pipeline returns the helper's exit code, so a failed mutation is reported as success.
107
+ - For portable multi-step writes, prefer \`dss batch\` (payload: a JSON array of argv arrays). It runs fail-fast, returns one envelope with per-step \`ok\`/\`result\`/\`error\`, and exits non-zero if any step fails.
108
+ - If separate processes are required, inspect each exit code and stop before launching the next step. Do not rely on shell chaining: syntax differs between POSIX shells, Windows PowerShell 5.1, PowerShell 7, and Command Prompt.
109
+ - Never pipe a mutation into a command that prints a fixed string or merges stderr: a pipeline may return the helper's exit code and report a failed mutation as success.
82
110
  - To branch in code, key off the exit code or the JSON ack on stdout — never a hardcoded label.
83
- - For multi-step writes, prefer \`dss batch\` (payload: a JSON array of argv arrays): it runs fail-fast, returns one envelope with per-step \`ok\`/\`result\`/\`error\`, and exits non-zero if any step fails — no shell chaining or per-step parsing.
84
111
 
85
112
  ## Platform & debugging notes
86
113
 
@@ -127,7 +154,7 @@ export const AGENTS = {
127
154
  name: "Claude Code",
128
155
  binary: "claude",
129
156
  configDir: ".claude",
130
- globalPath: (home) => join(home, ".claude", "skills", "dataiku-dss"),
157
+ globalPath: (home) => path.join(home, ".claude", "skills", "dataiku-dss"),
131
158
  projectPath: ".claude/skills/dataiku-dss",
132
159
  filename: "SKILL.md",
133
160
  content: skillContent,
@@ -136,7 +163,7 @@ export const AGENTS = {
136
163
  name: "Codex",
137
164
  binary: "codex",
138
165
  configDir: ".codex",
139
- globalPath: (home) => join(home, ".codex", "skills", "dataiku-dss"),
166
+ globalPath: (home) => path.join(home, ".codex", "skills", "dataiku-dss"),
140
167
  projectPath: ".codex/skills/dataiku-dss",
141
168
  filename: "SKILL.md",
142
169
  content: skillContent,
@@ -145,7 +172,7 @@ export const AGENTS = {
145
172
  name: "Cursor",
146
173
  binary: "cursor",
147
174
  configDir: ".cursor",
148
- globalPath: (home) => join(home, ".cursor", "skills", "dataiku-dss"),
175
+ globalPath: (home) => path.join(home, ".cursor", "skills", "dataiku-dss"),
149
176
  projectPath: ".cursor/skills/dataiku-dss",
150
177
  filename: "SKILL.md",
151
178
  content: skillContent,
@@ -154,7 +181,7 @@ export const AGENTS = {
154
181
  name: "Pi",
155
182
  binary: "pi",
156
183
  configDir: ".pi",
157
- globalPath: (home) => join(home, ".pi", "agent", "skills", "dataiku-dss"),
184
+ globalPath: (home) => path.join(home, ".pi", "agent", "skills", "dataiku-dss"),
158
185
  projectPath: ".pi/skills/dataiku-dss",
159
186
  filename: "SKILL.md",
160
187
  content: skillContent,
@@ -162,9 +189,9 @@ export const AGENTS = {
162
189
  omp: {
163
190
  name: "OhMyPi",
164
191
  binary: "omp",
165
- configDir: join(".omp", "agent"),
192
+ configDir: path.join(".omp", "agent"),
166
193
  configDirRequired: true,
167
- globalPath: (home) => join(home, ".omp", "agent", "skills", "dataiku-dss"),
194
+ globalPath: (home) => path.join(home, ".omp", "agent", "skills", "dataiku-dss"),
168
195
  projectPath: ".omp/skills/dataiku-dss",
169
196
  filename: "SKILL.md",
170
197
  content: skillContent,
@@ -184,11 +211,11 @@ function binaryExists(name) {
184
211
  }
185
212
  }
186
213
  export function detectAgents() {
187
- const home = homedir();
214
+ const home = os.homedir();
188
215
  const found = [];
189
216
  for (const [id, def,] of Object.entries(AGENTS)) {
190
217
  const hasBinary = binaryExists(def.binary);
191
- const hasConfigDir = existsSync(join(home, def.configDir));
218
+ const hasConfigDir = fs.existsSync(path.join(home, def.configDir));
192
219
  if (hasBinary && (!def.configDirRequired || hasConfigDir)) {
193
220
  found.push({ id, def, via: "binary", });
194
221
  }
@@ -213,10 +240,10 @@ export function findWorkspaceRoot(startDir) {
213
240
  let dir = startDir;
214
241
  for (let i = 0; i < 20; i++) {
215
242
  for (const marker of WORKSPACE_MARKERS) {
216
- if (existsSync(join(dir, marker)))
243
+ if (fs.existsSync(path.join(dir, marker)))
217
244
  return dir;
218
245
  }
219
- const parent = dirname(dir);
246
+ const parent = path.dirname(dir);
220
247
  if (parent === dir)
221
248
  break;
222
249
  dir = parent;
@@ -224,17 +251,17 @@ export function findWorkspaceRoot(startDir) {
224
251
  return startDir;
225
252
  }
226
253
  export function planSkillInstalls(agents, opts) {
227
- const home = homedir();
254
+ const home = os.homedir();
228
255
  const results = [];
229
256
  for (const { id, def, via, } of agents) {
230
257
  const dir = opts.global
231
258
  ? def.globalPath(home)
232
259
  : def.projectPath
233
- ? join(opts.cwd, def.projectPath)
260
+ ? path.join(opts.cwd, def.projectPath)
234
261
  : undefined;
235
262
  if (!dir)
236
263
  continue;
237
- results.push({ agent: id, path: join(dir, def.filename), via, });
264
+ results.push({ agent: id, path: path.join(dir, def.filename), via, });
238
265
  }
239
266
  return results;
240
267
  }
@@ -244,8 +271,8 @@ export function installSkill(agents, opts) {
244
271
  const def = AGENTS[result.agent];
245
272
  if (!def)
246
273
  continue;
247
- mkdirSync(dirname(result.path), { recursive: true, });
248
- writeFileSync(result.path, def.content(), "utf-8");
274
+ fs.mkdirSync(path.dirname(result.path), { recursive: true, });
275
+ fs.writeFileSync(result.path, def.content(), "utf-8");
249
276
  }
250
277
  return results;
251
278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -17,25 +17,28 @@
17
17
  "packages/types/dist/"
18
18
  ],
19
19
  "engines": {
20
- "bun": ">=1.0"
20
+ "node": ">=22.15.0",
21
+ "bun": ">=1.3.14"
21
22
  },
22
23
  "scripts": {
23
- "build": "tsc && cd packages/types && npx tsc",
24
- "prepack": "bun run build",
25
- "prepublishOnly": "bun run build",
24
+ "build": "tsc && tsc -p packages/types/tsconfig.json",
25
+ "prepack": "tsc && tsc -p packages/types/tsconfig.json",
26
+ "prepublishOnly": "tsc && tsc -p packages/types/tsconfig.json",
26
27
  "check": "tsc --noEmit",
27
28
  "lint": "oxlint src/",
28
29
  "lint:fix": "oxlint --fix src/",
29
30
  "format": "dprint fmt",
30
31
  "format:check": "dprint check",
31
32
  "test": "bun test",
32
- "test:integration": "RUN_DATAIKU_INTEGRATION=1 bun test tests/integration-playground.test.ts",
33
- "test:integration:mutating": "RUN_DATAIKU_INTEGRATION=1 RUN_DATAIKU_INTEGRATION_MUTATING=1 bun test tests/integration-playground.test.ts",
34
- "test:integration:rigorous": "RUN_DATAIKU_INTEGRATION=1 bun test tests/integration-rigorous.test.ts",
35
- "test:integration:rigorous:mutating": "RUN_DATAIKU_INTEGRATION=1 RUN_DATAIKU_INTEGRATION_MUTATING=1 bun test tests/integration-rigorous.test.ts"
33
+ "test:platform": "bun scripts/platform-smoke.mjs",
34
+ "test:integration": "cross-env RUN_DATAIKU_INTEGRATION=1 bun test tests/integration-playground.test.ts",
35
+ "test:integration:mutating": "cross-env RUN_DATAIKU_INTEGRATION=1 RUN_DATAIKU_INTEGRATION_MUTATING=1 bun test tests/integration-playground.test.ts",
36
+ "test:integration:rigorous": "cross-env RUN_DATAIKU_INTEGRATION=1 bun test tests/integration-rigorous.test.ts",
37
+ "test:integration:rigorous:mutating": "cross-env RUN_DATAIKU_INTEGRATION=1 RUN_DATAIKU_INTEGRATION_MUTATING=1 bun test tests/integration-rigorous.test.ts"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@types/bun": "^1.3.14",
41
+ "cross-env": "^10.1.0",
39
42
  "dprint": "^0.54.0",
40
43
  "oxlint": "^1.68.0",
41
44
  "typescript": "^6.0.3"
@@ -53,5 +56,8 @@
53
56
  },
54
57
  "publishConfig": {
55
58
  "access": "public"
56
- }
59
+ },
60
+ "bundleDependencies": [
61
+ "@sinclair/typebox"
62
+ ]
57
63
  }