@datarails-ai/dr-cli 1.6.152 → 1.6.153

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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@datarails-ai/dr-cli",
11
- version: "1.6.152",
11
+ version: "1.6.153",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -69,7 +69,7 @@ var package_default = {
69
69
  };
70
70
 
71
71
  // src/buildInfo.ts
72
- var BUILD_TIME = "2026-10-22T10:56:58.559Z";
72
+ var BUILD_TIME = "2026-10-22T11:50:40.297Z";
73
73
  function formatBuildTime(iso) {
74
74
  if (iso === "") return "";
75
75
  const d = new Date(iso);
@@ -69877,18 +69877,18 @@ async function preflightFile(filePath) {
69877
69877
  `${filePath}: unsupported file type "${ext}". Supported: ${[...EXT_TO_MIME.keys()].join(", ")}`
69878
69878
  );
69879
69879
  }
69880
- let stat3;
69880
+ let stat4;
69881
69881
  try {
69882
- stat3 = await fsPromises3.stat(filePath);
69882
+ stat4 = await fsPromises3.stat(filePath);
69883
69883
  } catch (err) {
69884
69884
  const m = err instanceof Error ? err.message : String(err);
69885
69885
  throw new Error(`${filePath}: ${m}`);
69886
69886
  }
69887
- if (!stat3.isFile()) {
69887
+ if (!stat4.isFile()) {
69888
69888
  throw new Error(`${filePath}: not a regular file`);
69889
69889
  }
69890
- if (stat3.size > MAX_FILE_BYTES) {
69891
- const mb = (stat3.size / (1024 * 1024)).toFixed(1);
69890
+ if (stat4.size > MAX_FILE_BYTES) {
69891
+ const mb = (stat4.size / (1024 * 1024)).toFixed(1);
69892
69892
  throw new Error(
69893
69893
  `${filePath}: file is ${mb}MB, exceeds ${MAX_FILE_BYTES / (1024 * 1024)}MB limit`
69894
69894
  );
@@ -72634,7 +72634,7 @@ async function resolveTicketTeam(client, team, signal) {
72634
72634
  function errText2(err) {
72635
72635
  return err instanceof Error ? err.message : String(err);
72636
72636
  }
72637
- async function planAttachments(paths, stat3 = async (p) => {
72637
+ async function planAttachments(paths, stat4 = async (p) => {
72638
72638
  const st = await fs34.stat(p);
72639
72639
  return { size: st.size, isFile: st.isFile() };
72640
72640
  }) {
@@ -72657,7 +72657,7 @@ async function planAttachments(paths, stat3 = async (p) => {
72657
72657
  }
72658
72658
  let info;
72659
72659
  try {
72660
- info = await stat3(filePath);
72660
+ info = await stat4(filePath);
72661
72661
  } catch (err) {
72662
72662
  throw new Error(`--attach ${filePath}: ${errText2(err)}`);
72663
72663
  }
@@ -73045,8 +73045,8 @@ async function readAppPassword(src, stdinReader) {
73045
73045
  let raw2;
73046
73046
  if (fromFile) {
73047
73047
  try {
73048
- const stat3 = await fs34.stat(src.file);
73049
- if (!stat3.isFile()) {
73048
+ const stat4 = await fs34.stat(src.file);
73049
+ if (!stat4.isFile()) {
73050
73050
  throw new Error(
73051
73051
  "--password-file must be a regular file: a device or pipe can yield without end, and its reported size means nothing"
73052
73052
  );
@@ -74132,7 +74132,7 @@ function registerAppSpaceCommand(root) {
74132
74132
 
74133
74133
  // src/commands/agents.ts
74134
74134
  import { mkdir as mkdir5, writeFile as writeFile8 } from "fs/promises";
74135
- import { join as join17 } from "path";
74135
+ import { join as join18 } from "path";
74136
74136
 
74137
74137
  // src/api/aiService.ts
74138
74138
  import { z as z3 } from "zod";
@@ -74500,13 +74500,26 @@ function renderScopeRegistry(registry) {
74500
74500
  const lines = [
74501
74501
  `Actions: ${[...actions, "*"].join(" | ")} (* = every action). Resource * = every resource.`
74502
74502
  ];
74503
+ const unenforced = [];
74503
74504
  for (const product of Array.isArray(registry?.products) ? registry.products : []) {
74504
- lines.push("", product.name ?? product.key ?? "?");
74505
+ const productName = product.name ?? product.key ?? "?";
74506
+ const accepted = [];
74505
74507
  for (const resource of product.resources ?? []) {
74506
- const note = resource.enforced === false ? " (not enforced yet)" : "";
74507
74508
  const description = resource.description ? ` \u2014 ${resource.description}` : "";
74508
- lines.push(` ${resource.name ?? "?"}${note}${description}`);
74509
+ if (resource.enforced === false) {
74510
+ unenforced.push(` ${resource.name ?? "?"} (${productName})${description}`);
74511
+ } else {
74512
+ accepted.push(` ${resource.name ?? "?"}${description}`);
74513
+ }
74509
74514
  }
74515
+ if (accepted.length > 0) lines.push("", productName, ...accepted);
74516
+ }
74517
+ if (unenforced.length > 0) {
74518
+ lines.push(
74519
+ "",
74520
+ "Not enforced yet \u2014 listed for reference; --cli-scopes does not accept these until their product checks them:",
74521
+ ...unenforced
74522
+ );
74510
74523
  }
74511
74524
  return lines.join("\n");
74512
74525
  }
@@ -74526,10 +74539,13 @@ function cliScopesWriteLine(requested, after) {
74526
74539
  if (requested === null && after !== null) {
74527
74540
  return `WARNING: CLI scopes reset was sent but a ceiling is still stored: ${after.join(", ")}.`;
74528
74541
  }
74542
+ if (requested !== null && requested.length > 0 && after === null) {
74543
+ return `WARNING: CLI scopes ${requested.join(", ")} were sent but nothing is stored: the requested ceiling did not land, and the read-only default (${DEFAULT_CLI_SCOPES.join(", ")}) applies.`;
74544
+ }
74529
74545
  return `CLI scopes: ${cliScopesLabel(after)}. Applies from the agent's next session; running sessions keep the ceiling they started with.`;
74530
74546
  }
74531
74547
  var SCOPE_ERROR_HINTS = {
74532
- INVALID_CLI_SCOPES: "run `agents scope-registry` for the resources and actions this environment knows.",
74548
+ INVALID_CLI_SCOPES: 'run `agents scope-registry` for the resources and actions this environment knows; resources it marks "not enforced yet" are not accepted.',
74533
74549
  CLI_SCOPES_WITHOUT_CLI: "CLI scopes only apply to a CLI-exposed agent. Add `--expose-cli true` (update) or `--expose-cli` (create), or use --reset-cli-scopes to clear them."
74534
74550
  };
74535
74551
  function describeCliScopesError(err) {
@@ -75080,10 +75096,11 @@ function renderToolsProposalText(proposal, resolved) {
75080
75096
 
75081
75097
  // src/commands/agents_skills.ts
75082
75098
  import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
75083
- import { dirname as dirname9, join as join16 } from "path";
75099
+ import { dirname as dirname9, join as join17 } from "path";
75084
75100
 
75085
75101
  // src/commands/_agentSkillBundle.ts
75086
- import { extname as extname5 } from "path";
75102
+ import { readdir as readdir4, stat as stat3 } from "fs/promises";
75103
+ import { extname as extname5, join as join16 } from "path";
75087
75104
  function inferContentType(filename) {
75088
75105
  switch (extname5(filename).toLowerCase()) {
75089
75106
  case ".md":
@@ -75121,7 +75138,60 @@ function inferContentType(filename) {
75121
75138
  return void 0;
75122
75139
  }
75123
75140
  }
75124
- async function dirToFiles(baseDir) {
75141
+ var SKILL_BUNDLE_LIMITS = {
75142
+ maxFileBytes: 20 * 1024 * 1024,
75143
+ maxTotalBytes: 14 * 1024 * 1024
75144
+ };
75145
+ var SKILL_ENTRY_OVERHEAD_BYTES = 128;
75146
+ async function statSkillFiles(skillRoot) {
75147
+ const out = [];
75148
+ async function walk(dir, prefix) {
75149
+ for (const e of await readdir4(dir, { withFileTypes: true })) {
75150
+ if (isIgnoredSkillEntry(e.name)) continue;
75151
+ const childAbs = join16(dir, e.name);
75152
+ const childRel = prefix === "" ? e.name : `${prefix}/${e.name}`;
75153
+ if (e.isDirectory()) {
75154
+ await walk(childAbs, childRel);
75155
+ } else if (e.isFile()) {
75156
+ out.push({ relPath: childRel, size: (await stat3(childAbs)).size });
75157
+ }
75158
+ }
75159
+ }
75160
+ await walk(skillRoot, "");
75161
+ out.sort((a, b) => a.relPath.localeCompare(b.relPath));
75162
+ return out;
75163
+ }
75164
+ function assertUploadableSizes(files, limits) {
75165
+ const oversized = files.find((f) => f.size > limits.maxFileBytes);
75166
+ if (oversized !== void 0) {
75167
+ throw new Error(
75168
+ `skill file "${oversized.relPath}" is ${oversized.size} bytes; ai-service accepts at most ${limits.maxFileBytes} bytes per file`
75169
+ );
75170
+ }
75171
+ const empty = files.filter((f) => f.size === 0).map((f) => `"${f.relPath}"`);
75172
+ if (empty.length === 1) {
75173
+ throw new Error(
75174
+ `skill file ${empty[0]} is empty; ai-service rejects empty files \u2014 add content or remove it`
75175
+ );
75176
+ }
75177
+ if (empty.length > 1) {
75178
+ throw new Error(
75179
+ `skill files ${empty.join(", ")} are empty; ai-service rejects empty files \u2014 add content or remove them`
75180
+ );
75181
+ }
75182
+ const total = files.reduce(
75183
+ (sum, f) => sum + f.size + Buffer.byteLength(f.relPath) + SKILL_ENTRY_OVERHEAD_BYTES,
75184
+ 0
75185
+ );
75186
+ if (total > limits.maxTotalBytes) {
75187
+ const largest = files.reduce((a, b) => b.size > a.size ? b : a);
75188
+ throw new Error(
75189
+ `skill bundle is ${total} bytes counting its paths (${SKILL_ENTRY_OVERHEAD_BYTES} bytes per file on top); ai-service accepts at most ${limits.maxTotalBytes} bytes in one upload (${files.length} files; largest: "${largest.relPath}", ${largest.size} bytes)`
75190
+ );
75191
+ }
75192
+ }
75193
+ async function dirToFiles(baseDir, limits = SKILL_BUNDLE_LIMITS) {
75194
+ assertUploadableSizes(await statSkillFiles(baseDir), limits);
75125
75195
  return (await readSkillFiles(baseDir)).map(({ relPath, bytes }) => {
75126
75196
  const contentType = inferContentType(relPath);
75127
75197
  return {
@@ -75289,7 +75359,7 @@ async function runSkillsPull(cfg) {
75289
75359
  const folder = bundleFolder(files);
75290
75360
  const targets = files.map((file) => ({
75291
75361
  file,
75292
- outPath: join16(baseDir, ...folder, ...relSegments(file.path))
75362
+ outPath: join17(baseDir, ...folder, ...relSegments(file.path))
75293
75363
  }));
75294
75364
  await assertNoOverwrite(
75295
75365
  targets.map((t) => t.outPath),
@@ -75537,6 +75607,11 @@ async function runAgentsSystemEntities(cfg) {
75537
75607
  const ids = (list28) => (list28 ?? []).length > 0 ? (list28 ?? []).join(", ") : "(none)";
75538
75608
  cfg.out.write(`agents: ${ids(res.agentIds)}
75539
75609
  `);
75610
+ const protectedIds = res.protectedAgentIds;
75611
+ cfg.out.write(
75612
+ Array.isArray(protectedIds) ? `protected: ${ids(protectedIds)} (no archive or reach change; content stays editable)
75613
+ ` : "protected: (not reported by this ai-service)\n"
75614
+ );
75540
75615
  cfg.out.write(`environments: ${ids(res.environmentIds)}
75541
75616
  `);
75542
75617
  cfg.out.write(`skills: ${ids(res.skillIds)}
@@ -75773,6 +75848,11 @@ async function runAgentsRefresh(cfg) {
75773
75848
  const apis = toCatalogEntries(catalog.apis ?? []);
75774
75849
  const name = agent.name ?? agentId;
75775
75850
  const v = versionLabel2(agent.version);
75851
+ if (!Array.isArray(agent.tools)) {
75852
+ throw new Error(
75853
+ `getAgent returned no tools list for ${agentId} ${v}; refusing to plan or write a tools change`
75854
+ );
75855
+ }
75776
75856
  if (proposal === void 0) {
75777
75857
  const plan = computeRefreshPlan(agent.tools, apis, topN);
75778
75858
  if (cfg.asJson) {
@@ -75886,15 +75966,15 @@ async function runAgentsPull(cfg) {
75886
75966
  throw new Error(`${agentId}: the response carried no system prompt, tools or skills to pull`);
75887
75967
  }
75888
75968
  await assertNoOverwrite(
75889
- files.map((f) => join17(baseDir, f.name)),
75969
+ files.map((f) => join18(baseDir, f.name)),
75890
75970
  cfg.force === true
75891
75971
  );
75892
75972
  await mkdir5(baseDir, { recursive: true });
75893
- for (const f of files) await writeFile8(join17(baseDir, f.name), f.content);
75973
+ for (const f of files) await writeFile8(join18(baseDir, f.name), f.content);
75894
75974
  cfg.out.write(`Wrote ${files.map((f) => f.name).join(", ")} to ${baseDir}/
75895
75975
  `);
75896
75976
  const base = typeof agent.version === "number" ? ` --version-number ${agent.version}` : "";
75897
- const fileFlags = files.map((f) => `${f.flag} ${shellArg(join17(baseDir, f.name))}`).join(" ");
75977
+ const fileFlags = files.map((f) => `${f.flag} ${shellArg(join18(baseDir, f.name))}`).join(" ");
75898
75978
  cfg.out.write(
75899
75979
  `Edit, then: dr agents update --id ${shellArg(agentId)}${base} ${fileFlags} ${serverFlagText(cfg.client)}
75900
75980
  `
@@ -76486,7 +76566,7 @@ function registerAgentsCommand(root) {
76486
76566
  });
76487
76567
  withTarget(cliScopes);
76488
76568
  const scopeRegistry = agents.command("scope-registry").description(
76489
- "List the resources and actions a CLI scope may name in this environment \u2014 what --cli-scopes accepts."
76569
+ 'List the resources and actions a CLI scope may name in this environment. Resources listed under "Not enforced yet" are not accepted by --cli-scopes.'
76490
76570
  ).option("--json", JSON_HELP).action(async (opts) => {
76491
76571
  const setup = await setupCall(opts.server, opts.account);
76492
76572
  try {
@@ -76891,7 +76971,7 @@ function commandPath(actionCommand) {
76891
76971
 
76892
76972
  // src/telemetry/config.ts
76893
76973
  import { homedir as homedir7 } from "os";
76894
- import { join as join19 } from "path";
76974
+ import { join as join20 } from "path";
76895
76975
  import { readFile as readFile9 } from "fs/promises";
76896
76976
  var TELEMETRY_ENV = "DR_CLI_TELEMETRY";
76897
76977
  var DISABLED_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
@@ -76925,7 +77005,7 @@ function parseCollectorConfig(raw2) {
76925
77005
  return { endpoint, headerName, headerValue };
76926
77006
  }
76927
77007
  function remoteSettingsPath() {
76928
- return join19(homedir7(), ".claude", "remote-settings.json");
77008
+ return join20(homedir7(), ".claude", "remote-settings.json");
76929
77009
  }
76930
77010
  async function readCollectorConfig(path13 = remoteSettingsPath()) {
76931
77011
  let raw2;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.152",
3
+ "cliVersion": "1.6.153",
4
4
  "_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
5
5
  "commands": {
6
6
  "login": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: schedule-drip-task
3
- description: Use when a customer needs files or data pushed OUT of Datarails on a schedule through DRIP (the connectors platform) - an SFTP export, "send this CSV to the customer's SFTP every hour", "push the file to their server", "schedule an export", "scheduled task", "dr connectors tasks", "sftp_export", "lib/sftp_export", or when operating an existing schedule - "is the export running", "why did the export fail", "the task is paused", "run the export now", "pause the export", "task runs", "consecutive failures", "paused (failures)", "paused (schedule)", "no active version", "newest source file is older than max_age_seconds", "the target must not be a customer org-*-sftp container", a 403 "scheduled tasks need a support user or an org admin". Not for pulling data INTO Datarails (drip-implement-connector) or for polled SFTP ingestion (dr connectors poll).
3
+ description: Use when a customer needs files or data pushed OUT of Datarails on a schedule through DRIP (the connectors platform) - an SFTP export, "send this CSV to the customer's SFTP every hour", "push the file to their server", "schedule an export", "scheduled task", "dr connectors tasks", "sftp_export", "lib/sftp_export", or when operating an existing schedule - "is the export running", "why did the export fail", "the task is paused", "run the export now", "pause the export", "task runs", "consecutive failures", "paused (failures)", "paused (schedule)", "no active version", "newest source file is older than max_age_seconds", "the target must not be a customer org-*-sftp container", a 403 "scheduled tasks need a support user or an org admin", or when writing a new task type - "write a scheduled task", "new DRIP task", "process() TaskResult", "run hook", "task state". Not for pulling data INTO Datarails (drip-implement-connector) or for polled SFTP ingestion (dr connectors poll).
4
4
  ---
5
5
 
6
6
  # Schedule a DRIP task
@@ -10,6 +10,9 @@ datasources, and keeps a run history. The control plane only schedules; the runn
10
10
  artifact's `process()`. The first task type is **SFTP Export** (`lib/sftp_export`): copy files from
11
11
  our hosted SFTP to a customer's SFTP under a name the customer wants.
12
12
 
13
+ To write a task type other than SFTP Export (the `process()` hook, `TaskResult`, state, retries),
14
+ see [reference/writing-a-task.md](reference/writing-a-task.md).
15
+
13
16
  Commands: `dr connectors tasks create | list | get | update | delete | run | pause | resume | runs`.
14
17
  Every one takes `--server`, `--account` and `--json`; a `<task>` is its id or its name.
15
18
 
@@ -16,10 +16,11 @@ datasources (the control plane refuses secret-looking keys with a 422).
16
16
  | `settle_seconds` | no | 120 | Skip files modified more recently (the producer may still be writing). 0 to 7 days |
17
17
  | `max_age_seconds` | no | none | `newest` only: fail the run if the newest file is older than this before the scheduled time, or if no file matches (producer late or stopped) |
18
18
  | `max_files_per_run` | no | 50 | `all_new`: at most this many files per run (1 to 1000); the rest go next run |
19
+ | `upload` | no | `atomic` | `atomic`: write a `.part`, then rename it, so no reader sees a half-written file. `direct`: write the final name itself, for a target folder that takes files away as they arrive (the `.part` would be gone before the rename). `newest` only |
19
20
 
20
21
  Behaviour worth knowing:
21
22
 
22
- - Upload is atomic: a random `.<name>.<hex>.part` is written, then renamed. The customer's login must
23
+ - Upload is atomic by default: a random `.<name>.<hex>.part` is written, then renamed. A run that fails with "a file vanished while publishing the upload" means the target takes files as they arrive: set `"upload": "direct"`. The customer's login must
23
24
  be allowed to create and rename files in `target_dir`. Stale parts of ours older than 2 h are swept.
24
25
  - The source is never written, moved or deleted. Cleaning it is the producer's job; with `all_new`,
25
26
  a folder that keeps every sent file eventually fails the run with "archive or delete them".
@@ -0,0 +1,92 @@
1
+ # Writing a new task type
2
+
3
+ A scheduled task is an ordinary DRIP artifact in dr-connectors (a connection plus a dataquery,
4
+ published and made active like any connector, see `drip-implement-connector`), whose dataquery
5
+ implements `process()`. The control plane only schedules it. On each run it sends the flavor's
6
+ runner a job with `hook: "run"`, and the runner calls `connect()`, then `process()`, in one job.
7
+ The full contract is in dr-connectors-runner `docs/connector-kit.md`, "Scheduled tasks: the `run`
8
+ hook". `lib/sftp_export` in dr-connectors is the reference implementation.
9
+
10
+ ## The code
11
+
12
+ ```python
13
+ # lib/<task>/dataqueries/<name>.py
14
+ from dr_integrations_sdk import DataQuery, PermanentError, SyncRequest, TaskResult, kit
15
+
16
+ OBJECT_TYPES = ("<objectType>",)
17
+
18
+
19
+ class MyTask(DataQuery):
20
+ async def process(self, session, req: SyncRequest) -> TaskResult:
21
+ config = req.config # the task's --config, validated here
22
+ state = (req.metadata or {}).get("state") or {} # what the last ok run returned
23
+ scheduled_for = req.metadata["scheduled_for"] # ISO UTC time of the slot this run is for
24
+ other = await kit.auth.get_credentials_for(req, "source") # a non-primary datasource, by role
25
+ sent = await do_the_work(session, other, config, state)
26
+ return TaskResult(state={"sent": sent}, summary={"sent": len(sent)})
27
+
28
+
29
+ def get_query_class(object_type: str):
30
+ if object_type == "<objectType>":
31
+ return MyTask
32
+ raise PermanentError(f"unknown objectType {object_type!r}")
33
+ ```
34
+
35
+ | Piece | Rule |
36
+ |---|---|
37
+ | `connect()` | Gets the **primary** datasource's credentials as for any connector. Log in here, so a wrong password or host key fails before any work |
38
+ | `req.config` | The task's `--config` JSON. Must carry `objectType` (it picks the dataquery class). Validate it before any login. Never put credentials in it: the control plane refuses secret-looking keys with a 422 |
39
+ | Other datasources | `kit.auth.get_credentials_for(req, "<role>")`, one per `--datasource <role>=<id>`. An unbound role is a `PermanentError` naming it |
40
+ | `req.metadata` | `state`, `scheduled_for`, `run_id`, `attempt` (1, or 2 on the retry), `trigger` (`schedule` or `manual`) |
41
+ | Imports | stdlib, `dr_integrations_sdk`, `lib` and the flavor's allowlist only (dr-connector-images). Set `RUNTIME_FLAVOR` in the connection |
42
+ | Logs | `kit.logging.get_logger("<task>")`. They reach Groundcover under the runner flavor's workload |
43
+
44
+ ## The result
45
+
46
+ `TaskResult(state=…, summary=…, verdict=…)`. Every field is optional.
47
+
48
+ - **`state`**: the task's own bookkeeping (ids, cursors, names already sent), never the data
49
+ processed. It must be a JSON object or `None`, at most 64 KiB. The control plane keeps it only
50
+ from an `ok` run; after a failure the next run gets the last good state.
51
+ - **`summary`**: what the run did, shown by `dr connectors tasks runs`.
52
+ - **`verdict`**: `ok` (default), `retry` (the run is sent once more after 60 s) or `block` (the run
53
+ fails).
54
+ - The whole result must be JSON (no `datetime`, `set`, `Decimal`, NaN) and at most 128 KiB.
55
+ Anything else fails the run as a connector error.
56
+
57
+ ## Errors
58
+
59
+ | Raised | Run |
60
+ |---|---|
61
+ | `PermanentError`, `AuthError` | Fails, no retry. Use for bad config, refused credentials, missing folders |
62
+ | `TransientError`, `httpx` 429/5xx, `httpx.TransportError` | Retried once after 60 s, then fails |
63
+ | Anything else | Fails as a connector error |
64
+
65
+ - After 5 failed runs in a row the task pauses itself (`paused (failures)`).
66
+ - The control plane stores only the first few hundred characters of a run's error. Keep the message
67
+ short. Log anything longer, such as a key or a listing, and read it in Groundcover.
68
+ - Never put a secret in an error or a log line.
69
+
70
+ ## Make it safe to run twice
71
+
72
+ A retry runs `process()` again, a manual run can land next to a scheduled one, and a run that dies
73
+ leaves no state. So:
74
+
75
+ - Record what was done in `state`, and skip it next time.
76
+ - Write the output so that a half-finished run leaves nothing a reader would take as done. For
77
+ example, upload to a temporary name and rename it.
78
+ - Use `scheduled_for`, not the clock, for anything named after the run's time: a retry then makes
79
+ the same name.
80
+
81
+ ## Ship it
82
+
83
+ 1. **Shared code goes in `lib/<task>/`**, with tests. A thin per-org artifact re-exports it. The
84
+ dataquery must set its own `CONNECTION` (the thin connection file's name), as in
85
+ [SKILL.md](../SKILL.md) step 3.
86
+ 2. **Merge to dr-connectors.** CI publishes the artifact to JFrog.
87
+ 3. **Register and activate it on the region:** `dr connectors publish <dataquery file>`, then
88
+ `dr connectors promote <artifact> --artifact-version v<N>`, as the customer's support user.
89
+ 4. **Schedule it:** `dr connectors tasks create` (see [SKILL.md](../SKILL.md)), then `tasks run` once
90
+ and check `tasks runs`.
91
+ 5. **Test on dev-1 first.** The heartbeat artifact in dr-connectors
92
+ (`dev-1/69_(test-connectors.com)/heartbeat/`) is a minimal task that carries a counter in `state`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.152",
3
+ "version": "1.6.153",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {