tines 0.0.76 → 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.
- package/dist/index.js +70 -36
- 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
|
|
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.
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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>",
|
|
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
|
|
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>",
|
|
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 (
|
|
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(
|
|
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
|
|
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:
|
|
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 =
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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>",
|
|
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 (
|
|
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);
|