tines 0.0.262 → 0.0.266

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 (2) hide show
  1. package/dist/index.js +183 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3368,6 +3368,9 @@ function useColor() {
3368
3368
  // ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/index.js
3369
3369
  var program = new Command();
3370
3370
 
3371
+ // src/commands/context.ts
3372
+ import { readFileSync as readFileSync4 } from "node:fs";
3373
+
3371
3374
  // src/body-value.ts
3372
3375
  import { readFileSync } from "node:fs";
3373
3376
  var processStdin = {
@@ -3432,6 +3435,7 @@ var LABEL_NAME_MAX = 50;
3432
3435
  var PROMPT_MAX_BYTES = 32 * 1024;
3433
3436
  var SKILL_MAX_FILES = 20;
3434
3437
  var SKILL_MAX_TOTAL_BYTES = 100 * 1024;
3438
+ var ENV_VALUE_MAX_BYTES = 16 * 1024;
3435
3439
  var AGENT_GUIDELINES_NAME = "agent-guidelines";
3436
3440
  var JOURNAL_NAME = "journal";
3437
3441
  var AGENT_GUIDELINES_BODY = `You are an agent working on a Tines issue over its HTTP API / CLI. Beyond doing the work, leave the workspace smarter than you found it. Four places to write, chosen by who should inherit what you learned:
@@ -4005,6 +4009,7 @@ function createApiClient(options) {
4005
4009
  const artifactPath = (issueId, name2, suffix = "") => `/api/v1/issues/${issueId}/artifacts/${encodeURIComponent(name2)}${suffix}`;
4006
4010
  return {
4007
4011
  getTime: () => get("/api/time"),
4012
+ getVersion: () => get("/api/version"),
4008
4013
  // Projects
4009
4014
  listProjects: (params = {}) => get(`/api/v1/projects${query(params)}`),
4010
4015
  createProject: (body) => request("POST", "/api/v1/projects", body),
@@ -5635,6 +5640,8 @@ function contextItemSummary(item) {
5635
5640
  return `${item.repo_url}${item.repo_branch ? `#${item.repo_branch}` : ""}`;
5636
5641
  case "artifact":
5637
5642
  return item.artifact_type ?? "artifact";
5643
+ case "env":
5644
+ return item.secret ? `secret \xB7 set${item.hint ? ` \xB7 ${item.hint}` : ""}` : `= ${item.value ?? ""}`;
5638
5645
  }
5639
5646
  }
5640
5647
  function recurrenceLabel(schedule2) {
@@ -6233,6 +6240,12 @@ async function resolveScopeFlags(api, opts) {
6233
6240
  if (opts.label !== void 0) scope.label_id = await resolveLabelFlag(api, opts.label);
6234
6241
  return scope;
6235
6242
  }
6243
+ function readEnvValue(raw) {
6244
+ if (raw === "-") return readFileSync4(0, "utf8").replace(/\r?\n$/, "");
6245
+ if (raw.startsWith("@@")) return raw.slice(1);
6246
+ if (raw.startsWith("@")) return readFileSync4(raw.slice(1), "utf8").replace(/\r?\n$/, "");
6247
+ return raw;
6248
+ }
6236
6249
  function printContextItem(item) {
6237
6250
  console.log(`${item.kind} "${item.name}" [${item.id}] v${item.version}`);
6238
6251
  if (item.description) console.log(item.description);
@@ -6255,6 +6268,15 @@ files (seeded at skills/${item.name}/):`);
6255
6268
  console.log(` ${f.path} (${Buffer.byteLength(f.content, "utf8")} bytes)`);
6256
6269
  }
6257
6270
  if ((item.files ?? []).length === 0) console.log(" (none)");
