dataiku-sdk 0.6.2 → 0.7.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.
@@ -112,6 +112,15 @@ function isSqlRecipeType(recipeType) {
112
112
  return typeof recipeType === "string" && recipeType.toLowerCase().includes("sql");
113
113
  }
114
114
  function rewriteSqlTableReferences(payload, rewrites) {
115
+ const bareIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
116
+ const escapeQuotedIdentifier = (identifier, quote) => {
117
+ if (quote === "\"")
118
+ return identifier.replace(/"/g, '""');
119
+ if (quote === "`")
120
+ return identifier.replace(/`/g, "``");
121
+ return identifier;
122
+ };
123
+ const escapeBracketIdentifier = (identifier) => identifier.replace(/\]/g, "]]");
115
124
  let next = payload;
116
125
  for (const [from, to,] of Object.entries(rewrites)) {
117
126
  if (!from)
@@ -120,10 +129,17 @@ function rewriteSqlTableReferences(payload, rewrites) {
120
129
  const pattern = new RegExp(String
121
130
  .raw `\b(FROM|JOIN)(\s+)(?:(["\`])${escaped}\3|(\[)${escaped}\]|${escaped})(?![A-Za-z0-9_.])`, "gi");
122
131
  next = next.replace(pattern, (_match, keyword, space, quote, bracket) => {
123
- if (quote)
124
- return `${keyword}${space}${quote}${to}${quote}`;
125
- if (bracket)
126
- return `${keyword}${space}[${to}]`;
132
+ if (quote) {
133
+ const escapedTo = escapeQuotedIdentifier(to, quote);
134
+ return `${keyword}${space}${quote}${escapedTo}${quote}`;
135
+ }
136
+ if (bracket) {
137
+ const escapedTo = escapeBracketIdentifier(to);
138
+ return `${keyword}${space}[${escapedTo}]`;
139
+ }
140
+ if (!bareIdentifierPattern.test(to)) {
141
+ throw new Error(`Unsafe SQL rewrite target for ${from}: ${to}`);
142
+ }
127
143
  return `${keyword}${space}${to}`;
128
144
  });
129
145
  }
@@ -31,6 +31,17 @@ export interface ScenarioUpdateResult extends ScenarioUpdatePreview {
31
31
  verified: true;
32
32
  mismatches: [];
33
33
  }
34
+ export interface ScenarioScriptRunResult {
35
+ scenarioId: string;
36
+ runId: string;
37
+ outcome: string;
38
+ success: boolean;
39
+ elapsedMs: number;
40
+ pollCount: number;
41
+ output?: string;
42
+ log: string;
43
+ envName?: string;
44
+ }
34
45
  export declare function normalizeScenarioUpdateData(data: Record<string, unknown>): {
35
46
  normalizedData: Record<string, unknown>;
36
47
  normalization: ScenarioUpdateNormalization[];
@@ -68,4 +79,17 @@ export declare class ScenariosResource extends BaseResource {
68
79
  timeoutMs?: number;
69
80
  projectKey?: string;
70
81
  }): Promise<ScenarioWaitResult>;
82
+ /**
83
+ * Run a one-off Python script in a throwaway custom-python scenario and return
84
+ * its outcome plus the captured run log. The scenario is deleted afterward
85
+ * unless `keepScenario` is set. This is the only DSS public-API path to execute
86
+ * ad-hoc code in a code env without a persisted recipe or notebook.
87
+ */
88
+ runScript(script: string, opts?: {
89
+ envName?: string;
90
+ projectKey?: string;
91
+ timeoutMs?: number;
92
+ pollIntervalMs?: number;
93
+ keepScenario?: boolean;
94
+ }): Promise<ScenarioScriptRunResult>;
71
95
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID, } from "node:crypto";
1
2
  import { DataikuError, } from "../errors.js";
2
3
  import { ScenarioDetailsSchema, ScenarioStatusSchema, ScenarioSummaryArraySchema, } from "../schemas.js";
3
4
  import { deepMerge, } from "../utils/deep-merge.js";
@@ -11,6 +12,76 @@ export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
11
12
  "active",
12
13
  "name",
13
14
  ];
15
+ const CODE_RUN_OUTPUT_START = "<<<DSS_CODE_RUN_OUTPUT_b7e3a1>>>";
16
+ const CODE_RUN_OUTPUT_END = "<<<DSS_CODE_RUN_OUTPUT_END_b7e3a1>>>";
17
+ /**
18
+ * Wrap a user Python script so its stdout/stderr (and any traceback) are captured
19
+ * into a buffer and re-emitted between unique markers, isolated from DSS scenario
20
+ * wrapper noise. The script is base64-encoded to avoid quoting/escaping issues and
21
+ * exec'd as `__main__`. A failing script re-raises SystemExit(1) so the scenario
22
+ * outcome is FAILED while the captured traceback still lands between the markers.
23
+ */
24
+ function buildCodeRunScript(script) {
25
+ const encoded = Buffer.from(script, "utf-8").toString("base64");
26
+ return [
27
+ "import base64 as _dku_b64, sys as _dku_sys, io as _dku_io, traceback as _dku_tb",
28
+ `_dku_src = _dku_b64.b64decode("${encoded}").decode("utf-8")`,
29
+ "_dku_buf = _dku_io.StringIO()",
30
+ "_dku_out, _dku_err = _dku_sys.stdout, _dku_sys.stderr",
31
+ "_dku_sys.stdout = _dku_sys.stderr = _dku_buf",
32
+ "_dku_code = 0",
33
+ "try:",
34
+ '\texec(compile(_dku_src, "<dss_code_run>", "exec"), {"__name__": "__main__"})',
35
+ "except SystemExit as _dku_e:",
36
+ "\t_dku_code = _dku_e.code if isinstance(_dku_e.code, int) else (0 if _dku_e.code is None else 1)",
37
+ "except BaseException:",
38
+ "\t_dku_code = 1",
39
+ "\t_dku_tb.print_exc()",
40
+ "finally:",
41
+ "\t_dku_sys.stdout, _dku_sys.stderr = _dku_out, _dku_err",
42
+ `\t_dku_out.write("${CODE_RUN_OUTPUT_START}\\n")`,
43
+ "\t_dku_out.write(_dku_buf.getvalue())",
44
+ `\t_dku_out.write("\\n${CODE_RUN_OUTPUT_END}\\n")`,
45
+ "\t_dku_out.flush()",
46
+ "if _dku_code:",
47
+ "\traise SystemExit(_dku_code)",
48
+ "",
49
+ ].join("\n");
50
+ }
51
+ /**
52
+ * Pull the script's own stdout/stderr back out of the full DSS run log by slicing
53
+ * the `[process]` lines between the markers emitted by {@link buildCodeRunScript}.
54
+ * Returns undefined if the markers are absent (e.g. the harness never ran), in which
55
+ * case callers should fall back to the full log.
56
+ */
57
+ function extractCodeRunOutput(log) {
58
+ const messageRe = /^\[[^\]]*\] \[[^\]]*\] \[[^\]]*\] \[process\] - (.*)$/;
59
+ const contents = [];
60
+ for (const rawLine of log.split("\n")) {
61
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
62
+ const match = messageRe.exec(line);
63
+ if (match)
64
+ contents.push(match[1] ?? "");
65
+ }
66
+ // First start + last end, so a script that prints a marker string stays body content.
67
+ const start = contents.indexOf(CODE_RUN_OUTPUT_START);
68
+ if (start < 0)
69
+ return undefined;
70
+ let end = -1;
71
+ for (let i = contents.length - 1; i > start; i--) {
72
+ if (contents[i] === CODE_RUN_OUTPUT_END) {
73
+ end = i;
74
+ break;
75
+ }
76
+ }
77
+ if (end < 0)
78
+ return undefined;
79
+ const body = contents.slice(start + 1, end);
80
+ // Drop only the single trailing separator the harness writes before the end marker.
81
+ if (body.length > 0 && body[body.length - 1] === "")
82
+ body.pop();
83
+ return body.join("\n");
84
+ }
14
85
  function isRecord(value) {
15
86
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
87
  }
@@ -260,4 +331,94 @@ export class ScenariosResource extends BaseResource {
260
331
  await new Promise((r) => setTimeout(r, Math.min(nextDelayMs, timeout - elapsedMs)));
261
332
  }
262
333
  }
