tines 0.0.77 → 0.0.78

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 +70 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,11 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "node:fs";
5
5
  import { hostname as hostname2 } from "node:os";
6
6
  import { basename, dirname as dirname3, join as join3 } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
 
9
+ // src/body-value.ts
10
+ import { readFileSync } from "node:fs";
11
+ var processStdin = {
12
+ get isTTY() {
13
+ return Boolean(process.stdin.isTTY);
14
+ },
15
+ read: () => readFileSync(0, "utf8")
16
+ };
17
+ var BODY_VALUE_HELP = 'inline text, @file, or "-" to read stdin; escape a literal leading @ as @@';
18
+ function readBodyValue(value, stdin = processStdin) {
19
+ if (value.startsWith("@@")) return value.slice(1);
20
+ if (value === "-" || value === "@-") {
21
+ if (stdin.isTTY) {
22
+ throw new Error(
23
+ '"-" reads the body from stdin, but stdin is a terminal \u2014 pipe or redirect the Markdown (e.g. a quoted heredoc)'
24
+ );
25
+ }
26
+ const raw = stdin.read();
27
+ if (raw.trim() === "") throw new Error("no Markdown on stdin");
28
+ return raw;
29
+ }
30
+ if (value.startsWith("@")) {
31
+ const file = value.slice(1);
32
+ try {
33
+ return readFileSync(file, "utf8");
34
+ } catch (err) {
35
+ throw new Error(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
36
+ }
37
+ }
38
+ return value;
39
+ }
40
+
9
41
  // src/daemon/daemon.ts
10
42
  import { spawn } from "node:child_process";
11
43
  import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
@@ -26,7 +58,16 @@ var AGENT_GUIDELINES_NAME = "agent-guidelines";
26
58
  var JOURNAL_NAME = "journal";
27
59
  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:
28
60
 
29
- - **Issue comments** \u2014 all prose about this issue: progress, findings, dead ends, questions, and instructions for whoever picks it up next. \`tines issues comment <project>/<number> "<markdown>"\`
61
+ - **Issue comments** \u2014 all prose about this issue: progress, findings, dead ends, questions, and instructions for whoever picks it up next. Pass the body on stdin with a quoted heredoc, so backticks, $VARS, quotes and apostrophes reach the thread untouched by the shell (a mangled comment cannot be deleted):
62
+
63
+ \`\`\`
64
+ tines issues comment <project>/<number> - <<'EOF'
65
+ <markdown>
66
+ EOF
67
+ \`\`\`
68
+
69
+ A \`tines\` too old for that form posts a literal \`-\` instead of your body, without failing. If \`tines issues comment --help\` does not mention \`@file\`, use \`tines issues comment <project>/<number> "<markdown>"\` and mind the shell quoting.
70
+
30
71
  - **Issue context (artifacts)** \u2014 things this issue needs *attached*, not said: a skill, a repo/branch pin, or an override of a broader item (reuse its name): \`tines context create --kind <k> --name <n> --issue <project>/<number> \u2026\`. Never notes \u2014 notes are comments.
31
72
  - **Your journal** \u2014 shared notes for anyone doing this stage of work in this project. Append a dated bullet whenever you learn something they would want: commands that actually work, gotchas, where things live (see "Journal" at the end of this prompt for the exact commands). If an entry is wrong or stale, rewrite the journal to fix it \u2014 do not append a correction on top. Keep it short; prune when you touch it.
32
73
  - **Context change requests** \u2014 never edit shared context (project-, state-, or global-scoped items) directly. Propose instead: file an issue in the project you are working in, titled \`Context change: <scope label>\`, naming the item (kind, name, scope) with the full proposed text in the description. A human reviews and applies it.
@@ -345,7 +386,7 @@ var PLACEHOLDER_DESCRIPTIONS = {
345
386
  var TEMPLATE_PLACEHOLDERS = Object.keys(PLACEHOLDER_DESCRIPTIONS).map((key) => ({ key, description: PLACEHOLDER_DESCRIPTIONS[key] }));
346
387
 
347
388
  // src/daemon/store.ts
348
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
389
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
349
390
  import { homedir } from "node:os";
350
391
  import { dirname, join } from "node:path";
351
392
  function defaultConfigDir() {
@@ -354,7 +395,7 @@ function defaultConfigDir() {
354
395
  function readJsonFile(path) {
355
396
  if (!existsSync(path)) return null;
356
397
  try {
357
- return JSON.parse(readFileSync(path, "utf8"));
398
+ return JSON.parse(readFileSync2(path, "utf8"));
358
399
  } catch {
359
400
  return null;
360
401
  }
@@ -405,10 +446,10 @@ function saveDaemonState(path, runs) {
405
446
  }
406
447
  function processStartTimeMs(pid) {
407
448
  try {
408
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
449
+ const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
409
450
  const afterComm = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
410
451
  const startTicks = Number(afterComm[19]);
411
- const btimeLine = readFileSync("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
452
+ const btimeLine = readFileSync2("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
412
453
  const btime = Number(btimeLine?.slice("btime ".length));
413
454
  if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
414
455
  return btime * 1e3 + startTicks / 100 * 1e3;
@@ -922,14 +963,14 @@ function readJsonBody(inline, file) {
922
963
  if (file !== void 0 && file !== "-") {
923
964
  let raw;
924
965
  try {
925
- raw = readFileSync2(file, "utf8");
966
+ raw = readFileSync3(file, "utf8");
926
967
  } catch (err) {
927
968
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
928
969
  }
929
970
  return parseJsonObject(raw, file);
930
971
  }
931
972
  if (file === "-" || !process.stdin.isTTY) {
932
- const raw = readFileSync2(0, "utf8");
973
+ const raw = readFileSync3(0, "utf8");
933
974
  if (raw.trim() === "") {
934
975
  if (file === "-") die("no JSON on stdin");
935
976
  return void 0;
@@ -1059,18 +1100,6 @@ async function resolveScopeFlags(api, opts) {
1059
1100
  if (opts.issue !== void 0) scope.issue_id = (await resolveIssue(api, opts.issue)).id;
1060
1101
  return scope;
1061
1102
  }
1062
- function readBodyValue(value) {
1063
- if (value.startsWith("@@")) return value.slice(1);
1064
- if (value.startsWith("@")) {
1065
- const file = value.slice(1);
1066
- try {
1067
- return readFileSync2(file, "utf8");
1068
- } catch (err) {
1069
- die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
1070
- }
1071
- }
1072
- return value;
1073
- }
1074
1103
  function parseFileSpec(spec) {
1075
1104
  const sep = spec.indexOf("=");
1076
1105
  if (sep < 1 || sep === spec.length - 1) {
@@ -1083,7 +1112,7 @@ function parseFileSpec(spec) {
1083
1112
  }
1084
1113
  const file = source.slice(1);
1085
1114
  try {
1086
- return { path, content: readFileSync2(file, "utf8") };
1115
+ return { path, content: readFileSync3(file, "utf8") };
1087
1116
  } catch (err) {
1088
1117
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
1089
1118
  }
@@ -1344,7 +1373,7 @@ function eventSummary(ev) {
1344
1373
  function cliVersion() {
1345
1374
  try {
1346
1375
  const manifest = new URL("../package.json", import.meta.url);
1347
- return JSON.parse(readFileSync2(manifest, "utf8")).version ?? "0.0.0-unknown";
1376
+ return JSON.parse(readFileSync3(manifest, "utf8")).version ?? "0.0.0-unknown";
1348
1377
  } catch {
1349
1378
  return "0.0.0-unknown";
1350
1379
  }
@@ -1544,9 +1573,10 @@ withList(
1544
1573
  }
1545
1574
  );
1546
1575
  withCommon(
1547
- issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption("-t, --title <title>", "issue title (doubles as the title template with a recurrence)").option("-d, --description <markdown>", "issue description (Markdown)").option("-w, --workflow <id-or-name>", "workflow (defaults to project default, else standard)").option("-s, --state <name>", "starting state (defaults to the workflow's initial state)").option("--every <preset>", 'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly').option("--at <when>", "preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)").option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "schedule timezone (defaults to the system timezone)").option("--if-closed", "only create a new instance when all previous instances are closed").option("--schedule-name <name>", "schedule name, unique per project (defaults to the title)")
1576
+ issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption("-t, --title <title>", "issue title (doubles as the title template with a recurrence)").option("-d, --description <markdown>", `issue description (Markdown) \u2014 ${BODY_VALUE_HELP}`).option("-w, --workflow <id-or-name>", "workflow (defaults to project default, else standard)").option("-s, --state <name>", "starting state (defaults to the workflow's initial state)").option("--every <preset>", 'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly').option("--at <when>", "preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)").option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "schedule timezone (defaults to the system timezone)").option("--if-closed", "only create a new instance when all previous instances are closed").option("--schedule-name <name>", "schedule name, unique per project (defaults to the title)")
1548
1577
  ).action(
1549
1578
  async (projectRef, opts) => {
1579
+ const description = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
1550
1580
  const api = client(opts);
1551
1581
  const project = await resolveProject(api, projectRef);
1552
1582
  const workflowId = opts.workflow ? (await resolveWorkflow(api, opts.workflow)).id : void 0;
@@ -1562,7 +1592,7 @@ withCommon(
1562
1592
  } : void 0;
1563
1593
  const issue = await api.createIssue(project.id, {
1564
1594
  title: opts.title,
1565
- description: opts.description,
1595
+ description,
1566
1596
  workflow_id: workflowId,
1567
1597
  state: opts.state,
1568
1598
  schedule
@@ -1586,17 +1616,18 @@ withCommon(
1586
1616
  printIssueDetail(issue);
1587
1617
  });
1588
1618
  withCommon(
1589
- issues.command("edit <ref>").description("Edit an issue: title, description, workflow, or force-set state").option("-t, --title <title>", "set the title").option("-d, --description <markdown>", "set the description (Markdown)").option(
1619
+ issues.command("edit <ref>").description("Edit an issue: title, description, workflow, or force-set state").option("-t, --title <title>", "set the title").option("-d, --description <markdown>", `set the description (Markdown) \u2014 ${BODY_VALUE_HELP}`).option(
1590
1620
  "-s, --state <name>",
1591
1621
  "force-set the state, bypassing the workflow's transitions (records a forced move)"
1592
1622
  ).option("-w, --workflow <id-or-name>", "move the issue onto another workflow")
1593
1623
  ).action(
1594
1624
  async (ref, opts) => {
1625
+ const description = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
1595
1626
  const api = client(opts);
1596
1627
  const issue = await resolveIssue(api, ref);
1597
1628
  const body = {};
1598
1629
  if (opts.title !== void 0) body.title = opts.title;
1599
- if (opts.description !== void 0) body.description = opts.description;
1630
+ if (description !== void 0) body.description = description;
1600
1631
  if (opts.state !== void 0) body.state = opts.state;
1601
1632
  if (opts.workflow !== void 0) body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
1602
1633
  if (Object.keys(body).length === 0) {
@@ -1626,12 +1657,13 @@ withCommon(
1626
1657
  );
1627
1658
  });
1628
1659
  withCommon(
1629
- issues.command("comment <ref> <markdown>").description("Comment on an issue (Markdown body)").passThroughOptions()
1660
+ issues.command("comment <ref> <markdown>").description(`Comment on an issue \u2014 Markdown body: ${BODY_VALUE_HELP}`).passThroughOptions()
1630
1661
  ).action(async (ref, markdown, opts, command) => {
1631
1662
  if (helpGuard(command, markdown)) return;
1663
+ const body = readBodyValue(markdown);
1632
1664
  const api = client(opts);
1633
1665
  const issue = await resolveIssue(api, ref);
1634
- const comment = await api.createComment(issue.id, { body: markdown });
1666
+ const comment = await api.createComment(issue.id, { body });
1635
1667
  if (opts.json) return printJson(comment);
1636
1668
  console.log(`commented on ${issue.project_name}/#${issue.number} as ${actorLabel(comment.actor)}`);
1637
1669
  });
@@ -1802,7 +1834,7 @@ function walkFolder(dir) {
1802
1834
  const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
1803
1835
  if (entry.isDirectory()) walk(nextAbs, nextRel);
1804
1836
  else if (entry.isFile()) {
1805
- files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync2(nextAbs) });
1837
+ files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync3(nextAbs) });
1806
1838
  }
1807
1839
  }
1808
1840
  };
@@ -1889,7 +1921,7 @@ withCommon(
1889
1921
  } else if (opts.file !== void 0) {
1890
1922
  let bytes;
1891
1923
  try {
1892
- bytes = readFileSync2(opts.file);
1924
+ bytes = readFileSync3(opts.file);
1893
1925
  } catch (err) {
1894
1926
  die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
1895
1927
  }
@@ -2321,15 +2353,16 @@ start one: tines journal append ${issue.project_name}/${issue.number} "- <date>:
2321
2353
  console.log(full.body ?? "");
2322
2354
  });
2323
2355
  withCommon(
2324
- journal.command("append <ref> <markdown>").description("Append a lesson (creates the journal on first use)").option("--state <workflow>/<state>", STATE_FLAG_HELP).passThroughOptions()
2356
+ journal.command("append <ref> <markdown>").description(`Append a lesson, creating the journal on first use \u2014 ${BODY_VALUE_HELP}`).option("--state <workflow>/<state>", STATE_FLAG_HELP).passThroughOptions()
2325
2357
  ).action(
2326
2358
  async (ref, markdown, opts, command) => {
2327
2359
  if (helpGuard(command, markdown)) return;
2360
+ const text = readBodyValue(markdown);
2328
2361
  const api = client(opts);
2329
2362
  const { scope, note, item } = await resolveJournal(api, ref, opts.state);
2330
2363
  printNote(note);
2331
2364
  if (item) {
2332
- const updated = await api.appendContextItem(item.id, { text: markdown });
2365
+ const updated = await api.appendContextItem(item.id, { text });
2333
2366
  if (opts.json) return printJson(updated);
2334
2367
  return console.log(`appended to the ${scope.label} journal (now v${updated.version})`);
2335
2368
  }
@@ -2339,7 +2372,7 @@ withCommon(
2339
2372
  name: JOURNAL_NAME,
2340
2373
  project_id: scope.project_id ?? void 0,
2341
2374
  workflow_state_id: scope.workflow_state_id ?? void 0,
2342
- body: markdown.trim()
2375
+ body: text.trim()
2343
2376
  });
2344
2377
  if (opts.json) return printJson(created);
2345
2378
  console.log(`started the ${scope.label} journal (${created.id})`);
@@ -2347,7 +2380,7 @@ withCommon(
2347
2380
  if (!(err instanceof ApiError) || err.code !== "duplicate_context_name") throw err;
2348
2381
  const { item: fresh } = await resolveJournal(api, ref, opts.state);
2349
2382
  if (!fresh) throw err;
2350
- const updated = await api.appendContextItem(fresh.id, { text: markdown });
2383
+ const updated = await api.appendContextItem(fresh.id, { text });
2351
2384
  if (opts.json) return printJson(updated);
2352
2385
  console.log(`appended to the ${scope.label} journal (now v${updated.version})`);
2353
2386
  }
@@ -2437,7 +2470,7 @@ recent instances:`);
2437
2470
  }
2438
2471
  });
2439
2472
  withCommon(
2440
- schedules.command("edit <ref>").description("Edit a schedule: templates, workflow, start state, recurrence, timezone, gate, or name").option("-t, --title <template>", "set the title template").option("-d, --description <markdown>", "set the description template (Markdown)").option(
2473
+ schedules.command("edit <ref>").description("Edit a schedule: templates, workflow, start state, recurrence, timezone, gate, or name").option("-t, --title <template>", "set the title template").option("-d, --description <markdown>", `set the description template (Markdown) \u2014 ${BODY_VALUE_HELP}`).option(
2441
2474
  "-w, --workflow <id-or-name>",
2442
2475
  "move future instances onto another workflow (resets the start state to its initial state unless --state is also given)"
2443
2476
  ).option(
@@ -2446,11 +2479,12 @@ withCommon(
2446
2479
  ).option("--every <preset>", 'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly').option("--at <when>", "preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)").option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "set the schedule timezone").option("--if-closed", "only create a new instance when all previous instances are closed").option("--no-if-closed", "clear the only-when-closed gate").option("--name <new-name>", "rename the schedule")
2447
2480
  ).action(
2448
2481
  async (ref, opts) => {
2482
+ const descriptionTemplate = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
2449
2483
  const api = client(opts);
2450
2484
  const schedule = await resolveSchedule(api, ref);
2451
2485
  const body = {};
2452
2486
  if (opts.title !== void 0) body.title_template = opts.title;
2453
- if (opts.description !== void 0) body.description_template = opts.description;
2487
+ if (descriptionTemplate !== void 0) body.description_template = descriptionTemplate;
2454
2488
  if (opts.workflow !== void 0) body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
2455
2489
  if (opts.state !== void 0) body.state = opts.state;
2456
2490
  const recurrence = buildRecurrence(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.77",
3
+ "version": "0.0.78",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",