6271
+ } else if (item.kind === "env") {
6272
+ if (item.secret) {
6273
+ console.log(`
6274
+ value: (secret, set)${item.hint ? ` hint: ${item.hint}` : ""}`);
6275
+ } else {
6276
+ console.log(`
6277
+ value: ${item.value ?? ""}`);
6278
+ if (item.hint) console.log(`hint: ${item.hint}`);
6279
+ }
6258
6280
  } else {
6259
6281
  console.log(`
6260
6282
  url: ${item.repo_url}`);
@@ -6276,13 +6298,13 @@ function withScopeFlags(cmd) {
6276
6298
  }
6277
6299
  function register(program3) {
6278
6300
  const context2 = program3.command("context").description(
6279
- "Manage context items (prompts, skills, repo pointers) scoped to projects, states, and issues"
6301
+ "Manage context items (prompts, skills, repo pointers, environment variables) scoped to projects, states, and issues"
6280
6302
  );
6281
6303
  withList(
6282
6304
  withScopeFlags(
6283
6305
  context2.command("list").description(
6284
6306
  "List context items (scope filters match every item whose scope includes the element)"
6285
- ).option("-k, --kind <kind>", "filter by kind: prompt, skill, or repo").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
6307
+ ).option("-k, --kind <kind>", "filter by kind: prompt, skill, repo, artifact, or env").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
6286
6308
  )
6287
6309
  ).action(
6288
6310
  async (opts) => {
@@ -6326,21 +6348,27 @@ function register(program3) {
6326
6348
  });
6327
6349
  withCommon(
6328
6350
  withScopeFlags(
6329
- context2.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
6351
+ context2.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, repo, or env").requiredOption(
6330
6352
  "-n, --name <name>",
6331
- "item name (slug-like for skills; the dedup/override key)"
6353
+ "item name (slug-like for skills, the variable name for env; the dedup/override key)"
6332
6354
  ).option("-d, --description <text>", "one-liner shown in lists").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option(
6333
6355
  "--file <path>=@<local>",
6334
6356
  "skill file: workspace path = local file (repeatable)",
6335
6357
  collect,
6336
6358
  []
6337
- ).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)")
6359
+ ).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)").option(
6360
+ "--value <v>",
6361
+ "env: the value \u2014 inline, @file, or - for stdin (keeps a secret out of shell history)"
6362
+ ).option("--secret", "env: encrypt the value at rest; it is write-only afterwards").option("--hint <text>", 'env: a safe display hint for a secret (e.g. "github_pat_\u2026abcd")')
6338
6363
  )