334
+ /**
335
+ * Run a one-off Python script in a throwaway custom-python scenario and return
336
+ * its outcome plus the captured run log. The scenario is deleted afterward
337
+ * unless `keepScenario` is set. This is the only DSS public-API path to execute
338
+ * ad-hoc code in a code env without a persisted recipe or notebook.
339
+ */
340
+ async runScript(script, opts) {
341
+ const pk = this.resolveProjectKey(opts?.projectKey);
342
+ const pkEnc = this.enc(opts?.projectKey);
343
+ const scenarioId = `dss_cli_code_run_${Date.now()}_${randomUUID().replace(/-/g, "")}`;
344
+ const base = `/public/api/projects/${pkEnc}/scenarios/${encodeURIComponent(scenarioId)}`;
345
+ const envSelection = opts?.envName
346
+ ? { envMode: "EXPLICIT_ENV", envName: opts.envName, }
347
+ : { envMode: "INHERIT", };
348
+ const startedAt = Date.now();
349
+ const baseIntervalMs = Math.max(1, opts?.pollIntervalMs ?? 2_000);
350
+ const adaptivePolling = opts?.pollIntervalMs === undefined;
351
+ const timeout = Math.max(baseIntervalMs, opts?.timeoutMs ?? 120_000);
352
+ try {
353
+ await this.client.post(`/public/api/projects/${pkEnc}/scenarios/`, {
354
+ id: scenarioId,
355
+ name: `dss code run (${scenarioId})`,
356
+ projectKey: pk,
357
+ type: "custom_python",
358
+ params: { envSelection, },
359
+ });
360
+ await this.client.putVoid(`${base}/payload`, { script: buildCodeRunScript(script), extension: "py", });
361
+ const trigger = await this.client.post(`${base}/run/`, {});
362
+ const triggerObj = trigger.trigger;
363
+ const triggerId = triggerObj?.id ?? "manual";
364
+ const triggerRunId = String(trigger.runId ?? "");
365
+ if (!triggerRunId) {
366
+ throw new DataikuError(500, "Scenario run not started", `Scenario "${scenarioId}" run trigger returned no run id; cannot track the run.`);
367
+ }
368
+ const trigQuery = `triggerId=${encodeURIComponent(triggerId)}&triggerRunId=${encodeURIComponent(triggerRunId)}`;
369
+ let runId = "";
370
+ let outcome = "UNKNOWN";
371
+ let pollCount = 0;
372
+ while (true) {
373
+ if (Date.now() - startedAt >= timeout) {
374
+ outcome = "TIMEOUT";
375
+ break;
376
+ }
377
+ pollCount += 1;
378
+ const run = await this.client.get(`${base}/get-run-for-trigger?${trigQuery}`);
379
+ const scenarioRun = run.scenarioRun;
380
+ if (scenarioRun) {
381
+ runId = scenarioRun.runId ?? runId;
382
+ const result = scenarioRun.result;
383
+ const finished = result?.outcome;
384
+ if (finished) {
385
+ outcome = finished;
386
+ break;
387
+ }
388
+ }
389
+ const nextDelayMs = computeNextPollDelayMs({
390
+ pollCount,
391
+ baseIntervalMs,
392
+ adaptiveEnabled: adaptivePolling,
393
+ });
394
+ await new Promise((r) => setTimeout(r, Math.min(nextDelayMs, Math.max(1, timeout - (Date.now() - startedAt)))));
395
+ }
396
+ let log = "";
397
+ if (runId && outcome !== "TIMEOUT") {
398
+ log = await this.client.getText(`${base}/${encodeURIComponent(runId)}/log`);
399
+ }
400
+ const output = extractCodeRunOutput(log);
401
+ return {
402
+ scenarioId,
403
+ runId,
404
+ outcome,
405
+ success: outcome === "SUCCESS",
406
+ elapsedMs: Date.now() - startedAt,
407
+ pollCount,
408
+ output,
409
+ log,
410
+ ...(opts?.envName ? { envName: opts.envName, } : {}),
411
+ };
412
+ }
413
+ finally {
414
+ if (opts?.keepScenario !== true) {
415
+ try {
416
+ await this.client.del(base);
417
+ }
418
+ catch {
419
+ // Best-effort cleanup of the throwaway scenario.
420
+ }
421
+ }
422
+ }
423
+ }
263
424
  }
