@plinth-music/cli 0.14.0 → 0.15.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/CHANGELOG.md +28 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +252 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.15.0 - 2026-08-31
|
|
6
|
+
|
|
7
|
+
One code PR since `0.14.0` (`v0.14.0..f293dd0`, #153). A minor because sync gains a
|
|
8
|
+
refusal: there is now a case where Plinth declines to upload a file.
|
|
9
|
+
|
|
10
|
+
- ⚠️ **A memory file that looks like it carries a credential is held back instead of
|
|
11
|
+
synced.** The file is never modified and nothing is lost - it stays on your machine
|
|
12
|
+
until you decide. Plinth is not approved to store credentials, so it will not take one
|
|
13
|
+
without you saying so explicitly. Sharing one with your own team is not the problem and
|
|
14
|
+
the message does not treat it as one.
|
|
15
|
+
- **You clear it in one of two ways:** take the line out, or mark it `plinth-allow-secret`
|
|
16
|
+
on the line itself. The marker sits in the file where your teammates can see it, so an
|
|
17
|
+
allow is a decision on the record rather than a setting nobody can find.
|
|
18
|
+
- **`plinth status` names the file, the line and the remedy**, and the daemon logs it. The
|
|
19
|
+
finding is local only - it is never posted to your team, because "there is a key at line
|
|
20
|
+
42 of workspace-memory.md" is a pointer straight at the key.
|
|
21
|
+
- **It matches vendor-prefixed shapes only** (AWS, GitHub, Slack, Stripe, Google,
|
|
22
|
+
OpenAI/Anthropic, private-key blocks, JWTs, signed URLs). Recall is deliberately low:
|
|
23
|
+
this stops a recognisable token going up by accident, and it is not a guarantee against
|
|
24
|
+
a determined leak. Measured against every real memory file on the first workspace
|
|
25
|
+
(81,897 bytes across 12 files): no false positives.
|
|
26
|
+
- Covers `workspace-memory.md`, `user-memory.md` and `artists/<slug>/memory.md`.
|
|
27
|
+
|
|
28
|
+
⚠️ **The check runs under the daemon only.** `plinth sync` is pull-only and never scans, so
|
|
29
|
+
a workspace with no daemon running is not protected.
|
|
30
|
+
|
|
31
|
+
The desktop app pins `0.14.0` exact and picks none of this up until its own release repins.
|
|
32
|
+
|
|
5
33
|
## 0.14.0 - 2026-08-31
|
|
6
34
|
|
|
7
35
|
One code PR since `0.13.0` (`v0.13.0..50ab852`, #150; #149 was 0.13.0's own History row).
|
package/dist/build-stamp.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.
|
|
7587
|
+
version: "0.15.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -30541,6 +30541,108 @@ function calendarNoticeLine(kind) {
|
|
|
30541
30541
|
return `[${token} reason=${kind}]`;
|
|
30542
30542
|
}
|
|
30543
30543
|
|
|
30544
|
+
// src/lib/context/secret-scan.ts
|
|
30545
|
+
var HOLD_ON_SECRET = true;
|
|
30546
|
+
var ALLOW_SECRET_MARKER = "plinth-allow-secret";
|
|
30547
|
+
var SECRET_HELD_REASON = "possible_secret";
|
|
30548
|
+
var SECRET_RULES = [
|
|
30549
|
+
{
|
|
30550
|
+
id: "aws-access-key-id",
|
|
30551
|
+
label: "an AWS access key id",
|
|
30552
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/
|
|
30553
|
+
},
|
|
30554
|
+
{
|
|
30555
|
+
id: "github-token",
|
|
30556
|
+
label: "a GitHub token",
|
|
30557
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/
|
|
30558
|
+
},
|
|
30559
|
+
{
|
|
30560
|
+
id: "slack-token",
|
|
30561
|
+
label: "a Slack token",
|
|
30562
|
+
pattern: /\bxox[baprse]-[0-9A-Za-z-]{10,}/
|
|
30563
|
+
},
|
|
30564
|
+
{
|
|
30565
|
+
id: "google-api-key",
|
|
30566
|
+
label: "a Google API key",
|
|
30567
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/
|
|
30568
|
+
},
|
|
30569
|
+
{
|
|
30570
|
+
id: "anthropic-openai-key",
|
|
30571
|
+
label: "an Anthropic or OpenAI API key",
|
|
30572
|
+
pattern: /\bsk-(?:ant-|proj-)?(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[0-9])[A-Za-z0-9_-]{20,}/
|
|
30573
|
+
},
|
|
30574
|
+
{
|
|
30575
|
+
id: "stripe-secret-key",
|
|
30576
|
+
label: "a Stripe secret key",
|
|
30577
|
+
pattern: /\b[sr]k_live_[0-9A-Za-z]{16,}\b/
|
|
30578
|
+
},
|
|
30579
|
+
{
|
|
30580
|
+
id: "private-key-block",
|
|
30581
|
+
label: "a private key block",
|
|
30582
|
+
pattern: /-----BEGIN(?: [A-Z]+)* PRIVATE KEY-----/
|
|
30583
|
+
},
|
|
30584
|
+
{
|
|
30585
|
+
id: "jwt",
|
|
30586
|
+
label: "a JSON Web Token",
|
|
30587
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}/
|
|
30588
|
+
},
|
|
30589
|
+
{
|
|
30590
|
+
id: "signed-url",
|
|
30591
|
+
label: "a signed URL carrying its own credential",
|
|
30592
|
+
pattern: /[?&](?:X-Amz-Signature|X-Goog-Signature|Signature|sig)=[A-Za-z0-9%._-]{16,}/i
|
|
30593
|
+
}
|
|
30594
|
+
];
|
|
30595
|
+
function redactMatch(match) {
|
|
30596
|
+
const prefix = match.length >= 12 ? match.slice(0, 4) : "";
|
|
30597
|
+
return `${prefix}… ${match.length} characters`;
|
|
30598
|
+
}
|
|
30599
|
+
function lineIsAllowed(line) {
|
|
30600
|
+
return line.toLowerCase().includes(ALLOW_SECRET_MARKER);
|
|
30601
|
+
}
|
|
30602
|
+
function scanForSecrets(raw) {
|
|
30603
|
+
const findings = [];
|
|
30604
|
+
const lines = raw.split(`
|
|
30605
|
+
`);
|
|
30606
|
+
for (let i = 0;i < lines.length; i++) {
|
|
30607
|
+
const line = lines[i] ?? "";
|
|
30608
|
+
if (lineIsAllowed(line))
|
|
30609
|
+
continue;
|
|
30610
|
+
for (const rule of SECRET_RULES) {
|
|
30611
|
+
const m = rule.pattern.exec(line);
|
|
30612
|
+
if (!m)
|
|
30613
|
+
continue;
|
|
30614
|
+
findings.push({
|
|
30615
|
+
line: i + 1,
|
|
30616
|
+
ruleId: rule.id,
|
|
30617
|
+
label: rule.label,
|
|
30618
|
+
excerpt: redactMatch(m[0])
|
|
30619
|
+
});
|
|
30620
|
+
break;
|
|
30621
|
+
}
|
|
30622
|
+
}
|
|
30623
|
+
return findings;
|
|
30624
|
+
}
|
|
30625
|
+
var MAX_NAMED_SECRET_FINDINGS = 3;
|
|
30626
|
+
function secretHoldReason(findings, hold = HOLD_ON_SECRET) {
|
|
30627
|
+
const named = findings.slice(0, MAX_NAMED_SECRET_FINDINGS);
|
|
30628
|
+
const rest = findings.length - named.length;
|
|
30629
|
+
const detail = named.map((f) => `line ${f.line} looks like ${f.label} (${f.excerpt})`).join("; ");
|
|
30630
|
+
const more = rest > 0 ? `, and ${rest} more` : "";
|
|
30631
|
+
return `${detail.charAt(0).toUpperCase()}${detail.slice(1)}${more}. ` + `Plinth is not approved to store credentials yet, so ` + (hold ? `this file stays on this machine until you say otherwise. ` + `Remove the value, or add ${ALLOW_SECRET_MARKER} to that line to ` + `store it anyway. ` : `it has already reached Plinth on this sync. Rotate it if it is ` + `live, then remove the value — or add ${ALLOW_SECRET_MARKER} to that ` + `line to stop being told. `) + `Nothing is changed in your file either way.`;
|
|
30632
|
+
}
|
|
30633
|
+
function secretHoldSummary(findings) {
|
|
30634
|
+
const n = findings.length;
|
|
30635
|
+
return `${n} possible credential${n === 1 ? "" : "s"} found`;
|
|
30636
|
+
}
|
|
30637
|
+
function secretHoldMessage(filename, findings, hold = HOLD_ON_SECRET) {
|
|
30638
|
+
const verb = hold ? "was not synced" : "was synced";
|
|
30639
|
+
return `${filename} ${verb}. ${secretHoldReason(findings, hold)}`;
|
|
30640
|
+
}
|
|
30641
|
+
function secretHoldRecord(findings, hold = HOLD_ON_SECRET) {
|
|
30642
|
+
const verdict = hold ? "so this file was not synced" : "and it synced anyway";
|
|
30643
|
+
return `${secretHoldSummary(findings)}, ${verdict}. ${secretHoldReason(findings, hold)}`;
|
|
30644
|
+
}
|
|
30645
|
+
|
|
30544
30646
|
// src/lib/rejected.ts
|
|
30545
30647
|
var PERMANENT_REJECT_REASONS = new Set(["bad_request", "invalid_frontmatter"]);
|
|
30546
30648
|
function isPermanentReject(err) {
|
|
@@ -30690,6 +30792,45 @@ function clearReportedSafeFileRejected(state, relPath) {
|
|
|
30690
30792
|
return state;
|
|
30691
30793
|
return clearFileRejected(state, relPath);
|
|
30692
30794
|
}
|
|
30795
|
+
function recordMemorySecretHeld(args) {
|
|
30796
|
+
let path3;
|
|
30797
|
+
try {
|
|
30798
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
30799
|
+
} catch {
|
|
30800
|
+
path3 = args.absolutePath;
|
|
30801
|
+
}
|
|
30802
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
30803
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30804
|
+
target: "entity",
|
|
30805
|
+
op: "upsert",
|
|
30806
|
+
path: path3,
|
|
30807
|
+
reason: SECRET_HELD_REASON,
|
|
30808
|
+
message: args.record,
|
|
30809
|
+
rejected_at: args.now().toISOString()
|
|
30810
|
+
}));
|
|
30811
|
+
}
|
|
30812
|
+
function secretHoldRecordFor(state, workspaceSlug, absolutePath) {
|
|
30813
|
+
let path3;
|
|
30814
|
+
try {
|
|
30815
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30816
|
+
} catch {
|
|
30817
|
+
path3 = absolutePath;
|
|
30818
|
+
}
|
|
30819
|
+
return (state.rejected ?? []).find((o) => o.target === "entity" && o.path === path3 && o.reason === SECRET_HELD_REASON);
|
|
30820
|
+
}
|
|
30821
|
+
function clearMemorySecretHeld(state, workspaceSlug, absolutePath) {
|
|
30822
|
+
let path3;
|
|
30823
|
+
try {
|
|
30824
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30825
|
+
} catch {
|
|
30826
|
+
path3 = absolutePath;
|
|
30827
|
+
}
|
|
30828
|
+
const list = state.rejected ?? [];
|
|
30829
|
+
const next = list.filter((o) => !(o.target === "entity" && o.path === path3 && o.reason === SECRET_HELD_REASON));
|
|
30830
|
+
if (next.length === list.length)
|
|
30831
|
+
return state;
|
|
30832
|
+
return commitState(state, { ...state, rejected: next });
|
|
30833
|
+
}
|
|
30693
30834
|
|
|
30694
30835
|
// src/lib/context/workspace-claude-md.ts
|
|
30695
30836
|
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
@@ -36186,6 +36327,41 @@ async function reconcileFilesSubtree(args) {
|
|
|
36186
36327
|
|
|
36187
36328
|
// src/lib/artist-memory/sync.ts
|
|
36188
36329
|
import { relative as relative9 } from "node:path";
|
|
36330
|
+
|
|
36331
|
+
// src/lib/context/secret-gate.ts
|
|
36332
|
+
function applySecretGate(args) {
|
|
36333
|
+
const hold = args.hold ?? HOLD_ON_SECRET;
|
|
36334
|
+
const release = () => ({
|
|
36335
|
+
state: clearMemorySecretHeld(args.state, args.workspaceSlug, args.absolutePath),
|
|
36336
|
+
held: false,
|
|
36337
|
+
findings: []
|
|
36338
|
+
});
|
|
36339
|
+
if (args.alreadyAcked)
|
|
36340
|
+
return release();
|
|
36341
|
+
const findings = scanForSecrets(args.body);
|
|
36342
|
+
if (findings.length === 0)
|
|
36343
|
+
return release();
|
|
36344
|
+
const record2 = secretHoldRecord(findings, hold);
|
|
36345
|
+
const standing = secretHoldRecordFor(args.state, args.workspaceSlug, args.absolutePath);
|
|
36346
|
+
if (standing?.message === record2) {
|
|
36347
|
+
return { state: args.state, held: hold, findings };
|
|
36348
|
+
}
|
|
36349
|
+
args.output?.warn(secretHoldMessage(args.filename, findings, hold));
|
|
36350
|
+
args.output?.log(`[${args.logTag}_SECRET_HELD ws=${args.workspaceSlug} ` + `findings=${findings.length} held=${hold}]`);
|
|
36351
|
+
return {
|
|
36352
|
+
state: recordMemorySecretHeld({
|
|
36353
|
+
state: args.state,
|
|
36354
|
+
workspaceSlug: args.workspaceSlug,
|
|
36355
|
+
absolutePath: args.absolutePath,
|
|
36356
|
+
record: record2,
|
|
36357
|
+
now: args.now
|
|
36358
|
+
}),
|
|
36359
|
+
held: hold,
|
|
36360
|
+
findings
|
|
36361
|
+
};
|
|
36362
|
+
}
|
|
36363
|
+
|
|
36364
|
+
// src/lib/artist-memory/sync.ts
|
|
36189
36365
|
var MAX_MEMORY_PUSH_ATTEMPTS = 3;
|
|
36190
36366
|
var ENTITY_TYPE = "artists";
|
|
36191
36367
|
function conflictSlug(slug) {
|
|
@@ -36293,11 +36469,26 @@ async function pushArtistMemory(args) {
|
|
|
36293
36469
|
}
|
|
36294
36470
|
const localHash = stableHash(local);
|
|
36295
36471
|
const runPatch = async () => {
|
|
36296
|
-
|
|
36472
|
+
let base2 = args.mutex ? loadState(workspaceSlug) : state;
|
|
36297
36473
|
const entry2 = getEntryBySlug(base2, ENTITY_TYPE, slug);
|
|
36298
36474
|
if (!entry2)
|
|
36299
36475
|
return { state: base2, outcome: { kind: "ignored", slug } };
|
|
36300
|
-
|
|
36476
|
+
const alreadyAcked = entry2.memory_hash === localHash;
|
|
36477
|
+
const gate = applySecretGate({
|
|
36478
|
+
state: base2,
|
|
36479
|
+
workspaceSlug,
|
|
36480
|
+
absolutePath: artistMemoryFilePath(workspaceSlug, slug),
|
|
36481
|
+
filename: `artists/${slug}/${ARTIST_MEMORY_ANCHOR}`,
|
|
36482
|
+
body: local,
|
|
36483
|
+
alreadyAcked,
|
|
36484
|
+
output,
|
|
36485
|
+
logTag: "ARTIST_MEMORY",
|
|
36486
|
+
now
|
|
36487
|
+
});
|
|
36488
|
+
base2 = gate.state;
|
|
36489
|
+
if (gate.held)
|
|
36490
|
+
return { state: base2, outcome: { kind: "secret_held", slug } };
|
|
36491
|
+
if (alreadyAcked) {
|
|
36301
36492
|
return { state: base2, outcome: { kind: "noop", slug } };
|
|
36302
36493
|
}
|
|
36303
36494
|
const acked = entry2.memory_hash;
|
|
@@ -38581,11 +38772,26 @@ async function pushWorkspaceMemory(args) {
|
|
|
38581
38772
|
}
|
|
38582
38773
|
const localHash = stableHash(local);
|
|
38583
38774
|
const acked = getWorkspaceMemoryState(state);
|
|
38584
|
-
|
|
38585
|
-
|
|
38775
|
+
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
38776
|
+
const gate = applySecretGate({
|
|
38777
|
+
state,
|
|
38778
|
+
workspaceSlug,
|
|
38779
|
+
absolutePath: contextFilePath(workspaceSlug, WORKSPACE_MEMORY_FILENAME),
|
|
38780
|
+
filename: WORKSPACE_MEMORY_FILENAME,
|
|
38781
|
+
body: local,
|
|
38782
|
+
alreadyAcked,
|
|
38783
|
+
output,
|
|
38784
|
+
logTag: "WORKSPACE_MEMORY",
|
|
38785
|
+
now
|
|
38786
|
+
});
|
|
38787
|
+
const gated = gate.state;
|
|
38788
|
+
if (gate.held)
|
|
38789
|
+
return { state: gated, outcome: { kind: "secret_held" } };
|
|
38790
|
+
if (alreadyAcked) {
|
|
38791
|
+
return { state: gated, outcome: { kind: "noop" } };
|
|
38586
38792
|
}
|
|
38587
38793
|
if (extractWorkspaceMemory(local) === null && (acked?.updated_at ?? null) === null) {
|
|
38588
|
-
const next2 = persistMemory(
|
|
38794
|
+
const next2 = persistMemory(gated, { body_hash: localHash, updated_at: null });
|
|
38589
38795
|
return { state: next2, outcome: { kind: "noop" } };
|
|
38590
38796
|
}
|
|
38591
38797
|
const res = await putWorkspaceMemory({
|
|
@@ -38596,7 +38802,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38596
38802
|
fetchImpl
|
|
38597
38803
|
});
|
|
38598
38804
|
if (res.ok) {
|
|
38599
|
-
const next2 = persistMemory(
|
|
38805
|
+
const next2 = persistMemory(gated, {
|
|
38600
38806
|
body_hash: localHash,
|
|
38601
38807
|
updated_at: res.row.updated_at
|
|
38602
38808
|
});
|
|
@@ -38605,7 +38811,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38605
38811
|
}
|
|
38606
38812
|
if (res.code === 413) {
|
|
38607
38813
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38608
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
38814
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38609
38815
|
}
|
|
38610
38816
|
{
|
|
38611
38817
|
const probe2 = await getWorkspaceMemory({ baseUrl, pat, fetchImpl });
|
|
@@ -38618,7 +38824,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38618
38824
|
fetchImpl
|
|
38619
38825
|
});
|
|
38620
38826
|
if (retry.ok) {
|
|
38621
|
-
const next2 = persistMemory(
|
|
38827
|
+
const next2 = persistMemory(gated, {
|
|
38622
38828
|
body_hash: localHash,
|
|
38623
38829
|
updated_at: retry.row.updated_at
|
|
38624
38830
|
});
|
|
@@ -38627,7 +38833,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38627
38833
|
}
|
|
38628
38834
|
if (retry.code === 413) {
|
|
38629
38835
|
output?.warn(`${WORKSPACE_MEMORY_FILENAME} is over the ${Math.round(MAX_WORKSPACE_MEMORY_BYTES / 1024)}KB cap — not synced.`);
|
|
38630
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
38836
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38631
38837
|
}
|
|
38632
38838
|
}
|
|
38633
38839
|
}
|
|
@@ -38639,7 +38845,7 @@ async function pushWorkspaceMemory(args) {
|
|
|
38639
38845
|
contents: local,
|
|
38640
38846
|
side: "local"
|
|
38641
38847
|
});
|
|
38642
|
-
const next = await restoreFromCloud2(args);
|
|
38848
|
+
const next = await restoreFromCloud2({ ...args, state: gated });
|
|
38643
38849
|
output?.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
38644
38850
|
return { state: next, outcome: { kind: "conflict", stashPath } };
|
|
38645
38851
|
}
|
|
@@ -38851,11 +39057,29 @@ async function pushSingleton(descriptor, args) {
|
|
|
38851
39057
|
}
|
|
38852
39058
|
const localHash = stableHash(local);
|
|
38853
39059
|
const acked = descriptor.getState(state);
|
|
38854
|
-
|
|
38855
|
-
|
|
39060
|
+
const alreadyAcked = Boolean(acked && acked.body_hash === localHash);
|
|
39061
|
+
let gated = state;
|
|
39062
|
+
if (descriptor.secretScanned) {
|
|
39063
|
+
const gate = applySecretGate({
|
|
39064
|
+
state,
|
|
39065
|
+
workspaceSlug,
|
|
39066
|
+
absolutePath: filePath(descriptor, workspaceSlug),
|
|
39067
|
+
filename: descriptor.filename,
|
|
39068
|
+
body: local,
|
|
39069
|
+
alreadyAcked,
|
|
39070
|
+
output,
|
|
39071
|
+
logTag: descriptor.logTag,
|
|
39072
|
+
now
|
|
39073
|
+
});
|
|
39074
|
+
gated = gate.state;
|
|
39075
|
+
if (gate.held)
|
|
39076
|
+
return { state: gated, outcome: { kind: "secret_held" } };
|
|
39077
|
+
}
|
|
39078
|
+
if (alreadyAcked) {
|
|
39079
|
+
return { state: gated, outcome: { kind: "noop" } };
|
|
38856
39080
|
}
|
|
38857
39081
|
if (descriptor.extractActive(local) === null && (acked?.updated_at ?? null) === null) {
|
|
38858
|
-
const next2 = persist2(descriptor,
|
|
39082
|
+
const next2 = persist2(descriptor, gated, { body_hash: localHash, updated_at: null });
|
|
38859
39083
|
return { state: next2, outcome: { kind: "noop" } };
|
|
38860
39084
|
}
|
|
38861
39085
|
const res = await descriptor.api.put({
|
|
@@ -38866,13 +39090,13 @@ async function pushSingleton(descriptor, args) {
|
|
|
38866
39090
|
fetchImpl
|
|
38867
39091
|
});
|
|
38868
39092
|
if (res.ok) {
|
|
38869
|
-
const next2 = persist2(descriptor,
|
|
39093
|
+
const next2 = persist2(descriptor, gated, ackedFrom(res.row, localHash));
|
|
38870
39094
|
output?.log(`[${descriptor.logTag}_PUT ws=${workspaceSlug} is_empty=${res.row.is_empty}` + `${res.row.status ? ` status=${res.row.status}` : ""}]`);
|
|
38871
39095
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
38872
39096
|
}
|
|
38873
39097
|
if (res.code === 413) {
|
|
38874
39098
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
38875
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
39099
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38876
39100
|
}
|
|
38877
39101
|
{
|
|
38878
39102
|
const probe2 = await descriptor.api.get({ baseUrl, pat, fetchImpl });
|
|
@@ -38885,13 +39109,13 @@ async function pushSingleton(descriptor, args) {
|
|
|
38885
39109
|
fetchImpl
|
|
38886
39110
|
});
|
|
38887
39111
|
if (retry.ok) {
|
|
38888
|
-
const next2 = persist2(descriptor,
|
|
39112
|
+
const next2 = persist2(descriptor, gated, ackedFrom(retry.row, localHash));
|
|
38889
39113
|
output?.log(`[${descriptor.logTag}_PUT ws=${workspaceSlug} is_empty=${retry.row.is_empty}` + `${retry.row.status ? ` status=${retry.row.status}` : ""}]`);
|
|
38890
39114
|
return { state: next2, outcome: { kind: "pushed" } };
|
|
38891
39115
|
}
|
|
38892
39116
|
if (retry.code === 413) {
|
|
38893
39117
|
output?.warn(`${descriptor.filename} is over the ${Math.round(descriptor.maxBytes / 1024)}KB cap — not synced.`);
|
|
38894
|
-
return { state, outcome: { kind: "over_cap" } };
|
|
39118
|
+
return { state: gated, outcome: { kind: "over_cap" } };
|
|
38895
39119
|
}
|
|
38896
39120
|
}
|
|
38897
39121
|
}
|
|
@@ -38903,7 +39127,7 @@ async function pushSingleton(descriptor, args) {
|
|
|
38903
39127
|
contents: local,
|
|
38904
39128
|
side: "local"
|
|
38905
39129
|
});
|
|
38906
|
-
const next = await restoreFromCloud3(descriptor, args);
|
|
39130
|
+
const next = await restoreFromCloud3(descriptor, { ...args, state: gated });
|
|
38907
39131
|
output?.warn(`Conflict on ${descriptor.filename} -> stashed local edit to ${stashPath} (cloud-wins; cloud copy restored).`);
|
|
38908
39132
|
return { state: next, outcome: { kind: "conflict", stashPath } };
|
|
38909
39133
|
}
|
|
@@ -38925,6 +39149,7 @@ var USER_MEMORY_DESCRIPTOR = {
|
|
|
38925
39149
|
mutexKey: USER_MEMORY_MUTEX_KEY,
|
|
38926
39150
|
logTag: "USER_MEMORY",
|
|
38927
39151
|
maxBytes: MAX_USER_MEMORY_BYTES,
|
|
39152
|
+
secretScanned: true,
|
|
38928
39153
|
extractActive: extractUserMemory,
|
|
38929
39154
|
getState: getUserMemoryState,
|
|
38930
39155
|
setState: setUserMemoryState,
|
|
@@ -38957,6 +39182,7 @@ var VOICE_DESCRIPTOR = {
|
|
|
38957
39182
|
mutexKey: VOICE_MUTEX_KEY,
|
|
38958
39183
|
logTag: "VOICE",
|
|
38959
39184
|
maxBytes: MAX_USER_VOICE_BYTES,
|
|
39185
|
+
secretScanned: false,
|
|
38960
39186
|
extractActive: extractVoiceProfile,
|
|
38961
39187
|
getState: getUserVoiceState,
|
|
38962
39188
|
setState: setUserVoiceState,
|
|
@@ -42127,6 +42353,7 @@ function reportWorkspaceMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
42127
42353
|
spinner.warn(`Conflict on ${WORKSPACE_MEMORY_FILENAME} → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
42128
42354
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
42129
42355
|
return;
|
|
42356
|
+
case "secret_held":
|
|
42130
42357
|
case "over_cap":
|
|
42131
42358
|
case "noop":
|
|
42132
42359
|
return;
|
|
@@ -42143,6 +42370,7 @@ function reportUserMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
42143
42370
|
spinner.warn(`Conflict on ${USER_MEMORY_FILENAME} → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
42144
42371
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
42145
42372
|
return;
|
|
42373
|
+
case "secret_held":
|
|
42146
42374
|
case "over_cap":
|
|
42147
42375
|
case "noop":
|
|
42148
42376
|
return;
|
|
@@ -42195,6 +42423,7 @@ function reportArtistMemoryOutcome(spinner, slug, state, outcome) {
|
|
|
42195
42423
|
spinner.warn(`Conflict on artists/${outcome.slug}/memory.md → ${outcome.stashPath} (cloud-wins; local copy stashed)`);
|
|
42196
42424
|
spinner.update(`Watching ${slug} · ${summary}`);
|
|
42197
42425
|
return;
|
|
42426
|
+
case "secret_held":
|
|
42198
42427
|
case "not_found":
|
|
42199
42428
|
case "noop":
|
|
42200
42429
|
case "ignored":
|
|
@@ -42541,6 +42770,8 @@ function scanPendingWrites(args) {
|
|
|
42541
42770
|
continue;
|
|
42542
42771
|
if (Buffer.byteLength(local) > lane.maxBytes)
|
|
42543
42772
|
continue;
|
|
42773
|
+
if (reported.entities.has(lane.filename))
|
|
42774
|
+
continue;
|
|
42544
42775
|
const localHash = stableHash(local);
|
|
42545
42776
|
if (lane.acked && lane.acked.body_hash === localHash)
|
|
42546
42777
|
continue;
|
|
@@ -42566,6 +42797,8 @@ function scanPendingWrites(args) {
|
|
|
42566
42797
|
continue;
|
|
42567
42798
|
if (isPristineMemoryStub(local))
|
|
42568
42799
|
continue;
|
|
42800
|
+
if (reported.entities.has(`artists/${entry2.slug}/memory.md`))
|
|
42801
|
+
continue;
|
|
42569
42802
|
const localHash = stableHash(local);
|
|
42570
42803
|
if (entry2.memory_hash === localHash)
|
|
42571
42804
|
continue;
|