harnery 0.5.0 → 0.7.0
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/README.md +16 -6
- package/dist/commander.d.ts +29 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +94 -33
- package/dist/commands/browse-ai.js +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +41 -9
- package/dist/commands/cookies.js +1 -1
- package/dist/commands/decision.d.ts +4 -0
- package/dist/commands/decision.d.ts.map +1 -0
- package/dist/commands/decision.js +354 -0
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/devtools.d.ts +4 -0
- package/dist/commands/devtools.d.ts.map +1 -0
- package/dist/commands/devtools.js +239 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +74 -2
- package/dist/commands/doctor.js +12 -4
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +3 -63
- package/dist/commands/fetch.js +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +54 -14
- package/dist/commands/scratch.js +1 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +273 -62
- package/dist/commands/web-fetch.js +1 -1
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +32 -8
- package/dist/core/agents/events/consume.d.ts +25 -2
- package/dist/core/agents/events/consume.d.ts.map +1 -1
- package/dist/core/agents/events/consume.js +55 -7
- package/dist/core/agents/events/emit.d.ts +2 -1
- package/dist/core/agents/events/emit.d.ts.map +1 -1
- package/dist/core/agents/events/emit.js +6 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +16 -5
- package/dist/core/agents/state/scratch.d.ts +1 -1
- package/dist/core/agents/state/scratch.js +2 -2
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/hooks/cli.js +3 -3
- package/dist/core/hooks/effects/index.d.ts +11 -7
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +15 -17
- package/dist/core/hooks/events/emit.d.ts.map +1 -1
- package/dist/core/hooks/events/emit.js +4 -0
- package/dist/core/hooks/events/rotate.d.ts +43 -0
- package/dist/core/hooks/events/rotate.d.ts.map +1 -0
- package/dist/core/hooks/events/rotate.js +142 -0
- package/dist/core/hooks/harness/events.d.ts +11 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +22 -3
- package/dist/core/hooks/harness/wiring.d.ts +8 -0
- package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
- package/dist/core/hooks/harness/wiring.js +34 -5
- package/dist/core/scratch/index.d.ts.map +1 -0
- package/dist/{lib → core}/scratch/index.js +2 -2
- package/dist/lib/agent-browser/client.js +1 -1
- package/dist/lib/browser/client.d.ts +14 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +20 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/runts.d.ts +44 -0
- package/dist/lib/browser/runts.d.ts.map +1 -0
- package/dist/lib/browser/runts.js +193 -0
- package/dist/lib/completion/walk.js +1 -1
- package/dist/lib/cookies/client.d.ts +1 -1
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +1 -1
- package/dist/lib/decision/index.d.ts +212 -0
- package/dist/lib/decision/index.d.ts.map +1 -0
- package/dist/lib/decision/index.js +523 -0
- package/dist/lib/devtools.d.ts +178 -0
- package/dist/lib/devtools.d.ts.map +1 -0
- package/dist/lib/devtools.js +1328 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
- package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter-migrate.js +364 -0
- package/dist/lib/docs-frontmatter.d.ts +33 -0
- package/dist/lib/docs-frontmatter.d.ts.map +1 -0
- package/dist/lib/docs-frontmatter.js +130 -0
- package/dist/lib/docs-index.d.ts +1 -0
- package/dist/lib/docs-index.d.ts.map +1 -1
- package/dist/lib/docs-index.js +4 -5
- package/dist/lib/docs-lint.d.ts +3 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +67 -12
- package/dist/lib/docs-meta.d.ts +14 -0
- package/dist/lib/docs-meta.d.ts.map +1 -0
- package/dist/lib/docs-meta.js +34 -0
- package/dist/lib/docs-sweep.d.ts +12 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -1
- package/dist/lib/docs-sweep.js +98 -103
- package/dist/lib/format.js +2 -2
- package/dist/lib/http/index.d.ts +1 -0
- package/dist/lib/http/index.d.ts.map +1 -1
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/http/request.d.ts +77 -0
- package/dist/lib/http/request.d.ts.map +1 -0
- package/dist/lib/http/request.js +105 -0
- package/dist/lib/instructions/apply.d.ts +63 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -0
- package/dist/lib/instructions/apply.js +255 -0
- package/dist/lib/instructions/splice.d.ts +73 -0
- package/dist/lib/instructions/splice.d.ts.map +1 -0
- package/dist/lib/instructions/splice.js +118 -0
- package/dist/lib/instructions/templates.d.ts +45 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -0
- package/dist/lib/instructions/templates.js +258 -0
- package/dist/lib/tunnel/gate.d.ts +1 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +15 -10
- package/dist/lib/tunnel/state.d.ts +11 -1
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +8 -3
- package/package.json +9 -6
- package/src/commander.ts +35 -0
- package/src/commands/agents.ts +97 -29
- package/src/commands/browse-ai.ts +1 -1
- package/src/commands/browse.ts +63 -8
- package/src/commands/cookies.ts +1 -1
- package/src/commands/decision.ts +438 -0
- package/src/commands/deinit.ts +5 -0
- package/src/commands/devtools.ts +284 -0
- package/src/commands/docs.ts +86 -2
- package/src/commands/doctor.ts +13 -4
- package/src/commands/env.ts +11 -77
- package/src/commands/fetch.ts +1 -1
- package/src/commands/init.ts +66 -15
- package/src/commands/scratch.ts +1 -1
- package/src/commands/tunnel.ts +316 -65
- package/src/commands/web-fetch.ts +1 -1
- package/src/core/agents/coord-client.ts +34 -7
- package/src/core/agents/events/consume.ts +65 -7
- package/src/core/agents/events/emit.ts +7 -1
- package/src/core/agents/rules/claim-conflict.ts +17 -6
- package/src/core/agents/state/scratch.ts +2 -2
- package/src/core/config.ts +15 -1
- package/src/core/hooks/cli.ts +3 -3
- package/src/core/hooks/effects/index.ts +23 -16
- package/src/core/hooks/events/emit.ts +5 -0
- package/src/core/hooks/events/rotate.ts +151 -0
- package/src/core/hooks/harness/events.ts +30 -3
- package/src/core/hooks/harness/wiring.ts +46 -5
- package/src/{lib → core}/scratch/index.ts +2 -2
- package/src/lib/agent-browser/client.ts +1 -1
- package/src/lib/browser/client.ts +28 -0
- package/src/lib/browser/index.ts +4 -0
- package/src/lib/browser/runts.ts +218 -0
- package/src/lib/completion/walk.ts +1 -1
- package/src/lib/cookies/client.ts +2 -2
- package/src/lib/decision/index.ts +685 -0
- package/src/lib/devtools.ts +1653 -0
- package/src/lib/docs-frontmatter-migrate.ts +427 -0
- package/src/lib/docs-frontmatter.ts +151 -0
- package/src/lib/docs-index.ts +4 -5
- package/src/lib/docs-lint.ts +61 -11
- package/src/lib/docs-meta.ts +44 -0
- package/src/lib/docs-sweep.ts +104 -102
- package/src/lib/format.ts +2 -2
- package/src/lib/http/index.ts +1 -0
- package/src/lib/http/request.ts +154 -0
- package/src/lib/instructions/apply.ts +318 -0
- package/src/lib/instructions/splice.ts +148 -0
- package/src/lib/instructions/templates.ts +295 -0
- package/src/lib/tunnel/gate.ts +15 -10
- package/src/lib/tunnel/state.ts +19 -4
- package/dist/lib/scratch/index.d.ts.map +0 -1
- /package/dist/{lib → core}/scratch/index.d.ts +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, } from "../core/agents/index.js";
|
|
3
|
+
import { resolveBinName } from "../core/config.js";
|
|
4
|
+
import { archiveDecision, claimDecision, DECISION_STAKES, fileDecision, isStakes, isStatus, isTier, isVerdict, listDecisions, reopenDecision, resolveDecision, reviewDecision, searchDecisions, showDecision, supersedeDecision, triageDecision, wontfixDecision, } from "../lib/decision/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* `harn decision`: the decision docket — a persistent queue of decisions an
|
|
7
|
+
* agent would otherwise route to a human, carried through triage → deliberation
|
|
8
|
+
* → an evidence-cited resolution → async review.
|
|
9
|
+
*
|
|
10
|
+
* The engine is generic: it stores `tier` (0/1/2) + `stakes` but never decides
|
|
11
|
+
* what belongs in which tier — that's host policy, applied by the filing agent
|
|
12
|
+
* (for a host, via its own decision skill + rubric).
|
|
13
|
+
*/
|
|
14
|
+
let emit;
|
|
15
|
+
export function registerDecisionCommand(program, emitParam) {
|
|
16
|
+
emit = emitParam;
|
|
17
|
+
const bin = () => resolveBinName();
|
|
18
|
+
const root = program
|
|
19
|
+
.command("decision")
|
|
20
|
+
.alias("decisions")
|
|
21
|
+
.description("Decision docket: file a decision an agent would otherwise escalate, " +
|
|
22
|
+
"deliberate it, resolve with cited evidence, review async.");
|
|
23
|
+
// ── file ─────────────────────────────────────────────────────────────────
|
|
24
|
+
root
|
|
25
|
+
.command("file <question...>")
|
|
26
|
+
.description("File a decision into the docket. Tier/stakes are the filer's triage call.")
|
|
27
|
+
.option("--tier <0|1|2>", "Human-involvement tier (0 none, 1 review, 2 decide-with-brief)", "2")
|
|
28
|
+
.option("--stakes <small|medium|high>", "Reversal cost / blast radius", "medium")
|
|
29
|
+
.option("--context <text>", "Why it matters, what's blocked")
|
|
30
|
+
.option("--default-taken <text>", "What you proceeded with (always-proceed, tier 0/1)")
|
|
31
|
+
.option("--brief <path>", "Path to a markdown file with the long-form brief")
|
|
32
|
+
.option("--filed-by <name>", "Filer agent name (else resolved from heartbeat)")
|
|
33
|
+
.action((question, opts) => {
|
|
34
|
+
const coordRoot = coordRootOrExit();
|
|
35
|
+
const tier = parseTier(opts.tier);
|
|
36
|
+
const stakes = parseStakes(opts.stakes);
|
|
37
|
+
let brief;
|
|
38
|
+
if (opts.brief) {
|
|
39
|
+
if (!existsSync(opts.brief)) {
|
|
40
|
+
emit.error({ code: "no_brief_file", message: `brief file not found: ${opts.brief}` });
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
brief = readFileSync(opts.brief, "utf8");
|
|
44
|
+
}
|
|
45
|
+
const owner = resolveOwner();
|
|
46
|
+
const hb = owner ? readHeartbeat(owner) : null;
|
|
47
|
+
const r = fileDecision(coordRoot, {
|
|
48
|
+
question: question.join(" "),
|
|
49
|
+
tier,
|
|
50
|
+
stakes,
|
|
51
|
+
context: opts.context,
|
|
52
|
+
defaultTaken: opts.defaultTaken,
|
|
53
|
+
brief,
|
|
54
|
+
filedBy: opts.filedBy ?? hb?.name ?? undefined,
|
|
55
|
+
filedById: owner ?? undefined,
|
|
56
|
+
});
|
|
57
|
+
if (!r.ok)
|
|
58
|
+
return fail("file_failed", r.reason);
|
|
59
|
+
emitDecisionEvent("decision.filed", {
|
|
60
|
+
decision_id: r.manifest.decision_id,
|
|
61
|
+
tier,
|
|
62
|
+
stakes,
|
|
63
|
+
});
|
|
64
|
+
emit.data(r.manifest);
|
|
65
|
+
});
|
|
66
|
+
// ── list ─────────────────────────────────────────────────────────────────
|
|
67
|
+
root
|
|
68
|
+
.command("list")
|
|
69
|
+
.description("List docket decisions (default: active only, newest first).")
|
|
70
|
+
.option("--status <status>", "Filter by status")
|
|
71
|
+
.option("--tier <0|1|2>", "Filter by tier")
|
|
72
|
+
.option("--stakes <small|medium|high>", "Filter by stakes")
|
|
73
|
+
.option("--open", "Only non-terminal decisions (the live queue)")
|
|
74
|
+
.option("--archived", "Include archived (graduated/terminal) decisions")
|
|
75
|
+
.action((opts) => {
|
|
76
|
+
const coordRoot = coordRootOrExit();
|
|
77
|
+
const status = opts.status ? parseStatus(opts.status) : undefined;
|
|
78
|
+
const tier = opts.tier !== undefined ? parseTier(opts.tier) : undefined;
|
|
79
|
+
const stakes = opts.stakes ? parseStakes(opts.stakes) : undefined;
|
|
80
|
+
const rows = listDecisions(coordRoot, {
|
|
81
|
+
status,
|
|
82
|
+
tier,
|
|
83
|
+
stakes,
|
|
84
|
+
openOnly: opts.open,
|
|
85
|
+
includeArchived: opts.archived,
|
|
86
|
+
}).map(summarize);
|
|
87
|
+
emit.data({
|
|
88
|
+
rows,
|
|
89
|
+
meta: {
|
|
90
|
+
total: rows.length,
|
|
91
|
+
filter: { status, tier, stakes, open: !!opts.open, archived: !!opts.archived },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ── show ─────────────────────────────────────────────────────────────────
|
|
96
|
+
root
|
|
97
|
+
.command("show <id>")
|
|
98
|
+
.description("Show one decision: manifest + long-form bodies.")
|
|
99
|
+
.action((id) => {
|
|
100
|
+
const coordRoot = coordRootOrExit();
|
|
101
|
+
const detail = showDecision(coordRoot, id);
|
|
102
|
+
if (!detail)
|
|
103
|
+
return fail("not_found", `no decision "${id}"`);
|
|
104
|
+
emit.data(detail);
|
|
105
|
+
});
|
|
106
|
+
// ── search ───────────────────────────────────────────────────────────────
|
|
107
|
+
root
|
|
108
|
+
.command("search <query...>")
|
|
109
|
+
.description("Substring search over questions, context, resolutions, bodies (incl. archive).")
|
|
110
|
+
.action((query) => {
|
|
111
|
+
const coordRoot = coordRootOrExit();
|
|
112
|
+
const hits = searchDecisions(coordRoot, query.join(" ")).map((h) => ({
|
|
113
|
+
...summarize(h.manifest),
|
|
114
|
+
where: h.where,
|
|
115
|
+
snippet: h.snippet,
|
|
116
|
+
}));
|
|
117
|
+
emit.data({ rows: hits, meta: { total: hits.length, query: query.join(" ") } });
|
|
118
|
+
});
|
|
119
|
+
// ── claim ────────────────────────────────────────────────────────────────
|
|
120
|
+
root
|
|
121
|
+
.command("claim <id>")
|
|
122
|
+
.description("Claim a decision for deliberation (last-writer-wins).")
|
|
123
|
+
.option("--owner <id>", "Claim as this owner (else the current agent)")
|
|
124
|
+
.action((id, opts) => {
|
|
125
|
+
const coordRoot = coordRootOrExit();
|
|
126
|
+
const owner = opts.owner ?? resolveOwner();
|
|
127
|
+
if (!owner) {
|
|
128
|
+
return fail("no_owner", `not in an agent session; pass --owner or run \`${bin()} agents whoami\` to check`);
|
|
129
|
+
}
|
|
130
|
+
const r = claimDecision(coordRoot, id, owner);
|
|
131
|
+
if (!r.ok)
|
|
132
|
+
return fail("claim_failed", r.reason);
|
|
133
|
+
emit.data(r.manifest);
|
|
134
|
+
});
|
|
135
|
+
// ── resolve ──────────────────────────────────────────────────────────────
|
|
136
|
+
root
|
|
137
|
+
.command("resolve <id>")
|
|
138
|
+
.description("Resolve a decision. Evidence (≥1 citation) is required.")
|
|
139
|
+
.requiredOption("--recommendation <text>", "The recommendation")
|
|
140
|
+
.option("--evidence <text>", "A cited fact (query run, file read, cost computed). Repeatable; ≥1 required.", collect, [])
|
|
141
|
+
.option("--confidence <text>", "Confidence in the recommendation")
|
|
142
|
+
.option("--reversal-cost <text>", "Cost to reverse if wrong")
|
|
143
|
+
.option("--wrong-if <text>", "What would make this wrong (pre-mortem)")
|
|
144
|
+
.option("--revisit-when <text>", "Revisit trigger")
|
|
145
|
+
.option("--resolved-by <name>", "Resolver (else the current agent)")
|
|
146
|
+
.action((id, opts) => {
|
|
147
|
+
const coordRoot = coordRootOrExit();
|
|
148
|
+
const owner = resolveOwner();
|
|
149
|
+
const resolvedBy = opts.resolvedBy ?? (owner ? readHeartbeat(owner)?.name : undefined);
|
|
150
|
+
if (!resolvedBy) {
|
|
151
|
+
return fail("no_resolver", "pass --resolved-by (no agent session to infer it from)");
|
|
152
|
+
}
|
|
153
|
+
const r = resolveDecision(coordRoot, id, {
|
|
154
|
+
recommendation: opts.recommendation,
|
|
155
|
+
evidence: opts.evidence,
|
|
156
|
+
confidence: opts.confidence,
|
|
157
|
+
reversal_cost: opts.reversalCost,
|
|
158
|
+
wrong_if: opts.wrongIf,
|
|
159
|
+
revisit_when: opts.revisitWhen,
|
|
160
|
+
resolved_by: resolvedBy,
|
|
161
|
+
});
|
|
162
|
+
if (!r.ok)
|
|
163
|
+
return fail("resolve_failed", r.reason);
|
|
164
|
+
emitDecisionEvent("decision.resolved", {
|
|
165
|
+
decision_id: id,
|
|
166
|
+
tier: r.manifest.tier,
|
|
167
|
+
stakes: r.manifest.stakes,
|
|
168
|
+
confidence: opts.confidence ?? null,
|
|
169
|
+
});
|
|
170
|
+
emit.data(r.manifest);
|
|
171
|
+
});
|
|
172
|
+
// ── review ───────────────────────────────────────────────────────────────
|
|
173
|
+
root
|
|
174
|
+
.command("review <id>")
|
|
175
|
+
.description("Record a review verdict (calibration, not approval — work already proceeded).")
|
|
176
|
+
.requiredOption("--verdict <verdict>", "ratified | overridden | wrong-tier-high | wrong-tier-low")
|
|
177
|
+
.option("--note <text>", "Reviewer note")
|
|
178
|
+
.action((id, opts) => {
|
|
179
|
+
const coordRoot = coordRootOrExit();
|
|
180
|
+
if (!isVerdict(opts.verdict)) {
|
|
181
|
+
return fail("bad_verdict", "verdict must be: ratified | overridden | wrong-tier-high | wrong-tier-low");
|
|
182
|
+
}
|
|
183
|
+
const r = reviewDecision(coordRoot, id, {
|
|
184
|
+
verdict: opts.verdict,
|
|
185
|
+
note: opts.note,
|
|
186
|
+
});
|
|
187
|
+
if (!r.ok)
|
|
188
|
+
return fail("review_failed", r.reason);
|
|
189
|
+
emitDecisionEvent("decision.reviewed", {
|
|
190
|
+
decision_id: id,
|
|
191
|
+
verdict: opts.verdict,
|
|
192
|
+
tier: r.manifest.tier,
|
|
193
|
+
});
|
|
194
|
+
emit.data(r.manifest);
|
|
195
|
+
});
|
|
196
|
+
// ── triage ─────────────────────────────────────────────────────────────────
|
|
197
|
+
// Re-set tier/stakes on an already-filed decision (e.g. after a sweeper or
|
|
198
|
+
// reviewer flags a wrong-tier). Cheap enough to surface in phase 1.
|
|
199
|
+
root
|
|
200
|
+
.command("triage <id>")
|
|
201
|
+
.description("Adjust an already-filed decision's tier/stakes.")
|
|
202
|
+
.option("--tier <0|1|2>", "New tier")
|
|
203
|
+
.option("--stakes <small|medium|high>", "New stakes")
|
|
204
|
+
.action((id, opts) => {
|
|
205
|
+
const coordRoot = coordRootOrExit();
|
|
206
|
+
if (opts.tier === undefined && opts.stakes === undefined) {
|
|
207
|
+
return fail("nothing_to_do", "pass --tier and/or --stakes");
|
|
208
|
+
}
|
|
209
|
+
const r = triageDecision(coordRoot, id, {
|
|
210
|
+
tier: opts.tier !== undefined ? parseTier(opts.tier) : undefined,
|
|
211
|
+
stakes: opts.stakes !== undefined ? parseStakes(opts.stakes) : undefined,
|
|
212
|
+
});
|
|
213
|
+
if (!r.ok)
|
|
214
|
+
return fail("triage_failed", r.reason);
|
|
215
|
+
emit.data(r.manifest);
|
|
216
|
+
});
|
|
217
|
+
// ── archive ─────────────────────────────────────────────────────────────────
|
|
218
|
+
// The graduation exit: a reviewed decision's output has landed in a canonical
|
|
219
|
+
// home (an ADR, AGENTS.md, a code change), so the decision closes and moves to
|
|
220
|
+
// the archive, still searchable as precedent. `--graduated-to` records where.
|
|
221
|
+
root
|
|
222
|
+
.command("archive <id>")
|
|
223
|
+
.description("Archive a reviewed decision (terminal). Record where its output graduated.")
|
|
224
|
+
.option("--graduated-to <ref>", "Where the resolved output landed (e.g. docs/decisions.md#foo)")
|
|
225
|
+
.action((id, opts) => {
|
|
226
|
+
const coordRoot = coordRootOrExit();
|
|
227
|
+
const r = archiveDecision(coordRoot, id, opts.graduatedTo);
|
|
228
|
+
if (!r.ok)
|
|
229
|
+
return fail("archive_failed", r.reason);
|
|
230
|
+
emitDecisionEvent("decision.archived", {
|
|
231
|
+
decision_id: id,
|
|
232
|
+
tier: r.manifest.tier,
|
|
233
|
+
graduated_to: opts.graduatedTo ?? null,
|
|
234
|
+
});
|
|
235
|
+
emit.data(r.manifest);
|
|
236
|
+
});
|
|
237
|
+
// ── reopen ───────────────────────────────────────────────────────────────────
|
|
238
|
+
// The inverse of archive: pull an archived decision back to `reviewed`. The one
|
|
239
|
+
// sanctioned way out of the terminal archive (e.g. a fat-fingered graduated_to
|
|
240
|
+
// or the wrong decision archived). Clears graduated_to; a re-archive sets it fresh.
|
|
241
|
+
root
|
|
242
|
+
.command("reopen <id>")
|
|
243
|
+
.alias("unarchive")
|
|
244
|
+
.description("Reopen an archived decision back to reviewed (the inverse of archive).")
|
|
245
|
+
.action((id) => {
|
|
246
|
+
const coordRoot = coordRootOrExit();
|
|
247
|
+
const r = reopenDecision(coordRoot, id);
|
|
248
|
+
if (!r.ok)
|
|
249
|
+
return fail("reopen_failed", r.reason);
|
|
250
|
+
emitDecisionEvent("decision.reopened", { decision_id: id, tier: r.manifest.tier });
|
|
251
|
+
emit.data(r.manifest);
|
|
252
|
+
});
|
|
253
|
+
// ── supersede ────────────────────────────────────────────────────────────────
|
|
254
|
+
root
|
|
255
|
+
.command("supersede <id>")
|
|
256
|
+
.description("Mark a decision superseded by a newer one (terminal).")
|
|
257
|
+
.option("--by <id>", "The superseding decision's id")
|
|
258
|
+
.action((id, opts) => {
|
|
259
|
+
const coordRoot = coordRootOrExit();
|
|
260
|
+
const r = supersedeDecision(coordRoot, id, opts.by);
|
|
261
|
+
if (!r.ok)
|
|
262
|
+
return fail("supersede_failed", r.reason);
|
|
263
|
+
emitDecisionEvent("decision.superseded", {
|
|
264
|
+
decision_id: id,
|
|
265
|
+
superseded_by: opts.by ?? null,
|
|
266
|
+
});
|
|
267
|
+
emit.data(r.manifest);
|
|
268
|
+
});
|
|
269
|
+
// ── wontfix ──────────────────────────────────────────────────────────────────
|
|
270
|
+
root
|
|
271
|
+
.command("wontfix <id>")
|
|
272
|
+
.description("Close an un-deliberated decision without action (terminal).")
|
|
273
|
+
.option("--reason <text>", "Why it's being closed")
|
|
274
|
+
.action((id, opts) => {
|
|
275
|
+
const coordRoot = coordRootOrExit();
|
|
276
|
+
const r = wontfixDecision(coordRoot, id, opts.reason);
|
|
277
|
+
if (!r.ok)
|
|
278
|
+
return fail("wontfix_failed", r.reason);
|
|
279
|
+
emitDecisionEvent("decision.wontfix", {
|
|
280
|
+
decision_id: id,
|
|
281
|
+
reason: opts.reason ?? null,
|
|
282
|
+
});
|
|
283
|
+
emit.data(r.manifest);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// ─── helpers ───────────────────────────────────────────────────────────────────
|
|
287
|
+
function coordRootOrExit() {
|
|
288
|
+
const root = monorepoRoot();
|
|
289
|
+
if (!root) {
|
|
290
|
+
emit.error({
|
|
291
|
+
code: "no_coord_root",
|
|
292
|
+
message: "not in a coord-aware repo (no .harnery/ found)",
|
|
293
|
+
});
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
return root;
|
|
297
|
+
}
|
|
298
|
+
function fail(code, message) {
|
|
299
|
+
emit.error({ code, message: message ?? code });
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
function parseTier(raw) {
|
|
303
|
+
const n = Number.parseInt(raw, 10);
|
|
304
|
+
if (!isTier(n))
|
|
305
|
+
fail("bad_tier", `tier must be 0, 1, or 2 (got "${raw}")`);
|
|
306
|
+
return n;
|
|
307
|
+
}
|
|
308
|
+
function parseStakes(raw) {
|
|
309
|
+
if (!isStakes(raw))
|
|
310
|
+
fail("bad_stakes", `stakes must be ${DECISION_STAKES.join(" | ")} (got "${raw}")`);
|
|
311
|
+
return raw;
|
|
312
|
+
}
|
|
313
|
+
function parseStatus(raw) {
|
|
314
|
+
if (!isStatus(raw))
|
|
315
|
+
fail("bad_status", `unknown status "${raw}"`);
|
|
316
|
+
return raw;
|
|
317
|
+
}
|
|
318
|
+
function collect(value, previous) {
|
|
319
|
+
return previous.concat([value]);
|
|
320
|
+
}
|
|
321
|
+
/** Compact row for list/search output — the full manifest is available via `show`. */
|
|
322
|
+
function summarize(m) {
|
|
323
|
+
return {
|
|
324
|
+
decision_id: m.decision_id,
|
|
325
|
+
status: m.status,
|
|
326
|
+
tier: m.tier,
|
|
327
|
+
stakes: m.stakes,
|
|
328
|
+
question: m.question,
|
|
329
|
+
filed_by: m.filed_by ?? null,
|
|
330
|
+
filed_at: m.filed_at,
|
|
331
|
+
claimed_by: m.claimed_by ?? null,
|
|
332
|
+
resolved: !!m.resolution,
|
|
333
|
+
reviewed: !!m.review,
|
|
334
|
+
graduated_to: m.graduated_to ?? null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Emit a canonical `decision.*` event. Soft: no-ops when there's no agent
|
|
339
|
+
* session to attribute it to (operator-side filing). Powers the docket metrics
|
|
340
|
+
* without new telemetry plumbing.
|
|
341
|
+
*/
|
|
342
|
+
function emitDecisionEvent(type, data) {
|
|
343
|
+
const owner = resolveOwner();
|
|
344
|
+
if (!owner)
|
|
345
|
+
return;
|
|
346
|
+
const hb = readHeartbeat(owner);
|
|
347
|
+
emitCanonical({
|
|
348
|
+
type,
|
|
349
|
+
owner,
|
|
350
|
+
session: hb?.session_id ?? owner,
|
|
351
|
+
harness: normalizeHarness(hb?.platform),
|
|
352
|
+
data,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAanD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CA+GjG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
|
package/dist/commands/deinit.js
CHANGED
|
@@ -31,6 +31,7 @@ import { relative, resolve } from "node:path";
|
|
|
31
31
|
import { createInterface } from "node:readline";
|
|
32
32
|
import { DEFAULT_BIN_NAME } from "../core/config.js";
|
|
33
33
|
import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
|
|
34
|
+
import { removeInstructions } from "../lib/instructions/apply.js";
|
|
34
35
|
import { unwireHooks } from "./init.js";
|
|
35
36
|
export function registerDeinitCommand(program, emit, binName) {
|
|
36
37
|
// The interactive prompt + engine-removal hint are standalone-harn niceties.
|
|
@@ -113,6 +114,9 @@ export function registerDeinitCommand(program, emit, binName) {
|
|
|
113
114
|
`(${remaining} other hook(s) kept)`);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
// ── 1b. agent-facing instructions block + skills ───────────────────────
|
|
118
|
+
const removed = removeInstructions(projectRoot, { harness, dryRun });
|
|
119
|
+
actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
|
|
116
120
|
// ── 2. coord root (opt-in; destructive) ────────────────────────────────
|
|
117
121
|
if (purgeState) {
|
|
118
122
|
if (!coordExists) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/commands/devtools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAkCnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsDjF"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { cursorApiKeyPath, enrichFromApi, probeEndpoints, readDevtools, resolveCursorApiKey, } from "../lib/devtools.js";
|
|
4
|
+
/**
|
|
5
|
+
* `devtools`: report the local state of the AI coding agents harnery supports
|
|
6
|
+
* — Claude Code, Codex, and Cursor — in one place: logged-in status, plan/seat
|
|
7
|
+
* tier, auth expiry, session counts, and (where the tool keeps them locally)
|
|
8
|
+
* rate-limit / quota windows.
|
|
9
|
+
*
|
|
10
|
+
* Reads files on disk by default (no network). When a Cursor API key is
|
|
11
|
+
* configured (`devtools cursor-key set`), it additionally verifies the key and
|
|
12
|
+
* pulls Cloud Agent activity. `--usage` adds an opt-in windowed scan of local
|
|
13
|
+
* transcripts for approximate token totals.
|
|
14
|
+
*/
|
|
15
|
+
const VALID = ["claude-code", "codex", "cursor"];
|
|
16
|
+
export function registerDevtoolsCommand(program, emit) {
|
|
17
|
+
const cmd = program
|
|
18
|
+
.command("devtools")
|
|
19
|
+
.description("Local status of the AI coding agents (Claude Code, Codex, Cursor)")
|
|
20
|
+
.option("--tool <name>", "Restrict to a tool (repeatable): claude-code | codex | cursor", collect, [])
|
|
21
|
+
.option("--usage", "Also scan local transcripts for approximate token totals (slower)")
|
|
22
|
+
.option("--window-days <n>", "With --usage: only count transcripts modified within N days", (v) => Number.parseInt(v, 10), 7)
|
|
23
|
+
.option("--no-api", "Skip the Cursor API enrichment even when a key is configured")
|
|
24
|
+
.option("--format <type>", "Output format: table, json", "table")
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
const only = (opts.tool ?? []).filter((t) => VALID.includes(t));
|
|
27
|
+
const bad = (opts.tool ?? []).filter((t) => !VALID.includes(t));
|
|
28
|
+
if (bad.length) {
|
|
29
|
+
emit.error({
|
|
30
|
+
code: "bad_tool",
|
|
31
|
+
message: `unknown --tool: ${bad.join(", ")}`,
|
|
32
|
+
hint: `valid: ${VALID.join(", ")}`,
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const report = readDevtools({
|
|
37
|
+
usage: opts.usage,
|
|
38
|
+
windowDays: opts.windowDays,
|
|
39
|
+
only: only.length ? only : undefined,
|
|
40
|
+
});
|
|
41
|
+
// Auto-enrich when a Cursor key is configured (the user opted in by
|
|
42
|
+
// storing one); --no-api forces pure-local.
|
|
43
|
+
if (opts.noApi !== true)
|
|
44
|
+
await enrichFromApi(report);
|
|
45
|
+
if (opts.format === "json") {
|
|
46
|
+
emit.config({ format: "json" });
|
|
47
|
+
emit.data({ ok: true, ...report });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
emit.text(renderTable(report.tools));
|
|
51
|
+
});
|
|
52
|
+
registerCursorKeyCommand(cmd, emit);
|
|
53
|
+
registerDoctorCommand(cmd, emit);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `devtools doctor` — one live call per usage endpoint to check the integrations
|
|
57
|
+
* still work. Surfaces `auth_rejected` (our headers/token stopped being
|
|
58
|
+
* accepted) and `shape_changed` (response schema drifted) distinctly from a
|
|
59
|
+
* mere rate limit. Bypasses the cache; run occasionally, by hand.
|
|
60
|
+
*/
|
|
61
|
+
function registerDoctorCommand(parent, emit) {
|
|
62
|
+
parent
|
|
63
|
+
.command("doctor")
|
|
64
|
+
.description("Probe the usage endpoints once each to detect header/schema drift")
|
|
65
|
+
.option("--tool <name>", "Restrict to a tool (repeatable): claude-code | cursor", collect, [])
|
|
66
|
+
.option("--format <type>", "Output format: table, json", "table")
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
const only = (opts.tool ?? []).filter((t) => VALID.includes(t));
|
|
69
|
+
const results = await probeEndpoints({ only: only.length ? only : undefined });
|
|
70
|
+
if (opts.format === "json") {
|
|
71
|
+
emit.config({ format: "json" });
|
|
72
|
+
emit.data({
|
|
73
|
+
ok: results.every((r) => r.outcome === "ok" || r.outcome === "rate_limited"),
|
|
74
|
+
results,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
emit.text(renderDoctor(results));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const DOCTOR_MARK = {
|
|
82
|
+
ok: "✓",
|
|
83
|
+
rate_limited: "~",
|
|
84
|
+
no_credential: "-",
|
|
85
|
+
auth_rejected: "✗",
|
|
86
|
+
shape_changed: "✗",
|
|
87
|
+
unreachable: "✗",
|
|
88
|
+
};
|
|
89
|
+
function renderDoctor(results) {
|
|
90
|
+
if (!results.length)
|
|
91
|
+
return "no probeable endpoints (Codex is local-only)";
|
|
92
|
+
const lines = [];
|
|
93
|
+
for (const r of results) {
|
|
94
|
+
lines.push(`${DOCTOR_MARK[r.outcome]} ${r.tool.padEnd(12)} ${r.outcome.padEnd(14)} ${r.detail}`);
|
|
95
|
+
lines.push(` ${r.endpoint}${r.clientVersion ? ` (client ${r.clientVersion})` : ""}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("✗ auth_rejected / shape_changed = the client's request contract likely changed — review the headers/parser.");
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
/** `devtools cursor-key set|clear|status` — store the machine-local Cursor API key. */
|
|
102
|
+
function registerCursorKeyCommand(parent, emit) {
|
|
103
|
+
const key = parent
|
|
104
|
+
.command("cursor-key")
|
|
105
|
+
.description("Manage the Cursor API key used for the Cloud Agent enrichment");
|
|
106
|
+
key
|
|
107
|
+
.command("set [value]")
|
|
108
|
+
.description("Store a Cursor API key (reads stdin when [value] is omitted)")
|
|
109
|
+
.action(async (value) => {
|
|
110
|
+
const raw = value ?? (await readStdin());
|
|
111
|
+
const trimmed = raw.trim();
|
|
112
|
+
if (!trimmed) {
|
|
113
|
+
emit.error({ code: "empty_key", message: "no key provided (arg or stdin)" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const path = cursorApiKeyPath();
|
|
117
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
118
|
+
writeFileSync(path, trimmed, { mode: 0o600 });
|
|
119
|
+
chmodSync(path, 0o600);
|
|
120
|
+
emit.file(path, { stored: true, length: trimmed.length });
|
|
121
|
+
});
|
|
122
|
+
key
|
|
123
|
+
.command("clear")
|
|
124
|
+
.description("Remove the stored Cursor API key")
|
|
125
|
+
.action(() => {
|
|
126
|
+
try {
|
|
127
|
+
rmSync(cursorApiKeyPath());
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// already absent
|
|
131
|
+
}
|
|
132
|
+
emit.data({ ok: true, cleared: true });
|
|
133
|
+
});
|
|
134
|
+
key
|
|
135
|
+
.command("status")
|
|
136
|
+
.description("Report whether a Cursor key is configured (env or file)")
|
|
137
|
+
.action(() => {
|
|
138
|
+
const fromEnv = Boolean(process.env.CURSOR_API_KEY?.trim());
|
|
139
|
+
const resolved = resolveCursorApiKey();
|
|
140
|
+
emit.data({
|
|
141
|
+
ok: true,
|
|
142
|
+
configured: Boolean(resolved),
|
|
143
|
+
source: fromEnv ? "env" : resolved ? "file" : null,
|
|
144
|
+
path: cursorApiKeyPath(),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function readStdin() {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
let data = "";
|
|
151
|
+
if (process.stdin.isTTY) {
|
|
152
|
+
resolve("");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
process.stdin.setEncoding("utf8");
|
|
156
|
+
process.stdin.on("data", (c) => {
|
|
157
|
+
data += c;
|
|
158
|
+
});
|
|
159
|
+
process.stdin.on("end", () => resolve(data));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function collect(value, prev) {
|
|
163
|
+
return [...prev, value];
|
|
164
|
+
}
|
|
165
|
+
function renderTable(tools) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
for (const t of tools) {
|
|
168
|
+
lines.push(`── ${t.tool} ${"─".repeat(Math.max(0, 40 - t.tool.length))}`);
|
|
169
|
+
if (!t.installed) {
|
|
170
|
+
lines.push(" not installed");
|
|
171
|
+
lines.push("");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
lines.push(` logged in ${fmtBool(t.loggedIn)}`);
|
|
175
|
+
if (t.account)
|
|
176
|
+
lines.push(` account ${t.account}`);
|
|
177
|
+
if (t.plan)
|
|
178
|
+
lines.push(` plan ${t.plan}`);
|
|
179
|
+
if (t.rateLimitTier)
|
|
180
|
+
lines.push(` rate tier ${t.rateLimitTier}`);
|
|
181
|
+
if (t.authExpiresAt)
|
|
182
|
+
lines.push(` auth expires ${fmtDate(t.authExpiresAt)}`);
|
|
183
|
+
if (t.sessions != null)
|
|
184
|
+
lines.push(` sessions ${t.sessions}`);
|
|
185
|
+
if (t.lastActivity)
|
|
186
|
+
lines.push(` last active ${fmtDate(t.lastActivity)}`);
|
|
187
|
+
if (t.quota?.length) {
|
|
188
|
+
for (const q of t.quota) {
|
|
189
|
+
const pct = q.usedPercent != null ? `${q.usedPercent}% used` : "usage unknown";
|
|
190
|
+
const reset = q.resetsAt ? `resets ${fmtDate(q.resetsAt)}` : "";
|
|
191
|
+
lines.push(` quota (${q.window}) ${pct}${reset ? ` · ${reset}` : ""}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (t.tokensUsed != null) {
|
|
195
|
+
lines.push(` tokens ${t.tokensUsed.toLocaleString()}`);
|
|
196
|
+
}
|
|
197
|
+
if (t.usage) {
|
|
198
|
+
const u = t.usage;
|
|
199
|
+
if (u.cycleEnd) {
|
|
200
|
+
const days = Math.round((Date.parse(u.cycleEnd) - Date.now()) / 86_400_000);
|
|
201
|
+
lines.push(` plan resets ${fmtDate(u.cycleEnd)} (${days}d)`);
|
|
202
|
+
}
|
|
203
|
+
const pct = (label, v) => (v != null ? `${label} ${v}%` : null);
|
|
204
|
+
const bars = [
|
|
205
|
+
pct("total", u.totalPercentUsed),
|
|
206
|
+
pct("api", u.apiPercentUsed),
|
|
207
|
+
pct("first-party", u.firstPartyPercentUsed),
|
|
208
|
+
].filter((s) => s != null);
|
|
209
|
+
if (bars.length)
|
|
210
|
+
lines.push(` usage ${bars.join(" · ")}`);
|
|
211
|
+
}
|
|
212
|
+
if (t.spend?.limitCents != null) {
|
|
213
|
+
const label = t.spend.label.toLowerCase().padEnd(12).slice(0, 12);
|
|
214
|
+
lines.push(` ${label} ${fmtUsd(t.spend.usedCents ?? 0)} / ${fmtUsd(t.spend.limitCents)}`);
|
|
215
|
+
}
|
|
216
|
+
if (t.api?.ok && t.api.cloudAgents) {
|
|
217
|
+
lines.push(` cloud agents ${t.api.cloudAgents.total} (${t.api.cloudAgents.active} active)`);
|
|
218
|
+
}
|
|
219
|
+
for (const n of t.notes)
|
|
220
|
+
lines.push(` · ${n}`);
|
|
221
|
+
lines.push("");
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n").trimEnd();
|
|
224
|
+
}
|
|
225
|
+
function fmtBool(v) {
|
|
226
|
+
if (v === true)
|
|
227
|
+
return "yes";
|
|
228
|
+
if (v === false)
|
|
229
|
+
return "no";
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
function fmtDate(iso) {
|
|
233
|
+
// Keep the machine-readable ISO but drop milliseconds for readability.
|
|
234
|
+
return iso.replace(/\.\d{3}Z$/, "Z");
|
|
235
|
+
}
|
|
236
|
+
/** Cents → "$X.XX". */
|
|
237
|
+
function fmtUsd(cents) {
|
|
238
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
239
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiC1E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA8GN"}
|