tines 0.0.265 → 0.0.267

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 +200 -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:
@@ -3462,6 +3466,9 @@ function repoDirFromUrl(url) {
3462
3466
  }
3463
3467
  var ARTIFACT_TYPES = ["file", "text", "link", "pr", "folder"];
3464
3468
  var ARTIFACT_FILE_MAX_BYTES = 25 * 1024 * 1024;
3469
+ var ISSUE_CREATE_FILES_MAX_BYTES = 50 * 1024 * 1024;
3470
+ var ISSUE_CREATE_METADATA_MAX_BYTES = 256 * 1024;
3471
+ var ISSUE_CREATE_MULTIPART_MAX_BYTES = 51 * 1024 * 1024;
3465
3472
  var ARTIFACT_TEXT_MAX_BYTES = 256 * 1024;
3466
3473
  var ARTIFACT_FOLDER_MAX_BYTES = 50 * 1024 * 1024;
3467
3474
  function canonicalGitHubRepoUrl(url) {
@@ -4035,6 +4042,21 @@ function createApiClient(options) {
4035
4042
  removeIssueLabel: (issueId, labelRef) => request("DELETE", `/api/v1/issues/${issueId}/labels/${encodeURIComponent(labelRef)}`),
4036
4043
  listProjectIssues: (projectId, filters = {}) => get(`/api/v1/projects/${projectId}/issues${query(filters)}`),
4037
4044
  createIssue: (projectId, body) => request("POST", `/api/v1/projects/${projectId}/issues`, body),
4045
+ createIssueWithFiles: async (projectId, issue, files2) => {
4046
+ const form = new FormData();
4047
+ const metadata = {
4048
+ issue,
4049
+ attachments: files2.map((file, index) => ({
4050
+ part: `file-${index}`,
4051
+ name: file.name,
4052
+ filename: file.filename
4053
+ }))
4054
+ };
4055
+ form.append("metadata", JSON.stringify(metadata));
4056
+ files2.forEach((file, index) => form.append(`file-${index}`, file.file, file.filename));
4057
+ const res = await raw("POST", `/api/v1/projects/${projectId}/issues`, { body: form });
4058
+ return await res.json();
4059
+ },
4038
4060
  getIssue: (id2) => get(`/api/v1/issues/${id2}`),
4039
4061
  getIssueByNumber: (projectId, number) => get(`/api/v1/projects/${projectId}/issues/${number}`),
4040
4062
  updateIssue: (id2, body) => request("PATCH", `/api/v1/issues/${id2}`, body),
@@ -5636,6 +5658,8 @@ function contextItemSummary(item) {
5636
5658
  return `${item.repo_url}${item.repo_branch ? `#${item.repo_branch}` : ""}`;
5637
5659
  case "artifact":
5638
5660
  return item.artifact_type ?? "artifact";
5661
+ case "env":
5662
+ return item.secret ? `secret \xB7 set${item.hint ? ` \xB7 ${item.hint}` : ""}` : `= ${item.value ?? ""}`;
5639
5663
  }
5640
5664
  }
5641
5665
  function recurrenceLabel(schedule2) {
@@ -6234,6 +6258,12 @@ async function resolveScopeFlags(api, opts) {
6234
6258
  if (opts.label !== void 0) scope.label_id = await resolveLabelFlag(api, opts.label);
6235
6259
  return scope;
6236
6260
  }
6261
+ function readEnvValue(raw) {
6262
+ if (raw === "-") return readFileSync4(0, "utf8").replace(/\r?\n$/, "");
6263
+ if (raw.startsWith("@@")) return raw.slice(1);
6264
+ if (raw.startsWith("@")) return readFileSync4(raw.slice(1), "utf8").replace(/\r?\n$/, "");
6265
+ return raw;
6266
+ }
6237
6267
  function printContextItem(item) {
6238
6268
  console.log(`${item.kind} "${item.name}" [${item.id}] v${item.version}`);
6239
6269
  if (item.description) console.log(item.description);
@@ -6256,6 +6286,15 @@ files (seeded at skills/${item.name}/):`);
6256
6286
  console.log(` ${f.path} (${Buffer.byteLength(f.content, "utf8")} bytes)`);
6257
6287
  }
6258
6288
  if ((item.files ?? []).length === 0) console.log(" (none)");
6289
+ } else if (item.kind === "env") {
6290
+ if (item.secret) {
6291
+ console.log(`
6292
+ value: (secret, set)${item.hint ? ` hint: ${item.hint}` : ""}`);
6293
+ } else {
6294
+ console.log(`
6295
+ value: ${item.value ?? ""}`);
6296
+ if (item.hint) console.log(`hint: ${item.hint}`);
6297
+ }
6259
6298
  } else {
6260
6299
  console.log(`
6261
6300
  url: ${item.repo_url}`);
@@ -6277,13 +6316,13 @@ function withScopeFlags(cmd) {
6277
6316
  }
6278
6317
  function register(program3) {
6279
6318
  const context2 = program3.command("context").description(
6280
- "Manage context items (prompts, skills, repo pointers) scoped to projects, states, and issues"
6319
+ "Manage context items (prompts, skills, repo pointers, environment variables) scoped to projects, states, and issues"
6281
6320
  );
6282
6321
  withList(
6283
6322
  withScopeFlags(
6284
6323
  context2.command("list").description(
6285
6324
  "List context items (scope filters match every item whose scope includes the element)"
6286
- ).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")
6325
+ ).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")
6287
6326
  )
6288
6327
  ).action(
6289
6328
  async (opts) => {
@@ -6327,21 +6366,27 @@ function register(program3) {
6327
6366
  });
6328
6367
  withCommon(
6329
6368
  withScopeFlags(
6330
- 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(
6369
+ 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(
6331
6370
  "-n, --name <name>",
6332
- "item name (slug-like for skills; the dedup/override key)"
6371
+ "item name (slug-like for skills, the variable name for env; the dedup/override key)"
6333
6372
  ).option("-d, --description <text>", "one-liner shown in lists").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option(
6334
6373
  "--file <path>=@<local>",
6335
6374
  "skill file: workspace path = local file (repeatable)",
6336
6375
  collect,
6337
6376
  []
6338
- ).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)")
6377
+ ).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(
6378
+ "--value <v>",
6379
+ "env: the value \u2014 inline, @file, or - for stdin (keeps a secret out of shell history)"
6380
+ ).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")')
6339
6381
  )
6340
6382
  ).action(
6341
6383
  async (opts) => {
6342
6384
  if (opts.kind === "repo" && opts.repoUrl === void 0) {
6343
6385
  die("--kind repo needs --repo-url <clone-url> (--url is the API base URL)");
6344
6386
  }
6387
+ if (opts.kind === "env" && opts.value === void 0) {
6388
+ die("--kind env needs --value <value|@file|->");
6389
+ }
6345
6390
  const api = client(opts);
6346
6391
  const scope = await resolveScopeFlags(api, opts);
6347
6392
  const body = {
@@ -6356,6 +6401,9 @@ function register(program3) {
6356
6401
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6357
6402
  if (opts.branch !== void 0) body.repo_branch = opts.branch;
6358
6403
  if (opts.dir !== void 0) body.repo_dir = opts.dir;
6404
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6405
+ if (opts.secret) body.secret = true;
6406
+ if (opts.hint !== void 0) body.hint = opts.hint;
6359
6407
  const item = await api.createContextItem(body);
6360
6408
  if (opts.json) return printJson(item);
6361
6409
  console.log(`created ${item.kind} "${item.name}" (${item.id}) \u2014 scope: ${item.scope.label}`);
@@ -6363,7 +6411,7 @@ function register(program3) {
6363
6411
  );
6364
6412
  withCommon(
6365
6413
  withScopeFlags(
6366
- 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(
6414
+ 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(
6367
6415
  "--unset <dimension>",
6368
6416
  "drop a scope dimension: project, state, issue, or label (repeatable)",
6369
6417
  collect,
@@ -6413,6 +6461,9 @@ function register(program3) {
6413
6461
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6414
6462
  if (opts.branch !== void 0) body.repo_branch = opts.branch === "" ? null : opts.branch;
6415
6463
  if (opts.dir !== void 0) body.repo_dir = opts.dir === "" ? null : opts.dir;
6464
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6465
+ if (opts.secret) body.secret = true;
6466
+ if (opts.hint !== void 0) body.hint = opts.hint === "" ? null : opts.hint;
6416
6467
  if (Object.keys(body).length === 0) {
6417
6468
  die(
6418
6469
  "nothing to update: pass payload flags, --name/--description, scope flags, and/or --unset"
@@ -6459,7 +6510,7 @@ function register(program3) {
6459
6510
  }
6460
6511
 
6461
6512
  // src/commands/issues.ts
6462
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6513
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6463
6514
  import { basename, dirname as dirname2, join as join2 } from "node:path";
6464
6515
  import { createInterface } from "node:readline/promises";
6465
6516
 
@@ -7171,7 +7222,7 @@ function readTextSource(source) {
7171
7222
  if (source.kind === "text-stdin") return readBodyValue("-");
7172
7223
  if (source.kind === "text-path") {
7173
7224
  try {
7174
- return readFileSync4(source.path, "utf8");
7225
+ return readFileSync5(source.path, "utf8");
7175
7226
  } catch (err) {
7176
7227
  die(`cannot read ${source.path}: ${err instanceof Error ? err.message : String(err)}`);
7177
7228
  }
@@ -7189,7 +7240,7 @@ function walkFolder(dir) {
7189
7240
  files2.push({
7190
7241
  path: nextRel,
7191
7242
  contentType: sniffContentType(entry.name),
7192
- bytes: readFileSync4(nextAbs)
7243
+ bytes: readFileSync5(nextAbs)
7193
7244
  });
7194
7245
  }
7195
7246
  }
@@ -7644,6 +7695,9 @@ function register2(program3) {
7644
7695
  console.log(`
7645
7696
  skills: ${context2.skills.map((s) => s.name).join(", ")}`);
7646
7697
  }
7698
+ for (const e of context2.env ?? []) {
7699
+ console.log(`env: ${e.name}${e.secret ? " (secret)" : ""}`);
7700
+ }
7647
7701
  for (const repo of context2.repos) {
7648
7702
  console.log(
7649
7703
  `repo: ${repo.name} ${repo.url}${repo.branch ? `#${repo.branch}` : ""} \u2192 ${repo.dir}/`
@@ -7827,10 +7881,10 @@ files (v${artifact.current_version.version}):`);
7827
7881
  let bytes2;
7828
7882
  if (fromPath === null) {
7829
7883
  if (process.stdin.isTTY) die('"-" reads the file from stdin, but stdin is a terminal');
7830
- bytes2 = readFileSync4(0);
7884
+ bytes2 = readFileSync5(0);
7831
7885
  } else {
7832
7886
  try {
7833
- bytes2 = readFileSync4(fromPath);
7887
+ bytes2 = readFileSync5(fromPath);
7834
7888
  } catch (err) {
7835
7889
  die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
7836
7890
  }
@@ -8285,13 +8339,13 @@ function register4(program3) {
8285
8339
  }
8286
8340
 
8287
8341
  // src/commands/login.ts
8288
- import { readFileSync as readFileSync5 } from "node:fs";
8342
+ import { readFileSync as readFileSync6 } from "node:fs";
8289
8343
  function maskKey(key) {
8290
8344
  return `${key.slice(0, 14)}\u2026`;
8291
8345
  }
8292
8346
  function readKeyArg(value) {
8293
8347
  if (value !== "-") return value;
8294
- const key = readFileSync5(0, "utf8").trim();
8348
+ const key = readFileSync6(0, "utf8").trim();
8295
8349
  if (!key) die("no API key on stdin");
8296
8350
  return key;
8297
8351
  }
@@ -8731,7 +8785,7 @@ import {
8731
8785
  createWriteStream,
8732
8786
  existsSync as existsSync5,
8733
8787
  mkdirSync as mkdirSync4,
8734
- readFileSync as readFileSync9,
8788
+ readFileSync as readFileSync10,
8735
8789
  realpathSync,
8736
8790
  rmSync as rmSync3,
8737
8791
  statSync as statSync4,
@@ -8743,11 +8797,11 @@ import { dirname as dirname4, join as join6 } from "node:path";
8743
8797
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
8744
8798
 
8745
8799
  // src/version.ts
8746
- import { readFileSync as readFileSync6 } from "node:fs";
8800
+ import { readFileSync as readFileSync7 } from "node:fs";
8747
8801
  function cliVersion() {
8748
8802
  try {
8749
8803
  const manifest = new URL("../package.json", import.meta.url);
8750
- return JSON.parse(readFileSync6(manifest, "utf8")).version ?? "0.0.0-unknown";
8804
+ return JSON.parse(readFileSync7(manifest, "utf8")).version ?? "0.0.0-unknown";
8751
8805
  } catch {
8752
8806
  return "0.0.0-unknown";
8753
8807
  }
@@ -9761,7 +9815,7 @@ var EffortCapabilityRefresher = class {
9761
9815
 
9762
9816
  // src/daemon/cli-refresh.ts
9763
9817
  import { spawn as spawn2 } from "node:child_process";
9764
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
9818
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8 } from "node:fs";
9765
9819
  import { dirname as dirname3, join as join4 } from "node:path";
9766
9820
 
9767
9821
  // src/daemon/support.ts
@@ -9946,10 +10000,12 @@ var LogBatcher = class {
9946
10000
  constructor(send, opts = {}) {
9947
10001
  this.send = send;
9948
10002
  this.opts = opts;
10003
+ this.redactor = new SecretRedactor(opts.redact ?? []);
9949
10004
  }
9950
10005
  send;
9951
10006
  opts;
9952
10007
  buffer = "";
10008
+ redactor;
9953
10009
  timer = null;
9954
10010
  sending = Promise.resolve();
9955
10011
  seq = 0;
@@ -9957,6 +10013,7 @@ var LogBatcher = class {
9957
10013
  pending = [];
9958
10014
  append(text4) {
9959
10015
  if (!text4) return;
10016
+ text4 = this.redactor.write(text4);
9960
10017
  this.buffer += text4;
9961
10018
  if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 32 * 1024)) {
9962
10019
  void this.flush();
@@ -9965,6 +10022,11 @@ var LogBatcher = class {
9965
10022
  this.timer.unref?.();
9966
10023
  }
9967
10024
  }
10025
+ /** Release an incomplete final match only after the producer has ended. */
10026
+ finish() {
10027
+ this.buffer += this.redactor.end();
10028
+ return this.flush();
10029
+ }
9968
10030
  /** Sends whatever is buffered now; resolves when every send so far settled. */
9969
10031
  flush() {
9970
10032
  if (this.timer) {
@@ -10077,15 +10139,68 @@ function pathWithin(path2, dir) {
10077
10139
  function buildSpawnEnv(base, opts) {
10078
10140
  const env = {
10079
10141
  ...base,
10142
+ ...Object.fromEntries((opts.extra ?? []).map((e) => [e.name, e.value])),
10080
10143
  TINES_API_KEY: opts.apiKey,
10081
10144
  // Own canonicalization at the final child-process boundary too. This
10082
10145
  // keeps the spawned CLI safe even if a future daemon call site passes
10083
10146
  // the original --url value instead of its normalized local variable.
10084
10147
  TINES_API_URL: opts.apiUrl.replace(/\/+$/, "")
10085
10148
  };
10149
+ if (opts.extra?.some((e) => e.name === "PATH")) env.PATH = base.PATH;
10086
10150
  if (opts.binDir) env.PATH = base.PATH ? `${opts.binDir}${delimiter}${base.PATH}` : opts.binDir;
10087
10151
  return env;
10088
10152
  }
10153
+ function secretForms(secrets) {
10154
+ return [
10155
+ ...new Set(
10156
+ secrets.filter(Boolean).flatMap((secret) => [secret, JSON.stringify(secret).slice(1, -1)])
10157
+ )
10158
+ ].sort((a, b) => b.length - a.length);
10159
+ }
10160
+ function secretPattern(forms) {
10161
+ return forms.length ? new RegExp(forms.map((form) => form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : null;
10162
+ }
10163
+ function redactSecrets(text4, secrets) {
10164
+ const pattern = secretPattern(secretForms(secrets));
10165
+ return pattern ? text4.replace(pattern, "***") : text4;
10166
+ }
10167
+ var SecretRedactor = class {
10168
+ pending = "";
10169
+ forms;
10170
+ pattern;
10171
+ constructor(secrets) {
10172
+ this.forms = secretForms(secrets);
10173
+ this.pattern = secretPattern(this.forms);
10174
+ }
10175
+ write(chunk) {
10176
+ if (!this.pattern) return chunk;
10177
+ const text4 = this.pending + chunk;
10178
+ let cut = text4.length;
10179
+ for (const form of this.forms) {
10180
+ for (let size = Math.min(form.length - 1, text4.length); size > text4.length - cut; size--) {
10181
+ if (text4.endsWith(form.slice(0, size))) {
10182
+ cut = text4.length - size;
10183
+ break;
10184
+ }
10185
+ }
10186
+ }
10187
+ this.pattern.lastIndex = 0;
10188
+ for (let match; match = this.pattern.exec(text4); ) {
10189
+ if (match.index >= cut) break;
10190
+ if (this.pattern.lastIndex > cut) {
10191
+ cut = match.index;
10192
+ break;
10193
+ }
10194
+ }
10195
+ this.pending = text4.slice(cut);
10196
+ return text4.slice(0, cut).replace(this.pattern, "***");
10197
+ }
10198
+ end() {
10199
+ const text4 = this.pending;
10200
+ this.pending = "";
10201
+ return this.pattern ? text4.replace(this.pattern, "***") : text4;
10202
+ }
10203
+ };
10089
10204
 
10090
10205
  // src/daemon/cli-refresh.ts
10091
10206
  var PACKAGE = "tines";
@@ -10098,7 +10213,7 @@ function binDirOf(prefix) {
10098
10213
  }
10099
10214
  function installedVersion(prefix) {
10100
10215
  try {
10101
- const pkg = readFileSync7(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10216
+ const pkg = readFileSync8(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10102
10217
  const version = JSON.parse(pkg).version;
10103
10218
  return typeof version === "string" ? version : null;
10104
10219
  } catch {
@@ -10178,7 +10293,7 @@ function message(err) {
10178
10293
  import { arch, hostname, platform } from "node:os";
10179
10294
 
10180
10295
  // src/daemon/store.ts
10181
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10296
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10182
10297
  import { join as join5 } from "node:path";
10183
10298
  function credentialsKey(url, name2) {
10184
10299
  return `${url.replace(/\/+$/, "")}#${name2}`;
@@ -10238,10 +10353,10 @@ function saveDaemonDeclines(path2, runIds) {
10238
10353
  }
10239
10354
  function processStartTimeMs(pid) {
10240
10355
  try {
10241
- const stat2 = readFileSync8(`/proc/${pid}/stat`, "utf8");
10356
+ const stat2 = readFileSync9(`/proc/${pid}/stat`, "utf8");
10242
10357
  const afterComm = stat2.slice(stat2.lastIndexOf(")") + 2).split(" ");
10243
10358
  const startTicks = Number(afterComm[19]);
10244
- const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10359
+ const btimeLine = readFileSync9("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10245
10360
  const btime = Number(btimeLine?.slice("btime ".length));
10246
10361
  if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
10247
10362
  return btime * 1e3 + startTicks / 100 * 1e3;
@@ -10368,7 +10483,7 @@ async function uploadRawLog(run3) {
10368
10483
  await new Promise((resolve2) => run3.rawSpool?.end(resolve2) ?? resolve2());
10369
10484
  const size = statSync4(path2).size;
10370
10485
  if (size > 0) {
10371
- let body = readFileSync9(path2);
10486
+ let body = readFileSync10(path2);
10372
10487
  if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
10373
10488
  const marker = Buffer.from(
10374
10489
  `{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
@@ -10535,7 +10650,7 @@ async function runDaemon(opts) {
10535
10650
  };
10536
10651
  run3.batcher.append(`[usage] ${JSON.stringify(accounting)}
10537
10652
  `);
10538
- await run3.batcher.flush();
10653
+ await run3.batcher.finish();
10539
10654
  const ended = await client2.finishRun(run3.runId, {
10540
10655
  status,
10541
10656
  ...run3.effortEvidence ? { effort_application: run3.effortEvidence } : {},
@@ -10636,6 +10751,7 @@ async function runDaemon(opts) {
10636
10751
  const effortRefresher = new EffortCapabilityRefresher(opts.harness, DAEMON_VERSION);
10637
10752
  let effortCapabilities = await effortRefresher.get();
10638
10753
  const launch = async (assignment) => {
10754
+ const secretEnvValues = (assignment.env ?? []).filter((e) => e.secret).map((e) => e.value);
10639
10755
  const runId = assignment.run.id;
10640
10756
  if (table2.has(runId)) return;
10641
10757
  const resume = assignment.resume && existsSync5(assignment.resume.workspace_path) ? assignment.resume : null;
@@ -10659,10 +10775,13 @@ async function runDaemon(opts) {
10659
10775
  batcher: new LogBatcher(
10660
10776
  (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
10661
10777
  }),
10662
- { onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
10778
+ {
10779
+ onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`),
10780
+ ...secretEnvValues.length > 0 ? { redact: secretEnvValues } : {}
10781
+ }
10663
10782
  )
10664
10783
  };
10665
- run3.flush = () => run3.batcher.flush();
10784
+ run3.flush = () => run3.batcher.finish();
10666
10785
  table2.track(run3);
10667
10786
  if (assignment.effort) effortCapabilities = await effortRefresher.get(true);
10668
10787
  const effortRejection = assignmentEffortRejection(assignment, effortCapabilities, opts.harness);
@@ -10760,8 +10879,15 @@ async function runDaemon(opts) {
10760
10879
  const spawnEnv = buildSpawnEnv(process.env, {
10761
10880
  binDir: cli.binDir,
10762
10881
  apiKey: assignment.run_key,
10763
- apiUrl: baseUrl
10882
+ apiUrl: baseUrl,
10883
+ ...assignment.env ? { extra: assignment.env } : {}
10764
10884
  });
10885
+ if (assignment.env?.length) {
10886
+ run3.batcher.append(
10887
+ `[env] ${assignment.env.length} environment variable(s) set: ${assignment.env.map((e) => `${e.name}${e.secret ? " (secret)" : ""}`).join(", ")}
10888
+ `
10889
+ );
10890
+ }
10765
10891
  if (opts.harness === "codex") run3.codexHome = resolveCodexHome(spawnEnv, workspace);
10766
10892
  const child = spawn3(invocation.file, invocation.args, {
10767
10893
  cwd: workspace,
@@ -10800,14 +10926,20 @@ async function runDaemon(opts) {
10800
10926
  run3.rawSpool = createWriteStream(spoolPath);
10801
10927
  run3.rawUpload = (body) => client2.putRunLogRaw(runId, body);
10802
10928
  const decoder = new StringDecoder2("utf8");
10929
+ const rawRedactor = new SecretRedactor(secretEnvValues);
10803
10930
  run3.drain = () => {
10804
10931
  const trailing = decoder.end();
10805
- if (trailing) renderer.write(trailing);
10932
+ if (trailing) {
10933
+ run3.rawSpool?.write(rawRedactor.write(trailing));
10934
+ renderer.write(trailing);
10935
+ }
10936
+ run3.rawSpool?.write(rawRedactor.end());
10806
10937
  renderer.finish();
10807
10938
  };
10808
10939
  child.stdout?.on("data", (data) => {
10809
- run3.rawSpool?.write(data);
10810
- renderer.write(decoder.write(data));
10940
+ const text4 = decoder.write(data);
10941
+ run3.rawSpool?.write(rawRedactor.write(text4));
10942
+ renderer.write(text4);
10811
10943
  });
10812
10944
  } else if (opts.harness === "codex") {
10813
10945
  const renderer = new CodexStreamRenderer((line) => run3.batcher.append(line));
@@ -10820,11 +10952,26 @@ async function runDaemon(opts) {
10820
10952
  };
10821
10953
  child.stdout?.on("data", (data) => renderer.write(decoder.write(data)));
10822
10954
  } else {
10823
- child.stdout?.on("data", (data) => run3.batcher.append(data.toString("utf8")));
10955
+ const decoder = new StringDecoder2("utf8");
10956
+ const redactor = new SecretRedactor(secretEnvValues);
10957
+ child.stdout?.on(
10958
+ "data",
10959
+ (data) => run3.batcher.append(redactor.write(decoder.write(data)))
10960
+ );
10961
+ run3.drain = () => {
10962
+ run3.batcher.append(redactor.write(decoder.end()) + redactor.end());
10963
+ };
10824
10964
  }
10965
+ const stderrDecoder = new StringDecoder2("utf8");
10966
+ const stderrRedactor = new SecretRedactor(secretEnvValues);
10967
+ const drainStdout = run3.drain;
10968
+ run3.drain = () => {
10969
+ drainStdout?.();
10970
+ run3.batcher.append(stderrRedactor.write(stderrDecoder.end()) + stderrRedactor.end());
10971
+ };
10825
10972
  child.stderr?.on("data", (data) => {
10826
- const text4 = data.toString("utf8");
10827
- run3.batcher.append(text4);
10973
+ const text4 = stderrDecoder.write(data);
10974
+ run3.batcher.append(stderrRedactor.write(text4));
10828
10975
  run3.limiter?.noteStderr(text4);
10829
10976
  });
10830
10977
  run3.timeout = setTimeout(() => {
@@ -10835,7 +10982,7 @@ async function runDaemon(opts) {
10835
10982
  if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
10836
10983
  }, assignment.timeout_minutes * 6e4);
10837
10984
  child.on("error", (err) => {
10838
- const reason = `failed to launch harness: ${message2(err)}`;
10985
+ const reason = redactSecrets(`failed to launch harness: ${message2(err)}`, secretEnvValues);
10839
10986
  if (assignment.effort)
10840
10987
  run3.effortEvidence = {
10841
10988
  status: "rejected",
@@ -10870,18 +11017,16 @@ async function runDaemon(opts) {
10870
11017
  const limited = signal ? null : run3.limiter?.signal() ?? null;
10871
11018
  const providerError = signal ? null : run3.limiter?.providerError() ?? null;
10872
11019
  if (limited) {
10873
- log(
10874
- `run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
10875
- );
10876
- void table2.finishAndCleanup(run3, "failed", `rate limited: ${limited.detail}`, {
11020
+ const detail = redactSecrets(limited.detail, secretEnvValues);
11021
+ log(`run ${runId}: harness rate limited (${detail}); reporting without a strike`);
11022
+ void table2.finishAndCleanup(run3, "failed", `rate limited: ${detail}`, {
10877
11023
  judgment: "rate_limited",
10878
11024
  ...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
10879
11025
  });
10880
11026
  } else if (providerError) {
10881
- log(
10882
- `run ${runId}: transient provider error (${providerError.detail}); reporting without a strike`
10883
- );
10884
- void table2.finishAndCleanup(run3, "failed", `provider error: ${providerError.detail}`, {
11027
+ const detail = redactSecrets(providerError.detail, secretEnvValues);
11028
+ log(`run ${runId}: transient provider error (${detail}); reporting without a strike`);
11029
+ void table2.finishAndCleanup(run3, "failed", `provider error: ${detail}`, {
10885
11030
  judgment: "interrupted"
10886
11031
  });
10887
11032
  } else {
@@ -10894,7 +11039,11 @@ async function runDaemon(opts) {
10894
11039
  }
10895
11040
  });
10896
11041
  } catch (err) {
10897
- void table2.finishAndCleanup(run3, "failed", `workspace setup failed: ${message2(err)}`);
11042
+ void table2.finishAndCleanup(
11043
+ run3,
11044
+ "failed",
11045
+ redactSecrets(`workspace setup failed: ${message2(err)}`, secretEnvValues)
11046
+ );
10898
11047
  }
10899
11048
  };
10900
11049
  const shutdown = async () => {
@@ -10934,6 +11083,8 @@ async function runDaemon(opts) {
10934
11083
  },
10935
11084
  ...declinedAssignments.size > 0 ? { declined_assignments: [...declinedAssignments].slice(0, 100) } : {},
10936
11085
  ...effortCapabilities ? { effort_capabilities: effortCapabilities } : {},
11086
+ // Capability: this daemon merges assignment.env into the harness environment.
11087
+ env_delivery: 1,
10937
11088
  // Stated on every poll while pending; absent otherwise, which
10938
11089
  // the server reads as "not draining" — so a daemon that died
10939
11090
  // mid-drain cannot pin its runner shut past its relaunch.
@@ -11112,7 +11263,7 @@ function harnessFlag(harness) {
11112
11263
 
11113
11264
  // src/commands/runner-service.ts
11114
11265
  import { execFile as execFile2 } from "node:child_process";
11115
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11266
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11116
11267
  import { homedir as homedir3, hostname as hostname3, userInfo } from "node:os";
11117
11268
  import { dirname as dirname6 } from "node:path";
11118
11269
  import { promisify } from "node:util";
@@ -11359,7 +11510,7 @@ async function waitForStart(logPath, offset, name2) {
11359
11510
  const deadline = Date.now() + STARTUP_TIMEOUT_MS;
11360
11511
  while (Date.now() < deadline) {
11361
11512
  if (existsSync7(logPath)) {
11362
- const content = readFileSync10(logPath, "utf8").slice(offset);
11513
+ const content = readFileSync11(logPath, "utf8").slice(offset);
11363
11514
  if (pattern.test(content)) return true;
11364
11515
  }
11365
11516
  await new Promise((resolve2) => setTimeout(resolve2, 500));
@@ -12612,18 +12763,18 @@ function register10(program3) {
12612
12763
  }
12613
12764
 
12614
12765
  // src/commands/workflows.ts
12615
- import { readFileSync as readFileSync12 } from "node:fs";
12766
+ import { readFileSync as readFileSync13 } from "node:fs";
12616
12767
  import { createInterface as createInterface5 } from "node:readline/promises";
12617
12768
 
12618
12769
  // src/workflow-packages.ts
12619
12770
  import { createInterface as createInterface4 } from "node:readline/promises";
12620
12771
  import { createHash as createHash2 } from "node:crypto";
12621
- import { readFileSync as readFileSync11 } from "node:fs";
12772
+ import { readFileSync as readFileSync12 } from "node:fs";
12622
12773
  var PACKAGE_STDIN = "-";
12623
12774
  var MAX_LOCAL_JSON_BYTES = 32 * 1024 * 1024;
12624
12775
  function readPackageSource(path2) {
12625
12776
  try {
12626
- const bytes2 = readFileSync11(path2 === PACKAGE_STDIN ? 0 : path2);
12777
+ const bytes2 = readFileSync12(path2 === PACKAGE_STDIN ? 0 : path2);
12627
12778
  if (bytes2.byteLength > MAX_LOCAL_JSON_BYTES)
12628
12779
  throw new Error(`input exceeds ${MAX_LOCAL_JSON_BYTES} bytes`);
12629
12780
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2);
@@ -13262,14 +13413,14 @@ function readJsonBody(inline, file) {
13262
13413
  if (file !== void 0 && file !== "-") {
13263
13414
  let raw;
13264
13415
  try {
13265
- raw = readFileSync12(file, "utf8");
13416
+ raw = readFileSync13(file, "utf8");
13266
13417
  } catch (err) {
13267
13418
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
13268
13419
  }
13269
13420
  return parseJsonObject(raw, file);
13270
13421
  }
13271
13422
  if (file === "-" || !process.stdin.isTTY) {
13272
- const raw = readFileSync12(0, "utf8");
13423
+ const raw = readFileSync13(0, "utf8");
13273
13424
  if (raw.trim() === "") {
13274
13425
  if (file === "-") die("no JSON on stdin");
13275
13426
  return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.265",
3
+ "version": "0.0.267",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",