@indigoai-us/hq-cli 5.345.143 → 5.345.145
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 +90 -0
- package/dist/command-catalog.generated.d.ts +286 -6
- package/dist/command-catalog.generated.js +351 -6
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +1 -0
- package/dist/commands/agent-enroll.js +7 -0
- package/dist/commands/agents.d.ts +25 -3
- package/dist/commands/agents.js +57 -4
- package/dist/commands/api-keys.js +66 -8
- package/dist/commands/billing-sponsor.d.ts +4 -0
- package/dist/commands/billing-sponsor.js +48 -5
- package/dist/commands/billing-usage.d.ts +59 -0
- package/dist/commands/billing-usage.js +162 -0
- package/dist/commands/billing.d.ts +9 -0
- package/dist/commands/billing.js +29 -2
- package/dist/commands/core-checkpoint-native.d.ts +30 -0
- package/dist/commands/core-checkpoint-native.js +167 -10
- package/dist/commands/core-job-notify-native.js +53 -3
- package/dist/commands/core-jobs-native.d.ts +8 -0
- package/dist/commands/core-jobs-native.js +34 -2
- package/dist/commands/core-policy.js +76 -13
- package/dist/commands/library-add.d.ts +120 -0
- package/dist/commands/library-add.js +434 -0
- package/dist/commands/library-caption.d.ts +112 -0
- package/dist/commands/library-caption.js +393 -0
- package/dist/commands/library-link.d.ts +32 -0
- package/dist/commands/library-link.js +95 -0
- package/dist/commands/library.d.ts +72 -0
- package/dist/commands/library.js +527 -0
- package/dist/commands/meetings.d.ts +8 -0
- package/dist/commands/meetings.js +25 -2
- package/dist/commands/members.d.ts +20 -0
- package/dist/commands/members.js +51 -2
- package/dist/commands/storage-offload.d.ts +16 -2
- package/dist/commands/storage-offload.js +55 -4
- package/dist/commands/storage.d.ts +8 -1
- package/dist/commands/storage.js +6 -2
- package/dist/commands/whoami.js +9 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2410 -2063
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/billing/plan-lock.d.ts +15 -1
- package/dist/lib/billing/plan-lock.js +45 -5
- package/dist/lib/billing/usage-card.d.ts +82 -0
- package/dist/lib/billing/usage-card.js +198 -0
- package/dist/lib/billing/usage-cost.d.ts +214 -0
- package/dist/lib/billing/usage-cost.js +440 -0
- package/dist/lib/billing/usage-only.d.ts +87 -0
- package/dist/lib/billing/usage-only.js +262 -0
- package/dist/lib/billing/usage-rate-card.d.ts +51 -0
- package/dist/lib/billing/usage-rate-card.js +62 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/plan-limit-nag.d.ts +4 -6
- package/dist/lib/plan-limit-nag.js +19 -1
- package/dist/lib/provider-launcher.d.ts +22 -0
- package/dist/lib/provider-launcher.js +65 -0
- package/dist/lib/search-index/index.d.ts +2 -0
- package/dist/lib/search-index/index.js +34 -0
- package/dist/lib/search-index/max-doc-bytes.d.ts +2 -1
- package/dist/lib/search-index/max-doc-bytes.js +2 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +20 -3
- package/dist/utils/billing-gate.d.ts +19 -4
- package/dist/utils/billing-gate.js +25 -4
- package/dist/utils/plan-gate-error.d.ts +10 -0
- package/dist/utils/plan-gate-error.js +54 -0
- package/dist/utils/vault-api.js +4 -1
- package/package.json +2 -2
|
@@ -92,6 +92,14 @@ const SIBLING_LOCK_TTL_MS = 60 * 60 * 1000;
|
|
|
92
92
|
export const SIBLING_MAX_RUNTIME_MS = 30 * 60 * 1000;
|
|
93
93
|
/** Newest-wins cap so a wedged sibling cannot grow an unbounded queue. */
|
|
94
94
|
const MAX_PENDING_PAYLOADS = 50;
|
|
95
|
+
/**
|
|
96
|
+
* Queued payloads one sibling run may process, across its launch claim and its
|
|
97
|
+
* own queue drain. Each one runs the full upgrade flow, so a 50-deep queue in
|
|
98
|
+
* one run outlasts SIBLING_MAX_RUNTIME_MS and the grok turn cap; the run is
|
|
99
|
+
* then killed with work half done. The launcher re-queues the overflow for the
|
|
100
|
+
* next sibling instead.
|
|
101
|
+
*/
|
|
102
|
+
export const MAX_QUEUED_PAYLOADS_PER_RUN = 5;
|
|
95
103
|
class CheckpointUsageError extends Error {
|
|
96
104
|
}
|
|
97
105
|
/**
|
|
@@ -202,16 +210,37 @@ report instead of guessing.
|
|
|
202
210
|
skip silently on any failure. Hook or automation improvements go ONLY under
|
|
203
211
|
personal/hooks/ as proposals.
|
|
204
212
|
7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.
|
|
205
|
-
8. Write <runDir>/report.md
|
|
206
|
-
(paths), and what you skipped and
|
|
207
|
-
|
|
213
|
+
8. Write <runDir>/report.md NOW, before you touch any queued payload — full
|
|
214
|
+
prose: what you read, what you changed (paths), and what you skipped and
|
|
215
|
+
why. List every policy or knowledge file you
|
|
216
|
+
amended, superseded, merged or deleted with the reason, so a human can
|
|
208
217
|
audit and reverse it; a deletion you do not name in the report is a defect.
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
The run can be stopped at its time or turn cap at any point after this, so
|
|
219
|
+
this run's own report must already exist.
|
|
220
|
+
9. QUEUED PAYLOADS CLAIMED AT LAUNCH: the payload's pending_payloads array
|
|
221
|
+
holds checkpoints that were queued while an earlier sibling was busy; the
|
|
222
|
+
same records are in <runDir>/pending-claimed.jsonl. The launcher hands one
|
|
223
|
+
run at most <maxQueued> of them and has already put any others back in
|
|
224
|
+
workspace/checkpoints/sibling/pending.jsonl. For EACH record in
|
|
225
|
+
pending_payloads, in order, run steps 1-5 with that record as the payload:
|
|
226
|
+
upgrade the thread file named in its thread_path IN PLACE and drop -auto-
|
|
227
|
+
from its filename. If that -auto- file no longer exists, the record was
|
|
228
|
+
already processed; note it and move on. For a queued record, skip the
|
|
229
|
+
journal close in step 4 (it belongs to the parent session) and skip step 5b
|
|
230
|
+
(ontology); in steps 3 and 7, <company> means that record's own company, not
|
|
231
|
+
this run's. After EACH record, append one line to <runDir>/report.md naming
|
|
232
|
+
its thread and what you did, so a run stopped mid-queue shows how far it got.
|
|
233
|
+
If the array is empty, skip this step.
|
|
234
|
+
10. Drain the queue, still within the same limit of <maxQueued> queued records
|
|
235
|
+
per run counted across step 9 and this step: while
|
|
236
|
+
workspace/checkpoints/sibling/pending.jsonl exists and is non-empty and you
|
|
237
|
+
have processed fewer than <maxQueued> queued records, claim
|
|
211
238
|
it atomically by renaming it aside —
|
|
212
239
|
mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl
|
|
213
|
-
(N counting up from 2) — and process
|
|
214
|
-
|
|
240
|
+
(N counting up from 2) — and process its records as in step 9. When you
|
|
241
|
+
reach the limit, append every record of that claimed file you did not
|
|
242
|
+
process back to workspace/checkpoints/sibling/pending.jsonl (append with >>,
|
|
243
|
+
never overwrite) so the next sibling takes it, and record that in the report.
|
|
215
244
|
NEVER read the queue and truncate it in place: a payload appended between
|
|
216
245
|
your read and the truncate is lost, and its checkpoint is never enriched.
|
|
217
246
|
`;
|
|
@@ -241,7 +270,8 @@ export function renderSiblingPrompt(runDir, payloadPath, options = {}) {
|
|
|
241
270
|
.replaceAll("<ontologyStep>", () => ontologyStep)
|
|
242
271
|
.replaceAll("<runDir>", () => runDir)
|
|
243
272
|
.replaceAll("<payloadPath>", () => payloadPath)
|
|
244
|
-
.replaceAll("<hqRoot>", () => options.hqRoot ?? "")
|
|
273
|
+
.replaceAll("<hqRoot>", () => options.hqRoot ?? "")
|
|
274
|
+
.replaceAll("<maxQueued>", () => String(MAX_QUEUED_PAYLOADS_PER_RUN));
|
|
245
275
|
}
|
|
246
276
|
function collect(value, previous) {
|
|
247
277
|
return [...previous, value];
|
|
@@ -1137,7 +1167,8 @@ function appendPending(pendingPath, payload) {
|
|
|
1137
1167
|
* describes is never enriched. Renaming is atomic, so this run receives exactly
|
|
1138
1168
|
* the file that existed at the moment of the claim while concurrent callers go
|
|
1139
1169
|
* on appending to a fresh queue. The claimed file is left in the run directory
|
|
1140
|
-
* as an audit trail and as a recovery point if this sibling dies
|
|
1170
|
+
* as an audit trail and as a recovery point if this sibling dies: the next
|
|
1171
|
+
* launch re-queues its unprocessed records through recoverOrphanedClaims.
|
|
1141
1172
|
*/
|
|
1142
1173
|
function claimPending(pendingPath, claimPath) {
|
|
1143
1174
|
try {
|
|
@@ -1162,6 +1193,124 @@ function claimPending(pendingPath, claimPath) {
|
|
|
1162
1193
|
}
|
|
1163
1194
|
return entries;
|
|
1164
1195
|
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Keep the oldest {@link MAX_QUEUED_PAYLOADS_PER_RUN} claimed payloads for this
|
|
1198
|
+
* run and append the rest back to the queue, so a deep queue is worked off
|
|
1199
|
+
* across several runs instead of being lost when one run hits its cap. The
|
|
1200
|
+
* claim file is rewritten to hold only what this run owns.
|
|
1201
|
+
*/
|
|
1202
|
+
export function limitClaimedPayloads(entries, claimPath, pendingPath, limit = MAX_QUEUED_PAYLOADS_PER_RUN) {
|
|
1203
|
+
if (entries.length <= limit)
|
|
1204
|
+
return entries;
|
|
1205
|
+
const kept = entries.slice(0, limit);
|
|
1206
|
+
const overflow = entries.slice(limit);
|
|
1207
|
+
// Re-queue first: if the rewrite below fails, every record is still on disk.
|
|
1208
|
+
fs.appendFileSync(pendingPath, overflow.map((entry) => `${JSON.stringify(entry)}\n`).join(""));
|
|
1209
|
+
fs.writeFileSync(claimPath, kept.map((entry) => `${JSON.stringify(entry)}\n`).join(""));
|
|
1210
|
+
return kept;
|
|
1211
|
+
}
|
|
1212
|
+
/** Times one queued record may be recovered from a finished run before it is dropped. */
|
|
1213
|
+
export const MAX_CLAIM_RECOVERIES = 3;
|
|
1214
|
+
/**
|
|
1215
|
+
* Put back the queued records a finished sibling claimed but never processed.
|
|
1216
|
+
*
|
|
1217
|
+
* A sibling stopped at its wall-clock or turn cap leaves its claim files
|
|
1218
|
+
* (pending-claimed*.jsonl) in its run directory. A record whose thread file
|
|
1219
|
+
* still exists at its thread_path was not upgraded (the sibling renames the
|
|
1220
|
+
* -auto- file when it is done), so it is appended back to pending.jsonl for
|
|
1221
|
+
* this launch to claim. Only runs whose process is gone are read, records
|
|
1222
|
+
* already queued are not duplicated, and each record is recovered at most
|
|
1223
|
+
* {@link MAX_CLAIM_RECOVERIES} times so one the sibling keeps skipping cannot
|
|
1224
|
+
* cycle forever. Each claim file read is renamed to *.recovered.
|
|
1225
|
+
*/
|
|
1226
|
+
export function recoverOrphanedClaims(siblingRoot, pendingPath, liveRoot) {
|
|
1227
|
+
let runDirs;
|
|
1228
|
+
try {
|
|
1229
|
+
runDirs = fs.readdirSync(siblingRoot).filter((entry) => entry !== "pending.jsonl");
|
|
1230
|
+
}
|
|
1231
|
+
catch {
|
|
1232
|
+
return 0;
|
|
1233
|
+
}
|
|
1234
|
+
const queuedThreads = new Set();
|
|
1235
|
+
try {
|
|
1236
|
+
for (const line of fs.readFileSync(pendingPath, "utf8").split("\n")) {
|
|
1237
|
+
if (!line.trim())
|
|
1238
|
+
continue;
|
|
1239
|
+
try {
|
|
1240
|
+
const threadPath = JSON.parse(line).thread_path;
|
|
1241
|
+
if (typeof threadPath === "string")
|
|
1242
|
+
queuedThreads.add(threadPath);
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
// A torn line names no thread.
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
// No queue yet.
|
|
1251
|
+
}
|
|
1252
|
+
const recovered = [];
|
|
1253
|
+
let count = 0;
|
|
1254
|
+
for (const runDir of runDirs.sort()) {
|
|
1255
|
+
const absolute = path.join(siblingRoot, runDir);
|
|
1256
|
+
let claimFiles;
|
|
1257
|
+
try {
|
|
1258
|
+
claimFiles = fs.readdirSync(absolute).filter((entry) => /^pending-claimed(?:-\d+)?\.jsonl$/u.test(entry));
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (claimFiles.length === 0)
|
|
1264
|
+
continue;
|
|
1265
|
+
let pid = Number.NaN;
|
|
1266
|
+
try {
|
|
1267
|
+
pid = Number.parseInt(fs.readFileSync(path.join(absolute, "sibling.pid"), "utf8").trim(), 10);
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
// A run that never recorded a PID cannot still be running.
|
|
1271
|
+
}
|
|
1272
|
+
if (Number.isSafeInteger(pid) && pid > 0 && processAlive(pid))
|
|
1273
|
+
continue;
|
|
1274
|
+
for (const claimFile of claimFiles.sort()) {
|
|
1275
|
+
const claimPath = path.join(absolute, claimFile);
|
|
1276
|
+
let content;
|
|
1277
|
+
try {
|
|
1278
|
+
content = fs.readFileSync(claimPath, "utf8");
|
|
1279
|
+
}
|
|
1280
|
+
catch {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
for (const line of content.split("\n")) {
|
|
1284
|
+
if (!line.trim())
|
|
1285
|
+
continue;
|
|
1286
|
+
let entry;
|
|
1287
|
+
try {
|
|
1288
|
+
entry = JSON.parse(line);
|
|
1289
|
+
}
|
|
1290
|
+
catch {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
const threadPath = entry.thread_path;
|
|
1294
|
+
if (typeof threadPath !== "string" || !threadPath || queuedThreads.has(threadPath))
|
|
1295
|
+
continue;
|
|
1296
|
+
if (!fs.existsSync(path.resolve(liveRoot, threadPath)))
|
|
1297
|
+
continue;
|
|
1298
|
+
const attempts = typeof entry.claim_recoveries === "number" ? entry.claim_recoveries : 0;
|
|
1299
|
+
if (attempts >= MAX_CLAIM_RECOVERIES)
|
|
1300
|
+
continue;
|
|
1301
|
+
queuedThreads.add(threadPath);
|
|
1302
|
+
recovered.push(`${JSON.stringify({ ...entry, claim_recoveries: attempts + 1 })}\n`);
|
|
1303
|
+
count += 1;
|
|
1304
|
+
}
|
|
1305
|
+
// Re-queue before retiring the claim file, so a failure leaves the
|
|
1306
|
+
// records on disk; a repeat read is deduplicated by thread_path above.
|
|
1307
|
+
if (recovered.length > 0)
|
|
1308
|
+
fs.appendFileSync(pendingPath, recovered.splice(0).join(""));
|
|
1309
|
+
fs.renameSync(claimPath, `${claimPath}.recovered`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return count;
|
|
1313
|
+
}
|
|
1165
1314
|
async function startSibling(liveRoot, input, threadPath, backend) {
|
|
1166
1315
|
const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
|
|
1167
1316
|
const siblingRoot = path.join(liveRoot, "workspace", "checkpoints", "sibling");
|
|
@@ -1183,9 +1332,17 @@ async function startSibling(liveRoot, input, threadPath, backend) {
|
|
|
1183
1332
|
printResult(`checkpoint: sibling busy — payload queued (${depth} pending)`);
|
|
1184
1333
|
return;
|
|
1185
1334
|
}
|
|
1335
|
+
// Before this run's directory exists, so only earlier runs are read.
|
|
1336
|
+
try {
|
|
1337
|
+
recoverOrphanedClaims(siblingRoot, pendingPath, liveRoot);
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
// Recovery is best effort; the claim files stay on disk for the next launch.
|
|
1341
|
+
}
|
|
1186
1342
|
const runDir = path.join(siblingRoot, `${formatTimestamp(new Date())}-${summarySlug(input.summary ?? "checkpoint")}`);
|
|
1187
1343
|
fs.mkdirSync(runDir, { recursive: true });
|
|
1188
|
-
|
|
1344
|
+
const claimPath = path.join(runDir, "pending-claimed.jsonl");
|
|
1345
|
+
payload.pending_payloads = limitClaimedPayloads(claimPending(pendingPath, claimPath), claimPath, pendingPath);
|
|
1189
1346
|
if (!payload.company) {
|
|
1190
1347
|
let queued;
|
|
1191
1348
|
try {
|
|
@@ -195,6 +195,40 @@ function emailFromWhoami(output) {
|
|
|
195
195
|
}
|
|
196
196
|
return "";
|
|
197
197
|
}
|
|
198
|
+
const EMAIL_RECIPIENT = /^[^\s@]+@[^\s@]+$/;
|
|
199
|
+
const COGNITO_SUB = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
200
|
+
/**
|
|
201
|
+
* Outpost jobs may record `owner` as the owner's Cognito user id (the box's
|
|
202
|
+
* OUTPOST_USER_ID; the reconciler matches company jobs on it). `hq dm` cannot
|
|
203
|
+
* address a Cognito id: any token that is not an email or prs_/agt_ uid is
|
|
204
|
+
* treated as a channel name and bounces with "No channel named …". Map the id
|
|
205
|
+
* to the signed-in identity's email when `hq whoami --json` reports the same
|
|
206
|
+
* subject, directly or as the human an Outpost session acts for.
|
|
207
|
+
*/
|
|
208
|
+
function resolveCognitoOwner(owner, deps) {
|
|
209
|
+
if (!deps.commandExists("hq"))
|
|
210
|
+
return "";
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
const raw = deps.run("hq", ["whoami", "--json"], { cwd: deps.cwd, env: deps.env, captureStdout: true }).stdout;
|
|
214
|
+
const value = JSON.parse(raw);
|
|
215
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
216
|
+
return "";
|
|
217
|
+
parsed = value;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
const target = owner.toLowerCase();
|
|
223
|
+
for (const [subKey, emailKey] of [["sub", "email"], ["delegatedSub", "delegatedEmail"]]) {
|
|
224
|
+
const sub = parsed[subKey];
|
|
225
|
+
const email = parsed[emailKey];
|
|
226
|
+
if (typeof sub === "string" && sub.toLowerCase() === target
|
|
227
|
+
&& typeof email === "string" && EMAIL_RECIPIENT.test(email))
|
|
228
|
+
return email;
|
|
229
|
+
}
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
198
232
|
function normalizedEpoch(value) {
|
|
199
233
|
if (!/^-?\d+$/.test(value))
|
|
200
234
|
throw new Error(`invalid HQ_JOB_NOTIFY_NOW_EPOCH: ${value}`);
|
|
@@ -260,6 +294,17 @@ export function runJobNotify(argv, options = {}) {
|
|
|
260
294
|
log("no owner recipient — cannot dm (logged only); never fails the job");
|
|
261
295
|
deps.appendFile(deliveryLog, '{"delivered":false,"reason":"missing_owner"}\n');
|
|
262
296
|
}
|
|
297
|
+
let recipient = owner;
|
|
298
|
+
const cognitoOwner = Boolean(owner) && COGNITO_SUB.test(owner);
|
|
299
|
+
if (cognitoOwner) {
|
|
300
|
+
recipient = resolveCognitoOwner(owner, deps);
|
|
301
|
+
if (recipient)
|
|
302
|
+
log(`owner ${owner} is a Cognito user id — delivering to ${recipient}`);
|
|
303
|
+
else {
|
|
304
|
+
log(`owner ${owner} is a Cognito user id that hq dm cannot address and hq whoami does not report it — set the job owner to an email or prs_ uid (logged only); never fails the job`);
|
|
305
|
+
deps.appendFile(deliveryLog, '{"delivered":false,"reason":"unresolved_owner_id"}\n');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
263
308
|
let previous = {};
|
|
264
309
|
if (deps.exists(stateFile))
|
|
265
310
|
previous = JSON.parse(deps.readFile(stateFile));
|
|
@@ -306,18 +351,22 @@ export function runJobNotify(argv, options = {}) {
|
|
|
306
351
|
log("skip dm send — missing owner");
|
|
307
352
|
deliverRc = 1;
|
|
308
353
|
}
|
|
354
|
+
else if (!recipient) {
|
|
355
|
+
log("skip dm send — owner id has no dm recipient");
|
|
356
|
+
deliverRc = 1;
|
|
357
|
+
}
|
|
309
358
|
else if (!dmBin) {
|
|
310
359
|
log("skip dm send — hq CLI not found");
|
|
311
360
|
deliverRc = 1;
|
|
312
361
|
}
|
|
313
362
|
else {
|
|
314
363
|
const args = dmBin === "hq"
|
|
315
|
-
? ["dm",
|
|
316
|
-
: [
|
|
364
|
+
? ["dm", recipient, headline, "--details", details]
|
|
365
|
+
: [recipient, headline, details];
|
|
317
366
|
deliverRc = deps.run(dmBin, args, { cwd: deps.cwd, env: deps.env, captureStdout: false, logPath: deliveryLog }).code;
|
|
318
367
|
if (deliverRc === 0) {
|
|
319
368
|
delivered = 1;
|
|
320
|
-
log(`dm delivered to ${
|
|
369
|
+
log(`dm delivered to ${recipient} kind=${kind} job=${parsed.jobId}`);
|
|
321
370
|
}
|
|
322
371
|
else
|
|
323
372
|
log(`dm delivery failed rc=${deliverRc} job=${parsed.jobId} (logged; not failing run)`);
|
|
@@ -344,6 +393,7 @@ export function runJobNotify(argv, options = {}) {
|
|
|
344
393
|
delivered: delivered === 1,
|
|
345
394
|
kind,
|
|
346
395
|
owner,
|
|
396
|
+
...(cognitoOwner ? { recipient: recipient || null } : {}),
|
|
347
397
|
channel_unimplemented: channelUnimplemented === 1,
|
|
348
398
|
deliver_rc: deliverRc,
|
|
349
399
|
}, "compact"));
|
|
@@ -15,6 +15,14 @@ export type ReconcileOptions = {
|
|
|
15
15
|
fetcher?: typeof fetch;
|
|
16
16
|
now?: () => Date;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* A system unit with User=ec2-user (outpost-jobs-reconcile.service) or a cron
|
|
20
|
+
* job has no login session, so XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS are
|
|
21
|
+
* unset and `systemctl --user` fails with "Failed to connect to bus: No medium
|
|
22
|
+
* found" (feedback_6ce027d7). When the lingering user manager's runtime dir
|
|
23
|
+
* exists, point systemctl at it. Caller-provided values are kept.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withUserBusEnvironment(env: NodeJS.ProcessEnv, uid: number | undefined, runtimeBase?: string, exists?: (file: string) => boolean): NodeJS.ProcessEnv;
|
|
18
26
|
export declare function runReconcile(argv: string[], rootArg?: string, options?: ReconcileOptions): Promise<number>;
|
|
19
27
|
export declare function registerCoreJobsNativeCommand(core: Command): void;
|
|
20
28
|
export declare function rawArgumentsForCommand(command: Command, fallback: string[], raw?: string[]): string[];
|
|
@@ -379,9 +379,36 @@ Env / overrides (tests):
|
|
|
379
379
|
|
|
380
380
|
Exit: 0 success; 1 partial failures; 2 usage/config.
|
|
381
381
|
`;
|
|
382
|
-
|
|
382
|
+
/**
|
|
383
|
+
* A system unit with User=ec2-user (outpost-jobs-reconcile.service) or a cron
|
|
384
|
+
* job has no login session, so XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS are
|
|
385
|
+
* unset and `systemctl --user` fails with "Failed to connect to bus: No medium
|
|
386
|
+
* found" (feedback_6ce027d7). When the lingering user manager's runtime dir
|
|
387
|
+
* exists, point systemctl at it. Caller-provided values are kept.
|
|
388
|
+
*/
|
|
389
|
+
export function withUserBusEnvironment(env, uid, runtimeBase = "/run/user", exists = fs.existsSync) {
|
|
390
|
+
if (uid === undefined)
|
|
391
|
+
return env;
|
|
392
|
+
const next = { ...env };
|
|
393
|
+
if (!next.XDG_RUNTIME_DIR) {
|
|
394
|
+
const directory = path.posix.join(runtimeBase, String(uid));
|
|
395
|
+
if (!exists(directory))
|
|
396
|
+
return next;
|
|
397
|
+
next.XDG_RUNTIME_DIR = directory;
|
|
398
|
+
}
|
|
399
|
+
if (!next.DBUS_SESSION_BUS_ADDRESS && exists(path.posix.join(next.XDG_RUNTIME_DIR, "bus"))) {
|
|
400
|
+
next.DBUS_SESSION_BUS_ADDRESS = `unix:path=${next.XDG_RUNTIME_DIR}/bus`;
|
|
401
|
+
}
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
404
|
+
function schedulerCommandResult(command, args) {
|
|
405
|
+
const env = withUserBusEnvironment(process.env, typeof process.getuid === "function" ? process.getuid() : undefined, process.env.HQ_JOB_USER_RUNTIME_BASE || "/run/user");
|
|
406
|
+
return commandResult(command, args, env);
|
|
407
|
+
}
|
|
408
|
+
function commandResult(command, args, env = process.env) {
|
|
383
409
|
const result = spawnSync(command, args, {
|
|
384
410
|
encoding: "utf8",
|
|
411
|
+
env,
|
|
385
412
|
stdio: ["ignore", "pipe", "pipe"],
|
|
386
413
|
});
|
|
387
414
|
return {
|
|
@@ -486,6 +513,11 @@ function resolveBoxOwners(options) {
|
|
|
486
513
|
email = shellText(record.email ?? user?.email);
|
|
487
514
|
add(email);
|
|
488
515
|
add(shellText(record.personUid ?? record.person_uid ?? record.uid));
|
|
516
|
+
// The person (or the human an Outpost session acts for) by Cognito
|
|
517
|
+
// id and email, so a job owner written either way matches the box.
|
|
518
|
+
add(shellText(record.sub));
|
|
519
|
+
add(shellText(record.delegatedEmail));
|
|
520
|
+
add(shellText(record.delegatedSub));
|
|
489
521
|
}
|
|
490
522
|
if (!email) {
|
|
491
523
|
const humanEmail = /.*Email:\s*(.*)/i.exec(who)?.[1]
|
|
@@ -757,7 +789,7 @@ function systemctlResult(args, dryRun, command, options) {
|
|
|
757
789
|
return { status: 127, stdout: "", stderr: "scheduler stub was requested but no injected runner was supplied" };
|
|
758
790
|
}
|
|
759
791
|
try {
|
|
760
|
-
const runner = options.schedulerRunner ??
|
|
792
|
+
const runner = options.schedulerRunner ?? schedulerCommandResult;
|
|
761
793
|
return runner(command, ["--user", ...args]);
|
|
762
794
|
}
|
|
763
795
|
catch (error) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { policyValidatorDiagnostic } from '../lib/policy-validator.js';
|
|
7
|
+
import { evaluateTrigger } from '../utils/core-eval-trigger.js';
|
|
7
8
|
import { packageRoot, resolveLiveRoot } from '../utils/hq-roots.js';
|
|
8
9
|
import { describeCoreCommand } from './core-command-catalog.js';
|
|
9
10
|
import { runPolicyFrontmatter as runPolicyFrontmatterImpl } from './core-policy-frontmatter.js';
|
|
@@ -165,12 +166,46 @@ function frontmatterScalar(file, key) {
|
|
|
165
166
|
value = value.replace(/^["']|["']$/g, '');
|
|
166
167
|
if (key === 'source' || key === 'created')
|
|
167
168
|
value = value.replaceAll('"', '');
|
|
168
|
-
if (key === '
|
|
169
|
+
if (key === 'updated')
|
|
170
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
171
|
+
if (key === 'created' || key === 'updated')
|
|
169
172
|
return value.slice(0, 10);
|
|
170
173
|
return value;
|
|
171
174
|
}
|
|
172
175
|
return '';
|
|
173
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* The `when:` expression as inject-policy-on-trigger.sh reads it: the last
|
|
179
|
+
* `when:` line in the frontmatter, trailing ` # comment` removed, surrounding
|
|
180
|
+
* quotes stripped. Returns the expression when the canonical trigger evaluator
|
|
181
|
+
* (the one behind eval-trigger.sh) rejects it as malformed, so the policy can
|
|
182
|
+
* never match an event; undefined when it parses or there is no `when:`.
|
|
183
|
+
*/
|
|
184
|
+
function malformedTrigger(text) {
|
|
185
|
+
const lines = text.split('\n');
|
|
186
|
+
if (!(lines[0] ?? '').startsWith('---'))
|
|
187
|
+
return undefined;
|
|
188
|
+
let depth = 0;
|
|
189
|
+
let when;
|
|
190
|
+
for (const rawLine of lines) {
|
|
191
|
+
const line = rawLine.replace(/\r$/, '');
|
|
192
|
+
if (/^---[ \t]*$/.test(line)) {
|
|
193
|
+
depth += 1;
|
|
194
|
+
if (depth === 2)
|
|
195
|
+
break;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (depth !== 1 || !line.startsWith('when:'))
|
|
199
|
+
continue;
|
|
200
|
+
when = line
|
|
201
|
+
.replace(/^when:[ \t]*/, '')
|
|
202
|
+
.replace(/[ \t]+#.*$/, '')
|
|
203
|
+
.replace(/^["']|["']$/g, '');
|
|
204
|
+
}
|
|
205
|
+
if (when === undefined || when.trim() === '')
|
|
206
|
+
return undefined;
|
|
207
|
+
return evaluateTrigger(when, '') === 2 ? when : undefined;
|
|
208
|
+
}
|
|
174
209
|
function ruleBody(file) {
|
|
175
210
|
const text = fileText(file) ?? '';
|
|
176
211
|
const lines = text.split('\n');
|
|
@@ -438,8 +473,18 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
438
473
|
const enforcement = frontmatterScalar(file, 'enforcement') || 'unset';
|
|
439
474
|
const source = frontmatterScalar(file, 'source');
|
|
440
475
|
const version = frontmatterScalar(file, 'version');
|
|
441
|
-
const
|
|
476
|
+
const mtimeEpoch = statMtimeSeconds(file);
|
|
477
|
+
const createdEpoch = epochFromDate(frontmatterScalar(file, 'created')) || mtimeEpoch || 0;
|
|
442
478
|
const ageDays = Math.trunc((now - createdEpoch) / 86400);
|
|
479
|
+
// Usage history counts only from the last change to the policy (and so
|
|
480
|
+
// to its trigger): `updated:` frontmatter or the file mtime, whichever is
|
|
481
|
+
// newer. A trigger that was malformed and was just repaired is not judged
|
|
482
|
+
// on the period when it could not fire. An mtime later than the report
|
|
483
|
+
// clock (pinned clock or clock skew) is not a usable change date.
|
|
484
|
+
const changedEpoch = mtimeEpoch !== undefined && mtimeEpoch <= now ? mtimeEpoch : 0;
|
|
485
|
+
const clockEpoch = Math.max(createdEpoch, epochFromDate(frontmatterScalar(file, 'updated')), changedEpoch);
|
|
486
|
+
const clockDays = Math.trunc((now - clockEpoch) / 86400);
|
|
487
|
+
const brokenTrigger = malformedTrigger(text);
|
|
443
488
|
const fired = usage.counts.get(id) ?? 0;
|
|
444
489
|
const lastEpoch = usage.newest.get(id);
|
|
445
490
|
const lastFired = lastEpoch === undefined ? null : new Date(lastEpoch * 1000).toISOString().slice(0, 10);
|
|
@@ -448,24 +493,30 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
448
493
|
const refs = refSummary(file, root);
|
|
449
494
|
const singleIncident = ['user-correction', 'session-learning', 'back-pressure-failure'].includes(source) && (version || '1') === '1';
|
|
450
495
|
const classes = [];
|
|
451
|
-
if (
|
|
452
|
-
|
|
496
|
+
if (brokenTrigger !== undefined) {
|
|
497
|
+
// A policy whose `when:` cannot parse never had a chance to fire, so
|
|
498
|
+
// its usage history says nothing about whether the rule is needed. It
|
|
499
|
+
// is reported as broken and never offered for retirement.
|
|
500
|
+
classes.push('malformed-trigger');
|
|
501
|
+
}
|
|
502
|
+
else if (fired === 0) {
|
|
503
|
+
const comparison = bashTestGreaterOrEqual(clockDays, parsed.minAge, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 173, platform);
|
|
453
504
|
stderr += comparison.stderr;
|
|
454
505
|
if (comparison.value && usage.anyFiles)
|
|
455
506
|
classes.push('never-fired');
|
|
456
507
|
}
|
|
457
|
-
if (fired > 0) {
|
|
458
|
-
const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 174, platform);
|
|
508
|
+
if (brokenTrigger === undefined && fired > 0) {
|
|
509
|
+
const comparison = bashTestGreaterOrEqual(Math.min(lastAge, clockDays), parsed.dormantDays, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 174, platform);
|
|
459
510
|
stderr += comparison.stderr;
|
|
460
511
|
if (comparison.value)
|
|
461
512
|
classes.push('dormant');
|
|
462
513
|
}
|
|
463
|
-
if (refs.stale.length > 0)
|
|
514
|
+
if (brokenTrigger === undefined && refs.stale.length > 0)
|
|
464
515
|
classes.push('stale-refs');
|
|
465
|
-
if (refs.renamed.length > 0)
|
|
516
|
+
if (brokenTrigger === undefined && refs.renamed.length > 0)
|
|
466
517
|
classes.push('renamed-refs');
|
|
467
|
-
if (singleIncident) {
|
|
468
|
-
const comparison = bashTestGreaterOrEqual(
|
|
518
|
+
if (brokenTrigger === undefined && singleIncident) {
|
|
519
|
+
const comparison = bashTestGreaterOrEqual(clockDays, parsed.incidentDays, formerScriptPath(POLICY_AGE_REPORT_SCRIPT), 178, platform);
|
|
469
520
|
stderr += comparison.stderr;
|
|
470
521
|
if (comparison.value && fired <= 1)
|
|
471
522
|
classes.push('single-incident-old');
|
|
@@ -484,6 +535,7 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
484
535
|
retrieved,
|
|
485
536
|
refs_judged: refs.judged,
|
|
486
537
|
renamed_refs: refs.renamed,
|
|
538
|
+
...(brokenTrigger === undefined ? {} : { malformed_trigger: brokenTrigger }),
|
|
487
539
|
});
|
|
488
540
|
}
|
|
489
541
|
}
|
|
@@ -491,10 +543,14 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
491
543
|
const selected = parsed.candidates ? rows.filter((row) => row.candidate_classes.length > 0) : rows;
|
|
492
544
|
return policyResult(0, `${JSON.stringify(selected)}\n`, stderr);
|
|
493
545
|
}
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
const
|
|
546
|
+
const malformed = rows.filter((row) => row.malformed_trigger !== undefined);
|
|
547
|
+
const never = rows.filter((row) => row.fired === 0 && row.malformed_trigger === undefined).length;
|
|
548
|
+
const staleTotal = rows.filter((row) => row.stale_refs.length > 0 && row.malformed_trigger === undefined).length;
|
|
549
|
+
const candidateCount = rows.filter((row) => row.candidate_classes.length > 0 && row.malformed_trigger === undefined).length;
|
|
497
550
|
let stdout = `policy-age-report: ${rows.length} policies, ${never} never fired, ${staleTotal} with stale references, ${candidateCount} retirement candidates (min-age ${parsed.minAge}d, dormant ${parsed.dormantDays}d, incident ${parsed.incidentDays}d)\n`;
|
|
551
|
+
if (malformed.length > 0) {
|
|
552
|
+
stdout += `policy-age-report: ${malformed.length} with a malformed when: trigger that cannot fire (fix the trigger; not retirement candidates): ${malformed.map((row) => row.id).sort().join(', ')}\n`;
|
|
553
|
+
}
|
|
498
554
|
if (!parsed.candidates)
|
|
499
555
|
stdout += ' (add --candidates to list them, --json for machine-readable rows)\n';
|
|
500
556
|
else {
|
|
@@ -805,6 +861,13 @@ function autoRetire(options, root, cwd, env, now) {
|
|
|
805
861
|
continue;
|
|
806
862
|
}
|
|
807
863
|
let why = '';
|
|
864
|
+
if (classes.split(',').includes('malformed-trigger')) {
|
|
865
|
+
const expression = candidate.malformed_trigger ?? '';
|
|
866
|
+
reportText += `- needs-fix (not retired) **${candidate.id}** (${candidate.path}): malformed when: trigger, so it can never fire: ${expression}\n`;
|
|
867
|
+
stderr += `policy-retire: ${candidate.id} (${candidate.path}) has a malformed when: trigger and was not retired; fix it: ${expression}\n`;
|
|
868
|
+
needfix += 1;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
808
871
|
if (classes.split(',').includes('renamed-refs')) {
|
|
809
872
|
reportText += `- needs-fix (not retired) **${candidate.id}** (${candidate.path}): referenced file was renamed: ${candidate.renamed_refs.join(', ')}\n`;
|
|
810
873
|
needfix += 1;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { type CloudAssetMediaKind } from "@indigoai-us/hq-cloud";
|
|
3
|
+
import type { LibraryCaptionNote, LibraryCaptionRequest } from "./library-caption.js";
|
|
4
|
+
export interface LibraryCompany {
|
|
5
|
+
uid: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LibrarySource {
|
|
9
|
+
kind: "file" | "url";
|
|
10
|
+
/** The argument as given. */
|
|
11
|
+
input: string;
|
|
12
|
+
/** File name the placeholder is written under. */
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
export interface OpenedSource {
|
|
16
|
+
stream: Readable;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PutResponse {
|
|
20
|
+
status: number;
|
|
21
|
+
headers: Record<string, string | undefined>;
|
|
22
|
+
}
|
|
23
|
+
export interface LibraryAddDeps {
|
|
24
|
+
hqRoot: string;
|
|
25
|
+
now: () => Date;
|
|
26
|
+
/** Resolve the slug to an active company membership. */
|
|
27
|
+
resolveCompany: (slug: string) => Promise<LibraryCompany>;
|
|
28
|
+
/** POST /v1/files/presign. */
|
|
29
|
+
presign: (body: Record<string, unknown>) => Promise<Response>;
|
|
30
|
+
/** Open a fresh byte stream of a source. Called once to hash and once to upload. */
|
|
31
|
+
openSource: (source: LibrarySource) => Promise<OpenedSource>;
|
|
32
|
+
/** PUT `body` (exactly `size` bytes) to a presigned URL with `headers`. */
|
|
33
|
+
put: (url: string, headers: Record<string, string>, body: Readable, size: number) => Promise<PutResponse>;
|
|
34
|
+
writePlaceholder: (file: string, text: string) => Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Caption one written image or video placeholder (US-006). Must not throw:
|
|
37
|
+
* a caption failure never fails the upload. Absent = no captioning.
|
|
38
|
+
*/
|
|
39
|
+
caption?: (request: LibraryCaptionRequest) => Promise<LibraryCaptionNote>;
|
|
40
|
+
}
|
|
41
|
+
export interface LibraryAddOptions {
|
|
42
|
+
company?: string;
|
|
43
|
+
to?: string;
|
|
44
|
+
tags: string[];
|
|
45
|
+
title?: string;
|
|
46
|
+
note?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface LibraryAddItem {
|
|
49
|
+
source: string;
|
|
50
|
+
ok: boolean;
|
|
51
|
+
/** HQ-relative placeholder path (set when written). */
|
|
52
|
+
placeholderPath?: string;
|
|
53
|
+
originalPath?: string;
|
|
54
|
+
key?: string;
|
|
55
|
+
size?: number;
|
|
56
|
+
sha256?: string;
|
|
57
|
+
/** False when the vault already held the bytes. */
|
|
58
|
+
uploaded?: boolean;
|
|
59
|
+
error?: string;
|
|
60
|
+
/** AI caption outcome, set when captioning ran for this item. */
|
|
61
|
+
caption?: LibraryCaptionNote;
|
|
62
|
+
}
|
|
63
|
+
export interface LibraryAddResult {
|
|
64
|
+
company: string;
|
|
65
|
+
items: LibraryAddItem[];
|
|
66
|
+
}
|
|
67
|
+
export declare function contentTypeFor(name: string, served?: string): string;
|
|
68
|
+
export declare function mediaKindFor(contentType: string): CloudAssetMediaKind;
|
|
69
|
+
/** A file name safe to place in one directory: no separators, no control chars. */
|
|
70
|
+
export declare function safeAssetName(raw: string): string;
|
|
71
|
+
/** Classify an argument: an http(s) URL, otherwise a local file. */
|
|
72
|
+
export declare function parseSource(input: string): LibrarySource;
|
|
73
|
+
/**
|
|
74
|
+
* The library directory, HQ-relative. `--to` resolves against the HQ root and
|
|
75
|
+
* must be inside `companies/<co>/`, so `hq storage fetch` can find the
|
|
76
|
+
* company vault from the placeholder path. The default is
|
|
77
|
+
* `companies/<co>/library`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveLibraryDir(hqRoot: string, options: Pick<LibraryAddOptions, "company" | "to">): {
|
|
80
|
+
slug: string;
|
|
81
|
+
dirRel: string;
|
|
82
|
+
dirAbs: string;
|
|
83
|
+
};
|
|
84
|
+
export declare function offloadTimestamp(now: Date): string;
|
|
85
|
+
/**
|
|
86
|
+
* Testable core. Every source is hashed first, then one presign request
|
|
87
|
+
* covers all of them (hq-pro rate-limits presign per user). A 402 for the
|
|
88
|
+
* request refuses every source and writes nothing; a per-key refusal fails
|
|
89
|
+
* only that source.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runLibraryAdd(inputs: readonly string[], options: LibraryAddOptions, deps: LibraryAddDeps): Promise<LibraryAddResult>;
|
|
92
|
+
export interface LibraryTagResult {
|
|
93
|
+
placeholderPath: string;
|
|
94
|
+
tags: string[];
|
|
95
|
+
changed: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Add and remove tags on a library placeholder. The placeholder is parsed and
|
|
99
|
+
* re-rendered by hq-cloud's placeholder writer, then replaced atomically
|
|
100
|
+
* (temp file + rename), so a sync watcher never reads a half-written file.
|
|
101
|
+
*/
|
|
102
|
+
export declare function runLibraryTag(hqRoot: string, target: string, edits: {
|
|
103
|
+
add: string[];
|
|
104
|
+
remove: string[];
|
|
105
|
+
}, now?: () => Date): Promise<LibraryTagResult>;
|
|
106
|
+
/** Replace a placeholder atomically (temp file + rename). */
|
|
107
|
+
export declare function replacePlaceholderText(file: string, text: string): Promise<void>;
|
|
108
|
+
/** Open a local file or GET a URL as a Node stream. */
|
|
109
|
+
export declare function openLibrarySource(source: LibrarySource): Promise<OpenedSource>;
|
|
110
|
+
/**
|
|
111
|
+
* Stream a PUT with an explicit Content-Length: S3 presigned PUTs refuse
|
|
112
|
+
* chunked transfer encoding, which fetch would use for a stream body.
|
|
113
|
+
*/
|
|
114
|
+
export declare function streamPut(url: string, headers: Record<string, string>, body: Readable, size: number): Promise<PutResponse>;
|
|
115
|
+
export declare const defaultLibraryIo: {
|
|
116
|
+
openSource: typeof openLibrarySource;
|
|
117
|
+
put: typeof streamPut;
|
|
118
|
+
writePlaceholder: (file: string, text: string) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=library-add.d.ts.map
|