@@ -31,7 +31,12 @@ export declare function findWorkspaceRoot(startDir: string): string;
31
31
  export interface InstallResult {
32
32
  agent: string;
33
33
  path: string;
34
+ via: DetectedAgent["via"];
34
35
  }
36
+ export declare function planSkillInstalls(agents: DetectedAgent[], opts: {
37
+ global: boolean;
38
+ cwd: string;
39
+ }): InstallResult[];
35
40
  export declare function installSkill(agents: DetectedAgent[], opts: {
36
41
  global: boolean;
37
42
  cwd: string;
package/dist/src/skill.js CHANGED
@@ -2,124 +2,118 @@ import { execFileSync, } from "node:child_process";
2
2
  import { existsSync, mkdirSync, writeFileSync, } from "node:fs";
3
3
  import { homedir, } from "node:os";
4
4
  import { dirname, join, } from "node:path";
5
- const SKILL_BODY = `# Dataiku DSS CLI
5
+ const SKILL_BODY = `# Dataiku DSS agent CLI
6
6
 
7
- The \`dss\` CLI (npm: dataiku-sdk) manages Dataiku DSS resources from the terminal.
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 ...\`.
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.
8
10
 
9
- ## When to use
11
+ ## Contract
10
12
 
11
- - Query, create, or modify DSS projects, datasets, recipes, jobs, or scenarios.
12
- - Build datasets or run scenarios and wait for completion.
13
- - Download or upload recipe code, dataset data, or managed folder files.
14
- - Run SQL queries against DSS connections.
15
- - Inspect project flows, job logs, or dataset schemas.
13
+ - Success writes exactly one JSON result to stdout.
14
+ - Failure writes exactly one JSON error envelope to stderr with \`ok:false\`, \`error\`, \`code\`, and \`exitCode\`.
15
+ - \`--verbose\` may add HTTP trace lines to stderr.
16
+ - No prompts, help screens, tables, banners, or prose output are part of the contract.
17
+ - Exit codes: 0 success, 1 usage/configuration error, 2 DSS or internal error, 3 transient/retryable DSS error, 4 completed command with failed long-running DSS work.
18
+ - \`--raw\` is the only stdout escape hatch: recipe payload commands emit raw bytes to stdout unless \`--output PATH\` is also set; with \`--output\`, stdout is the JSON string equal to \`PATH\` and the file receives exact raw bytes.
19
+ - \`--fields a,b,c\` projects those fields from object or array-of-objects results; dotted paths (\`a.b.c\`) drill into nested objects, and missing fields become \`null\`; string and scalar results pass through unchanged.
16
20
 
17
- ## Installation
18
-
19
- Requires [Bun](https://bun.sh) runtime.
21
+ ## Discover commands
20
22
 
21
23
  \`\`\`bash
22
- bun add -g dataiku-sdk # global install \u2014 provides the \`dss\` command
24
+ dss commands run
23
25
  \`\`\`
24
26
 
25
- Or run without installing:
26
-
27
- \`\`\`bash
28
- bunx dataiku-sdk <command> # e.g. bunx dataiku-sdk auth login
29
- \`\`\`
27
+ The registry is the canonical schema for resources, actions, flags, positional arguments, side effects, auth requirements, output shape, idempotency, dry-run support, payload schemas, cleanup hints, and exit codes. Use it before choosing command syntax.
28
+ Credential lookup order is flags first, then \`DATAIKU_*\` environment variables, then saved credentials.
29
+ Set \`DATAIKU_DISABLE_ENV=1\` when a test must ignore both \`.env\` files and \`DATAIKU_*\` environment variables.
30
+ 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\`.
31
+ For disposable agent tests, set \`DSS_CONFIG_DIR\` to a temporary directory so saved credentials never touch the real profile.
30
32
 
31
33
  ## Authentication
32
34
 
33
- \`\`\`bash
34
- dss auth login # interactive: prompts for URL, API key, project key
35
- dss auth login --url https://dss.example.com --api-key YOUR_KEY
36
- dss auth status # verify connection
37
- \`\`\`
38
-
39
- Credentials are saved to \`~/.config/dataiku/credentials.json\`. Alternatively set environment variables:
35
+ Prefer environment variables for ephemeral agent runs:
40
36
 
41
37
  \`\`\`bash
42
38
  export DATAIKU_URL=https://dss.example.com
43
39
  export DATAIKU_API_KEY=your-api-key
44
- export DATAIKU_PROJECT_KEY=MYPROJ # optional default project
40
+ export DATAIKU_PROJECT_KEY=MYPROJ
45
41
  \`\`\`
46
42
 
47
- ## Workflows
48
-
49
- ### Inspect a project
43
+ To persist credentials for later invocations:
50
44
 
51
45
  \`\`\`bash
52
- dss project list # find the project key
53
- dss dataset list --project-key MYPROJ # list its datasets
54
- dss dataset preview orders --max-rows 10 # peek at data
55
- dss dataset schema orders # inspect columns
46
+ dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ
56
47
  \`\`\`
57
48
 
58
- ### Edit recipe code
49
+ 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.
50
+ \`auth login\` validates by listing accessible projects before saving credentials, so the API key must be allowed to call DSS project-list APIs.
59
51
 
60
- \`\`\`bash
61
- dss recipe download-code my-recipe -o code.py # download
62
- # ... edit code.py ...
63
- dss recipe diff my-recipe --file code.py # review changes
64
- dss recipe set-payload my-recipe --file code.py # upload
65
- \`\`\`
52
+ TLS flags: \`--insecure\` disables certificate verification; \`--ca-cert PATH\` adds a PEM CA bundle. Environment equivalents: \`NODE_TLS_REJECT_UNAUTHORIZED\`, \`NODE_EXTRA_CA_CERTS\`.
66
53
 
67
- ### Build and monitor
54
+ ## Common workflows
68
55
 
69
56
  \`\`\`bash
70
- dss job build-and-wait my-dataset --include-logs # build + wait + stream logs
71
- dss job list # recent jobs
72
- dss job log <job-id> # full log output
57
+ dss version
58
+ dss project list
59
+ dss doctor --fast
60
+ dss dataset list --project-key MYPROJ
61
+ dss dataset list --project-key MYPROJ --fields name,type
62
+ dss dataset preview orders --max-rows 10 --project-key MYPROJ
63
+ dss recipe get-payload compute_orders --project-key MYPROJ
64
+ dss recipe get-payload compute_orders --raw --project-key MYPROJ
65
+ dss recipe get-payload compute_orders --raw --output code.py --project-key MYPROJ
66
+ dss recipe diff compute_orders --file code.py --project-key MYPROJ
67
+ dss recipe set-payload compute_orders --file code.py --project-key MYPROJ
68
+ dss job build-and-wait orders --include-logs --project-key MYPROJ
69
+ dss scenario run daily_build --project-key MYPROJ
70
+ dss sql query --connection analytics --sql "select 1" --project-key MYPROJ
71
+ dss batch --data-file steps.json
73
72
  \`\`\`
73
+ For fake-DSS smoke tests, return project lists as JSON arrays such as \`[{"projectKey":"MYPROJ","name":"My Project"}]\` from \`/public/api/projects/\`; recipe payload commands read \`/public/api/projects/<PROJECT>/recipes/<NAME>?includePayload=true\` and expect a JSON object shaped like \`{"recipe":{"name":"<NAME>","type":"python"},"payload":"..."}\`.
74
74
 
75
- ### Run a scenario
75
+ ## Confirming mutations
76
76
 
77
- \`\`\`bash
78
- dss scenario run my-scenario
79
- dss scenario status my-scenario # check if finished
80
- \`\`\`
77
+ 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.
81
78
 
82
- ## Command reference
79
+ - 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\`.
80
+ - 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.
81
+ - To branch in code, key off the exit code or the JSON ack on stdout — never a hardcoded label.
82
+ - 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.
83
83
 
84
- \`\`\`
85
- dss <resource> <action> [args...] [--flags]
84
+ ## Platform & debugging notes
86
85
 
87
- Resources: project, dataset, recipe, job, scenario, folder, notebook,
88
- variable, code-env, connection, sql, auth, install-skill
89
- \`\`\`
86
+ - Pass code and SQL via \`--file\`/\`--sql-file\`, not inline: shells (especially PowerShell) mangle quotes, \`$\`, and newlines in multi-line snippets.
87
+ - On a non-UTF-8 console (e.g. Windows cp1252), don't print non-ASCII results; write them to a UTF-8 file and read that, or use \`--output PATH\`.
88
+ - Build failures: \`dss job log <id> --errors-only\` surfaces just error/traceback lines, and \`--output PATH\` saves the full log to a file. Logs are one long line with JVM noise; the \`Error in Python process: At line <N>\` marker maps \`<N>\` straight to your recipe payload's source line.
89
+ - Schema changes aren't automatic: after changing a recipe's output columns run \`dss dataset refresh-schema\` (or rebuild) before downstream reads, and \`dss dataset validate-build\` to catch file-backed misconfig before launching a build.
90
+ - \`dss dataset download\` is capped (default 100k rows) and returns \`{ path, rows, truncated, limit }\`: check \`truncated\` and raise \`--limit N\` when you need more — treat it as a sample, not a guaranteed full export. For very large tables, aggregate in SQL or read inside a recipe instead.
90
91
 
91
- Use \`dss <resource> --help\` to see all actions and flags for any resource.
92
+ ## Error envelope
92
93
 
93
- ## Key flags
94
+ Parse stderr as JSON when exit code is non-zero:
94
95
 
96
+ \`\`\`json
97
+ {
98
+ "ok": false,
99
+ "error": "Missing API key.",
100
+ "code": "usage_error",
101
+ "category": "usage",
102
+ "exitCode": 1,
103
+ "resource": "dataset",
104
+ "action": "list"
105
+ }
95
106
  \`\`\`
96
- -f, --format FORMAT json (default) | tsv | table | quiet
97
- -o, --output PATH write output to file instead of stdout
98
- -v, --verbose log HTTP requests to stderr
99
- --project-key KEY override default project for any command
100
- --timeout MS request timeout (default: 30000)
101
- --insecure disable TLS certificate verification
102
- --ca-cert PATH trust an extra PEM CA bundle
103
- --stdin read command input from stdin (JSON or SQL, depending on command)
104
- \`\`\`
105
-
106
- ## Gotchas
107
107
 
108
- - **Most commands need a project key.** Set it once via \`dss auth login\` or \`DATAIKU_PROJECT_KEY\` to avoid passing \`--project-key\` on every call.
109
- - **Output is JSON by default.** Use \`-f table\` when showing results to a user; use \`-f tsv\` when piping to scripts.
110
- - **\`dss job build\` returns immediately.** Use \`dss job build-and-wait\` to block until the build finishes. Add \`--include-logs\` to stream log output.
111
- - **Folder commands accept names or IDs.** If a folder name contains spaces, quote it. The CLI resolves names to IDs automatically.
112
- - **Recipe set-payload overwrites the entire payload.** Always download first, edit, diff, then upload.
113
- - **Transient errors exit code 3, API errors exit code 2, usage errors exit code 1.** Check exit codes to distinguish retriable failures.
108
+ Use \`code\`, \`category\`, \`exitCode\`, \`retryable\`, \`status\`, and \`details\` for recovery logic. Do not scrape message text when a structured field is available.
114
109
  `;
115
110
  const SKILL_FRONTMATTER = `---
116
111
  name: dataiku-dss
117
112
  description: >-
118
- Interact with Dataiku DSS from the command line \u2014 list projects, query datasets,
119
- download and upload recipe code, build datasets, run scenarios, and manage jobs.
120
- Use when the user wants to work with Dataiku DSS resources, inspect a DSS project,
121
- modify recipes, trigger builds, check job logs, or run SQL against DSS connections,
122
- even if they don't explicitly mention the dss CLI.
113
+ Agent-only JSON CLI for Dataiku DSS. Use to inspect or mutate DSS projects,
114
+ datasets, recipes, jobs, scenarios, folders, notebooks, SQL, variables,
115
+ code envs, and connections. Discover the full machine-readable surface with
116
+ dss commands run.
123
117
  ---
124
118
 
125
119
  `;
@@ -227,30 +221,29 @@ export function findWorkspaceRoot(startDir) {
227
221
  }
228
222
  return startDir;
229
223
  }
230
- export function installSkill(agents, opts) {
224
+ export function planSkillInstalls(agents, opts) {
231
225
  const home = homedir();
232
226
  const results = [];
233
- for (const { id, def, } of agents) {
234
- let dir;
235
- if (opts.global) {
236
- const globalDir = def.globalPath(home);
237
- if (!globalDir) {
238
- process.stderr.write(` ${def.name}: skipped (no global path available)\n`);
239
- continue;
240
- }
241
- dir = globalDir;
242
- }
243
- else {
244
- if (!def.projectPath) {
245
- process.stderr.write(` ${def.name}: skipped (no project path available)\n`);
246
- continue;
247
- }
248
- dir = join(opts.cwd, def.projectPath);
249
- }
250
- mkdirSync(dir, { recursive: true, });
251
- const filePath = join(dir, def.filename);
252
- writeFileSync(filePath, def.content(), "utf-8");
253
- results.push({ agent: id, path: filePath, });
227
+ for (const { id, def, via, } of agents) {
228
+ const dir = opts.global
229
+ ? def.globalPath(home)
230
+ : def.projectPath
231
+ ? join(opts.cwd, def.projectPath)
232
+ : undefined;
233
+ if (!dir)
234
+ continue;
235
+ results.push({ agent: id, path: join(dir, def.filename), via, });
236
+ }
237
+ return results;
238
+ }
239
+ export function installSkill(agents, opts) {
240
+ const results = planSkillInstalls(agents, opts);
241
+ for (const result of results) {
242
+ const def = AGENTS[result.agent];
243
+ if (!def)
244
+ continue;
245
+ mkdirSync(dirname(result.path), { recursive: true, });
246
+ writeFileSync(result.path, def.content(), "utf-8");
254
247
  }
255
248
  return results;
256
249
  }
@@ -1,5 +1,20 @@
1
1
  import { appendFile, mkdir, readFile, } from "node:fs/promises";
2
2
  import { dirname, resolve, } from "node:path";
3
+ function isCleanupLedgerEntry(value) {
4
+ if (!value || typeof value !== "object")
5
+ return false;
6
+ const candidate = value;
7
+ if (typeof candidate.ts !== "string")
8
+ return false;
9
+ if (typeof candidate.action !== "string")
10
+ return false;
11
+ if (typeof candidate.resource !== "string")
12
+ return false;
13
+ if (!candidate.cleanup || typeof candidate.cleanup !== "object")
14
+ return false;
15
+ const cleanup = candidate.cleanup;
16
+ return Array.isArray(cleanup.argv) && cleanup.argv.every((arg) => typeof arg === "string");
17
+ }
3
18
  export async function appendCleanupLedgerEntry(filePath, entry) {
4
19
  const resolved = resolve(filePath);
5
20
  await mkdir(dirname(resolved), { recursive: true, });
@@ -11,5 +26,11 @@ export async function readCleanupLedger(filePath) {
11
26
  .split(/\r?\n/)
12
27
  .map((line) => line.trim())
13
28
  .filter((line) => line.length > 0)
14
- .map((line) => JSON.parse(line));
29
+ .map((line, index) => {
30
+ const parsed = JSON.parse(line);
31
+ if (!isCleanupLedgerEntry(parsed)) {
32
+ throw new Error(`Invalid cleanup ledger entry at line ${index + 1}`);
33
+ }
34
+ return parsed;
35
+ });
15
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "scripts": {
23
23
  "build": "tsc && cd packages/types && npx tsc",
24
+ "prepack": "bun run build",
24
25
  "prepublishOnly": "bun run build",
25
26
  "check": "tsc --noEmit",
26
27
  "lint": "oxlint src/",