@mafdet/cli 0.4.0 → 0.4.2
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/CHANGELOG.md +50 -0
- package/README.md +64 -3
- package/README.zh-CN.md +53 -1
- package/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +16 -3
- package/dist/agent/session.d.ts +5 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +1 -1
- package/dist/agent/tools/read-only.d.ts.map +1 -1
- package/dist/agent/tools/read-only.js +6 -0
- package/dist/api/account.d.ts +43 -0
- package/dist/api/account.d.ts.map +1 -1
- package/dist/api/account.js +40 -0
- package/dist/commands/sessions.d.ts +5 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +379 -0
- package/dist/config/paths.d.ts +7 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +24 -0
- package/dist/filesystem/protected-dirs.d.ts +26 -0
- package/dist/filesystem/protected-dirs.d.ts.map +1 -0
- package/dist/filesystem/protected-dirs.js +78 -0
- package/dist/filesystem/workspace.d.ts +12 -1
- package/dist/filesystem/workspace.d.ts.map +1 -1
- package/dist/filesystem/workspace.js +24 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/security/redact.d.ts +26 -3
- package/dist/security/redact.d.ts.map +1 -1
- package/dist/security/redact.js +48 -3
- package/dist/sessions/index-file.d.ts +38 -0
- package/dist/sessions/index-file.d.ts.map +1 -0
- package/dist/sessions/index-file.js +174 -0
- package/dist/sessions/lock.d.ts +39 -0
- package/dist/sessions/lock.d.ts.map +1 -0
- package/dist/sessions/lock.js +134 -0
- package/dist/sessions/open.d.ts +45 -0
- package/dist/sessions/open.d.ts.map +1 -0
- package/dist/sessions/open.js +96 -0
- package/dist/sessions/policy.d.ts +32 -0
- package/dist/sessions/policy.d.ts.map +1 -0
- package/dist/sessions/policy.js +57 -0
- package/dist/sessions/reconcile.d.ts +49 -0
- package/dist/sessions/reconcile.d.ts.map +1 -0
- package/dist/sessions/reconcile.js +115 -0
- package/dist/sessions/recorder.d.ts +42 -0
- package/dist/sessions/recorder.d.ts.map +1 -0
- package/dist/sessions/recorder.js +147 -0
- package/dist/sessions/resume.d.ts +67 -0
- package/dist/sessions/resume.d.ts.map +1 -0
- package/dist/sessions/resume.js +100 -0
- package/dist/sessions/store.d.ts +141 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +272 -0
- package/dist/sessions/usage.d.ts +35 -0
- package/dist/sessions/usage.d.ts.map +1 -0
- package/dist/sessions/usage.js +157 -0
- package/dist/tui/agent-surface.d.ts +10 -0
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +27 -3
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +61 -3
- package/dist/tui/input.d.ts +13 -1
- package/dist/tui/input.d.ts.map +1 -1
- package/dist/tui/input.js +9 -2
- package/dist/tui/route.d.ts.map +1 -1
- package/dist/tui/route.js +19 -2
- package/dist/tui/session-centre.d.ts +15 -0
- package/dist/tui/session-centre.d.ts.map +1 -0
- package/dist/tui/session-centre.js +59 -0
- package/dist/tui/start-agent.d.ts +16 -1
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +125 -1
- package/package.json +1 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// CLI-024 · `mafdet sessions` - list, show and delete what is on this machine.
|
|
2
|
+
//
|
|
3
|
+
// Read-only against the model: none of these commands sends a request, so
|
|
4
|
+
// browsing history is free (plan §20.2). Deleting removes local files and
|
|
5
|
+
// nothing else - the server's ledger is untouched, and the confirmation says
|
|
6
|
+
// so, because "delete" is a word people reasonably expect to mean "and stop
|
|
7
|
+
// charging me".
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { fetchUsageEvents } from "../api/account.js";
|
|
10
|
+
import { sessionsDir } from "../config/paths.js";
|
|
11
|
+
import { formatUsd } from "../output/money.js";
|
|
12
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
13
|
+
import { parseFormatFlags, renderJson, renderRecord, renderTable, } from "../output/format.js";
|
|
14
|
+
import { keyDigest } from "../security/redact.js";
|
|
15
|
+
import { loadIndex, removeIndexEntry } from "../sessions/index-file.js";
|
|
16
|
+
import { acquireSessionLock, SessionLockedError } from "../sessions/lock.js";
|
|
17
|
+
import { daysToCover, matchSettlements, planReconciliation, } from "../sessions/reconcile.js";
|
|
18
|
+
import { deleteSession, readSession, SessionWriter, } from "../sessions/store.js";
|
|
19
|
+
import { settledCaveat, summariseUsage } from "../sessions/usage.js";
|
|
20
|
+
import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
const USAGE = `Usage:
|
|
23
|
+
mafdet sessions list [--json|--plain]
|
|
24
|
+
mafdet sessions show <session-id> [--json]
|
|
25
|
+
mafdet sessions usage <session-id> [--refresh] [--json]
|
|
26
|
+
mafdet sessions delete <session-id> [--yes]
|
|
27
|
+
|
|
28
|
+
Conversations are stored on this computer only. Browsing them never
|
|
29
|
+
calls a model and is never charged.
|
|
30
|
+
|
|
31
|
+
--refresh asks the billing ledger what this key's requests actually
|
|
32
|
+
cost. It sends request IDs and nothing else - no messages, no titles.
|
|
33
|
+
`;
|
|
34
|
+
export async function sessionsCommand(argv, io) {
|
|
35
|
+
const [sub, ...rest] = argv;
|
|
36
|
+
const root = sessionsDir(io.env ?? process.env);
|
|
37
|
+
if (sub === undefined || sub === "help" || sub === "--help") {
|
|
38
|
+
io.stdout(USAGE);
|
|
39
|
+
return sub === undefined ? ExitCode.Usage : ExitCode.Ok;
|
|
40
|
+
}
|
|
41
|
+
if (sub === "list")
|
|
42
|
+
return list(rest, io, root);
|
|
43
|
+
if (sub === "show")
|
|
44
|
+
return show(rest, io, root);
|
|
45
|
+
if (sub === "usage")
|
|
46
|
+
return usage(rest, io, root);
|
|
47
|
+
if (sub === "delete")
|
|
48
|
+
return remove(rest, io, root);
|
|
49
|
+
io.stderr(`mafdet sessions: unknown subcommand '${sub}'\n${USAGE}`);
|
|
50
|
+
return ExitCode.Usage;
|
|
51
|
+
}
|
|
52
|
+
function list(argv, io, root) {
|
|
53
|
+
const { format } = parseFormatFlags(argv);
|
|
54
|
+
const { sessions } = loadIndex(root);
|
|
55
|
+
if (format === "json") {
|
|
56
|
+
io.stdout(renderJson({
|
|
57
|
+
sessions_path: root,
|
|
58
|
+
count: sessions.length,
|
|
59
|
+
sessions,
|
|
60
|
+
}));
|
|
61
|
+
return ExitCode.Ok;
|
|
62
|
+
}
|
|
63
|
+
if (sessions.length === 0) {
|
|
64
|
+
// Not an error: a fresh machine has none, and saying where they would go
|
|
65
|
+
// is more use than an empty table.
|
|
66
|
+
io.stderr(`no saved conversations yet (they would be in ${root})\n`);
|
|
67
|
+
return ExitCode.Ok;
|
|
68
|
+
}
|
|
69
|
+
io.stdout(renderTable(sessions, [
|
|
70
|
+
{ header: "SESSION", value: (s) => s.session_id },
|
|
71
|
+
{ header: "TITLE", value: (s) => s.title },
|
|
72
|
+
{ header: "MODEL", value: (s) => s.model_id },
|
|
73
|
+
{ header: "REQUESTS", value: (s) => `${s.model_requests}` },
|
|
74
|
+
{
|
|
75
|
+
header: "EST. COST",
|
|
76
|
+
value: (s) => formatUsd(s.estimated_charge_micro_usd),
|
|
77
|
+
},
|
|
78
|
+
{ header: "UPDATED", value: (s) => s.updated_at },
|
|
79
|
+
]));
|
|
80
|
+
return ExitCode.Ok;
|
|
81
|
+
}
|
|
82
|
+
function show(argv, io, root) {
|
|
83
|
+
const { format } = parseFormatFlags(argv);
|
|
84
|
+
const id = argv.find((arg) => !arg.startsWith("-"));
|
|
85
|
+
if (id === undefined) {
|
|
86
|
+
io.stderr(`mafdet sessions show: no session id\n${USAGE}`);
|
|
87
|
+
return ExitCode.Usage;
|
|
88
|
+
}
|
|
89
|
+
const directory = join(root, id);
|
|
90
|
+
if (!existsSync(directory)) {
|
|
91
|
+
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
92
|
+
return ExitCode.General;
|
|
93
|
+
}
|
|
94
|
+
let session;
|
|
95
|
+
try {
|
|
96
|
+
session = readSession(directory);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
io.stderr(`mafdet sessions: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
100
|
+
return ExitCode.General;
|
|
101
|
+
}
|
|
102
|
+
if (format === "json") {
|
|
103
|
+
io.stdout(renderJson({
|
|
104
|
+
metadata: session.metadata,
|
|
105
|
+
events: session.events,
|
|
106
|
+
truncated_tail: session.truncatedTail,
|
|
107
|
+
}));
|
|
108
|
+
return ExitCode.Ok;
|
|
109
|
+
}
|
|
110
|
+
const m = session.metadata;
|
|
111
|
+
io.stdout(renderRecord([
|
|
112
|
+
["session", m.session_id],
|
|
113
|
+
["title", m.title],
|
|
114
|
+
["model", m.model_id],
|
|
115
|
+
["workspace", m.workspace],
|
|
116
|
+
["mode", m.mode],
|
|
117
|
+
["created", m.created_at],
|
|
118
|
+
["updated", m.updated_at],
|
|
119
|
+
["messages", `${m.message_count}`],
|
|
120
|
+
["model requests", `${m.model_requests}`],
|
|
121
|
+
["tokens", `${m.prompt_tokens} in / ${m.completion_tokens} out`],
|
|
122
|
+
["estimated cost", formatUsd(m.estimated_charge_micro_usd)],
|
|
123
|
+
]));
|
|
124
|
+
if (session.truncatedTail) {
|
|
125
|
+
// Worth saying plainly: the user should know a crash cost them the tail.
|
|
126
|
+
io.stderr("\nnote: the last event was incomplete, most likely an interrupted run.\n");
|
|
127
|
+
}
|
|
128
|
+
return ExitCode.Ok;
|
|
129
|
+
}
|
|
130
|
+
function remove(argv, io, root) {
|
|
131
|
+
const id = argv.find((arg) => !arg.startsWith("-"));
|
|
132
|
+
const confirmed = argv.includes("--yes");
|
|
133
|
+
if (id === undefined) {
|
|
134
|
+
io.stderr(`mafdet sessions delete: no session id\n${USAGE}`);
|
|
135
|
+
return ExitCode.Usage;
|
|
136
|
+
}
|
|
137
|
+
const directory = join(root, id);
|
|
138
|
+
if (!existsSync(directory)) {
|
|
139
|
+
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
140
|
+
return ExitCode.General;
|
|
141
|
+
}
|
|
142
|
+
if (!confirmed) {
|
|
143
|
+
// No prompt here: this is the command surface, where a script must not be
|
|
144
|
+
// able to hang waiting for an answer. Say exactly what would happen and
|
|
145
|
+
// require the flag.
|
|
146
|
+
io.stderr(DELETION_NOTICE(id));
|
|
147
|
+
return ExitCode.Usage;
|
|
148
|
+
}
|
|
149
|
+
deleteSession(directory);
|
|
150
|
+
removeIndexEntry(root, id);
|
|
151
|
+
io.stdout(`deleted ${id}\n`);
|
|
152
|
+
return ExitCode.Ok;
|
|
153
|
+
}
|
|
154
|
+
export const DELETION_NOTICE = (id) => `Deleting ${id} removes local messages and its per-session usage mapping\n` +
|
|
155
|
+
`from this computer.\n\n` +
|
|
156
|
+
`It does not remove API usage, billing records, or charges.\n\n` +
|
|
157
|
+
// Plan §20.12 requires this said out loud. "Delete" reads as "gone", and on
|
|
158
|
+
// a modern filesystem it is not: an SSD may keep the blocks, and a Time
|
|
159
|
+
// Machine or restic snapshot certainly keeps the file. Promising an erase we
|
|
160
|
+
// cannot perform would be the more comfortable sentence and the false one.
|
|
161
|
+
`It is an ordinary file delete, not a secure erase: backups, snapshots\n` +
|
|
162
|
+
`and the filesystem itself may still hold copies.\n\n` +
|
|
163
|
+
`Re-run with --yes to delete.\n`;
|
|
164
|
+
/**
|
|
165
|
+
* What this conversation used.
|
|
166
|
+
*
|
|
167
|
+
* Estimated and settled are shown as separate lines, never added together and
|
|
168
|
+
* never collapsed: §20.7 is explicit that an estimate must not be displayed as
|
|
169
|
+
* the final charge, and a settled figure that covers only some of the requests
|
|
170
|
+
* would be a smaller number than the truth.
|
|
171
|
+
*/
|
|
172
|
+
async function usage(argv, io, root) {
|
|
173
|
+
const { format } = parseFormatFlags(argv);
|
|
174
|
+
const id = argv.find((arg) => !arg.startsWith("-"));
|
|
175
|
+
if (id === undefined) {
|
|
176
|
+
io.stderr(`mafdet sessions usage: no session id\n${USAGE}`);
|
|
177
|
+
return ExitCode.Usage;
|
|
178
|
+
}
|
|
179
|
+
const directory = join(root, id);
|
|
180
|
+
if (!existsSync(directory)) {
|
|
181
|
+
io.stderr(`mafdet sessions: no session '${id}' on this computer\n`);
|
|
182
|
+
return ExitCode.General;
|
|
183
|
+
}
|
|
184
|
+
let session;
|
|
185
|
+
try {
|
|
186
|
+
session = readSession(directory);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
io.stderr(`mafdet sessions: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
190
|
+
return ExitCode.General;
|
|
191
|
+
}
|
|
192
|
+
let refreshed;
|
|
193
|
+
if (argv.includes("--refresh")) {
|
|
194
|
+
refreshed = await refresh(directory, session, io);
|
|
195
|
+
// Re-read rather than patch the in-memory copy: the file is the record,
|
|
196
|
+
// and reading it back is the only way to know the settlements landed.
|
|
197
|
+
if (refreshed.appended > 0)
|
|
198
|
+
session = readSession(directory);
|
|
199
|
+
}
|
|
200
|
+
const totals = summariseUsage(session.events);
|
|
201
|
+
if (format === "json") {
|
|
202
|
+
io.stdout(renderJson({
|
|
203
|
+
session_id: id,
|
|
204
|
+
user_messages: totals.userMessages,
|
|
205
|
+
model_requests: totals.modelRequests,
|
|
206
|
+
tool_runs: totals.toolRuns,
|
|
207
|
+
tool_runs_refused: totals.toolRunsRefused,
|
|
208
|
+
rate_limit_hits: totals.rateLimitHits,
|
|
209
|
+
rate_limit_wait_ms: totals.rateLimitWaitMs,
|
|
210
|
+
prompt_tokens: totals.promptTokens,
|
|
211
|
+
completion_tokens: totals.completionTokens,
|
|
212
|
+
// Strings, so no parser turns a charge into a float.
|
|
213
|
+
estimated_charge_micro_usd: totals.estimatedMicroUsd.toString(),
|
|
214
|
+
settled_charge_micro_usd: totals.settledMicroUsd.toString(),
|
|
215
|
+
by_settlement: totals.bySettlement,
|
|
216
|
+
by_model: totals.byModel.map((m) => ({
|
|
217
|
+
model_id: m.modelId,
|
|
218
|
+
model_requests: m.modelRequests,
|
|
219
|
+
prompt_tokens: m.promptTokens,
|
|
220
|
+
completion_tokens: m.completionTokens,
|
|
221
|
+
estimated_charge_micro_usd: m.estimatedMicroUsd.toString(),
|
|
222
|
+
})),
|
|
223
|
+
by_api_key: totals.byKey.map((k) => ({
|
|
224
|
+
api_key_ref: k.apiKeyRef,
|
|
225
|
+
model_requests: k.modelRequests,
|
|
226
|
+
estimated_charge_micro_usd: k.estimatedMicroUsd.toString(),
|
|
227
|
+
})),
|
|
228
|
+
...(refreshed === undefined ? {} : { refresh: jsonRefresh(refreshed) }),
|
|
229
|
+
}));
|
|
230
|
+
return refreshed?.exitCode ?? ExitCode.Ok;
|
|
231
|
+
}
|
|
232
|
+
const rows = [
|
|
233
|
+
["user messages", `${totals.userMessages}`],
|
|
234
|
+
["model requests", `${totals.modelRequests}`],
|
|
235
|
+
[
|
|
236
|
+
"tool runs",
|
|
237
|
+
`${totals.toolRuns}${totals.toolRunsRefused > 0 ? ` (${totals.toolRunsRefused} refused)` : ""}`,
|
|
238
|
+
],
|
|
239
|
+
["tokens", `${totals.promptTokens} in / ${totals.completionTokens} out`],
|
|
240
|
+
["local estimate", formatUsd(totals.estimatedMicroUsd.toString())],
|
|
241
|
+
];
|
|
242
|
+
if (totals.bySettlement.settled > 0) {
|
|
243
|
+
rows.push([
|
|
244
|
+
"server settled",
|
|
245
|
+
`${formatUsd(totals.settledMicroUsd.toString())} (${totals.bySettlement.settled} of ${totals.modelRequests})`,
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
if (totals.rateLimitHits > 0) {
|
|
249
|
+
rows.push([
|
|
250
|
+
"rate limited",
|
|
251
|
+
`${totals.rateLimitHits} time(s), ${Math.round(totals.rateLimitWaitMs / 1000)}s waiting`,
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
io.stdout(renderRecord(rows));
|
|
255
|
+
if (totals.byModel.length > 1) {
|
|
256
|
+
io.stdout("\nBy model\n");
|
|
257
|
+
io.stdout(renderTable(totals.byModel, [
|
|
258
|
+
{ header: "MODEL", value: (m) => m.modelId },
|
|
259
|
+
{ header: "REQUESTS", value: (m) => `${m.modelRequests}` },
|
|
260
|
+
{
|
|
261
|
+
header: "ESTIMATE",
|
|
262
|
+
value: (m) => formatUsd(m.estimatedMicroUsd.toString()),
|
|
263
|
+
},
|
|
264
|
+
]));
|
|
265
|
+
}
|
|
266
|
+
if (totals.byKey.length > 1) {
|
|
267
|
+
// Split per key so two accounts' spending is never presented as one.
|
|
268
|
+
io.stdout("\nBy API key\n");
|
|
269
|
+
io.stdout(renderTable(totals.byKey, [
|
|
270
|
+
{ header: "KEY", value: (k) => k.apiKeyRef },
|
|
271
|
+
{ header: "REQUESTS", value: (k) => `${k.modelRequests}` },
|
|
272
|
+
{
|
|
273
|
+
header: "ESTIMATE",
|
|
274
|
+
value: (k) => formatUsd(k.estimatedMicroUsd.toString()),
|
|
275
|
+
},
|
|
276
|
+
]));
|
|
277
|
+
}
|
|
278
|
+
const caveat = settledCaveat(totals);
|
|
279
|
+
if (caveat !== undefined)
|
|
280
|
+
io.stderr(`\nnote: ${caveat}.\n`);
|
|
281
|
+
if (refreshed !== undefined) {
|
|
282
|
+
for (const line of describeRefresh(refreshed))
|
|
283
|
+
io.stderr(`${line}\n`);
|
|
284
|
+
}
|
|
285
|
+
return refreshed?.exitCode ?? ExitCode.Ok;
|
|
286
|
+
}
|
|
287
|
+
async function refresh(directory, session, io) {
|
|
288
|
+
const nothing = { appended: 0, missing: 0, exhausted: true };
|
|
289
|
+
try {
|
|
290
|
+
const runtime = buildRuntime(io);
|
|
291
|
+
requireApiKey(runtime.config);
|
|
292
|
+
const plan = planReconciliation(session.events, keyDigest(runtime.config.apiKey ?? ""));
|
|
293
|
+
// Nothing to ask about is a complete answer, and a network call that could
|
|
294
|
+
// only return rows we would throw away is not worth making.
|
|
295
|
+
if (plan.wanted.size === 0) {
|
|
296
|
+
return { ...nothing, plan, exitCode: ExitCode.Ok };
|
|
297
|
+
}
|
|
298
|
+
const { events, exhausted } = await fetchUsageEvents(runtime.accountPlane, {
|
|
299
|
+
days: daysToCover(session.metadata.created_at),
|
|
300
|
+
wanted: plan.wanted,
|
|
301
|
+
});
|
|
302
|
+
const matched = matchSettlements(plan, events, new Date().toISOString());
|
|
303
|
+
let appended = 0;
|
|
304
|
+
if (matched.settlements.length > 0) {
|
|
305
|
+
// Under the lock: a session open in another window is appending to the
|
|
306
|
+
// same file, and `reopen` reads the sequence number before writing.
|
|
307
|
+
const lock = acquireSessionLock(directory);
|
|
308
|
+
try {
|
|
309
|
+
const writer = SessionWriter.reopen(directory);
|
|
310
|
+
for (const settlement of matched.settlements) {
|
|
311
|
+
writer.append(settlement);
|
|
312
|
+
appended += 1;
|
|
313
|
+
}
|
|
314
|
+
writer.close();
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
lock.release();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
plan,
|
|
322
|
+
appended,
|
|
323
|
+
missing: matched.missing.length,
|
|
324
|
+
exhausted,
|
|
325
|
+
exitCode: ExitCode.Ok,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error instanceof SessionLockedError) {
|
|
330
|
+
io.stderr(`mafdet sessions: ${error.message}\n`);
|
|
331
|
+
return { ...nothing, error: error.message, exitCode: ExitCode.General };
|
|
332
|
+
}
|
|
333
|
+
// The local figures below are still correct - they were never in doubt -
|
|
334
|
+
// so they are printed anyway, and the exit code carries the failure.
|
|
335
|
+
return {
|
|
336
|
+
...nothing,
|
|
337
|
+
error: error instanceof Error ? error.message : String(error),
|
|
338
|
+
exitCode: reportError(error, io),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function jsonRefresh(refreshed) {
|
|
343
|
+
return {
|
|
344
|
+
settled_now: refreshed.appended,
|
|
345
|
+
not_in_ledger: refreshed.missing,
|
|
346
|
+
complete_walk: refreshed.exhausted,
|
|
347
|
+
already_settled: refreshed.plan?.alreadySettled ?? 0,
|
|
348
|
+
other_api_key: refreshed.plan?.otherKey ?? 0,
|
|
349
|
+
no_request_id: refreshed.plan?.unanswerable ?? 0,
|
|
350
|
+
...(refreshed.error === undefined ? {} : { error: refreshed.error }),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** One line per thing the user might otherwise misread as something else. */
|
|
354
|
+
function describeRefresh(refreshed) {
|
|
355
|
+
if (refreshed.error !== undefined)
|
|
356
|
+
return [];
|
|
357
|
+
const lines = [];
|
|
358
|
+
const plan = refreshed.plan;
|
|
359
|
+
if (refreshed.appended > 0) {
|
|
360
|
+
lines.push(`\nrefreshed: ${refreshed.appended} request(s) settled.`);
|
|
361
|
+
}
|
|
362
|
+
if (refreshed.missing > 0) {
|
|
363
|
+
// The distinction matters: one of these is "wait", the other is "look".
|
|
364
|
+
lines.push(refreshed.exhausted
|
|
365
|
+
? `note: ${refreshed.missing} request(s) are not in the ledger yet; try again shortly.`
|
|
366
|
+
: `note: ${refreshed.missing} request(s) were not found in the pages read; try a narrower window.`);
|
|
367
|
+
}
|
|
368
|
+
if (plan !== undefined && plan.otherKey > 0) {
|
|
369
|
+
lines.push(`note: ${plan.otherKey} request(s) were paid for by a different API key. ` +
|
|
370
|
+
`Run this again with that key configured to settle them.`);
|
|
371
|
+
}
|
|
372
|
+
if (plan !== undefined &&
|
|
373
|
+
refreshed.appended === 0 &&
|
|
374
|
+
refreshed.missing === 0 &&
|
|
375
|
+
plan.otherKey === 0) {
|
|
376
|
+
lines.push("\nrefreshed: nothing left to settle.");
|
|
377
|
+
}
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -8,4 +8,11 @@ export declare const PROJECT_CONFIG_FILE = ".mafdet.json";
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function userConfigDir(env?: NodeJS.ProcessEnv, os?: string, home?: string): string;
|
|
10
10
|
export declare function userConfigPath(env?: NodeJS.ProcessEnv, os?: string, home?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Per-platform state directory (plan §20.5). Separate from the config
|
|
13
|
+
* directory on purpose: sessions are machine-local and must not follow a
|
|
14
|
+
* roaming profile between machines, whereas settings reasonably do.
|
|
15
|
+
*/
|
|
16
|
+
export declare function userStateDir(env?: NodeJS.ProcessEnv, os?: string, home?: string): string;
|
|
17
|
+
export declare function sessionsDir(env?: NodeJS.ProcessEnv, os?: string, home?: string): string;
|
|
11
18
|
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAeR;AAED,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAER"}
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAeR;AAED,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAoBR;AAED,wBAAgB,WAAW,CACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,MAAM,CAER"}
|
package/dist/config/paths.js
CHANGED
|
@@ -24,3 +24,27 @@ export function userConfigDir(env = process.env, os = platform(), home = homedir
|
|
|
24
24
|
export function userConfigPath(env = process.env, os = platform(), home = homedir()) {
|
|
25
25
|
return join(userConfigDir(env, os, home), CONFIG_FILE);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Per-platform state directory (plan §20.5). Separate from the config
|
|
29
|
+
* directory on purpose: sessions are machine-local and must not follow a
|
|
30
|
+
* roaming profile between machines, whereas settings reasonably do.
|
|
31
|
+
*/
|
|
32
|
+
export function userStateDir(env = process.env, os = platform(), home = homedir()) {
|
|
33
|
+
if (os === "win32") {
|
|
34
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
35
|
+
return join(localAppData && localAppData.length > 0
|
|
36
|
+
? localAppData
|
|
37
|
+
: join(home, "AppData", "Local"),
|
|
38
|
+
// Lowercase, matching userConfigDir: one machine should not end up with
|
|
39
|
+
// both `Mafdet\` and `mafdet\`.
|
|
40
|
+
"mafdet");
|
|
41
|
+
}
|
|
42
|
+
if (os === "darwin") {
|
|
43
|
+
return join(home, "Library", "Application Support", "mafdet");
|
|
44
|
+
}
|
|
45
|
+
const xdg = env.XDG_STATE_HOME?.trim();
|
|
46
|
+
return join(xdg && xdg.length > 0 ? xdg : join(home, ".local", "state"), "mafdet");
|
|
47
|
+
}
|
|
48
|
+
export function sessionsDir(env = process.env, os = platform(), home = homedir()) {
|
|
49
|
+
return join(userStateDir(env, os, home), "sessions");
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ProtectedDirectory {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly reason: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Every directory the CLI owns, resolved as far as it exists.
|
|
7
|
+
*
|
|
8
|
+
* Resolved eagerly so the comparison later is real-path against real-path: a
|
|
9
|
+
* symlinked home, or a state directory reached through one, must not slip past.
|
|
10
|
+
*
|
|
11
|
+
* `os` and `home` are injectable for the same reason they are in `paths.ts`:
|
|
12
|
+
* §20.12b requires all three platform layouts to be covered, and a function
|
|
13
|
+
* that can only ever describe the machine running it cannot be shown to.
|
|
14
|
+
* On macOS the two directories coincide, which is why the list is deduplicated
|
|
15
|
+
* rather than assumed to hold two entries.
|
|
16
|
+
*/
|
|
17
|
+
export declare function protectedDirectories(env?: NodeJS.ProcessEnv, os?: string, home?: string): readonly ProtectedDirectory[];
|
|
18
|
+
/**
|
|
19
|
+
* Is this path inside one of the CLI's own directories?
|
|
20
|
+
*
|
|
21
|
+
* `relative()` rather than a prefix test: a sibling named `mafdet-notes` must
|
|
22
|
+
* not be caught by a `mafdet` prefix, and on win32 `relative` compares
|
|
23
|
+
* case-insensitively, which is the behaviour we want there.
|
|
24
|
+
*/
|
|
25
|
+
export declare function protectedBy(absolutePath: string, directories: readonly ProtectedDirectory[]): ProtectedDirectory | undefined;
|
|
26
|
+
//# sourceMappingURL=protected-dirs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protected-dirs.d.ts","sourceRoot":"","sources":["../../src/filesystem/protected-dirs.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,EAAE,GAAE,MAAmB,EACvB,IAAI,GAAE,MAAkB,GACvB,SAAS,kBAAkB,EAAE,CAqB/B;AAYD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,SAAS,kBAAkB,EAAE,GACzC,kBAAkB,GAAG,SAAS,CAUhC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// CLI-023 · The CLI's own directories are off limits to the agent.
|
|
2
|
+
//
|
|
3
|
+
// Plan §20.12b. Sessions are stored under the user's state directory, and a
|
|
4
|
+
// user may well run `mafdet` from $HOME - the 2026-08-07 screenshot shows
|
|
5
|
+
// exactly that. Without this, the store lands inside the workspace and an
|
|
6
|
+
// agent working in one project can read another project's transcript in full,
|
|
7
|
+
// which would make "plaintext, but disclosed" a false description of the deal.
|
|
8
|
+
//
|
|
9
|
+
// Matched on the real resolved path rather than on a path shape: XDG_STATE_HOME
|
|
10
|
+
// and LOCALAPPDATA can point anywhere, so a pattern like `.local/state/mafdet`
|
|
11
|
+
// would miss a relocated directory - and matching a bare `mafdet` segment
|
|
12
|
+
// would wrongly refuse a project that happens to contain one.
|
|
13
|
+
import { realpathSync } from "node:fs";
|
|
14
|
+
import { homedir, platform } from "node:os";
|
|
15
|
+
import { relative, resolve, sep } from "node:path";
|
|
16
|
+
import { userConfigDir, userStateDir } from "../config/paths.js";
|
|
17
|
+
/**
|
|
18
|
+
* Every directory the CLI owns, resolved as far as it exists.
|
|
19
|
+
*
|
|
20
|
+
* Resolved eagerly so the comparison later is real-path against real-path: a
|
|
21
|
+
* symlinked home, or a state directory reached through one, must not slip past.
|
|
22
|
+
*
|
|
23
|
+
* `os` and `home` are injectable for the same reason they are in `paths.ts`:
|
|
24
|
+
* §20.12b requires all three platform layouts to be covered, and a function
|
|
25
|
+
* that can only ever describe the machine running it cannot be shown to.
|
|
26
|
+
* On macOS the two directories coincide, which is why the list is deduplicated
|
|
27
|
+
* rather than assumed to hold two entries.
|
|
28
|
+
*/
|
|
29
|
+
export function protectedDirectories(env = process.env, os = platform(), home = homedir()) {
|
|
30
|
+
const candidates = [
|
|
31
|
+
{
|
|
32
|
+
path: userStateDir(env, os, home),
|
|
33
|
+
reason: "it holds saved conversations from your other sessions",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: userConfigDir(env, os, home),
|
|
37
|
+
reason: "it holds this CLI's own settings",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const directories = [];
|
|
42
|
+
for (const entry of candidates) {
|
|
43
|
+
const path = realOrSelf(entry.path);
|
|
44
|
+
if (seen.has(path))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(path);
|
|
47
|
+
directories.push({ ...entry, path });
|
|
48
|
+
}
|
|
49
|
+
return directories;
|
|
50
|
+
}
|
|
51
|
+
function realOrSelf(path) {
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(resolve(path));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Not created yet. The lexical form is still worth refusing, so that the
|
|
57
|
+
// rule holds before the first session is ever written.
|
|
58
|
+
return resolve(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Is this path inside one of the CLI's own directories?
|
|
63
|
+
*
|
|
64
|
+
* `relative()` rather than a prefix test: a sibling named `mafdet-notes` must
|
|
65
|
+
* not be caught by a `mafdet` prefix, and on win32 `relative` compares
|
|
66
|
+
* case-insensitively, which is the behaviour we want there.
|
|
67
|
+
*/
|
|
68
|
+
export function protectedBy(absolutePath, directories) {
|
|
69
|
+
const target = resolve(absolutePath);
|
|
70
|
+
for (const directory of directories) {
|
|
71
|
+
const rel = relative(directory.path, target);
|
|
72
|
+
const inside = rel === "" ||
|
|
73
|
+
(!rel.startsWith(`..${sep}`) && rel !== ".." && !rel.startsWith("/"));
|
|
74
|
+
if (inside)
|
|
75
|
+
return directory;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProtectedDirectory } from "./protected-dirs.js";
|
|
1
2
|
/** Plan §7.4. */
|
|
2
3
|
export declare const MAX_FILE_BYTES: number;
|
|
3
4
|
export declare const MAX_TOOL_OUTPUT_BYTES: number;
|
|
@@ -36,13 +37,23 @@ export interface FileRead {
|
|
|
36
37
|
*/
|
|
37
38
|
export declare class Workspace {
|
|
38
39
|
readonly root: string;
|
|
40
|
+
/** The CLI's own directories, which the agent may never read. */
|
|
41
|
+
private readonly protectedDirs;
|
|
39
42
|
private constructor();
|
|
40
|
-
static open(root: string): Workspace;
|
|
43
|
+
static open(root: string, protectedDirs?: readonly ProtectedDirectory[]): Workspace;
|
|
41
44
|
/**
|
|
42
45
|
* Validate a model-supplied path. Throws unless it lands inside the
|
|
43
46
|
* workspace and is not on the sensitive list.
|
|
44
47
|
*/
|
|
45
48
|
resolvePath(input: string): ResolvedPath;
|
|
49
|
+
/**
|
|
50
|
+
* Is this absolute path one of the CLI's own directories?
|
|
51
|
+
*
|
|
52
|
+
* Exposed so the directory walk can skip them, rather than relying on them
|
|
53
|
+
* happening to start with a dot - which is true on Linux and Windows and
|
|
54
|
+
* false on macOS.
|
|
55
|
+
*/
|
|
56
|
+
isOwnedByCli(absolutePath: string): boolean;
|
|
46
57
|
/** Read a text file, refusing binaries and anything oversized. */
|
|
47
58
|
readTextFile(input: string): FileRead;
|
|
48
59
|
/** True when the path is inside and allowed; never throws. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/filesystem/workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/filesystem/workspace.ts"],"names":[],"mappings":"AAgBA,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAG7B,iBAAiB;AACjB,eAAO,MAAM,cAAc,QAAa,CAAC;AACzC,eAAO,MAAM,qBAAqB,QAAc,CAAC;AAEjD,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,IAAwB;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAS;IAElB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAHhC,OAAO;IAMP,MAAM,CAAC,IAAI,CACT,IAAI,EAAE,MAAM,EACZ,aAAa,GAAE,SAAS,kBAAkB,EAA2B,GACpE,SAAS;IAaZ;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY;IA+CxC;;;;;;OAMG;IACH,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI3C,kEAAkE;IAClE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ;IA8BrC,8DAA8D;IAC9D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAS9B,OAAO,CAAC,YAAY;IAYpB;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;CAuBlC;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAEvD;AAMD,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE;IACL,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA,WAAW,CA0Db;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CACxB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvC,WAAW,CAsBb"}
|
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { unifiedDiff } from "./diff.js";
|
|
5
5
|
import { ExitCode } from "../output/exit-codes.js";
|
|
6
|
+
import { protectedBy, protectedDirectories, } from "./protected-dirs.js";
|
|
6
7
|
import { classifySensitivePath } from "./sensitive-files.js";
|
|
7
8
|
/** Plan §7.4. */
|
|
8
9
|
export const MAX_FILE_BYTES = 512 * 1024;
|
|
@@ -32,10 +33,14 @@ export class WorkspaceError extends Error {
|
|
|
32
33
|
*/
|
|
33
34
|
export class Workspace {
|
|
34
35
|
root;
|
|
35
|
-
|
|
36
|
+
protectedDirs;
|
|
37
|
+
constructor(root,
|
|
38
|
+
/** The CLI's own directories, which the agent may never read. */
|
|
39
|
+
protectedDirs) {
|
|
36
40
|
this.root = root;
|
|
41
|
+
this.protectedDirs = protectedDirs;
|
|
37
42
|
}
|
|
38
|
-
static open(root) {
|
|
43
|
+
static open(root, protectedDirs = protectedDirectories()) {
|
|
39
44
|
let real;
|
|
40
45
|
try {
|
|
41
46
|
real = realpathSync(resolve(root));
|
|
@@ -46,7 +51,7 @@ export class Workspace {
|
|
|
46
51
|
if (!statSync(real).isDirectory()) {
|
|
47
52
|
throw new WorkspaceError(`workspace root is not a directory: ${root}`);
|
|
48
53
|
}
|
|
49
|
-
return new Workspace(real);
|
|
54
|
+
return new Workspace(real, protectedDirs);
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* Validate a model-supplied path. Throws unless it lands inside the
|
|
@@ -69,6 +74,12 @@ export class Workspace {
|
|
|
69
74
|
const real = this.realpathOfNearestExisting(candidate);
|
|
70
75
|
this.assertInside(real.path, input);
|
|
71
76
|
const rel = toPosix(relative(this.root, real.exists ? real.path : candidate));
|
|
77
|
+
// Checked on the resolved absolute path, before the name-based rules: a
|
|
78
|
+
// session transcript is not recognisable by its filename.
|
|
79
|
+
const owned = protectedBy(real.exists ? real.path : candidate, this.protectedDirs);
|
|
80
|
+
if (owned !== undefined) {
|
|
81
|
+
throw new WorkspaceError(`refusing to touch ${rel}: that is the Mafdet CLI's own directory and ${owned.reason}.`);
|
|
82
|
+
}
|
|
72
83
|
const verdict = classifySensitivePath(rel);
|
|
73
84
|
if (verdict.denied) {
|
|
74
85
|
throw new WorkspaceError(`refusing to touch ${rel}: ${verdict.reason}. Authorise that exact path if you really need it.`);
|
|
@@ -79,6 +90,16 @@ export class Workspace {
|
|
|
79
90
|
exists: real.exists,
|
|
80
91
|
};
|
|
81
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Is this absolute path one of the CLI's own directories?
|
|
95
|
+
*
|
|
96
|
+
* Exposed so the directory walk can skip them, rather than relying on them
|
|
97
|
+
* happening to start with a dot - which is true on Linux and Windows and
|
|
98
|
+
* false on macOS.
|
|
99
|
+
*/
|
|
100
|
+
isOwnedByCli(absolutePath) {
|
|
101
|
+
return protectedBy(absolutePath, this.protectedDirs) !== undefined;
|
|
102
|
+
}
|
|
82
103
|
/** Read a text file, refusing binaries and anything oversized. */
|
|
83
104
|
readTextFile(input) {
|
|
84
105
|
const target = this.resolvePath(input);
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAmDlD,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyGpE"}
|