6339
6364
  ).action(
6340
6365
  async (opts) => {
6341
6366
  if (opts.kind === "repo" && opts.repoUrl === void 0) {
6342
6367
  die("--kind repo needs --repo-url <clone-url> (--url is the API base URL)");
6343
6368
  }
6369
+ if (opts.kind === "env" && opts.value === void 0) {
6370
+ die("--kind env needs --value <value|@file|->");
6371
+ }
6344
6372
  const api = client(opts);
6345
6373
  const scope = await resolveScopeFlags(api, opts);
6346
6374
  const body = {
@@ -6355,6 +6383,9 @@ function register(program3) {
6355
6383
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6356
6384
  if (opts.branch !== void 0) body.repo_branch = opts.branch;
6357
6385
  if (opts.dir !== void 0) body.repo_dir = opts.dir;
6386
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6387
+ if (opts.secret) body.secret = true;
6388
+ if (opts.hint !== void 0) body.hint = opts.hint;
6358
6389
  const item = await api.createContextItem(body);
6359
6390
  if (opts.json) return printJson(item);
6360
6391
  console.log(`created ${item.kind} "${item.name}" (${item.id}) \u2014 scope: ${item.scope.label}`);
@@ -6362,7 +6393,7 @@ function register(program3) {
6362
6393
  );
6363
6394
  withCommon(
6364
6395
  withScopeFlags(
6365
- context2.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
6396
+ context2.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option("--value <v>", "env: replace the value (inline, @file, or - for stdin)").option("--secret", "env: encrypt the stored value (irreversible; delete to make public)").option("--hint <text>", "env: display hint for a secret (empty string clears it)").option(
6366
6397
  "--unset <dimension>",
6367
6398
  "drop a scope dimension: project, state, issue, or label (repeatable)",
6368
6399
  collect,
@@ -6412,6 +6443,9 @@ function register(program3) {
6412
6443
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6413
6444
  if (opts.branch !== void 0) body.repo_branch = opts.branch === "" ? null : opts.branch;
6414
6445
  if (opts.dir !== void 0) body.repo_dir = opts.dir === "" ? null : opts.dir;
6446
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6447
+ if (opts.secret) body.secret = true;
6448
+ if (opts.hint !== void 0) body.hint = opts.hint === "" ? null : opts.hint;
6415
6449
  if (Object.keys(body).length === 0) {
6416
6450
  die(
6417
6451
  "nothing to update: pass payload flags, --name/--description, scope flags, and/or --unset"
@@ -6458,7 +6492,7 @@ function register(program3) {
6458
6492
  }
6459
6493
 
6460
6494
  // src/commands/issues.ts
6461
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6495
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6462
6496
  import { basename, dirname as dirname2, join as join2 } from "node:path";
6463
6497
  import { createInterface } from "node:readline/promises";
6464
6498
 
@@ -7170,7 +7204,7 @@ function readTextSource(source) {
7170
7204
  if (source.kind === "text-stdin") return readBodyValue("-");
7171
7205
  if (source.kind === "text-path") {
7172
7206
  try {
7173
- return readFileSync4(source.path, "utf8");
7207
+ return readFileSync5(source.path, "utf8");
7174
7208
  } catch (err) {
7175
7209
  die(`cannot read ${source.path}: ${err instanceof Error ? err.message : String(err)}`);
7176
7210
  }
@@ -7188,7 +7222,7 @@ function walkFolder(dir) {
7188
7222
  files2.push({
7189
7223
  path: nextRel,
7190
7224
  contentType: sniffContentType(entry.name),
7191
- bytes: readFileSync4(nextAbs)
7225
+ bytes: readFileSync5(nextAbs)
7192
7226
  });
7193
7227
  }
7194
7228
  }
@@ -7643,6 +7677,9 @@ function register2(program3) {
7643
7677
  console.log(`
7644
7678
  skills: ${context2.skills.map((s) => s.name).join(", ")}`);
7645
7679
  }
7680
+ for (const e of context2.env ?? []) {
7681
+ console.log(`env: ${e.name}${e.secret ? " (secret)" : ""}`);
7682
+ }
7646
7683
  for (const repo of context2.repos) {
7647
7684
  console.log(
7648
7685
  `repo: ${repo.name} ${repo.url}${repo.branch ? `#${repo.branch}` : ""} \u2192 ${repo.dir}/`
@@ -7826,10 +7863,10 @@ files (v${artifact.current_version.version}):`);
7826
7863
  let bytes2;
7827
7864
  if (fromPath === null) {
7828
7865
  if (process.stdin.isTTY) die('"-" reads the file from stdin, but stdin is a terminal');
7829
- bytes2 = readFileSync4(0);
7866
+ bytes2 = readFileSync5(0);
7830
7867
  } else {
7831
7868
  try {
7832
- bytes2 = readFileSync4(fromPath);
7869
+ bytes2 = readFileSync5(fromPath);
7833
7870
  } catch (err) {
7834
7871
  die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
7835
7872
  }
@@ -8284,13 +8321,13 @@ function register4(program3) {
8284
8321
  }
8285
8322
 
8286
8323
  // src/commands/login.ts
8287
- import { readFileSync as readFileSync5 } from "node:fs";
8324
+ import { readFileSync as readFileSync6 } from "node:fs";
8288
8325
  function maskKey(key) {
8289
8326
  return `${key.slice(0, 14)}\u2026`;
8290
8327
  }
8291
8328
  function readKeyArg(value) {
8292
8329
  if (value !== "-") return value;
8293
- const key = readFileSync5(0, "utf8").trim();
8330
+ const key = readFileSync6(0, "utf8").trim();
8294
8331
  if (!key) die("no API key on stdin");
8295
8332
  return key;
8296
8333
  }
@@ -8730,7 +8767,7 @@ import {
8730
8767
  createWriteStream,
8731
8768
  existsSync as existsSync5,
8732
8769
  mkdirSync as mkdirSync4,
8733
- readFileSync as readFileSync9,
8770
+ readFileSync as readFileSync10,
8734
8771
  realpathSync,
8735
8772
  rmSync as rmSync3,
8736
8773
  statSync as statSync4,
@@ -8742,11 +8779,11 @@ import { dirname as dirname4, join as join6 } from "node:path";
8742
8779
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
8743
8780
 
8744
8781
  // src/version.ts
8745
- import { readFileSync as readFileSync6 } from "node:fs";
8782
+ import { readFileSync as readFileSync7 } from "node:fs";
8746
8783
  function cliVersion() {
8747
8784
  try {
8748
8785
  const manifest = new URL("../package.json", import.meta.url);
8749
- return JSON.parse(readFileSync6(manifest, "utf8")).version ?? "0.0.0-unknown";
8786
+ return JSON.parse(readFileSync7(manifest, "utf8")).version ?? "0.0.0-unknown";
8750
8787
  } catch {
8751
8788
  return "0.0.0-unknown";
8752
8789
  }
@@ -9760,7 +9797,7 @@ var EffortCapabilityRefresher = class {
9760
9797
 
9761
9798
  // src/daemon/cli-refresh.ts
9762
9799
  import { spawn as spawn2 } from "node:child_process";
9763
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
9800
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8 } from "node:fs";
9764
9801
  import { dirname as dirname3, join as join4 } from "node:path";
9765
9802
 
9766
9803
  // src/daemon/support.ts
@@ -9945,10 +9982,12 @@ var LogBatcher = class {
9945
9982
  constructor(send, opts = {}) {
9946
9983
  this.send = send;
9947
9984
  this.opts = opts;
9985
+ this.redactor = new SecretRedactor(opts.redact ?? []);
9948
9986
  }
9949
9987
  send;
9950
9988
  opts;
9951
9989
  buffer = "";
9990
+ redactor;
9952
9991
  timer = null;
9953
9992
  sending = Promise.resolve();
9954
9993
  seq = 0;
@@ -9956,6 +9995,7 @@ var LogBatcher = class {
9956
9995
  pending = [];
9957
9996
  append(text4) {
9958
9997
  if (!text4) return;
9998
+ text4 = this.redactor.write(text4);
9959
9999
  this.buffer += text4;
9960
10000
  if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 32 * 1024)) {
9961
10001
  void this.flush();
@@ -9964,6 +10004,11 @@ var LogBatcher = class {
9964
10004
  this.timer.unref?.();
9965
10005
  }
9966
10006
  }
10007
+ /** Release an incomplete final match only after the producer has ended. */
10008
+ finish() {
10009
+ this.buffer += this.redactor.end();
10010
+ return this.flush();
10011
+ }
9967
10012
  /** Sends whatever is buffered now; resolves when every send so far settled. */
9968
10013
  flush() {
9969
10014
  if (this.timer) {
@@ -10076,15 +10121,68 @@ function pathWithin(path2, dir) {
10076
10121
  function buildSpawnEnv(base, opts) {
10077
10122
  const env = {
10078
10123
  ...base,
10124
+ ...Object.fromEntries((opts.extra ?? []).map((e) => [e.name, e.value])),
10079
10125
  TINES_API_KEY: opts.apiKey,
10080
10126
  // Own canonicalization at the final child-process boundary too. This
10081
10127
  // keeps the spawned CLI safe even if a future daemon call site passes
10082
10128
  // the original --url value instead of its normalized local variable.
10083
10129
  TINES_API_URL: opts.apiUrl.replace(/\/+$/, "")
10084
10130
  };
10131
+ if (opts.extra?.some((e) => e.name === "PATH")) env.PATH = base.PATH;
10085
10132
  if (opts.binDir) env.PATH = base.PATH ? `${opts.binDir}${delimiter}${base.PATH}` : opts.binDir;
10086
10133
  return env;
10087
10134
  }
10135
+ function secretForms(secrets) {
10136
+ return [
10137
+ ...new Set(
10138
+ secrets.filter(Boolean).flatMap((secret) => [secret, JSON.stringify(secret).slice(1, -1)])
10139
+ )
10140
+ ].sort((a, b) => b.length - a.length);
10141
+ }
10142
+ function secretPattern(forms) {
10143
+ return forms.length ? new RegExp(forms.map((form) => form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : null;
10144
+ }
10145
+ function redactSecrets(text4, secrets) {
10146
+ const pattern = secretPattern(secretForms(secrets));
10147
+ return pattern ? text4.replace(pattern, "***") : text4;
10148
+ }
10149
+ var SecretRedactor = class {
10150
+ pending = "";
10151
+ forms;
10152
+ pattern;
10153
+ constructor(secrets) {
10154
+ this.forms = secretForms(secrets);
10155
+ this.pattern = secretPattern(this.forms);
10156
+ }
10157
+ write(chunk) {
10158
+ if (!this.pattern) return chunk;
10159
+ const text4 = this.pending + chunk;
10160
+ let cut = text4.length;
10161
+ for (const form of this.forms) {
10162
+ for (let size = Math.min(form.length - 1, text4.length); size > text4.length - cut; size--) {
10163
+ if (text4.endsWith(form.slice(0, size))) {
10164
+ cut = text4.length - size;
10165
+ break;
10166
+ }
10167
+ }
10168
+ }
10169
+ this.pattern.lastIndex = 0;
10170
+ for (let match; match = this.pattern.exec(text4); ) {
10171
+ if (match.index >= cut) break;
10172
+ if (this.pattern.lastIndex > cut) {
10173
+ cut = match.index;
10174
+ break;
10175
+ }
10176
+ }
10177
+ this.pending = text4.slice(cut);
10178
+ return text4.slice(0, cut).replace(this.pattern, "***");
10179
+ }
10180
+ end() {
10181
+ const text4 = this.pending;
10182
+ this.pending = "";
10183
+ return this.pattern ? text4.replace(this.pattern, "***") : text4;
10184
+ }
10185
+ };
10088
10186
 
10089
10187
  // src/daemon/cli-refresh.ts
10090
10188
  var PACKAGE = "tines";
@@ -10097,7 +10195,7 @@ function binDirOf(prefix) {
10097
10195
  }
10098
10196
  function installedVersion(prefix) {
10099
10197
  try {
10100
- const pkg = readFileSync7(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10198
+ const pkg = readFileSync8(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10101
10199
  const version = JSON.parse(pkg).version;
10102
10200
  return typeof version === "string" ? version : null;
10103
10201
  } catch {
@@ -10177,7 +10275,7 @@ function message(err) {
10177
10275
  import { arch, hostname, platform } from "node:os";
10178
10276
 
10179
10277
  // src/daemon/store.ts
10180
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10278
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10181
10279
  import { join as join5 } from "node:path";
10182
10280
  function credentialsKey(url, name2) {
10183
10281
  return `${url.replace(/\/+$/, "")}#${name2}`;
@@ -10237,10 +10335,10 @@ function saveDaemonDeclines(path2, runIds) {
10237
10335
  }
10238
10336
  function processStartTimeMs(pid) {
10239
10337
  try {
10240
- const stat2 = readFileSync8(`/proc/${pid}/stat`, "utf8");
10338
+ const stat2 = readFileSync9(`/proc/${pid}/stat`, "utf8");
10241
10339
  const afterComm = stat2.slice(stat2.lastIndexOf(")") + 2).split(" ");
10242
10340
  const startTicks = Number(afterComm[19]);
10243
- const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10341
+ const btimeLine = readFileSync9("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10244
10342
  const btime = Number(btimeLine?.slice("btime ".length));
10245
10343
  if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
10246
10344
  return btime * 1e3 + startTicks / 100 * 1e3;
@@ -10367,7 +10465,7 @@ async function uploadRawLog(run3) {
10367
10465
  await new Promise((resolve2) => run3.rawSpool?.end(resolve2) ?? resolve2());
10368
10466
  const size = statSync4(path2).size;
10369
10467
  if (size > 0) {
10370
- let body = readFileSync9(path2);
10468
+ let body = readFileSync10(path2);
10371
10469
  if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
10372
10470
  const marker = Buffer.from(
10373
10471
  `{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
@@ -10534,7 +10632,7 @@ async function runDaemon(opts) {
10534
10632
  };
10535
10633
  run3.batcher.append(`[usage] ${JSON.stringify(accounting)}
10536
10634
  `);
10537
- await run3.batcher.flush();
10635
+ await run3.batcher.finish();
10538
10636
  const ended = await client2.finishRun(run3.runId, {
10539
10637
  status,
10540
10638
  ...run3.effortEvidence ? { effort_application: run3.effortEvidence } : {},
@@ -10635,6 +10733,7 @@ async function runDaemon(opts) {
10635
10733
  const effortRefresher = new EffortCapabilityRefresher(opts.harness, DAEMON_VERSION);
10636
10734
  let effortCapabilities = await effortRefresher.get();
10637
10735
  const launch = async (assignment) => {
10736
+ const secretEnvValues = (assignment.env ?? []).filter((e) => e.secret).map((e) => e.value);
10638
10737
  const runId = assignment.run.id;
10639
10738
  if (table2.has(runId)) return;
10640
10739
  const resume = assignment.resume && existsSync5(assignment.resume.workspace_path) ? assignment.resume : null;
@@ -10658,10 +10757,13 @@ async function runDaemon(opts) {
10658
10757
  batcher: new LogBatcher(
10659
10758
  (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
10660
10759
  }),
10661
- { onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
10760
+ {
10761
+ onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`),
10762
+ ...secretEnvValues.length > 0 ? { redact: secretEnvValues } : {}
10763
+ }
10662
10764
  )
10663
10765
  };
10664
- run3.flush = () => run3.batcher.flush();
10766
+ run3.flush = () => run3.batcher.finish();
10665
10767
  table2.track(run3);
10666
10768
  if (assignment.effort) effortCapabilities = await effortRefresher.get(true);
10667
10769
  const effortRejection = assignmentEffortRejection(assignment, effortCapabilities, opts.harness);
@@ -10759,8 +10861,15 @@ async function runDaemon(opts) {
10759
10861
  const spawnEnv = buildSpawnEnv(process.env, {
10760
10862
  binDir: cli.binDir,
10761
10863
  apiKey: assignment.run_key,
10762
- apiUrl: baseUrl
10864
+ apiUrl: baseUrl,
10865
+ ...assignment.env ? { extra: assignment.env } : {}
10763
10866
  });
10867
+ if (assignment.env?.length) {
10868
+ run3.batcher.append(
10869
+ `[env] ${assignment.env.length} environment variable(s) set: ${assignment.env.map((e) => `${e.name}${e.secret ? " (secret)" : ""}`).join(", ")}
10870
+ `
10871
+ );
10872
+ }
10764
10873
  if (opts.harness === "codex") run3.codexHome = resolveCodexHome(spawnEnv, workspace);
10765
10874
  const child = spawn3(invocation.file, invocation.args, {
10766
10875
  cwd: workspace,
@@ -10799,14 +10908,20 @@ async function runDaemon(opts) {
10799
10908
  run3.rawSpool = createWriteStream(spoolPath);
10800
10909
  run3.rawUpload = (body) => client2.putRunLogRaw(runId, body);
10801
10910
  const decoder = new StringDecoder2("utf8");
10911
+ const rawRedactor = new SecretRedactor(secretEnvValues);
10802
10912
  run3.drain = () => {
10803
10913
  const trailing = decoder.end();
10804
- if (trailing) renderer.write(trailing);
10914
+ if (trailing) {
10915
+ run3.rawSpool?.write(rawRedactor.write(trailing));
10916
+ renderer.write(trailing);
10917
+ }
10918
+ run3.rawSpool?.write(rawRedactor.end());
10805
10919
  renderer.finish();
10806
10920
  };
10807
10921
  child.stdout?.on("data", (data) => {
10808
- run3.rawSpool?.write(data);
10809
- renderer.write(decoder.write(data));
10922
+ const text4 = decoder.write(data);
10923
+ run3.rawSpool?.write(rawRedactor.write(text4));
10924
+ renderer.write(text4);
10810
10925
  });
10811
10926
  } else if (opts.harness === "codex") {
10812
10927
  const renderer = new CodexStreamRenderer((line) => run3.batcher.append(line));
@@ -10819,11 +10934,26 @@ async function runDaemon(opts) {
10819
10934
  };
10820
10935
  child.stdout?.on("data", (data) => renderer.write(decoder.write(data)));
10821
10936
  } else {
10822
- child.stdout?.on("data", (data) => run3.batcher.append(data.toString("utf8")));
10937
+ const decoder = new StringDecoder2("utf8");
10938
+ const redactor = new SecretRedactor(secretEnvValues);
10939
+ child.stdout?.on(
10940
+ "data",
10941
+ (data) => run3.batcher.append(redactor.write(decoder.write(data)))
10942
+ );
10943
+ run3.drain = () => {
10944
+ run3.batcher.append(redactor.write(decoder.end()) + redactor.end());
10945
+ };
10823
10946
  }
10947
+ const stderrDecoder = new StringDecoder2("utf8");
10948
+ const stderrRedactor = new SecretRedactor(secretEnvValues);
10949
+ const drainStdout = run3.drain;
10950
+ run3.drain = () => {
10951
+ drainStdout?.();
10952
+ run3.batcher.append(stderrRedactor.write(stderrDecoder.end()) + stderrRedactor.end());
10953
+ };
10824
10954
  child.stderr?.on("data", (data) => {
10825
- const text4 = data.toString("utf8");
10826
- run3.batcher.append(text4);
10955
+ const text4 = stderrDecoder.write(data);
10956
+ run3.batcher.append(stderrRedactor.write(text4));
10827
10957
  run3.limiter?.noteStderr(text4);
10828
10958
  });
10829
10959
  run3.timeout = setTimeout(() => {
@@ -10834,7 +10964,7 @@ async function runDaemon(opts) {
10834
10964
  if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
10835
10965
  }, assignment.timeout_minutes * 6e4);
10836
10966
  child.on("error", (err) => {
10837
- const reason = `failed to launch harness: ${message2(err)}`;
10967
+ const reason = redactSecrets(`failed to launch harness: ${message2(err)}`, secretEnvValues);
10838
10968
  if (assignment.effort)
10839
10969
  run3.effortEvidence = {
10840
10970
  status: "rejected",
@@ -10869,18 +10999,16 @@ async function runDaemon(opts) {
10869
10999
  const limited = signal ? null : run3.limiter?.signal() ?? null;
10870
11000
  const providerError = signal ? null : run3.limiter?.providerError() ?? null;
10871
11001
  if (limited) {
10872
- log(
10873
- `run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
10874
- );
10875
- void table2.finishAndCleanup(run3, "failed", `rate limited: ${limited.detail}`, {
11002
+ const detail = redactSecrets(limited.detail, secretEnvValues);
11003
+ log(`run ${runId}: harness rate limited (${detail}); reporting without a strike`);
11004
+ void table2.finishAndCleanup(run3, "failed", `rate limited: ${detail}`, {
10876
11005
  judgment: "rate_limited",
10877
11006
  ...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
10878
11007
  });
10879
11008
  } else if (providerError) {
10880
- log(
10881
- `run ${runId}: transient provider error (${providerError.detail}); reporting without a strike`
10882
- );
10883
- void table2.finishAndCleanup(run3, "failed", `provider error: ${providerError.detail}`, {
11009
+ const detail = redactSecrets(providerError.detail, secretEnvValues);
11010
+ log(`run ${runId}: transient provider error (${detail}); reporting without a strike`);
11011
+ void table2.finishAndCleanup(run3, "failed", `provider error: ${detail}`, {
10884
11012
  judgment: "interrupted"
10885
11013
  });
10886
11014
  } else {
@@ -10893,7 +11021,11 @@ async function runDaemon(opts) {
10893
11021
  }
10894
11022
  });
10895
11023
  } catch (err) {
10896
- void table2.finishAndCleanup(run3, "failed", `workspace setup failed: ${message2(err)}`);
11024
+ void table2.finishAndCleanup(
11025
+ run3,
11026
+ "failed",
11027
+ redactSecrets(`workspace setup failed: ${message2(err)}`, secretEnvValues)
11028
+ );
10897
11029
  }
10898
11030
  };
10899
11031
  const shutdown = async () => {
@@ -10933,6 +11065,8 @@ async function runDaemon(opts) {
10933
11065
  },
10934
11066
  ...declinedAssignments.size > 0 ? { declined_assignments: [...declinedAssignments].slice(0, 100) } : {},
10935
11067
  ...effortCapabilities ? { effort_capabilities: effortCapabilities } : {},
11068
+ // Capability: this daemon merges assignment.env into the harness environment.
11069
+ env_delivery: 1,
10936
11070
  // Stated on every poll while pending; absent otherwise, which
10937
11071
  // the server reads as "not draining" — so a daemon that died
10938
11072
  // mid-drain cannot pin its runner shut past its relaunch.
@@ -11111,7 +11245,7 @@ function harnessFlag(harness) {
11111
11245
 
11112
11246
  // src/commands/runner-service.ts
11113
11247
  import { execFile as execFile2 } from "node:child_process";
11114
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11248
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11115
11249
  import { homedir as homedir3, hostname as hostname3, userInfo } from "node:os";
11116
11250
  import { dirname as dirname6 } from "node:path";
11117
11251
  import { promisify } from "node:util";
@@ -11358,7 +11492,7 @@ async function waitForStart(logPath, offset, name2) {
11358
11492
  const deadline = Date.now() + STARTUP_TIMEOUT_MS;
11359
11493
  while (Date.now() < deadline) {
11360
11494
  if (existsSync7(logPath)) {
11361
- const content = readFileSync10(logPath, "utf8").slice(offset);
11495
+ const content = readFileSync11(logPath, "utf8").slice(offset);
11362
11496
  if (pattern.test(content)) return true;
11363
11497
  }
11364
11498
  await new Promise((resolve2) => setTimeout(resolve2, 500));
@@ -12611,18 +12745,18 @@ function register10(program3) {
12611
12745
  }
12612
12746
 
12613
12747
  // src/commands/workflows.ts
12614
- import { readFileSync as readFileSync12 } from "node:fs";
12748
+ import { readFileSync as readFileSync13 } from "node:fs";
12615
12749
  import { createInterface as createInterface5 } from "node:readline/promises";
12616
12750
 
12617
12751
  // src/workflow-packages.ts
12618
12752
  import { createInterface as createInterface4 } from "node:readline/promises";
12619
12753
  import { createHash as createHash2 } from "node:crypto";
12620
- import { readFileSync as readFileSync11 } from "node:fs";
12754
+ import { readFileSync as readFileSync12 } from "node:fs";
12621
12755
  var PACKAGE_STDIN = "-";
12622
12756
  var MAX_LOCAL_JSON_BYTES = 32 * 1024 * 1024;
12623
12757
  function readPackageSource(path2) {
12624
12758
  try {
12625
- const bytes2 = readFileSync11(path2 === PACKAGE_STDIN ? 0 : path2);
12759
+ const bytes2 = readFileSync12(path2 === PACKAGE_STDIN ? 0 : path2);
12626
12760
  if (bytes2.byteLength > MAX_LOCAL_JSON_BYTES)
12627
12761
  throw new Error(`input exceeds ${MAX_LOCAL_JSON_BYTES} bytes`);
12628
12762
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2);
@@ -13261,14 +13395,14 @@ function readJsonBody(inline, file) {
13261
13395
  if (file !== void 0 && file !== "-") {
13262
13396
  let raw;
13263
13397
  try {
13264
- raw = readFileSync12(file, "utf8");
13398
+ raw = readFileSync13(file, "utf8");
13265
13399
  } catch (err) {
13266
13400
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
13267
13401
  }
13268
13402
  return parseJsonObject(raw, file);
13269
13403
  }
13270
13404
  if (file === "-" || !process.stdin.isTTY) {
13271
- const raw = readFileSync12(0, "utf8");
13405
+ const raw = readFileSync13(0, "utf8");
13272
13406
  if (raw.trim() === "") {
13273
13407
  if (file === "-") die("no JSON on stdin");
13274
13408
  return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.262",
3
+ "version": "0.0.266",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",