portable-agent-layer 0.68.0 → 0.70.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 +2 -0
- package/assets/templates/ledger-page.html +213 -0
- package/package.json +1 -1
- package/src/cli/index.ts +17 -0
- package/src/cli/ledger.ts +313 -0
- package/src/cli/server.ts +195 -0
- package/src/hooks/lib/agent.ts +63 -7
- package/src/hooks/lib/paths.ts +2 -0
- package/src/tools/agent/project.ts +28 -3
- package/src/tools/ledger/query.ts +318 -0
- package/src/tools/ledger/server.ts +111 -0
- package/src/tools/ledger/view.ts +130 -0
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ pal cli status # check your setup
|
|
|
88
88
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
89
89
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
90
90
|
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
91
|
+
| `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
|
|
92
|
+
| `pal cli server` | Local page over the action ledger for showing the log to a person — `start [--port <n>]`, `stop`, `status`. Loopback only, default port 7250; project and date window are chosen on the page |
|
|
91
93
|
| `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
|
|
92
94
|
| `pal cli skill doctor <name>` | Evaluate a skill against the authoring best practices (folder/file-name match, name, description, body length, point-of-view, reference depth) |
|
|
93
95
|
| `pal cli subagent link <name>` | Install a personal `~/.pal/agents/<name>.md` (merged multi-platform definition) into every installed agent, split per platform |
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Action Ledger</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #eef0f2;
|
|
10
|
+
--panel: #ffffff;
|
|
11
|
+
--ink: #1b2026;
|
|
12
|
+
--muted: #6b7480;
|
|
13
|
+
--line: #d5dae0;
|
|
14
|
+
--accent: #0f6e6e;
|
|
15
|
+
--accent-ink: #ffffff;
|
|
16
|
+
--block: #b4471c;
|
|
17
|
+
--block-bg: #fbf0ea;
|
|
18
|
+
--ok: #2f7a3e;
|
|
19
|
+
--ok-bg: #edf6ee;
|
|
20
|
+
--pending: #8a6d00;
|
|
21
|
+
--pending-bg: #fbf5df;
|
|
22
|
+
--agent: #4a3fa3;
|
|
23
|
+
--agent-bg: #efedf9;
|
|
24
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
25
|
+
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { height: 100%; }
|
|
29
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 13px/1.45 var(--sans); }
|
|
30
|
+
header {
|
|
31
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
32
|
+
padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
|
|
33
|
+
}
|
|
34
|
+
header h1 { font-size: 14px; margin: 0; font-weight: 600; letter-spacing: .01em; }
|
|
35
|
+
header .model { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
|
36
|
+
header .who { font-size: 12px; color: var(--muted); }
|
|
37
|
+
header .who b { color: var(--ink); font-weight: 600; }
|
|
38
|
+
|
|
39
|
+
main { background: var(--panel); min-height: calc(100% - 45px); }
|
|
40
|
+
h2 {
|
|
41
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
|
42
|
+
margin: 0; padding: 12px 14px 8px; font-weight: 600;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.filters { display: flex; gap: 10px; padding: 0 14px 12px; flex-wrap: wrap; align-items: end; }
|
|
46
|
+
.filters label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
47
|
+
select, input { font: inherit; padding: 4px 6px; border: 1px solid var(--line); border-radius: 3px; }
|
|
48
|
+
|
|
49
|
+
.stats { display: grid; grid-template-columns: 1.4fr repeat(4, 1fr); gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
50
|
+
.stat { background: var(--panel); padding: 12px 14px; }
|
|
51
|
+
.stat label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
52
|
+
.stat .n { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
|
53
|
+
.stat.headline { background: var(--block-bg); }
|
|
54
|
+
.stat.headline .n { font-size: 40px; color: var(--block); }
|
|
55
|
+
.stat.applied .n { color: var(--ok); }
|
|
56
|
+
.stat.failed .n { color: var(--pending); }
|
|
57
|
+
.stat.denied .n, .stat.blocked .n { color: var(--block); }
|
|
58
|
+
.st { font-size: 11px; color: var(--muted); }
|
|
59
|
+
|
|
60
|
+
table { width: 100%; border-collapse: collapse; }
|
|
61
|
+
td, th { text-align: left; padding: 7px 14px; border-top: 1px solid var(--line); vertical-align: top; font-variant-numeric: tabular-nums; }
|
|
62
|
+
th { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); border-top: 0; font-weight: 600; }
|
|
63
|
+
td.ts { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
64
|
+
td.target { font-family: var(--mono); font-size: 11px; word-break: break-all; }
|
|
65
|
+
td.diff { font-family: var(--mono); font-size: 11px; }
|
|
66
|
+
.actor { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; }
|
|
67
|
+
.actor.human { background: #eef0f2; }
|
|
68
|
+
.actor.agent { background: var(--agent-bg); color: var(--agent); }
|
|
69
|
+
.out { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
|
|
70
|
+
.out.applied { background: var(--ok-bg); color: var(--ok); }
|
|
71
|
+
.out.failed { background: var(--pending-bg); color: var(--pending); }
|
|
72
|
+
.out.denied { background: var(--block-bg); color: var(--block); }
|
|
73
|
+
.out.blocked { background: var(--block-bg); color: var(--block); }
|
|
74
|
+
footer { font-family: var(--mono); font-size: 10.5px; color: var(--muted); padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<header>
|
|
79
|
+
<h1>Action Ledger</h1>
|
|
80
|
+
<span class="model" id="modelLine"></span>
|
|
81
|
+
<span class="who" id="who"></span>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<main>
|
|
85
|
+
<h2>Window</h2>
|
|
86
|
+
<div class="filters">
|
|
87
|
+
<div><label for="project">Project</label><select id="project"><option value="">all projects</option></select></div>
|
|
88
|
+
<div><label for="since">From</label><input type="date" id="since"></div>
|
|
89
|
+
<div><label for="until">To</label><input type="date" id="until"></div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="stats" id="stats"></div>
|
|
93
|
+
|
|
94
|
+
<h2>Action log</h2>
|
|
95
|
+
<table>
|
|
96
|
+
<thead><tr><th>When</th><th>Actor</th><th>Action</th><th>Target</th><th>Change</th><th>Outcome</th></tr></thead>
|
|
97
|
+
<tbody id="log"></tbody>
|
|
98
|
+
</table>
|
|
99
|
+
<footer id="footer">Every row is one action an agent tried to make. The ledger saw each from both sides, before and after. Nothing here was written by hand.</footer>
|
|
100
|
+
</main>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
const $ = id => document.getElementById(id);
|
|
104
|
+
const OUTCOMES = ['applied', 'failed', 'denied', 'blocked'];
|
|
105
|
+
let ledgerFiles = 0;
|
|
106
|
+
|
|
107
|
+
function el(tag, cls, text) {
|
|
108
|
+
const node = document.createElement(tag);
|
|
109
|
+
if (cls) node.className = cls;
|
|
110
|
+
if (text !== undefined) node.textContent = text;
|
|
111
|
+
return node;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pad = n => String(n).padStart(2, '0');
|
|
115
|
+
function when(ts) {
|
|
116
|
+
const d = new Date(ts);
|
|
117
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const authorityWord = key => key === 'user' ? 'human' : key;
|
|
121
|
+
function tally(counts, word = k => k) {
|
|
122
|
+
const parts = Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${word(k)} ${v}`);
|
|
123
|
+
return parts.length ? parts.join(' · ') : '—';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function query() {
|
|
127
|
+
const p = new URLSearchParams();
|
|
128
|
+
if ($('project').value) p.set('project', $('project').value);
|
|
129
|
+
if ($('since').value) p.set('since', $('since').value);
|
|
130
|
+
if ($('until').value) p.set('until', `${$('until').value}T23:59:59.999Z`);
|
|
131
|
+
return p.toString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function windowText() {
|
|
135
|
+
const from = $('since').value, to = $('until').value;
|
|
136
|
+
if (!from && !to) return 'all time';
|
|
137
|
+
return `${from || 'start'} → ${to || 'now'}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderStats(s) {
|
|
141
|
+
const strip = $('stats');
|
|
142
|
+
strip.replaceChildren();
|
|
143
|
+
const headline = el('div', 'stat headline');
|
|
144
|
+
headline.append(el('label', '', 'Refusals'), el('div', 'n', String(s.refusals)),
|
|
145
|
+
el('div', 'st', `denied ${s.outcomes.denied.total} · blocked ${s.outcomes.blocked.total}`));
|
|
146
|
+
strip.append(headline);
|
|
147
|
+
for (const key of OUTCOMES) {
|
|
148
|
+
const o = s.outcomes[key];
|
|
149
|
+
const card = el('div', `stat ${key}`);
|
|
150
|
+
card.append(el('label', '', key), el('div', 'n', String(o.total)),
|
|
151
|
+
el('div', 'st', tally(o.byAuthority, authorityWord)), el('div', 'st', tally(o.byRuntime)));
|
|
152
|
+
strip.append(card);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function actorCell(r) {
|
|
157
|
+
const td = el('td');
|
|
158
|
+
const agent = r.authority === 'agent';
|
|
159
|
+
td.append(el('span', `actor ${agent ? 'agent' : 'human'}`, agent ? `agent · ${r.runtime}` : r.actor));
|
|
160
|
+
td.append(el('div', 'st', agent ? `on behalf of ${r.actor}` : `via ${r.runtime}`));
|
|
161
|
+
return td;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function changeCell(r) {
|
|
165
|
+
const td = el('td', 'diff');
|
|
166
|
+
td.append(r.reason ? el('span', 'st', r.reason) : document.createTextNode(r.change));
|
|
167
|
+
return td;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderRows(rows) {
|
|
171
|
+
const body = $('log');
|
|
172
|
+
body.replaceChildren();
|
|
173
|
+
if (!rows.length) {
|
|
174
|
+
const td = el('td', 'st', 'No actions in this window.');
|
|
175
|
+
td.colSpan = 6;
|
|
176
|
+
const tr = el('tr'); tr.append(td); body.append(tr);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const r of rows) {
|
|
180
|
+
const tr = el('tr');
|
|
181
|
+
const outcome = el('td'); outcome.append(el('span', `out ${r.outcome}`, r.outcome));
|
|
182
|
+
tr.append(el('td', 'ts', when(r.ts)), actorCell(r), el('td', '', r.tool), el('td', 'target', r.target), changeCell(r), outcome);
|
|
183
|
+
body.append(tr);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function load() {
|
|
188
|
+
const res = await fetch(`/api/ledger?${query()}`);
|
|
189
|
+
const body = await res.json();
|
|
190
|
+
if (!res.ok) { $('modelLine').textContent = body.error; return; }
|
|
191
|
+
renderStats(body.stats);
|
|
192
|
+
renderRows(body.rows);
|
|
193
|
+
$('modelLine').textContent = `${body.stats.total} actions · ${windowText()} · ${ledgerFiles} ledger file(s)`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function loadProjects() {
|
|
197
|
+
const list = await (await fetch('/api/projects')).json();
|
|
198
|
+
for (const { slug } of list) {
|
|
199
|
+
const o = el('option', '', slug); o.value = slug; $('project').append(o);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function loadStatus() {
|
|
204
|
+
const s = await (await fetch('/api/status')).json();
|
|
205
|
+
ledgerFiles = s.ledgerFiles;
|
|
206
|
+
$('who').replaceChildren('machine ', Object.assign(el('b', '', s.machine)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const id of ['project', 'since', 'until']) $(id).addEventListener('change', load);
|
|
210
|
+
Promise.all([loadProjects(), loadStatus()]).then(load);
|
|
211
|
+
</script>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* status Show current PAL configuration
|
|
17
17
|
* doctor Check prerequisites and system health
|
|
18
18
|
* usage Summarize token usage and cost
|
|
19
|
+
* ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
20
|
+
* server start|stop|status Local page over the ledger
|
|
19
21
|
* skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
|
|
20
22
|
* skill doctor <name|--all> Evaluate one skill, or every installed skill, against the authoring best practices
|
|
21
23
|
* subagent link <name> Install a personal ~/.pal/agents/<name>.md into installed agents
|
|
@@ -232,6 +234,18 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
232
234
|
if (code !== 0) process.exit(code);
|
|
233
235
|
break;
|
|
234
236
|
}
|
|
237
|
+
case "ledger": {
|
|
238
|
+
const { runLedger } = await import("./ledger");
|
|
239
|
+
const code = await runLedger(args);
|
|
240
|
+
if (code !== 0) process.exit(code);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "server": {
|
|
244
|
+
const { runServer } = await import("./server");
|
|
245
|
+
const code = await runServer(args);
|
|
246
|
+
if (code !== 0) process.exit(code);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
235
249
|
case "subagent": {
|
|
236
250
|
const { runSubagent } = await import("./subagent");
|
|
237
251
|
const code = await runSubagent(args);
|
|
@@ -299,6 +313,9 @@ function showHelp() {
|
|
|
299
313
|
pal cli machine [label <name>] Show or rename this install (where it was written)
|
|
300
314
|
pal cli knowledge <sub> [args] Query & manage the knowledge store
|
|
301
315
|
(search · graph · stats · hubs · find · show · add · ls)
|
|
316
|
+
pal cli ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
317
|
+
e.g. ledger log --project X --since 7d
|
|
318
|
+
pal cli server start|stop|status Local page over the ledger, for showing the log to a person
|
|
302
319
|
pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
|
|
303
320
|
pal cli skill doctor <name|--all> Evaluate one skill, or every installed skill
|
|
304
321
|
pal cli skill author-model Print the flagship model that authors skills for the active agent
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli ledger — query the action ledger.
|
|
3
|
+
*
|
|
4
|
+
* Thin presentation layer over src/tools/ledger/query.ts. Owns formatting and
|
|
5
|
+
* argv parsing only; every question about the records themselves is answered
|
|
6
|
+
* there.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* log [filters] Matching actions, oldest first
|
|
10
|
+
* show <id> One action in full, with its change and current standing
|
|
11
|
+
* stats [filters] Counts by outcome, runtime, actor, tool and target
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseArgs } from "node:util";
|
|
15
|
+
import type { LedgerEntry } from "../hooks/lib/ledger";
|
|
16
|
+
import {
|
|
17
|
+
type ChainVerdict,
|
|
18
|
+
chainVerdict,
|
|
19
|
+
changedLines,
|
|
20
|
+
changeShape,
|
|
21
|
+
findEntry,
|
|
22
|
+
type LedgerFilter,
|
|
23
|
+
ledgerFiles,
|
|
24
|
+
locate,
|
|
25
|
+
parseSince,
|
|
26
|
+
queryLedger,
|
|
27
|
+
type Standing,
|
|
28
|
+
standing,
|
|
29
|
+
summarize,
|
|
30
|
+
} from "../tools/ledger/query";
|
|
31
|
+
|
|
32
|
+
const FILTER_OPTIONS = {
|
|
33
|
+
project: { type: "string" },
|
|
34
|
+
since: { type: "string" },
|
|
35
|
+
until: { type: "string" },
|
|
36
|
+
actor: { type: "string" },
|
|
37
|
+
machine: { type: "string" },
|
|
38
|
+
runtime: { type: "string" },
|
|
39
|
+
outcome: { type: "string" },
|
|
40
|
+
tool: { type: "string" },
|
|
41
|
+
target: { type: "string" },
|
|
42
|
+
limit: { type: "string" },
|
|
43
|
+
json: { type: "boolean" },
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export async function runLedger(args: string[]): Promise<number> {
|
|
47
|
+
const [sub, ...rest] = args;
|
|
48
|
+
switch (sub) {
|
|
49
|
+
case "log":
|
|
50
|
+
return cmdLog(rest);
|
|
51
|
+
case "show":
|
|
52
|
+
return cmdShow(rest);
|
|
53
|
+
case "stats":
|
|
54
|
+
return cmdStats(rest);
|
|
55
|
+
case undefined:
|
|
56
|
+
case "help":
|
|
57
|
+
case "--help":
|
|
58
|
+
case "-h":
|
|
59
|
+
showHelp();
|
|
60
|
+
return 0;
|
|
61
|
+
default:
|
|
62
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
63
|
+
showHelp();
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function showHelp(): void {
|
|
69
|
+
console.log(`
|
|
70
|
+
Usage:
|
|
71
|
+
pal cli ledger <subcommand> [filters]
|
|
72
|
+
|
|
73
|
+
Subcommands:
|
|
74
|
+
log [filters] Matching actions, oldest first
|
|
75
|
+
show <id> One action in full: change, target, standing
|
|
76
|
+
stats [filters] Counts by outcome, runtime, actor, tool, target
|
|
77
|
+
|
|
78
|
+
Filters:
|
|
79
|
+
--project <slug> Actions against a registered project
|
|
80
|
+
--since <7d|2026-09-01> A duration back from now, or a date
|
|
81
|
+
--until <date> Upper bound on the timestamp
|
|
82
|
+
--actor <id> Who caused it
|
|
83
|
+
--machine <id> Which install wrote it
|
|
84
|
+
--runtime <agent> claude, cursor, codex, copilot, opencode
|
|
85
|
+
--outcome <applied|failed|denied>
|
|
86
|
+
--tool <Edit|Write>
|
|
87
|
+
--target <substring> Match anywhere in the recorded path
|
|
88
|
+
--limit <n> Keep the newest n matches
|
|
89
|
+
--json Machine-readable output
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
pal cli ledger log --project portable-agent-layer --since 7d
|
|
93
|
+
pal cli ledger log --target memory/ --outcome applied
|
|
94
|
+
pal cli ledger stats --since 24h
|
|
95
|
+
`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A filter that silently ignored an unparseable window would answer the wrong question. */
|
|
99
|
+
function buildFilter(values: Record<string, unknown>): LedgerFilter | string {
|
|
100
|
+
const filter: LedgerFilter = {};
|
|
101
|
+
for (const key of [
|
|
102
|
+
"project",
|
|
103
|
+
"actor",
|
|
104
|
+
"machine",
|
|
105
|
+
"runtime",
|
|
106
|
+
"outcome",
|
|
107
|
+
"tool",
|
|
108
|
+
"target",
|
|
109
|
+
] as const) {
|
|
110
|
+
const value = values[key];
|
|
111
|
+
if (typeof value === "string") filter[key] = value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const key of ["since", "until"] as const) {
|
|
115
|
+
const spec = values[key];
|
|
116
|
+
if (typeof spec !== "string") continue;
|
|
117
|
+
const at = parseSince(spec);
|
|
118
|
+
if (!at) return `Unrecognised --${key}: ${spec} (use 7d, 24h, or a date)`;
|
|
119
|
+
filter[key] = at;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof values.limit === "string") {
|
|
123
|
+
const limit = Number(values.limit);
|
|
124
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
125
|
+
return `--limit must be a positive integer`;
|
|
126
|
+
filter.limit = limit;
|
|
127
|
+
}
|
|
128
|
+
return filter;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseFilters(args: string[]): { filter: LedgerFilter; json: boolean } | string {
|
|
132
|
+
try {
|
|
133
|
+
const { values } = parseArgs({
|
|
134
|
+
args,
|
|
135
|
+
options: FILTER_OPTIONS,
|
|
136
|
+
allowPositionals: true,
|
|
137
|
+
});
|
|
138
|
+
const filter = buildFilter(values);
|
|
139
|
+
return typeof filter === "string" ? filter : { filter, json: values.json === true };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return error instanceof Error ? error.message : String(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shortId(id: string): string {
|
|
146
|
+
return id.slice(0, 11).padEnd(11);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function cmdLog(args: string[]): number {
|
|
150
|
+
const parsed = parseFilters(args);
|
|
151
|
+
if (typeof parsed === "string") return fail(parsed);
|
|
152
|
+
|
|
153
|
+
const entries = queryLedger(parsed.filter);
|
|
154
|
+
if (parsed.json) {
|
|
155
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (entries.length === 0) {
|
|
160
|
+
console.log("No actions match.");
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
console.log(
|
|
166
|
+
`${entry.ts} ${shortId(entry.id)} ${entry.outcome.padEnd(7)} ${entry.runtime.padEnd(8)} ${entry.tool.padEnd(5)} ${changedLines(entry).padEnd(9)} ${entry.target}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
console.log(
|
|
170
|
+
`\n${entries.length} action(s) across ${ledgerFiles().length} ledger file(s).`
|
|
171
|
+
);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function describeStanding(verdict: Standing): string {
|
|
176
|
+
switch (verdict.state) {
|
|
177
|
+
case "in-place":
|
|
178
|
+
return "still in place on disk";
|
|
179
|
+
case "reverted":
|
|
180
|
+
return verdict.replays
|
|
181
|
+
? "reverted since — the stored change replays cleanly onto the file as it stands"
|
|
182
|
+
: "reverted since, but the stored change no longer replays";
|
|
183
|
+
case "superseded":
|
|
184
|
+
return `superseded — the file has changed again since (now ${verdict.hash.slice(0, 12)})`;
|
|
185
|
+
case "missing":
|
|
186
|
+
return "the target no longer exists";
|
|
187
|
+
default:
|
|
188
|
+
return verdict.why;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function describeChain(verdict: ChainVerdict): string {
|
|
193
|
+
switch (verdict.state) {
|
|
194
|
+
case "latest":
|
|
195
|
+
return "the newest recorded action on this target";
|
|
196
|
+
case "undone":
|
|
197
|
+
return `undone by ${verdict.by} at ${verdict.at}, which put the file back as this one found it`;
|
|
198
|
+
default:
|
|
199
|
+
return `changed again by ${verdict.by} at ${verdict.at}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const UNKEPT_CHANGE: Record<string, string> = {
|
|
204
|
+
redacted: "contents withheld — the target is one the ledger never keeps",
|
|
205
|
+
truncated: "the change was too large to keep; its size and hashes remain",
|
|
206
|
+
none: "no change was recorded, which is what a refused action looks like",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function printChange(entry: LedgerEntry): void {
|
|
210
|
+
const shape = changeShape(entry);
|
|
211
|
+
if (shape.kind !== "hunks") {
|
|
212
|
+
console.log(` ${UNKEPT_CHANGE[shape.kind]}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const hunk of shape.delta.hunks) {
|
|
216
|
+
console.log(` @@ line ${hunk.at + 1}, -${hunk.remove} +${hunk.insert.length}`);
|
|
217
|
+
for (const line of hunk.insert) console.log(` + ${line}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdShow(args: string[]): number {
|
|
222
|
+
const [id, ...rest] = args;
|
|
223
|
+
if (!id) return fail("Usage: pal cli ledger show <id>");
|
|
224
|
+
|
|
225
|
+
const entry = findEntry(id);
|
|
226
|
+
if (!entry) return fail(`No action with id ${id}`);
|
|
227
|
+
|
|
228
|
+
if (rest.includes("--json")) {
|
|
229
|
+
console.log(
|
|
230
|
+
JSON.stringify(
|
|
231
|
+
{
|
|
232
|
+
...entry,
|
|
233
|
+
resolved: locate(entry),
|
|
234
|
+
standing: standing(entry),
|
|
235
|
+
chain: chainVerdict(entry),
|
|
236
|
+
},
|
|
237
|
+
null,
|
|
238
|
+
2
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`
|
|
245
|
+
${entry.id} ${entry.ts}
|
|
246
|
+
${entry.tool} → ${outcomeLine(entry)}
|
|
247
|
+
target ${entry.target}
|
|
248
|
+
on disk ${diskLine(entry)}
|
|
249
|
+
runtime ${entry.runtime}, authority ${entry.authority}
|
|
250
|
+
actor ${entry.actor}
|
|
251
|
+
machine ${entry.machine}
|
|
252
|
+
size ${sizeOf(entry.before, "created")} → ${sizeOf(entry.after, "nothing landed")}
|
|
253
|
+
standing ${describeStanding(standing(entry))}
|
|
254
|
+
in ledger ${describeChain(chainVerdict(entry))}
|
|
255
|
+
|
|
256
|
+
change`);
|
|
257
|
+
printChange(entry);
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function outcomeLine(entry: LedgerEntry): string {
|
|
262
|
+
return entry.reason ? `${entry.outcome} (${entry.reason})` : entry.outcome;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function diskLine(entry: LedgerEntry): string {
|
|
266
|
+
const found = locate(entry);
|
|
267
|
+
if (found.path) return found.path;
|
|
268
|
+
return `unresolvable: project ${found.unresolvable} is not registered here`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sizeOf(state: LedgerEntry["before"], absent: string): string {
|
|
272
|
+
return state ? `${state.bytes}b` : absent;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function printTally(label: string, counts: Record<string, number>): void {
|
|
276
|
+
const rows = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
277
|
+
if (rows.length === 0) return;
|
|
278
|
+
console.log(` ${label}`);
|
|
279
|
+
for (const [key, count] of rows)
|
|
280
|
+
console.log(` ${String(count).padStart(6)} ${key}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function cmdStats(args: string[]): number {
|
|
284
|
+
const parsed = parseFilters(args);
|
|
285
|
+
if (typeof parsed === "string") return fail(parsed);
|
|
286
|
+
|
|
287
|
+
const stats = summarize(queryLedger(parsed.filter));
|
|
288
|
+
if (parsed.json) {
|
|
289
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log(
|
|
294
|
+
`\n ${stats.total} action(s) across ${ledgerFiles().length} ledger file(s)`
|
|
295
|
+
);
|
|
296
|
+
if (stats.span) console.log(` ${stats.span.first} → ${stats.span.last}\n`);
|
|
297
|
+
printTally("outcome", stats.byOutcome);
|
|
298
|
+
printTally("runtime", stats.byRuntime);
|
|
299
|
+
printTally("tool", stats.byTool);
|
|
300
|
+
printTally("actor", stats.byActor);
|
|
301
|
+
if (stats.topTargets.length > 0) {
|
|
302
|
+
console.log(" most-changed targets");
|
|
303
|
+
for (const { target, count } of stats.topTargets) {
|
|
304
|
+
console.log(` ${String(count).padStart(6)} ${target}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fail(message: string): number {
|
|
311
|
+
console.error(message);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|