@plinth-music/cli 0.11.0 → 0.12.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 +207 -0
- package/README.md +10 -5
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2142 -1172
- package/package.json +1 -1
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.12.0",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -23651,17 +23651,6 @@ function writeMirrorFile(workspaceSlug, entityType, entitySlug, content) {
|
|
|
23651
23651
|
throw err;
|
|
23652
23652
|
}
|
|
23653
23653
|
}
|
|
23654
|
-
function deleteMirrorFile(workspaceSlug, entityType, entitySlug) {
|
|
23655
|
-
const target = entityFilePath(workspaceSlug, entityType, entitySlug);
|
|
23656
|
-
try {
|
|
23657
|
-
unlinkSync(target);
|
|
23658
|
-
return true;
|
|
23659
|
-
} catch (err) {
|
|
23660
|
-
if (err?.code === "ENOENT")
|
|
23661
|
-
return false;
|
|
23662
|
-
throw err;
|
|
23663
|
-
}
|
|
23664
|
-
}
|
|
23665
23654
|
var ARTIST_MEMORY_ANCHOR = "memory.md";
|
|
23666
23655
|
function artistMemoryFilePath(workspaceSlug, entitySlug) {
|
|
23667
23656
|
assertSlug("entity slug", entitySlug);
|
|
@@ -23746,6 +23735,49 @@ function ensureFilesSubtree(workspaceSlug) {
|
|
|
23746
23735
|
const target = filesSubtreeRoot(workspaceSlug);
|
|
23747
23736
|
return mkdirSync(target, { recursive: true, mode: 448 }) !== undefined;
|
|
23748
23737
|
}
|
|
23738
|
+
function ensureEntityFolders(workspaceSlug) {
|
|
23739
|
+
const created = [];
|
|
23740
|
+
for (const type of allEntityTypes()) {
|
|
23741
|
+
try {
|
|
23742
|
+
const target = entityFolderRoot(workspaceSlug, type);
|
|
23743
|
+
if (mkdirSync(target, { recursive: true, mode: 448 }) !== undefined) {
|
|
23744
|
+
created.push(type);
|
|
23745
|
+
}
|
|
23746
|
+
} catch {}
|
|
23747
|
+
}
|
|
23748
|
+
return created;
|
|
23749
|
+
}
|
|
23750
|
+
function observeMirrorFolders(workspaceSlug, names) {
|
|
23751
|
+
const seen = [];
|
|
23752
|
+
for (const name of names) {
|
|
23753
|
+
let target;
|
|
23754
|
+
try {
|
|
23755
|
+
target = entityFolderRoot(workspaceSlug, name);
|
|
23756
|
+
} catch {
|
|
23757
|
+
continue;
|
|
23758
|
+
}
|
|
23759
|
+
try {
|
|
23760
|
+
const raw = readdirSync(target);
|
|
23761
|
+
const entries = name === FILES_SUBTREE_DIR ? raw : raw.filter((e) => !e.startsWith("."));
|
|
23762
|
+
seen.push({ name, empty: entries.length === 0 });
|
|
23763
|
+
} catch (err) {
|
|
23764
|
+
if (err?.code === "ENOENT")
|
|
23765
|
+
continue;
|
|
23766
|
+
seen.push({ name, empty: false });
|
|
23767
|
+
}
|
|
23768
|
+
}
|
|
23769
|
+
return seen;
|
|
23770
|
+
}
|
|
23771
|
+
function entityFolderRoot(workspaceSlug, name) {
|
|
23772
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23773
|
+
const target = resolve(ws, name);
|
|
23774
|
+
const prefix = ws.endsWith(sep) ? ws : ws + sep;
|
|
23775
|
+
if (!target.startsWith(prefix)) {
|
|
23776
|
+
throw new MirrorPathError(`Resolved folder path escapes the workspace mirror`);
|
|
23777
|
+
}
|
|
23778
|
+
assertNoSymlinkedComponent(ws, target);
|
|
23779
|
+
return target;
|
|
23780
|
+
}
|
|
23749
23781
|
function filesSubtreeRoot(workspaceSlug) {
|
|
23750
23782
|
const ws = workspaceMirrorPath(workspaceSlug);
|
|
23751
23783
|
const target = join2(ws, FILES_SUBTREE_DIR);
|
|
@@ -24863,11 +24895,20 @@ var WhoamiSchema = exports_external.object({
|
|
|
24863
24895
|
workspace_id: exports_external.string().uuid(),
|
|
24864
24896
|
workspace_slug: exports_external.string().min(1),
|
|
24865
24897
|
workspace_name: exports_external.string().min(1),
|
|
24898
|
+
role: exports_external.string().min(1).nullable().optional(),
|
|
24866
24899
|
profile: exports_external.object({
|
|
24867
24900
|
email: exports_external.string().email(),
|
|
24868
|
-
display_name: exports_external.string().nullable().optional()
|
|
24901
|
+
display_name: exports_external.string().nullable().optional(),
|
|
24902
|
+
onboarding_completed_at: exports_external.string().nullable().optional()
|
|
24869
24903
|
})
|
|
24870
24904
|
});
|
|
24905
|
+
var MemberPrincipalSchema = exports_external.object({
|
|
24906
|
+
email: exports_external.string().email().max(254),
|
|
24907
|
+
display_name: exports_external.string().min(1).max(200).nullable().optional(),
|
|
24908
|
+
role: exports_external.string().min(1).max(64).nullable().optional(),
|
|
24909
|
+
onboarding_completed_at: exports_external.string().nullable().optional(),
|
|
24910
|
+
fetched_at: exports_external.string().datetime({ offset: true }).optional()
|
|
24911
|
+
});
|
|
24871
24912
|
var SyncCursorsSchema = exports_external.record(exports_external.string(), exports_external.string().nullable());
|
|
24872
24913
|
var WorkspaceEntrySchema = exports_external.object({
|
|
24873
24914
|
workspace_id: exports_external.string().uuid(),
|
|
@@ -24875,6 +24916,7 @@ var WorkspaceEntrySchema = exports_external.object({
|
|
|
24875
24916
|
api_base_url: exports_external.string().url(),
|
|
24876
24917
|
last_synced_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
24877
24918
|
workspace_name: exports_external.string().min(1).optional(),
|
|
24919
|
+
member: MemberPrincipalSchema.optional(),
|
|
24878
24920
|
sync_cursors: SyncCursorsSchema.optional()
|
|
24879
24921
|
});
|
|
24880
24922
|
var ConfigFileSchema = exports_external.object({
|
|
@@ -24978,6 +25020,32 @@ function updateSyncCursor(slug, entityType, cursor) {
|
|
|
24978
25020
|
writeAtomic(next);
|
|
24979
25021
|
return next;
|
|
24980
25022
|
}
|
|
25023
|
+
function setMemberPrincipal(slug, member) {
|
|
25024
|
+
const existing = loadConfig();
|
|
25025
|
+
if (!existing)
|
|
25026
|
+
return false;
|
|
25027
|
+
const entry = existing.workspaces[slug];
|
|
25028
|
+
if (!entry)
|
|
25029
|
+
return false;
|
|
25030
|
+
const nextEntry = { ...entry, member };
|
|
25031
|
+
writeAtomic({
|
|
25032
|
+
...existing,
|
|
25033
|
+
workspaces: { ...existing.workspaces, [slug]: nextEntry }
|
|
25034
|
+
});
|
|
25035
|
+
return true;
|
|
25036
|
+
}
|
|
25037
|
+
var MEMBER_REFRESH_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
25038
|
+
function memberNeedsRefresh(entry, now = () => new Date) {
|
|
25039
|
+
const member = entry.member;
|
|
25040
|
+
if (member === undefined)
|
|
25041
|
+
return true;
|
|
25042
|
+
if (member.fetched_at === undefined)
|
|
25043
|
+
return true;
|
|
25044
|
+
const age = now().getTime() - new Date(member.fetched_at).getTime();
|
|
25045
|
+
if (Number.isNaN(age))
|
|
25046
|
+
return true;
|
|
25047
|
+
return age >= MEMBER_REFRESH_AFTER_MS;
|
|
25048
|
+
}
|
|
24981
25049
|
function updateLastSyncedAt(slug, iso) {
|
|
24982
25050
|
const existing = loadConfig();
|
|
24983
25051
|
if (!existing) {
|
|
@@ -25684,12 +25752,12 @@ function runDeclare(args, deps = {}) {
|
|
|
25684
25752
|
const now = deps.now ?? (() => new Date);
|
|
25685
25753
|
const declaredPath = args.path?.trim();
|
|
25686
25754
|
if (!declaredPath) {
|
|
25687
|
-
out.error(
|
|
25755
|
+
out.error(`Nothing to declare. Usage: plinth declare "<path>" --scratchpad="<dir>" [--label='<text>']`);
|
|
25688
25756
|
return { exitCode: 1 };
|
|
25689
25757
|
}
|
|
25690
25758
|
const rootArg = args.scratchpad?.trim() || env2[SCRATCHPAD_ENV]?.trim();
|
|
25691
25759
|
if (!rootArg) {
|
|
25692
|
-
out.error(`No scratchpad directory given, so there is nothing to declare into. ` + `Pass --scratchpad
|
|
25760
|
+
out.error(`No scratchpad directory given, so there is nothing to declare into. ` + `Pass --scratchpad="<dir>" (the scratchpad directory your harness told ` + `you to use), or set ${SCRATCHPAD_ENV}. This is never guessed: a ` + `wrongly guessed root would widen the containment check silently.`);
|
|
25693
25761
|
return { exitCode: 1 };
|
|
25694
25762
|
}
|
|
25695
25763
|
let rootStat;
|
|
@@ -25887,6 +25955,86 @@ function renderFreshnessStamp(today, updatedAt) {
|
|
|
25887
25955
|
return `This index was last updated on ${bankedDay}, ${age}.`;
|
|
25888
25956
|
}
|
|
25889
25957
|
var GROUNDING_BUDGET_BYTES = 20000;
|
|
25958
|
+
var GROUNDING_CATEGORY_ORDER = [
|
|
25959
|
+
"grounding",
|
|
25960
|
+
"identity",
|
|
25961
|
+
"budgetNote",
|
|
25962
|
+
"voice",
|
|
25963
|
+
"rules",
|
|
25964
|
+
"workspaceFacts",
|
|
25965
|
+
"personalFacts"
|
|
25966
|
+
];
|
|
25967
|
+
var GROUNDING_CATEGORY_LABELS = {
|
|
25968
|
+
grounding: "Plinth's own grounding",
|
|
25969
|
+
identity: "who you are",
|
|
25970
|
+
budgetNote: "the trim notes",
|
|
25971
|
+
voice: "voice pointer",
|
|
25972
|
+
rules: "this workspace's rules",
|
|
25973
|
+
workspaceFacts: "workspace facts",
|
|
25974
|
+
personalFacts: "your own facts"
|
|
25975
|
+
};
|
|
25976
|
+
var MEMBER_EDITABLE_CATEGORIES = [
|
|
25977
|
+
"rules",
|
|
25978
|
+
"workspaceFacts",
|
|
25979
|
+
"personalFacts"
|
|
25980
|
+
];
|
|
25981
|
+
function segmentBytes(lines, cats) {
|
|
25982
|
+
const out = {
|
|
25983
|
+
grounding: 0,
|
|
25984
|
+
identity: 0,
|
|
25985
|
+
rules: 0,
|
|
25986
|
+
workspaceFacts: 0,
|
|
25987
|
+
personalFacts: 0,
|
|
25988
|
+
voice: 0,
|
|
25989
|
+
budgetNote: 0
|
|
25990
|
+
};
|
|
25991
|
+
lines.forEach((text, i) => {
|
|
25992
|
+
const cat = cats[i];
|
|
25993
|
+
out[cat] += Buffer.byteLength(text) + (i < lines.length - 1 ? 1 : 0);
|
|
25994
|
+
});
|
|
25995
|
+
return out;
|
|
25996
|
+
}
|
|
25997
|
+
var IDENTITY_LINE_MAX_BYTES = 300;
|
|
25998
|
+
function renderMemberIdentity(member, workspaceSlug) {
|
|
25999
|
+
if (member === null)
|
|
26000
|
+
return null;
|
|
26001
|
+
const tail = `. Tasks you create are assigned to this member unless told otherwise.`;
|
|
26002
|
+
const budget = IDENTITY_LINE_MAX_BYTES - Buffer.byteLength(tail);
|
|
26003
|
+
const email3 = oneLine(member.email);
|
|
26004
|
+
const build = (withName, withRole, withZone) => {
|
|
26005
|
+
const who = withName && member.displayName ? `${oneLine(member.displayName)} (${email3})` : email3;
|
|
26006
|
+
const clauses = [`You are working for ${who}`];
|
|
26007
|
+
if (withRole && member.role)
|
|
26008
|
+
clauses.push(`role ${oneLine(member.role)}`);
|
|
26009
|
+
if (workspaceSlug !== null)
|
|
26010
|
+
clauses.push(`in workspace ${workspaceSlug}`);
|
|
26011
|
+
if (withZone && member.timezone)
|
|
26012
|
+
clauses.push(`timezone ${oneLine(member.timezone)}`);
|
|
26013
|
+
return clauses.join(", ");
|
|
26014
|
+
};
|
|
26015
|
+
const candidates = [
|
|
26016
|
+
build(true, true, true),
|
|
26017
|
+
build(true, true, false),
|
|
26018
|
+
build(true, false, false),
|
|
26019
|
+
build(false, false, false)
|
|
26020
|
+
];
|
|
26021
|
+
if (member.displayName) {
|
|
26022
|
+
const slugClause = workspaceSlug === null ? `` : `, in workspace ${workspaceSlug}`;
|
|
26023
|
+
candidates.push(`You are working for ${oneLine(member.displayName)}${slugClause}`);
|
|
26024
|
+
}
|
|
26025
|
+
const head = candidates.find((c) => Buffer.byteLength(c) <= budget);
|
|
26026
|
+
if (head === undefined)
|
|
26027
|
+
return null;
|
|
26028
|
+
return head + tail;
|
|
26029
|
+
}
|
|
26030
|
+
function oneLine(value) {
|
|
26031
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
26032
|
+
}
|
|
26033
|
+
function shouldOfferOnboarding(rosterIsEmpty, member) {
|
|
26034
|
+
if (member?.onboardingCompletedAt)
|
|
26035
|
+
return false;
|
|
26036
|
+
return rosterIsEmpty;
|
|
26037
|
+
}
|
|
25890
26038
|
function bankedDate(line) {
|
|
25891
26039
|
const matches = line.match(/\d{4}-\d{2}-\d{2}/g);
|
|
25892
26040
|
if (matches === null || matches.length === 0)
|
|
@@ -25897,6 +26045,28 @@ function bankedDate(line) {
|
|
|
25897
26045
|
latest = m;
|
|
25898
26046
|
return latest;
|
|
25899
26047
|
}
|
|
26048
|
+
var POINTER_RE = /\[\[[^\]]+\]\]/g;
|
|
26049
|
+
var TITLE_TRUNCATION_MARK = " …";
|
|
26050
|
+
function factTitle(fact) {
|
|
26051
|
+
const firstLine = fact.split(`
|
|
26052
|
+
`)[0];
|
|
26053
|
+
const match = /[.?!](\s|$)/.exec(firstLine);
|
|
26054
|
+
const sentence = match === null ? firstLine : firstLine.slice(0, match.index + 1);
|
|
26055
|
+
const missing = [...fact.matchAll(POINTER_RE)].map((m) => m[0]).filter((p) => !sentence.includes(p));
|
|
26056
|
+
const unique = [...new Set(missing)];
|
|
26057
|
+
const named = unique.length === 0 ? sentence : `${sentence} ${unique.join(" ")}`;
|
|
26058
|
+
const truncated = named !== fact;
|
|
26059
|
+
return { text: truncated ? `${named}${TITLE_TRUNCATION_MARK}` : named, truncated };
|
|
26060
|
+
}
|
|
26061
|
+
function rankFacts(facts) {
|
|
26062
|
+
return facts.map((line, index) => ({ line, index, date: bankedDate(line) })).sort((a, b) => {
|
|
26063
|
+
const ad = a.date ?? "";
|
|
26064
|
+
const bd = b.date ?? "";
|
|
26065
|
+
if (ad !== bd)
|
|
26066
|
+
return ad < bd ? 1 : -1;
|
|
26067
|
+
return b.index - a.index;
|
|
26068
|
+
}).map(({ index, line }) => ({ index, line }));
|
|
26069
|
+
}
|
|
25900
26070
|
function groupFacts(body) {
|
|
25901
26071
|
const facts = [];
|
|
25902
26072
|
for (const line of body.split(`
|
|
@@ -25912,32 +26082,64 @@ ${line}`;
|
|
|
25912
26082
|
}
|
|
25913
26083
|
return facts;
|
|
25914
26084
|
}
|
|
25915
|
-
function
|
|
26085
|
+
function selectIndexContent(body, capBytes) {
|
|
25916
26086
|
const lines = groupFacts(body);
|
|
25917
26087
|
if (Buffer.byteLength(body) <= capBytes) {
|
|
25918
|
-
return { text: body, kept: lines.length, total: lines.length };
|
|
25919
|
-
}
|
|
25920
|
-
const ranked = lines
|
|
25921
|
-
|
|
25922
|
-
|
|
25923
|
-
|
|
25924
|
-
|
|
25925
|
-
|
|
25926
|
-
|
|
25927
|
-
|
|
25928
|
-
|
|
25929
|
-
|
|
25930
|
-
|
|
25931
|
-
|
|
26088
|
+
return { text: body, titles: "", kept: lines.length, named: 0, total: lines.length };
|
|
26089
|
+
}
|
|
26090
|
+
const ranked = rankFacts(lines);
|
|
26091
|
+
const titleOf = new Map;
|
|
26092
|
+
const info2 = new Map;
|
|
26093
|
+
for (const { index, line } of ranked) {
|
|
26094
|
+
const t = factTitle(line);
|
|
26095
|
+
info2.set(index, t);
|
|
26096
|
+
titleOf.set(index, t.text);
|
|
26097
|
+
}
|
|
26098
|
+
const carried = new Map;
|
|
26099
|
+
const used = () => {
|
|
26100
|
+
let bytes = 0;
|
|
26101
|
+
let bodies = 0;
|
|
26102
|
+
let titles = 0;
|
|
26103
|
+
for (const [index, kind] of carried) {
|
|
26104
|
+
const line = lines[index];
|
|
26105
|
+
if (kind === "body") {
|
|
26106
|
+
bytes += Buffer.byteLength(line);
|
|
26107
|
+
bodies++;
|
|
26108
|
+
} else {
|
|
26109
|
+
bytes += Buffer.byteLength(titleOf.get(index));
|
|
26110
|
+
titles++;
|
|
26111
|
+
}
|
|
26112
|
+
}
|
|
26113
|
+
return bytes + Math.max(0, bodies - 1) + Math.max(0, titles - 1);
|
|
26114
|
+
};
|
|
26115
|
+
for (const { index } of ranked) {
|
|
26116
|
+
if (info2.get(index)?.truncated !== true)
|
|
26117
|
+
continue;
|
|
26118
|
+
carried.set(index, "title");
|
|
26119
|
+
if (used() > capBytes)
|
|
26120
|
+
carried.delete(index);
|
|
26121
|
+
}
|
|
26122
|
+
for (const { index } of ranked) {
|
|
26123
|
+
const was = carried.get(index);
|
|
26124
|
+
carried.set(index, "body");
|
|
26125
|
+
if (used() > capBytes) {
|
|
26126
|
+
if (was === undefined)
|
|
26127
|
+
carried.delete(index);
|
|
26128
|
+
else
|
|
26129
|
+
carried.set(index, was);
|
|
25932
26130
|
break;
|
|
25933
|
-
|
|
25934
|
-
used += add;
|
|
26131
|
+
}
|
|
25935
26132
|
}
|
|
25936
|
-
|
|
26133
|
+
const inFileOrder = (kind) => [...carried.entries()].filter(([, k]) => k === kind).map(([i]) => i).sort((a, b) => a - b);
|
|
26134
|
+
const bodyIdx = inFileOrder("body");
|
|
26135
|
+
const titleIdx = inFileOrder("title");
|
|
25937
26136
|
return {
|
|
25938
|
-
text:
|
|
26137
|
+
text: bodyIdx.map((i) => lines[i]).join(`
|
|
25939
26138
|
`),
|
|
25940
|
-
|
|
26139
|
+
titles: titleIdx.map((i) => titleOf.get(i)).join(`
|
|
26140
|
+
`),
|
|
26141
|
+
kept: bodyIdx.length,
|
|
26142
|
+
named: titleIdx.length,
|
|
25941
26143
|
total: lines.length
|
|
25942
26144
|
};
|
|
25943
26145
|
}
|
|
@@ -25947,83 +26149,103 @@ function trimmedIndexNote(kept, total, filename) {
|
|
|
25947
26149
|
}
|
|
25948
26150
|
return `Those are the ${kept} most recently banked of ${total} facts, not all of ` + `them: this injection carries a fixed budget and the rest of the index did ` + `not fit. The other ${total - kept} are in \`${filename}\` at the mirror ` + `root. READ THAT FILE IN FULL before you rely on what this workspace has ` + `taught you, and before you answer that no fact covers something. An index ` + `that is silent because it was trimmed reads exactly like one that has ` + `settled a subject.`;
|
|
25949
26151
|
}
|
|
25950
|
-
function
|
|
26152
|
+
function titlesLeadIn(kept, named, unnamed, total, filename) {
|
|
26153
|
+
const parts = [`${kept} above in full`];
|
|
26154
|
+
parts.push(`${named} below as ${named === 1 ? "its opening" : "their openings"} only`);
|
|
26155
|
+
if (unnamed > 0)
|
|
26156
|
+
parts.push(`${unnamed} not shown at all`);
|
|
26157
|
+
return `Not all of this index fits. It holds ${total} facts: ${parts.join(", ")}. ` + `Every line below ends \`…\` because it is an opening and not the whole ` + `fact. They are all in \`${filename}\` at the mirror root: READ THAT FILE ` + `IN FULL before you answer that no fact covers something.`;
|
|
26158
|
+
}
|
|
26159
|
+
function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemory, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, workspaceMemoryNote, userMemoryNote, member, workspaceTitles = null, userTitles = null) {
|
|
25951
26160
|
const gateFlag = workspaceSlug === null ? `--workspace="<this workspace>"` : `--workspace ${workspaceSlug}`;
|
|
25952
|
-
const
|
|
25953
|
-
|
|
25954
|
-
|
|
25955
|
-
|
|
25956
|
-
|
|
25957
|
-
|
|
25958
|
-
|
|
25959
|
-
|
|
25960
|
-
|
|
25961
|
-
|
|
25962
|
-
|
|
25963
|
-
|
|
25964
|
-
|
|
25965
|
-
|
|
25966
|
-
``,
|
|
25967
|
-
|
|
25968
|
-
|
|
25969
|
-
`Check the voice yourself, because nothing else checks it now. Two files carry ` + `it: \`workspace-rules.md\` at the mirror root holds this workspace's house ` + `style, and \`${VOICE_FILENAME}\` beside it holds your own voice profile, the ` + `sign-off and the habits that belong to one member rather than the team. Read ` + `them before you write and follow them as written, rather than drafting in ` + `your own register and correcting after. Nothing inspects a draft between you ` + `and the person reading it, so the rules in those files are the whole of it.`,
|
|
25970
|
-
``,
|
|
25971
|
-
`Read the draft back before you say it is ready, and read what the tool actually ` + `produced rather than what you sent it. Do this every time you write one, not ` + `once a session: a later edit can undo what an earlier check confirmed. A ` + `draft surface takes your text and renders it, and it may escape what you ` + `wrote, wrap it, quote an earlier message underneath it, thread it somewhere ` + `you did not intend, or leave the first one in place and add a second. What ` + `the person opens is the rendered result, not your input, and those are not ` + `always the same thing. So fetch the staged draft, look at its body, its ` + `recipients, the thread it landed on and that there is one of it, and only ` + `then say it is ready. Separately, and once per session is enough for this ` + `part: if a tool's own description tells you how it handles your text, treat ` + `that as a claim like any other and check it against what came out. A tool ` + `that is documented wrong will put markup, an empty body, or a stranger's ` + `address in front of a person whose name is on the message, and you are the ` + `only thing standing between those two.`
|
|
25972
|
-
];
|
|
26161
|
+
const identity = renderMemberIdentity(member, workspaceSlug);
|
|
26162
|
+
const block = [];
|
|
26163
|
+
const cats = [];
|
|
26164
|
+
const add = (cat, ...lines) => {
|
|
26165
|
+
for (const line of lines) {
|
|
26166
|
+
block.push(line);
|
|
26167
|
+
cats.push(cat);
|
|
26168
|
+
}
|
|
26169
|
+
};
|
|
26170
|
+
add("grounding", `Plinth session grounding:`, ``, `Today's date: ${today}. You have no inherent sense of the current date and ` + `your training has a knowledge cutoff, so treat this injected date as ` + `authoritative for anything time-relative: deadlines, what counts as ` + `"recent" or "upcoming", and the age of any data or of your own knowledge.`);
|
|
26171
|
+
if (identity !== null)
|
|
26172
|
+
add("identity", ``, identity);
|
|
26173
|
+
add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying and forwarding are not yours, by any route: not a mail tool, and ` + `not a shell command line either. The mail tools are refused for you where ` + `Plinth has configured this workspace. Nothing refuses a shell, anywhere, ` + `and where Plinth has not configured this workspace nothing is refused at ` + `all: you are the only thing holding those. This is not a comment on your ` + `writing. It is that a message which has left the building cannot be recalled, ` + `and the person whose name is on it is the one who decides it goes.`);
|
|
26174
|
+
if (workspaceRules === null && voiceProfile === null) {
|
|
26175
|
+
add("voice", ``, `Check the voice yourself, because nothing else checks it now. Two files carry ` + `it: \`workspace-rules.md\` at the mirror root holds this workspace's house ` + `style, and \`${VOICE_FILENAME}\` beside it holds your own voice profile, the ` + `sign-off and the habits that belong to one member rather than the team. Read ` + `them before you write and follow them as written, rather than drafting in ` + `your own register and correcting after. Nothing inspects a draft between you ` + `and the person reading it, so the rules in those files are the whole of it.`);
|
|
26176
|
+
}
|
|
26177
|
+
add("grounding", ``, `Read the draft back before you say it is ready, and read what the tool actually ` + `produced rather than what you sent it. Do this every time you write one, not ` + `once a session: a later edit can undo what an earlier check confirmed. A ` + `draft surface takes your text and renders it, and it may escape what you ` + `wrote, wrap it, quote an earlier message underneath it, thread it somewhere ` + `you did not intend, or leave the first one in place and add a second. What ` + `the person opens is the rendered result, not your input, and those are not ` + `always the same thing. So fetch the staged draft, look at its body, its ` + `recipients, the thread it landed on and that there is one of it, and only ` + `then say it is ready. Separately, and once per session is enough for this ` + `part: if a tool's own description tells you how it handles your text, treat ` + `that as a claim like any other and check it against what came out. A tool ` + `that is documented wrong will put markup, an empty body, or a stranger's ` + `address in front of a person whose name is on the message, and you are the ` + `only thing standing between those two.`, ``, `The most recent artefact wins on facts, and it is not a judgement call. ` + `Dates, prices, availability, capacities, routings, who is on a thread, ` + `who said what. Do not weigh a newer message against an older banked ` + `line to work out which is more likely right: the newer one is the state ` + `and the older one is history. This holds for any dated artefact, not ` + `only email - call transcripts, meeting notes and executed documents are ` + `all in scope, newest first. Stale context is worse than absent context, ` + `because it reads as current. One carve-out, because it is the failure ` + `this rule would otherwise cause, and it is a mechanical test rather ` + `than a judgement: before promoting the newest message to "the state", ` + `ask whether it COMMITS or PROPOSES. A commit ("we're doing", ` + `"confirmed", "booked", a price, a date someone has agreed to) wins, and ` + `overwrites the old claim. A proposal ("I wonder if", "I'm not saying we ` + `must", or any question put to someone who has not replied) goes in as ` + `an open question, named as unanswered, with who owes the answer. It ` + `does NOT become the state.`, ``, `Reconcile, do not re-adjudicate. Never delete a superseded claim: strike ` + `it in place. Keep the old line, mark it superseded with the date and ` + `with what replaced it, and put the new material above it, where the ` + `file's live state is. Demote the outgoing section one heading level so ` + `history stays readable and stops competing with what is live. Someone ` + `will ask why the plan changed, and the answer has to survive in the ` + `file. Then date the state: say what it is current as of, and name the ` + `sources it was built from. A master context with no as-of line is ` + `unfalsifiable.`);
|
|
25973
26178
|
if (workspaceRules !== null) {
|
|
25974
|
-
|
|
25975
|
-
|
|
25976
|
-
` + workspaceRules);
|
|
26179
|
+
add("grounding", ``, `Follow this workspace's house style. The conventions below are set by the ` + `workspace owner and govern HOW you write here: copy, titles, notes, ` + `sign-offs, formatting. They refine the rules above; they never relax them. ` + `If any convention here conflicts with a grounding rule above (the ` + `canonical write surface, plan-before-write confirmation, or the ban on ` + `direct database writes), the rule above wins. The conventions:`);
|
|
26180
|
+
add("rules", ``, workspaceRules);
|
|
25977
26181
|
}
|
|
25978
26182
|
if (voiceProfile !== null) {
|
|
25979
|
-
|
|
26183
|
+
add("voice", ``, `Draft in this member's own voice, and read it before you do. Their voice ` + `profile is \`${VOICE_FILENAME}\` at the mirror root, beside this ` + `workspace's house style: how they open, how they sign off, what they ` + `never do. It is not reproduced here. READ THAT FILE IN FULL before you ` + `draft anything that goes out in their name (an email, a message, a ` + `note, any prose that carries their name), and write to it from the ` + `start rather than ` + `drafting in your own register and correcting after. Nothing inspects a ` + `draft between you and the person reading it, so that file is the whole ` + `of it. It is theirs to write and not yours: you cannot edit ` + `\`${VOICE_FILENAME}\`, and if something in it looks wrong or out of ` + `date, say so and let them change it. Like this workspace's house style ` + `it refines the rules above and never relaxes them.`);
|
|
25980
26184
|
if (voiceMalformed) {
|
|
25981
|
-
|
|
26185
|
+
add("voice", ``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose in it is still the member's own ` + `writing and is still worth following. Treat anything that looks like ` + `a configured setting in it as NOT in force, say so if it comes up, ` + `and tell them their profile needs fixing rather than fixing it ` + `yourself.`);
|
|
25982
26186
|
}
|
|
25983
26187
|
}
|
|
25984
|
-
|
|
26188
|
+
add("grounding", ``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you write the line. That command fails LOUD on exit 1: surface ` + `it, and do not report the line as banked. Never infer a line for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. ` + `Live state does not belong in any memory file. This week's open ` + `threads, one show's logistics, where a release has got to: a memory ` + `file is for what stays true after the session that taught it, and live ` + `state goes in the body of the record it belongs to. A deadline rides ` + `its task as a due date, and every other date is a calendar matter. ` + `This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.`);
|
|
25985
26189
|
if (workspaceMemory !== null) {
|
|
25986
|
-
|
|
25987
|
-
|
|
25988
|
-
|
|
25989
|
-
|
|
25990
|
-
|
|
26190
|
+
add("grounding", ``, `Draw on this workspace's memory. The lines below are this workspace's ` + `memory index: durable facts it has taught you, each a one-line hook that ` + `points (via a \`[[wikilink]]\`) to the warm store where the full detail ` + `lives. Treat them as recall, not gospel: use a line to load the right ` + `context and verify it against the live workspace before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.workspaceMemoryUpdatedAt)} ` + `The index:`);
|
|
26191
|
+
add("workspaceFacts", ``, workspaceMemory);
|
|
26192
|
+
if (workspaceTitles !== null) {
|
|
26193
|
+
add("budgetNote", ``, workspaceTitles.leadIn);
|
|
26194
|
+
add("workspaceFacts", ``, workspaceTitles.text);
|
|
26195
|
+
}
|
|
26196
|
+
if (workspaceMemoryNote !== null)
|
|
26197
|
+
add("budgetNote", ``, workspaceMemoryNote);
|
|
25991
26198
|
}
|
|
25992
26199
|
if (userMemory !== null) {
|
|
25993
|
-
|
|
25994
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
25997
|
-
|
|
26200
|
+
add("grounding", ``, `Draw on your personal memory. The lines below are YOUR personal memory ` + `index: member-private facts you have taught the harness about yourself ` + `(your preferences, working style, tools), each a one-line hook ` + `that points (via a \`[[wikilink]]\`) to the warm store where the full ` + `detail lives. Only you can read this. Treat it as recall, not gospel: use ` + `a line to load the right context and verify it before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.userMemoryUpdatedAt)} ` + `The index:`);
|
|
26201
|
+
add("personalFacts", ``, userMemory);
|
|
26202
|
+
if (userTitles !== null) {
|
|
26203
|
+
add("budgetNote", ``, userTitles.leadIn);
|
|
26204
|
+
add("personalFacts", ``, userTitles.text);
|
|
26205
|
+
}
|
|
26206
|
+
if (userMemoryNote !== null)
|
|
26207
|
+
add("budgetNote", ``, userMemoryNote);
|
|
25998
26208
|
}
|
|
25999
26209
|
if (workspaceMemory !== null || userMemory !== null) {
|
|
26000
|
-
|
|
26210
|
+
add("grounding", ``, `Dated recall. Every line in these indexes carries the date it was banked, ` + `and today's date is injected above, so the age of any line is ` + `computable. When you rely on a line in an answer, say how old it is - ` + `"banked 21 July, seventeen days ago" - rather than presenting it as ` + `current. Age is not a reason to distrust a line, it is what tells the ` + `reader whether to check: a working convention banked a year ago is ` + `probably still the convention, a fact about a live negotiation banked ` + `three weeks ago probably is not. And say what the indexes do NOT cover. ` + `If you are asked about something no line here addresses, say so plainly ` + `rather than answering only from what happens to be present - an index ` + `that is silent on a subject reads exactly like one that has settled it.`);
|
|
26001
26211
|
}
|
|
26002
|
-
|
|
26003
|
-
if (rosterIsEmpty) {
|
|
26004
|
-
|
|
26212
|
+
add("grounding", ``, `Declare a work product before you finish with it. Your scratchpad ` + `directory sits outside Plinth: temporary, scoped to this session, and ` + `invisible to the person who asked for the work. Prefer not to leave a ` + `deliverable there at all. If it belongs to this workspace, write it to ` + `a mirror file instead, where it syncs and is durable. When the ` + `scratchpad genuinely is a file's only home, meaning a document, ` + `profile, report or analysis that was the POINT of the work rather than ` + `a step toward it, declare it the moment you write it, with ` + `\`plinth declare "<path>" --scratchpad="<the scratchpad directory ` + `your harness told you to use>" --label='<short description>'\`. ` + `Declare only ` + `those. A file that already has another home is not a deliverable: not ` + `raw API dumps, thread JSON, helper scripts, or the body files you write ` + `for a tool that reads a path, where the durable deliverable is the ` + `draft the tool creates. Declaring everything you wrote buries the one ` + `that mattered, which is the failure this exists to prevent. ` + `Re-declaring a path replaces the earlier entry, so refine a label ` + `freely. This is about work products, not memory: durable facts still go ` + `to the index above. Like every rule here it is additive and ` + `subordinate, and never relaxes a safety primitive above.`);
|
|
26213
|
+
if (shouldOfferOnboarding(rosterIsEmpty, member)) {
|
|
26214
|
+
add("grounding", ``, `This workspace has no artists in it yet. OFFER to set it up before you ` + `get into anything else, because an empty roster reported as a summary ` + `reads as a broken product rather than a new one. Say in one or two ` + `lines that ` + `${ONBOARD_COMMAND_INVOCATION} walks them through connecting their ` + `inbox, calendar and files, gets their roster in, and turns the work ` + `they already have running into projects and tasks, then ask whether ` + `to start now. Then STOP AND WAIT for their answer. Do not begin the ` + `interview, do not connect anything, and do not create an artist, a ` + `project or a task until they have said yes. If they say no, or they ` + `asked for something else in the same message, drop it and do the ` + `thing they asked for. Offer once, and never twice in one session. ` + `And a workspace can have no artists on purpose: if they have turned ` + `this down before, or this is not an artist workspace, leave it alone ` + `rather than raising it every time you open one.`);
|
|
26005
26215
|
}
|
|
26006
|
-
return block.join(`
|
|
26007
|
-
`);
|
|
26216
|
+
return { text: block.join(`
|
|
26217
|
+
`), split: segmentBytes(block, cats) };
|
|
26008
26218
|
}
|
|
26009
|
-
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null) {
|
|
26010
|
-
const compose = (wmBody, umBody, wmNote, umNote) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote);
|
|
26219
|
+
function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null, member = null) {
|
|
26220
|
+
const compose = (wmBody, umBody, wmNote, umNote, wmTitles = null, umTitles = null) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote, member, wmTitles, umTitles);
|
|
26011
26221
|
const whole = compose(workspaceMemory, userMemory, null, null);
|
|
26012
|
-
const untrimmed = () =>
|
|
26013
|
-
|
|
26014
|
-
|
|
26015
|
-
|
|
26016
|
-
|
|
26017
|
-
|
|
26018
|
-
|
|
26019
|
-
|
|
26222
|
+
const untrimmed = () => {
|
|
26223
|
+
const all = (body) => {
|
|
26224
|
+
const n = groupFacts(body).length;
|
|
26225
|
+
return { text: body, titles: "", kept: n, named: 0, total: n };
|
|
26226
|
+
};
|
|
26227
|
+
return {
|
|
26228
|
+
block: whole.text,
|
|
26229
|
+
split: whole.split,
|
|
26230
|
+
picks: {
|
|
26231
|
+
wmPick: workspaceMemory === null ? null : all(workspaceMemory),
|
|
26232
|
+
umPick: userMemory === null ? null : all(userMemory)
|
|
26233
|
+
}
|
|
26234
|
+
};
|
|
26235
|
+
};
|
|
26236
|
+
if (Buffer.byteLength(whole.text) <= GROUNDING_BUDGET_BYTES)
|
|
26020
26237
|
return untrimmed();
|
|
26021
26238
|
if (workspaceMemory === null && userMemory === null)
|
|
26022
26239
|
return untrimmed();
|
|
26023
26240
|
const factCount = (body) => body === null ? 0 : groupFacts(body).length;
|
|
26024
26241
|
const wmFacts = factCount(workspaceMemory);
|
|
26025
26242
|
const umFacts = factCount(userMemory);
|
|
26026
|
-
const
|
|
26243
|
+
const titlesFor = (pick2, filename) => pick2 === null || pick2.named === 0 ? null : {
|
|
26244
|
+
leadIn: titlesLeadIn(pick2.kept, pick2.named, pick2.total - pick2.kept - pick2.named, pick2.total, filename),
|
|
26245
|
+
text: pick2.titles
|
|
26246
|
+
};
|
|
26247
|
+
const noteFor = (pick2, filename) => pick2 === null || pick2.kept === pick2.total ? null : trimmedIndexNote(pick2.kept, pick2.total, filename);
|
|
26248
|
+
const measureFixed = (withNotes2, withTitles) => Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", withNotes2 && workspaceMemory !== null ? trimmedIndexNote(wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME) : null, withNotes2 && userMemory !== null ? trimmedIndexNote(umFacts, umFacts, USER_MEMORY_FILENAME) : null, withTitles && workspaceMemory !== null ? { leadIn: titlesLeadIn(wmFacts, wmFacts, wmFacts, wmFacts, WORKSPACE_MEMORY_FILENAME), text: "" } : null, withTitles && userMemory !== null ? { leadIn: titlesLeadIn(umFacts, umFacts, umFacts, umFacts, USER_MEMORY_FILENAME), text: "" } : null).text);
|
|
26027
26249
|
const wmSize = workspaceMemory === null ? 0 : Buffer.byteLength(workspaceMemory);
|
|
26028
26250
|
const umSize = userMemory === null ? 0 : Buffer.byteLength(userMemory);
|
|
26029
26251
|
const allocate = (remaining) => {
|
|
@@ -26040,23 +26262,45 @@ function buildGroundingReport(today, workspaceRules = null, workspaceMemory = nu
|
|
|
26040
26262
|
umCap = half;
|
|
26041
26263
|
wmCap = remaining - half;
|
|
26042
26264
|
}
|
|
26043
|
-
const umPick = userMemory === null ? null :
|
|
26044
|
-
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text);
|
|
26265
|
+
const umPick = userMemory === null ? null : selectIndexContent(userMemory, umCap);
|
|
26266
|
+
const umUsed = umPick === null ? 0 : Buffer.byteLength(umPick.text) + Buffer.byteLength(umPick.titles);
|
|
26045
26267
|
return {
|
|
26046
|
-
wmPick: workspaceMemory === null ? null :
|
|
26268
|
+
wmPick: workspaceMemory === null ? null : selectIndexContent(workspaceMemory, Math.max(wmCap, remaining - umUsed)),
|
|
26047
26269
|
umPick
|
|
26048
26270
|
};
|
|
26049
26271
|
};
|
|
26050
|
-
const
|
|
26051
|
-
|
|
26052
|
-
|
|
26053
|
-
|
|
26054
|
-
const
|
|
26055
|
-
|
|
26056
|
-
|
|
26272
|
+
const build = (picks, withNotes2) => {
|
|
26273
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
26274
|
+
return compose(picks.wmPick === null ? null : picks.wmPick.text, picks.umPick === null ? null : picks.umPick.text, keep(picks.wmPick) ? noteFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(picks.umPick) ? noteFor(picks.umPick, USER_MEMORY_FILENAME) : null, titlesFor(picks.wmPick, WORKSPACE_MEMORY_FILENAME), titlesFor(picks.umPick, USER_MEMORY_FILENAME));
|
|
26275
|
+
};
|
|
26276
|
+
const refine2 = (seed, withNotes2) => {
|
|
26277
|
+
const asReserve = (section) => section === null ? null : { leadIn: section.leadIn, text: "" };
|
|
26278
|
+
const keep = (pick2) => withNotes2 || pick2 !== null && pick2.named === 0;
|
|
26279
|
+
const fixed = Buffer.byteLength(compose(workspaceMemory === null ? null : "", userMemory === null ? null : "", keep(seed.wmPick) ? noteFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME) : null, keep(seed.umPick) ? noteFor(seed.umPick, USER_MEMORY_FILENAME) : null, asReserve(titlesFor(seed.wmPick, WORKSPACE_MEMORY_FILENAME)), asReserve(titlesFor(seed.umPick, USER_MEMORY_FILENAME))).text);
|
|
26280
|
+
return allocate(Math.max(0, GROUNDING_BUDGET_BYTES - fixed));
|
|
26281
|
+
};
|
|
26282
|
+
const noNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(false, true)));
|
|
26283
|
+
const withNotes = allocate(Math.max(0, GROUNDING_BUDGET_BYTES - measureFixed(true, true)));
|
|
26284
|
+
const candidates = [
|
|
26285
|
+
{ picks: withNotes, composed: build(withNotes, true) },
|
|
26286
|
+
{ picks: noNotes, composed: build(noNotes, false) },
|
|
26287
|
+
{ picks: withNotes, composed: build(withNotes, false) }
|
|
26288
|
+
].concat([
|
|
26289
|
+
{ seed: noNotes, notes: false },
|
|
26290
|
+
{ seed: withNotes, notes: true }
|
|
26291
|
+
].map(({ seed, notes }) => {
|
|
26292
|
+
const picks = refine2(seed, notes);
|
|
26293
|
+
return { picks, composed: build(picks, notes) };
|
|
26294
|
+
})).map((c) => ({
|
|
26295
|
+
...c,
|
|
26296
|
+
bytes: Buffer.byteLength(c.composed.text),
|
|
26297
|
+
named: (c.picks.wmPick?.kept ?? 0) + (c.picks.wmPick?.named ?? 0) + (c.picks.umPick?.kept ?? 0) + (c.picks.umPick?.named ?? 0),
|
|
26298
|
+
whole: (c.picks.wmPick?.kept ?? 0) + (c.picks.umPick?.kept ?? 0)
|
|
26299
|
+
}));
|
|
26057
26300
|
const fitting = candidates.filter((c) => c.bytes <= GROUNDING_BUDGET_BYTES);
|
|
26058
|
-
const
|
|
26059
|
-
|
|
26301
|
+
const better = (a, b) => b.named !== a.named ? b.named > a.named : b.whole > a.whole;
|
|
26302
|
+
const best = fitting.length > 0 ? fitting.reduce((a, b) => better(a, b) ? b : a) : candidates.reduce((a, b) => b.bytes < a.bytes ? b : a);
|
|
26303
|
+
return { block: best.composed.text, picks: best.picks, split: best.composed.split };
|
|
26060
26304
|
}
|
|
26061
26305
|
var buildGroundingReportFor = buildGroundingReport;
|
|
26062
26306
|
function formatLocalDate(d) {
|
|
@@ -26227,7 +26471,8 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26227
26471
|
workspace_memory: WorkspaceMemoryStateSchema.optional(),
|
|
26228
26472
|
user_memory: UserMemoryStateSchema.optional(),
|
|
26229
26473
|
user_voice: UserVoiceStateSchema.optional(),
|
|
26230
|
-
derived_text_version: exports_external.number().int().nonnegative().optional()
|
|
26474
|
+
derived_text_version: exports_external.number().int().nonnegative().optional(),
|
|
26475
|
+
frontmatter_id_backfill_version: exports_external.number().int().nonnegative().optional()
|
|
26231
26476
|
});
|
|
26232
26477
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26233
26478
|
version: exports_external.literal(1),
|
|
@@ -26408,7 +26653,8 @@ function commitState(before, after) {
|
|
|
26408
26653
|
workspace_memory: memory.value,
|
|
26409
26654
|
user_memory: userMemory.value,
|
|
26410
26655
|
user_voice: userVoice.value,
|
|
26411
|
-
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version)
|
|
26656
|
+
derived_text_version: mergeScalar(fresh.derived_text_version, before.derived_text_version, after.derived_text_version),
|
|
26657
|
+
frontmatter_id_backfill_version: mergeScalar(fresh.frontmatter_id_backfill_version, before.frontmatter_id_backfill_version, after.frontmatter_id_backfill_version)
|
|
26412
26658
|
};
|
|
26413
26659
|
writeState(merged);
|
|
26414
26660
|
const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
|
|
@@ -26531,6 +26777,14 @@ function allFileEntries(state) {
|
|
|
26531
26777
|
function fileEntriesEqual(a, b) {
|
|
26532
26778
|
return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode;
|
|
26533
26779
|
}
|
|
26780
|
+
function clearFrontmatterIdBackfillMarker(state) {
|
|
26781
|
+
if (state.frontmatter_id_backfill_version === undefined) {
|
|
26782
|
+
return { state, changed: false };
|
|
26783
|
+
}
|
|
26784
|
+
const next = { ...state };
|
|
26785
|
+
delete next.frontmatter_id_backfill_version;
|
|
26786
|
+
return { state: next, changed: true };
|
|
26787
|
+
}
|
|
26534
26788
|
function listUnsynced(state) {
|
|
26535
26789
|
return state.unsynced ?? [];
|
|
26536
26790
|
}
|
|
@@ -26674,6 +26928,25 @@ function readUserMemoryFile(slug) {
|
|
|
26674
26928
|
function readVoiceProfileFile(slug) {
|
|
26675
26929
|
return readContextFile(slug, VOICE_FILENAME);
|
|
26676
26930
|
}
|
|
26931
|
+
function readMemberFromConfig(slug) {
|
|
26932
|
+
const entry = loadConfig()?.workspaces[slug];
|
|
26933
|
+
const member = entry?.member;
|
|
26934
|
+
if (!member)
|
|
26935
|
+
return null;
|
|
26936
|
+
return {
|
|
26937
|
+
email: member.email,
|
|
26938
|
+
displayName: member.display_name ?? null,
|
|
26939
|
+
role: member.role ?? null,
|
|
26940
|
+
onboardingCompletedAt: member.onboarding_completed_at ?? null
|
|
26941
|
+
};
|
|
26942
|
+
}
|
|
26943
|
+
function readMachineTimezone() {
|
|
26944
|
+
try {
|
|
26945
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
26946
|
+
} catch {
|
|
26947
|
+
return null;
|
|
26948
|
+
}
|
|
26949
|
+
}
|
|
26677
26950
|
function resolveUpdatedAt(raw, record2) {
|
|
26678
26951
|
if (record2 === null || record2.updated_at === null || raw === null)
|
|
26679
26952
|
return null;
|
|
@@ -26697,6 +26970,8 @@ function runGroundingReport(deps = {}) {
|
|
|
26697
26970
|
const readVoiceProfile = deps.readVoiceProfile ?? readVoiceProfileFile;
|
|
26698
26971
|
const readFreshness = deps.readFreshness ?? readMemoryFreshness;
|
|
26699
26972
|
const readRosterIsEmpty = deps.readRosterIsEmpty ?? rosterIsEmpty;
|
|
26973
|
+
const readMember = deps.readMember ?? readMemberFromConfig;
|
|
26974
|
+
const readTimezone = deps.readTimezone ?? readMachineTimezone;
|
|
26700
26975
|
let rawRules = null;
|
|
26701
26976
|
let rawMemory = null;
|
|
26702
26977
|
let rawUserMemory = null;
|
|
@@ -26707,6 +26982,7 @@ function runGroundingReport(deps = {}) {
|
|
|
26707
26982
|
userVoice: null
|
|
26708
26983
|
};
|
|
26709
26984
|
let noArtists = false;
|
|
26985
|
+
let member = null;
|
|
26710
26986
|
const workspaceSlug = isValidWorkspaceSlug(deps.workspaceSlug ?? "") ? deps.workspaceSlug : null;
|
|
26711
26987
|
if (workspaceSlug) {
|
|
26712
26988
|
try {
|
|
@@ -26739,6 +27015,12 @@ function runGroundingReport(deps = {}) {
|
|
|
26739
27015
|
} catch {
|
|
26740
27016
|
noArtists = false;
|
|
26741
27017
|
}
|
|
27018
|
+
try {
|
|
27019
|
+
const stored = readMember(workspaceSlug);
|
|
27020
|
+
member = stored === null ? null : { ...stored, timezone: readTimezone() };
|
|
27021
|
+
} catch {
|
|
27022
|
+
member = null;
|
|
27023
|
+
}
|
|
26742
27024
|
}
|
|
26743
27025
|
const rules = extractWorkspaceRules(rawRules);
|
|
26744
27026
|
const memory = extractWorkspaceMemory(rawMemory);
|
|
@@ -26747,7 +27029,7 @@ function runGroundingReport(deps = {}) {
|
|
|
26747
27029
|
return buildGroundingReportFor(formatLocalDate(now()), rules, memory, userMemory, {
|
|
26748
27030
|
workspaceMemoryUpdatedAt: resolveUpdatedAt(rawMemory, freshness.workspaceMemory),
|
|
26749
27031
|
userMemoryUpdatedAt: resolveUpdatedAt(rawUserMemory, freshness.userMemory)
|
|
26750
|
-
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug);
|
|
27032
|
+
}, noArtists, voiceProfile, resolveVoiceMalformed(rawVoiceProfile, freshness.userVoice), workspaceSlug, member);
|
|
26751
27033
|
}
|
|
26752
27034
|
var groundingCommand = defineCommand({
|
|
26753
27035
|
meta: {
|
|
@@ -26822,6 +27104,21 @@ async function whoami(baseUrl, pat, fetchImpl = fetch) {
|
|
|
26822
27104
|
}
|
|
26823
27105
|
return parsed.data;
|
|
26824
27106
|
}
|
|
27107
|
+
function memberPrincipalFrom(principal, now = () => new Date) {
|
|
27108
|
+
const member = {
|
|
27109
|
+
email: principal.profile.email,
|
|
27110
|
+
fetched_at: now().toISOString()
|
|
27111
|
+
};
|
|
27112
|
+
if (principal.profile.display_name) {
|
|
27113
|
+
member.display_name = principal.profile.display_name;
|
|
27114
|
+
}
|
|
27115
|
+
if (principal.role)
|
|
27116
|
+
member.role = principal.role;
|
|
27117
|
+
if (principal.profile.onboarding_completed_at) {
|
|
27118
|
+
member.onboarding_completed_at = principal.profile.onboarding_completed_at;
|
|
27119
|
+
}
|
|
27120
|
+
return member;
|
|
27121
|
+
}
|
|
26825
27122
|
async function syncEntity(args) {
|
|
26826
27123
|
const { strategy, baseUrl, pat, cursor, limit, fetchImpl = fetch } = args;
|
|
26827
27124
|
const params = new URLSearchParams;
|
|
@@ -27245,7 +27542,8 @@ async function runLogin(workspaceSlug, apiBaseUrl, deps = {}, options2 = {}) {
|
|
|
27245
27542
|
user_id: principal.user_id,
|
|
27246
27543
|
api_base_url: base2,
|
|
27247
27544
|
last_synced_at: null,
|
|
27248
|
-
workspace_name: principal.workspace_name
|
|
27545
|
+
workspace_name: principal.workspace_name,
|
|
27546
|
+
member: memberPrincipalFrom(principal)
|
|
27249
27547
|
};
|
|
27250
27548
|
let prior;
|
|
27251
27549
|
try {
|
|
@@ -27853,7 +28151,7 @@ var memoryGateCommand = defineCommand({
|
|
|
27853
28151
|
});
|
|
27854
28152
|
|
|
27855
28153
|
// src/lib/context/artist-context.ts
|
|
27856
|
-
import { existsSync } from "node:fs";
|
|
28154
|
+
import { existsSync, readFileSync as readFileSync12, unlinkSync as unlinkSync7 } from "node:fs";
|
|
27857
28155
|
|
|
27858
28156
|
// src/lib/context/entity-catalog.ts
|
|
27859
28157
|
var ENTITY_CATALOG = [
|
|
@@ -28010,8 +28308,8 @@ function buildBody(input) {
|
|
|
28010
28308
|
lines.push(`- Master context (stable profile + current state) → \`./_artist.md\`; edit its body and the daemon syncs both ways. Blocks labelled "rendered from Plinth" are the exception: they are overwritten on the next sync`);
|
|
28011
28309
|
lines.push(`- Closed/archived, meetings & full lists: \`mcp__plinth__list_projects\` / \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}`);
|
|
28012
28310
|
lines.push(`- Schema: ${MCP_INTROSPECTION_POINTER}`);
|
|
28013
|
-
lines.push(`- Long-term facts → \`./memory.md\`, which syncs to this artist's Plinth record. Most artists have none yet: the file appears once the record carries memory, and creating it here is how you start one.
|
|
28014
|
-
lines.push(`- What a session did → a dated section under \`## Session log\` at the bottom of \`./_artist.md\` but ABOVE any \`<!-- BEGIN: contacts -->\` block, which is rendered from Plinth and overwrites anything inside it on the next sync. That body syncs, so a teammate opens the record in the app. Keep the five most recent; a fact that outlives them belongs in \`./memory.md\` above
|
|
28311
|
+
lines.push(`- Long-term facts → \`./memory.md\`, which syncs to this artist's Plinth record. Most artists have none yet: the file appears once the record carries memory, and creating it here is how you start one. The session grounding states how to write a fact; follow it rather than a copy`);
|
|
28312
|
+
lines.push(`- What a session did → a dated section under \`## Session log\` at the bottom of \`./_artist.md\` but ABOVE any \`<!-- BEGIN: contacts -->\` block, which is rendered from Plinth and overwrites anything inside it on the next sync. That body syncs, so a teammate opens the record in the app. Keep the five most recent; a fact that outlives them belongs in \`./memory.md\` above`);
|
|
28015
28313
|
lines.push("");
|
|
28016
28314
|
lines.push(USER_BLOCK_BEGIN);
|
|
28017
28315
|
lines.push(USER_BLOCK_PLACEHOLDER);
|
|
@@ -28258,7 +28556,16 @@ var MANAGED_COMMENT = [
|
|
|
28258
28556
|
"or in .claude/settings.local.json.",
|
|
28259
28557
|
"The permissions.deny entries below are managed the same way. They stop an agent",
|
|
28260
28558
|
"in this workspace sending, replying to or forwarding mail and writing a calendar;",
|
|
28261
|
-
"drafting and reading stay allowed.
|
|
28559
|
+
"drafting and reading stay allowed. They match by VERB FAMILY rather than by",
|
|
28560
|
+
"tool name - send_mail, sendEmail, send-mail and outlook_send_message are one",
|
|
28561
|
+
"rule set, not four - and the matcher cannot express an alternation, so each",
|
|
28562
|
+
"spelling needs its own line. That width also catches a few reads whose name",
|
|
28563
|
+
"merely begins with a send verb - getForwardingSettings is the live example.",
|
|
28564
|
+
"You cannot re-open one of these here: a deny beats an allow, including yours,",
|
|
28565
|
+
"and this list is rewritten every sync. Tell us and we will narrow the pattern.",
|
|
28566
|
+
"We took that trade knowingly - a refusal is visible and recoverable, and a",
|
|
28567
|
+
"message that has gone cannot be recalled.",
|
|
28568
|
+
"One more entry stops an agent EDITING voice.md, the",
|
|
28262
28569
|
"voice profile it is judged against: that file is yours to write and theirs to read.",
|
|
28263
28570
|
"Your own allow list is untouched, and your own",
|
|
28264
28571
|
"deny rules are kept and listed first. One exception, stated because it is",
|
|
@@ -28269,7 +28576,11 @@ var MANAGED_COMMENT = [
|
|
|
28269
28576
|
"since retired is removed here even if you wrote it yourself, because we cannot",
|
|
28270
28577
|
"tell the two apart. Today that is `Edit(./voice.md)`, which Claude Code itself",
|
|
28271
28578
|
"suggests - it is superseded by `Edit(//**/voice.md)` below, which is the same",
|
|
28272
|
-
"protection at every depth
|
|
28579
|
+
"protection at every depth - plus the seven exact-verb rules the verb families",
|
|
28580
|
+
"replaced (`mcp__*__reply`, `mcp__*__create_event` and five more of that shape).",
|
|
28581
|
+
"Every one of those is still covered by a wider rule below, so nothing you were",
|
|
28582
|
+
"protected from yesterday is allowed today. Put anything you want kept in",
|
|
28583
|
+
"settings.local.json."
|
|
28273
28584
|
];
|
|
28274
28585
|
var MANAGED_DENY = [
|
|
28275
28586
|
"mcp__claude_ai_Gmail__send_message",
|
|
@@ -28279,17 +28590,70 @@ var MANAGED_DENY = [
|
|
|
28279
28590
|
"mcp__claude_ai_Google_Calendar__update_event",
|
|
28280
28591
|
"mcp__claude_ai_Google_Calendar__delete_event",
|
|
28281
28592
|
"mcp__claude_ai_Google_Calendar__respond_to_event",
|
|
28593
|
+
"mcp__*__send*",
|
|
28594
|
+
"mcp__*__*_send*",
|
|
28595
|
+
"mcp__*__*-send*",
|
|
28596
|
+
"mcp__*__*Send*",
|
|
28597
|
+
"mcp__*__reply*",
|
|
28598
|
+
"mcp__*__*_reply*",
|
|
28599
|
+
"mcp__*__*-reply*",
|
|
28600
|
+
"mcp__*__*Reply*",
|
|
28601
|
+
"mcp__*__forward*",
|
|
28602
|
+
"mcp__*__*_forward*",
|
|
28603
|
+
"mcp__*__*-forward*",
|
|
28604
|
+
"mcp__*__*Forward*",
|
|
28605
|
+
"mcp__*__respond*",
|
|
28606
|
+
"mcp__*__*_respond*",
|
|
28607
|
+
"mcp__*__*-respond*",
|
|
28608
|
+
"mcp__*__*Respond*",
|
|
28609
|
+
"mcp__*__rsvp*",
|
|
28610
|
+
"mcp__*__*_rsvp*",
|
|
28611
|
+
"mcp__*__*-rsvp*",
|
|
28612
|
+
"mcp__*__*Rsvp*",
|
|
28613
|
+
"mcp__*__*RSVP*",
|
|
28614
|
+
"mcp__*__create*event*",
|
|
28615
|
+
"mcp__*__*_create*event*",
|
|
28616
|
+
"mcp__*__*-create*event*",
|
|
28617
|
+
"mcp__*__*Create*event*",
|
|
28618
|
+
"mcp__*__create*Event*",
|
|
28619
|
+
"mcp__*__*_create*Event*",
|
|
28620
|
+
"mcp__*__*-create*Event*",
|
|
28621
|
+
"mcp__*__*Create*Event*",
|
|
28622
|
+
"mcp__*__update*event*",
|
|
28623
|
+
"mcp__*__*_update*event*",
|
|
28624
|
+
"mcp__*__*-update*event*",
|
|
28625
|
+
"mcp__*__*Update*event*",
|
|
28626
|
+
"mcp__*__update*Event*",
|
|
28627
|
+
"mcp__*__*_update*Event*",
|
|
28628
|
+
"mcp__*__*-update*Event*",
|
|
28629
|
+
"mcp__*__*Update*Event*",
|
|
28630
|
+
"mcp__*__delete*event*",
|
|
28631
|
+
"mcp__*__*_delete*event*",
|
|
28632
|
+
"mcp__*__*-delete*event*",
|
|
28633
|
+
"mcp__*__*Delete*event*",
|
|
28634
|
+
"mcp__*__delete*Event*",
|
|
28635
|
+
"mcp__*__*_delete*Event*",
|
|
28636
|
+
"mcp__*__*-delete*Event*",
|
|
28637
|
+
"mcp__*__*Delete*Event*",
|
|
28638
|
+
"mcp__*__cancel*event*",
|
|
28639
|
+
"mcp__*__*_cancel*event*",
|
|
28640
|
+
"mcp__*__*-cancel*event*",
|
|
28641
|
+
"mcp__*__*Cancel*event*",
|
|
28642
|
+
"mcp__*__cancel*Event*",
|
|
28643
|
+
"mcp__*__*_cancel*Event*",
|
|
28644
|
+
"mcp__*__*-cancel*Event*",
|
|
28645
|
+
"mcp__*__*Cancel*Event*",
|
|
28646
|
+
"Edit(//**/voice.md)"
|
|
28647
|
+
];
|
|
28648
|
+
var RETIRED_DENY = [
|
|
28649
|
+
"Edit(./voice.md)",
|
|
28282
28650
|
"mcp__*__send_message",
|
|
28283
28651
|
"mcp__*__reply",
|
|
28284
28652
|
"mcp__*__forward",
|
|
28285
28653
|
"mcp__*__create_event",
|
|
28286
28654
|
"mcp__*__update_event",
|
|
28287
28655
|
"mcp__*__delete_event",
|
|
28288
|
-
"mcp__*__respond_to_event"
|
|
28289
|
-
"Edit(//**/voice.md)"
|
|
28290
|
-
];
|
|
28291
|
-
var RETIRED_DENY = [
|
|
28292
|
-
"Edit(./voice.md)"
|
|
28656
|
+
"mcp__*__respond_to_event"
|
|
28293
28657
|
];
|
|
28294
28658
|
var BIN_SENTINEL = "__PLINTH_BIN__";
|
|
28295
28659
|
var ALL_SENTINEL = "__PLINTH_ALL__";
|
|
@@ -28553,6 +28917,7 @@ function ensurePerArtistContext(deps) {
|
|
|
28553
28917
|
const resolved = resolvePlinthBinary();
|
|
28554
28918
|
reportResolution(out, resolved);
|
|
28555
28919
|
let created = 0;
|
|
28920
|
+
let removedDailyLogs = 0;
|
|
28556
28921
|
for (const artist of roster) {
|
|
28557
28922
|
try {
|
|
28558
28923
|
const entities = entityIndex.get(artist.slug) ?? {
|
|
@@ -28571,7 +28936,8 @@ function ensurePerArtistContext(deps) {
|
|
|
28571
28936
|
if (regenerateClaudeMd(workspaceSlug, artist.slug, generated, force, out, now)) {
|
|
28572
28937
|
created++;
|
|
28573
28938
|
}
|
|
28574
|
-
|
|
28939
|
+
if (retireDailyLog(workspaceSlug, artist.slug))
|
|
28940
|
+
removedDailyLogs++;
|
|
28575
28941
|
if (ensureSessionHook(workspaceSlug, `artists/${artist.slug}/`, out, resolved) === "skipped") {
|
|
28576
28942
|
deps.onHookSkipped?.();
|
|
28577
28943
|
}
|
|
@@ -28583,6 +28949,9 @@ function ensurePerArtistContext(deps) {
|
|
|
28583
28949
|
if (created > 0) {
|
|
28584
28950
|
out.info(`Scaffolded per-artist context for ${created} artist${created === 1 ? "" : "s"}.`);
|
|
28585
28951
|
}
|
|
28952
|
+
if (removedDailyLogs > 0) {
|
|
28953
|
+
out.info(`Removed ${removedDailyLogs} unused daily-log.md file${removedDailyLogs === 1 ? "" : "s"} (never written to; any with your own notes in them were left alone).`);
|
|
28954
|
+
}
|
|
28586
28955
|
}
|
|
28587
28956
|
function regenerateClaudeMd(workspaceSlug, artistSlug, generated, force, out, now) {
|
|
28588
28957
|
const claudeRel = `artists/${artistSlug}/${ARTIST_CLAUDE_MD}`;
|
|
@@ -28604,17 +28973,55 @@ function regenerateClaudeMd(workspaceSlug, artistSlug, generated, force, out, no
|
|
|
28604
28973
|
}
|
|
28605
28974
|
return decision.action === "create";
|
|
28606
28975
|
}
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
return;
|
|
28610
|
-
writeContextFile(workspaceSlug, relativePath, content);
|
|
28611
|
-
}
|
|
28612
|
-
function dailyLogStub(name) {
|
|
28613
|
-
return `# ${name} - Daily log
|
|
28976
|
+
var DAILY_LOG_STUB_TEMPLATES = [
|
|
28977
|
+
(name) => `# ${name} - Daily log
|
|
28614
28978
|
|
|
28615
28979
|
` + `<!-- Your own scratch notes; edit freely, Plinth never overwrites this file. -->
|
|
28616
28980
|
` + `<!-- Nothing writes here now: a session's record goes under ## Session log in ./_artist.md, and durable facts in ./memory.md. Both sync; this file does not. -->
|
|
28617
|
-
|
|
28981
|
+
`,
|
|
28982
|
+
(name) => `# ${name} - Daily log
|
|
28983
|
+
|
|
28984
|
+
` + `<!-- Rolling activity, newest first; edit freely, Plinth never overwrites this file. -->
|
|
28985
|
+
` + `<!-- Durable facts do not belong here - they go in ./memory.md, which syncs to this artist's Plinth record. -->
|
|
28986
|
+
`,
|
|
28987
|
+
(name) => `# ${name} - Daily log
|
|
28988
|
+
|
|
28989
|
+
` + `<!-- Rolling activity, newest first; edit freely, Plinth never overwrites this file. -->
|
|
28990
|
+
`,
|
|
28991
|
+
(name) => `# ${name} — Daily log
|
|
28992
|
+
|
|
28993
|
+
` + `<!-- Rolling activity, newest first; edit freely, Plinth never overwrites this file. -->
|
|
28994
|
+
`
|
|
28995
|
+
];
|
|
28996
|
+
var DAILY_LOG_H1_RE = /^# (.+) [-—] Daily log\n/;
|
|
28997
|
+
function isPristineDailyLogStub(content) {
|
|
28998
|
+
const m = content.match(DAILY_LOG_H1_RE);
|
|
28999
|
+
if (m === null)
|
|
29000
|
+
return false;
|
|
29001
|
+
const name = m[1];
|
|
29002
|
+
return DAILY_LOG_STUB_TEMPLATES.some((t) => t(name) === content);
|
|
29003
|
+
}
|
|
29004
|
+
function retireDailyLog(workspaceSlug, artistSlug) {
|
|
29005
|
+
let target;
|
|
29006
|
+
try {
|
|
29007
|
+
target = contextFilePath(workspaceSlug, `artists/${artistSlug}/${ARTIST_DAILY_LOG_MD}`);
|
|
29008
|
+
} catch {
|
|
29009
|
+
return false;
|
|
29010
|
+
}
|
|
29011
|
+
let content;
|
|
29012
|
+
try {
|
|
29013
|
+
content = readFileSync12(target, "utf8");
|
|
29014
|
+
} catch {
|
|
29015
|
+
return false;
|
|
29016
|
+
}
|
|
29017
|
+
if (!isPristineDailyLogStub(content))
|
|
29018
|
+
return false;
|
|
29019
|
+
try {
|
|
29020
|
+
unlinkSync7(target);
|
|
29021
|
+
return true;
|
|
29022
|
+
} catch {
|
|
29023
|
+
return false;
|
|
29024
|
+
}
|
|
28618
29025
|
}
|
|
28619
29026
|
function backupStamp(d) {
|
|
28620
29027
|
return d.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
@@ -28623,6 +29030,28 @@ function backupStamp(d) {
|
|
|
28623
29030
|
// src/lib/context/generate.ts
|
|
28624
29031
|
import { existsSync as existsSync2 } from "node:fs";
|
|
28625
29032
|
|
|
29033
|
+
// src/lib/cli/command-names.ts
|
|
29034
|
+
var PLINTH_COMMAND_NAMES = [
|
|
29035
|
+
"login",
|
|
29036
|
+
"sync",
|
|
29037
|
+
"start",
|
|
29038
|
+
"status",
|
|
29039
|
+
"confirm",
|
|
29040
|
+
"version",
|
|
29041
|
+
"refresh-context",
|
|
29042
|
+
"grounding",
|
|
29043
|
+
"voice-gate",
|
|
29044
|
+
"backlinks",
|
|
29045
|
+
"declare",
|
|
29046
|
+
"review-tier",
|
|
29047
|
+
"session",
|
|
29048
|
+
"memory-gate"
|
|
29049
|
+
];
|
|
29050
|
+
function isPlinthCommand(token) {
|
|
29051
|
+
const needle = token.trim().toLowerCase();
|
|
29052
|
+
return PLINTH_COMMAND_NAMES.some((name) => name === needle);
|
|
29053
|
+
}
|
|
29054
|
+
|
|
28626
29055
|
// src/lib/context/skill-commands.ts
|
|
28627
29056
|
var COMMAND_USER_BLOCK_PLACEHOLDER = "_This workspace's own steps go here: anything specific to your team, your " + "tools or your data. Plinth preserves this block verbatim on every " + "regeneration; everything outside it is overwritten._";
|
|
28628
29057
|
var OWNERSHIP_NOTE = "<!-- Auto-generated by Plinth into every mirror on every sync. Edit only " + "between the BEGIN/END markers at the foot; everything else is overwritten. " + "Your own commands in `.claude/commands/` are never read or touched by " + "Plinth, only this `plinth/` folder is generated. -->";
|
|
@@ -28655,9 +29084,9 @@ function buildStartCommandMd(rosterIsEmpty2) {
|
|
|
28655
29084
|
"",
|
|
28656
29085
|
"## Step 1: use the two memory indexes you were already given",
|
|
28657
29086
|
"",
|
|
28658
|
-
"The SessionStart hook injects **both memory indexes
|
|
29087
|
+
"The SessionStart hook injects **both memory indexes**, above: the workspace index (durable facts and conventions that hold for anyone here) and your personal index (facts true of **this member**: how they work, what they prefer). They may arrive trimmed to their newest lines, and the injection says so where it happens. **Do not re-read `workspace-memory.md` or `user-memory.md` wholesale** - they are already in this context window and re-reading them is the single most expensive thing this command could do for nothing - but if the injection says an index was trimmed and you need what is missing, read that file.",
|
|
28659
29088
|
"",
|
|
28660
|
-
"**
|
|
29089
|
+
"**Who you are working for is in the grounding above, not in an index.** The hook renders the member, their email and the workspace from the credentials this machine logged in with, so do not go looking for identity in either memory file and do not ask the user for it. **Use the personal index for what it is actually for**, which is the half that gets skipped: how this member works and what they prefer.",
|
|
28661
29090
|
"",
|
|
28662
29091
|
"If neither index appears above, the hook did not run - then, and only then, read the two files at the **mirror root**, the workspace folder holding `CLAUDE.md` (if this session started inside `artists/<slug>/`, that is the parent). Read them silently; do not dump either file into the conversation.",
|
|
28663
29092
|
"",
|
|
@@ -28705,7 +29134,7 @@ function buildStartCommandMd(rosterIsEmpty2) {
|
|
|
28705
29134
|
"",
|
|
28706
29135
|
"## Loading area context",
|
|
28707
29136
|
"",
|
|
28708
|
-
"Once the user names something, resolve it against the workspace before answering: search the Plinth MCP, or read the roster in `CLAUDE.md`. **Never answer from name recognition alone.** Then read the surfaces that carry it. An artist's master context is `artists/<slug>/_artist.md`, whose `## Session log` carries what recent sessions did; their durable facts are `artists/<slug>/memory.md`. Older local history may sit in `artists/<slug>/daily-log.md`, which nothing writes to any more. Brief on what is live, not on what you opened.",
|
|
29137
|
+
"Once the user names something, resolve it against the workspace before answering: search the Plinth MCP, or read the roster in `CLAUDE.md`. **Never answer from name recognition alone.** Then read the surfaces that carry it. An artist's master context is `artists/<slug>/_artist.md`, whose `## Session log` carries what recent sessions did; their durable facts are `artists/<slug>/memory.md`. Older local history may sit in `artists/<slug>/daily-log.md`, which is no longer created and nothing writes to any more. Brief on what is live, not on what you opened.",
|
|
28709
29138
|
"",
|
|
28710
29139
|
"If nothing resolves, say so plainly and ask. Do not invent an entity.",
|
|
28711
29140
|
"",
|
|
@@ -28835,7 +29264,7 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28835
29264
|
"",
|
|
28836
29265
|
"If the file is not there yet, create it the same way as the artist store: a one-line title, the same conventions in a comment, and a real fact in the same edit. **Check what the workspace already says before you write a personal line** - a convention that is already in `workspace-rules.md` or the workspace's own house-style file is in force for everyone, and copying it here duplicates a live rule into a second injected surface instead of adding anything.",
|
|
28837
29266
|
"",
|
|
28838
|
-
"## Step 5: summarise
|
|
29267
|
+
"## Step 5: summarise",
|
|
28839
29268
|
"",
|
|
28840
29269
|
"**First, re-read the conflicts list.** Everything this close wrote - the session record in Step 3, the memory files in Step 4 - landed AFTER Step 2 looked, and a losing write is set aside within seconds of being made. Step 2 therefore cannot have seen it. Read the list again now:",
|
|
28841
29270
|
"",
|
|
@@ -28851,6 +29280,38 @@ function buildCloseCommandMd(workspaceSlug) {
|
|
|
28851
29280
|
"",
|
|
28852
29281
|
"**Five to seven lines**, and no file-by-file dump: **where the session record went, named per artist**, what now lives on a durable surface, what is still in flight and on whom, **what was banked to memory and to which of the two piles**, anything you named as having no home yet, any fact you judged true of the whole team and left unwritten, any date you handed to a person, and the obvious next move. **This is how the user sees what was stored without opening a file**, so name the files rather than saying that memory was updated.",
|
|
28853
29282
|
"",
|
|
29283
|
+
"## Step 6: offer to trim what has outgrown its file, and do not trim it",
|
|
29284
|
+
"",
|
|
29285
|
+
"The `plinth status` you ran a moment ago has a row labelled **`Too long`**, with one line per file beneath it. **That row is this step's whole trigger.** If it is not there, **say nothing at all** - no line, no mention, and no note that there was nothing to say. This signal is worth reading precisely because it is rare, and one that fires at every close is one nobody reads.",
|
|
29286
|
+
"",
|
|
29287
|
+
"**Nothing on the `Injection` row above it is a trigger, whatever it says.** That row reports how much of an index rides the session start, in either of two wordings - `Only N of M facts ... fit in the injection`, and `NONE of ... fits in the injection: all N facts are read from the file on demand`. **The second reads like an emergency and is not one.** Most workspaces are partly trimmed most of the time, and the remedy there is shorter lines rather than fewer facts, which that row states itself. Treating either line as the bar turns a rare offer into one that fires at nearly every close, on the member's private index, which is the outcome this step exists to avoid.",
|
|
29288
|
+
"",
|
|
29289
|
+
"If it named one or more, **offer in a single clause and stop there**, then wait for an answer:",
|
|
29290
|
+
"",
|
|
29291
|
+
"> Two artist stores and `voice.md` are past the size they are useful at. Want me to propose a trim?",
|
|
29292
|
+
"",
|
|
29293
|
+
"**Name every file the status named, including the ones you cannot touch.** Some of them you may only report on, and which those are is settled below - but a member who is told about two files out of three has been given a worse answer than one who is told about all three, not a tidier one.",
|
|
29294
|
+
"",
|
|
29295
|
+
"**Running the trim on your own initiative is the failure this step exists to prevent**, and it is the same failure as reporting a sweep nobody asked for. A `no` ends it here, in one line, and the next close offers it again in the same clause without arguing.",
|
|
29296
|
+
"",
|
|
29297
|
+
"**On yes: propose the cut, and write nothing yet.** Take one file at a time and show the member what would change before anything does:",
|
|
29298
|
+
"",
|
|
29299
|
+
"- **Every surviving fact appears either unchanged or beside its rewrite**, so the member is reading a diff rather than trusting a summary of one.",
|
|
29300
|
+
"- **A fact that is now wrong is edited where it stands, not deleted** - the correction and the date it changed are the record. The file's own header comment states its conventions and is the version that stays current; follow it rather than a copy.",
|
|
29301
|
+
"- **Provenance and the absolute date survive on every line you keep.** A fact whose source you drop while shortening it has been damaged, not trimmed.",
|
|
29302
|
+
"- **Nothing banked in the last 30 days is dropped**, measured against the date the session hook injected. Recent facts have not had time to prove whether they were durable, and a trim is not the place to decide.",
|
|
29303
|
+
"- **Merging two facts into one is a rewrite and shows as one**, with both original lines visible in the proposal.",
|
|
29304
|
+
"",
|
|
29305
|
+
"**Then write only what the member confirms**, and only to the files this command can reach:",
|
|
29306
|
+
"",
|
|
29307
|
+
"- `artists/<slug>/memory.md` syncs, and a teammate may be in the same file. **Re-read it at the moment you write**, exactly as Step 4b requires, and change only the lines the proposal named. This is the only file this step ever writes.",
|
|
29308
|
+
`- \`${VOICE_FILENAME}\` is named and nothing else. You are denied editing it, and that is deliberate: the profile a draft is judged against is not the drafter's to rewrite. Say its size, say the member edits it themselves, and propose nothing.`,
|
|
29309
|
+
"- **A memory file the whole workspace shares is not this command's to trim.** `plinth status` names it and says how far over it is; trimming it is a deliberate act by a person who can see who else it belongs to, not a step at the end of somebody's session.",
|
|
29310
|
+
"",
|
|
29311
|
+
"**Anything you write here lands after Step 5 looked at the conflicts list**, so it can be set aside within seconds like any other write. If you wrote a trim, run the Step 5 check once more on what you wrote and report it the same way.",
|
|
29312
|
+
"",
|
|
29313
|
+
"## Step 7: end the session",
|
|
29314
|
+
"",
|
|
28854
29315
|
"Then, as the **very last step**:",
|
|
28855
29316
|
"",
|
|
28856
29317
|
"```sh",
|
|
@@ -28881,7 +29342,7 @@ function buildOnboardCommandMd(workspaceSlug) {
|
|
|
28881
29342
|
"",
|
|
28882
29343
|
"**Propose, then stop. Never sweep.** Every step below that creates anything puts the list to the user first and waits for an answer. This is not politeness and it is not a style: the first live run of the opening command on a real workspace created **63 tasks** against an explicit instruction to offer rather than run, and this command creates far more than that one did.",
|
|
28883
29344
|
"",
|
|
28884
|
-
"**And the tools will not
|
|
29345
|
+
"**And the tools will not agree to anything for you.** Creating an artist, a task or a project makes you come back with a confirmation token, so the workspace refuses a write you have not repeated. **What that token proves is that you called twice with the same arguments. It does not prove that anybody said yes** - nothing in that exchange asks the user a question. **A contact has no token at all** and is written the moment you call it. Either way, the only thing standing between this workspace and a hundred unwanted rows is you putting the list to them first and waiting for the answer.",
|
|
28885
29346
|
"",
|
|
28886
29347
|
"## Step 0: find out where they already are",
|
|
28887
29348
|
"",
|
|
@@ -28936,7 +29397,7 @@ function buildOnboardCommandMd(workspaceSlug) {
|
|
|
28936
29397
|
"- **Their name.**",
|
|
28937
29398
|
"- **Their Spotify link.** Ask for it and take whatever they paste. The bit you need is the ID in the middle of the URL: from `open.spotify.com/artist/<the long string on the end>` the ID is that string. Pass it as `spotify_artist_id`. **If they do not have it to hand, create the artist without it and move on** rather than stalling the run; it can be added later.",
|
|
28938
29399
|
"",
|
|
28939
|
-
"Create each one with `create_artist`. **It will hand you back a confirmation token rather than creating anything**,
|
|
29400
|
+
"Create each one with `create_artist`. **It will hand you back a confirmation token rather than creating anything**, so nothing exists until you call again with the same arguments and that token. **The token is not the user agreeing** - it asks you nothing about them - so put the artist to them first, and complete the write only once they have said yes. If they say no to one, drop it.",
|
|
28940
29401
|
"",
|
|
28941
29402
|
"**`create_artist` also requires a `slug`, and it is yours to derive rather than theirs to supply.** Do not ask for it: it is a detail of how the workspace stores things, and asking turns a two-question step into a three-question one for no gain.",
|
|
28942
29403
|
"",
|
|
@@ -28962,7 +29423,7 @@ function buildOnboardCommandMd(workspaceSlug) {
|
|
|
28962
29423
|
"",
|
|
28963
29424
|
"**If you cannot name the role, do not propose the person.** An inbox is full of names, and a list of everyone who has ever appeared in a thread is worse than no list at all: it buries the eight people who matter and it makes the user do the sorting you were meant to do. A signature block, a company domain or the thread itself will usually tell you the role. When none of them does, leave the person out.",
|
|
28964
29425
|
"",
|
|
28965
|
-
"Put the ones you are sure of to the user with their role and email, per artist, and write only the ones they confirm with `create_artist_contact`.",
|
|
29426
|
+
"Put the ones you are sure of to the user with their role and email, per artist, and write only the ones they confirm with `create_artist_contact`. **This is the one write in this run with no confirmation step**: it lands the moment you call it, so the user's yes has to come before the call rather than after it.",
|
|
28966
29427
|
"",
|
|
28967
29428
|
"**Every contact carries its artist, and the write REFUSES one without it.** `create_artist_contact` needs `artist_id` or `artist_name` and errors with `Either artist_id or artist_name is required` when neither is there, so this is not a field you can leave blank and tidy up later. A person who genuinely spans several artists is therefore a decision rather than an omission: name them to the user, file them under the artist they belong to most, or leave them out and say that you have.",
|
|
28968
29429
|
"",
|
|
@@ -28977,10 +29438,10 @@ function buildOnboardCommandMd(workspaceSlug) {
|
|
|
28977
29438
|
"**Show them the whole list before you create any of it**, numbered, and **name the thread each one came from** so every proposal can be traced back to something real. Then gate it:",
|
|
28978
29439
|
"",
|
|
28979
29440
|
"- Put it to them with **`AskUserQuestion`**: create all of them, pick through them, or none.",
|
|
28980
|
-
"- **If they pick through them, walk the list in batches of four as multi-select questions** (the tool takes four options per question and four questions per ask). Selected means create it. Unselected means drop it.",
|
|
29441
|
+
"- **If they pick through them, walk the list in batches of four as multi-select questions** (the tool takes four options per question and four questions per ask). Selected means create it. Unselected means drop it. **A batch with a single item left cannot be asked that way** - a one-option question is refused by the tool - so put that one to them in plain words instead and take the answer as given.",
|
|
28981
29442
|
'- **The free-text "Other" choice is how they rewrite one in their own words**, and their wording wins over yours.',
|
|
28982
29443
|
"",
|
|
28983
|
-
"**Create nothing until an answer comes back, and create only what came back.**
|
|
29444
|
+
"**Create nothing until an answer comes back, and create only what came back.** A task and a project each answer your first call with a preview and a confirmation token, and write nothing at all until you call again with the same arguments plus that token. **The second call is yours to make and not another question for the user** - they have already answered. The token is single-use, expires in five minutes and is refused if any argument changed, so confirm each one as you go rather than previewing the whole batch and coming back to it. **If you find yourself about to create something nobody named, stop**: the token will not stop you, because you are the one holding it.",
|
|
28984
29445
|
"",
|
|
28985
29446
|
"**`create_project` requires a `category` and will refuse an invalid one**, so settle it before you start writing rather than guessing partway through a confirmed batch. **Read the exact values off the tool's own schema and pass one of them character for character.** Do not work from a list written down anywhere, including this file: the set changes, a near miss is refused, and the plural forms are not what you would guess.",
|
|
28986
29447
|
"",
|
|
@@ -28988,7 +29449,19 @@ function buildOnboardCommandMd(workspaceSlug) {
|
|
|
28988
29449
|
"",
|
|
28989
29450
|
"**Where you know who owns something, put a real owner and a real due date on it.** Where you do not, leave them empty rather than guessing: a wrong due date on day one teaches them the dates here cannot be trusted.",
|
|
28990
29451
|
"",
|
|
28991
|
-
"## Step 7:
|
|
29452
|
+
"## Step 7: their voice, in their own words",
|
|
29453
|
+
"",
|
|
29454
|
+
`**One file here is theirs alone: \`${VOICE_FILENAME}\`, at the root of this folder.** It is how they write, and it is read before anything goes out in their name. **You cannot edit it** - the workspace denies that on purpose, so that what an agent is judged against is not something the agent can rewrite. Ask, and let them type.`,
|
|
29455
|
+
"",
|
|
29456
|
+
'**Ask for mechanical rules first, and only four or five of them.** The ones that are either followed or broken with no judgement in between: how they sign off, whether they use dashes, whether a signature block goes on the end, how long an opening paragraph runs, anything they know they always do. Those are worth more than a page about tone, because they can be checked against and "warm but direct" cannot.',
|
|
29457
|
+
"",
|
|
29458
|
+
"**Say where the rest goes: below the rules, not above them.** Examples, past emails and longer notes are useful and they are not the part that has to survive a reader who stops early. A profile is read top to bottom.",
|
|
29459
|
+
"",
|
|
29460
|
+
"**And say plainly that nothing checks a draft automatically.** No gate sits between what gets written and the person who reads it. That file and the agent applying it are the whole of it, which is exactly why the mechanical rules are worth the two minutes now.",
|
|
29461
|
+
"",
|
|
29462
|
+
"**If they would rather skip it, say what it costs in one line** and move on: everything drafted for them will read as the assistant's writing rather than theirs, and they will be correcting the same things by hand each time.",
|
|
29463
|
+
"",
|
|
29464
|
+
"## Step 8: tell them what they have, and what they have not",
|
|
28992
29465
|
"",
|
|
28993
29466
|
"**Five or six lines, no file-by-file dump:**",
|
|
28994
29467
|
"",
|
|
@@ -29068,7 +29541,7 @@ function scanStaleUserBlocks(deps) {
|
|
|
29068
29541
|
}
|
|
29069
29542
|
checked++;
|
|
29070
29543
|
const haystack = block.toLowerCase();
|
|
29071
|
-
const tokens = retired.filter((r) => r.commandRel === rel && haystack.includes(r.token.toLowerCase()));
|
|
29544
|
+
const tokens = retired.filter((r) => r.commandRel === rel && haystack.includes(r.token.toLowerCase())).map((r) => ({ ...r, stillACommand: isPlinthCommand(r.token) }));
|
|
29072
29545
|
if (tokens.length > 0) {
|
|
29073
29546
|
stale.push({ commandRel: rel, invocation: invocationFor(rel), tokens });
|
|
29074
29547
|
}
|
|
@@ -29076,215 +29549,748 @@ function scanStaleUserBlocks(deps) {
|
|
|
29076
29549
|
return { checked, unchecked, stale };
|
|
29077
29550
|
}
|
|
29078
29551
|
|
|
29079
|
-
// src/lib/
|
|
29080
|
-
var
|
|
29081
|
-
|
|
29082
|
-
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29088
|
-
|
|
29089
|
-
|
|
29090
|
-
|
|
29552
|
+
// src/lib/files/api.ts
|
|
29553
|
+
var MAX_WORKSPACE_FILE_BYTES = 4000000;
|
|
29554
|
+
var HASH_RE = /^[0-9a-f]{64}$/;
|
|
29555
|
+
var TextQualitySchema = exports_external.enum(["ok", "partial", "garbled"]);
|
|
29556
|
+
var FileLiveSchema = exports_external.object({
|
|
29557
|
+
id: exports_external.string().uuid(),
|
|
29558
|
+
path: exports_external.string().min(1),
|
|
29559
|
+
mime_type: exports_external.string(),
|
|
29560
|
+
size_bytes: exports_external.number().int().nonnegative(),
|
|
29561
|
+
content_hash: exports_external.string().regex(HASH_RE),
|
|
29562
|
+
artist_id: exports_external.string().uuid().nullable(),
|
|
29563
|
+
project_id: exports_external.string().uuid().nullable(),
|
|
29564
|
+
updated_at: exports_external.string().datetime({ offset: true }),
|
|
29565
|
+
deleted_at: exports_external.null(),
|
|
29566
|
+
last_edited_by: exports_external.string().uuid().nullable(),
|
|
29567
|
+
content_quality: TextQualitySchema.nullable().optional().default(null).catch(null),
|
|
29568
|
+
has_extracted_text: exports_external.boolean().optional().catch(undefined),
|
|
29569
|
+
has_stored_bytes: exports_external.boolean().optional().catch(undefined)
|
|
29570
|
+
});
|
|
29571
|
+
function storedBytesSignal(row) {
|
|
29572
|
+
if (row.has_stored_bytes === true)
|
|
29573
|
+
return "present";
|
|
29574
|
+
if (row.has_stored_bytes === false)
|
|
29575
|
+
return "absent";
|
|
29576
|
+
return "unknown";
|
|
29091
29577
|
}
|
|
29092
|
-
function
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
29096
|
-
|
|
29097
|
-
|
|
29098
|
-
schemaVersion
|
|
29099
|
-
} = input;
|
|
29100
|
-
const entityLines = ENTITY_CATALOG.map((e) => `- **${e.label}**: ${e.blurb}${e.file ? "" : " _(MCP only)_"}`);
|
|
29101
|
-
const skillLines = SKILL_LOCATIONS.map((s) => `- ${s.label}: ${s.detail}`);
|
|
29102
|
-
const skillCount = SKILL_LOCATIONS.length;
|
|
29103
|
-
const lines = [
|
|
29104
|
-
`<!-- plinth-schema-version: ${schemaVersion} -->`,
|
|
29105
|
-
`<!-- Auto-generated by Plinth. ${GENERATED_AT_PREFIX}${GENERATED_AT_PLACEHOLDER}; ` + `that stamp moves only when this file's content changes, so an unchanged file keeps an older one ` + `and is still re-checked against Plinth on every sync. Run \`plinth status\` for when the mirror last synced. ` + `Edit only between the BEGIN/END markers below; everything else is overwritten on sync. -->`,
|
|
29106
|
-
``,
|
|
29107
|
-
`# ${workspaceName} · Plinth workspace`,
|
|
29108
|
-
``,
|
|
29109
|
-
`Local mirror of the **${workspaceName}** workspace (\`${workspaceSlug}\`). ${SECURITY_NOTE}`,
|
|
29110
|
-
``,
|
|
29111
|
-
`> **Session grounding**: the current date, an entity-resolution rule, and a write-confirmation rule are injected at every session start by the Plinth hook in \`.claude/settings.json\` (and each \`artists/<slug>/.claude/settings.json\`).`,
|
|
29112
|
-
``,
|
|
29113
|
-
`> **Filing a file the user hands you - BOTH halves, and the first one is unconditional**: put a copy in \`files/\`, which is how Plinth gets the text (folder map below) - do this even if everything else here fails. Then put the **original** in the user's own file storage with whatever tool you have for that, and write down where it went somewhere durable: \`artists/<slug>/memory.md\` if it is that artist's, otherwise your own \`user-memory.md\`. A pointer that exists only in this conversation is lost when it ends. If you have no tool that reaches their file storage, SAY SO - do not leave them believing the original was filed.`,
|
|
29114
|
-
``,
|
|
29115
|
-
`## Folder map`,
|
|
29116
|
-
`- \`artists/<slug>/\` one folder per artist, the working dir for artist-scoped sessions · \`threads/<slug>/_thread.md\` one folder per thread`,
|
|
29117
|
-
`- \`projects/\` \`tasks/\` \`documents/\` \`meetings/\` workspace-root flat entity files (one \`.md\` per entity, NOT nested under \`artists/\`)`,
|
|
29118
|
-
`- \`files/\` free-form document store: PDFs, executed agreements, spreadsheets, anything that is a FILE rather than an entity. Mirrored byte-for-byte both ways, folders and all`,
|
|
29119
|
-
`- \`${extractedTextRelPath("<path>")}\` where \`<path>\` is the file's path under \`files/\`: the text Plinth extracted from that file, so a PDF's contents can be searched. Not every file has one - text files are already their own text, and extraction can fail. Where it produced junk the file exists and says so instead of carrying it, so read what is there before trusting a silence. Derived: edits here are overwritten and never reach Plinth`,
|
|
29120
|
-
`- \`.claude/commands/\` local skills, incl. generated \`${START_COMMAND_INVOCATION}\` + \`${CLOSE_COMMAND_INVOCATION}\` + \`${ONBOARD_COMMAND_INVOCATION}\` · at the mirror root: \`workspace-memory.md\` \`workspace-rules.md\` (workspace-wide) + \`user-memory.md\` (yours), synced to Plinth · \`voice.md\` (yours: how you write, read by the agent, edited only by you) · \`CLAUDE.local.md\` private, gitignored`,
|
|
29121
|
-
``,
|
|
29122
|
-
`## Where key context lives: look here before hunting`,
|
|
29123
|
-
`- Each artist's **master context** (stable profile + current state) → \`artists/<slug>/_artist.md\`; edit its body and the daemon syncs both ways. Blocks labelled "rendered from Plinth" are the exception: they are overwritten on the next sync`,
|
|
29124
|
-
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
29125
|
-
`- Workspace-wide **notes, briefs, bios & context docs** → \`documents/\` (one markdown file per doc, shared across artists)`,
|
|
29126
|
-
`- Per-artist long-term facts → \`artists/<slug>/memory.md\`, which syncs to that artist's Plinth record; most have none yet, and creating the file is how you start one · what a session did → a dated section under \`## Session log\` in \`artists/<slug>/_artist.md\`, which syncs too · \`artists/<slug>/daily-log.md\` is local scratch, never synced; an older one may hold real history, and nothing writes there now`,
|
|
29127
|
-
`- ${MCP_AVAILABILITY_POINTER}`,
|
|
29128
|
-
``,
|
|
29129
|
-
`## Navigating the corpus`,
|
|
29130
|
-
`- **What points at X?** → \`plinth backlinks "<name>"\`, a mirror path, or a wikilink target as written. Render-only blocks, code spans and comments are narrowed out; \`--unfiltered\` shows them. A bare name several files answer to is refused with the candidates listed, so re-run with the path it names`,
|
|
29131
|
-
`- **What does this file link to?** → grep \`[[\` in it`,
|
|
29132
|
-
`- **Find a file by name** → glob under \`files/\` \`documents/\` \`artists/\` · **text inside a PDF** → \`${EXTRACTED_TEXT_DIR}\` (folder map above) · **write to a row you have the mirror file for** → pass its frontmatter \`id:\` to the \`mcp__plinth__*\` write tool; if you do not have the file, find it with \`search_entities\` / \`list_*\``,
|
|
29133
|
-
``,
|
|
29134
|
-
`## Entity model: ${MCP_INTROSPECTION_POINTER}`,
|
|
29135
|
-
...entityLines,
|
|
29136
|
-
``,
|
|
29137
|
-
`## Roster (${roster.length})`,
|
|
29138
|
-
rosterBlock(roster, activeProjectCountBySlug),
|
|
29139
|
-
``,
|
|
29140
|
-
`## Skills: ${skillCount} discoverable location${skillCount === 1 ? "" : "s"}`,
|
|
29141
|
-
...skillLines,
|
|
29142
|
-
``,
|
|
29143
|
-
USER_BLOCK_BEGIN,
|
|
29144
|
-
USER_BLOCK_PLACEHOLDER,
|
|
29145
|
-
USER_BLOCK_END
|
|
29146
|
-
];
|
|
29147
|
-
return lines.join(`
|
|
29148
|
-
`) + `
|
|
29149
|
-
`;
|
|
29578
|
+
function extractedTextSignal(row) {
|
|
29579
|
+
if (row.has_extracted_text === true)
|
|
29580
|
+
return "present";
|
|
29581
|
+
if (row.has_extracted_text === false)
|
|
29582
|
+
return "absent";
|
|
29583
|
+
return "unknown";
|
|
29150
29584
|
}
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
29155
|
-
|
|
29156
|
-
|
|
29157
|
-
|
|
29158
|
-
|
|
29159
|
-
|
|
29160
|
-
|
|
29161
|
-
|
|
29162
|
-
|
|
29163
|
-
|
|
29164
|
-
|
|
29165
|
-
|
|
29166
|
-
|
|
29167
|
-
|
|
29168
|
-
|
|
29169
|
-
|
|
29170
|
-
|
|
29585
|
+
var FileTombstoneSchema = exports_external.object({
|
|
29586
|
+
id: exports_external.string().uuid(),
|
|
29587
|
+
path: exports_external.string().min(1),
|
|
29588
|
+
updated_at: exports_external.string().datetime({ offset: true }),
|
|
29589
|
+
deleted_at: exports_external.string().datetime({ offset: true })
|
|
29590
|
+
});
|
|
29591
|
+
var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
|
|
29592
|
+
var FilesPageSchema = exports_external.object({
|
|
29593
|
+
files: exports_external.array(FileRowSchema),
|
|
29594
|
+
next_cursor: exports_external.string().nullable()
|
|
29595
|
+
});
|
|
29596
|
+
var FileWithDownloadSchema = FileLiveSchema.extend({
|
|
29597
|
+
download_url: exports_external.string().url().nullable()
|
|
29598
|
+
});
|
|
29599
|
+
var FileTextSchema = exports_external.object({
|
|
29600
|
+
id: exports_external.string().uuid(),
|
|
29601
|
+
path: exports_external.string().min(1),
|
|
29602
|
+
mime_type: exports_external.string(),
|
|
29603
|
+
updated_at: exports_external.string().datetime({ offset: true }),
|
|
29604
|
+
content_quality: TextQualitySchema.nullable(),
|
|
29605
|
+
source_uri: exports_external.string().nullable(),
|
|
29606
|
+
text: exports_external.string().nullable(),
|
|
29607
|
+
chars: exports_external.number().int().nonnegative(),
|
|
29608
|
+
withheld: exports_external.enum(["bytes_are_text", "no_text"]).nullable()
|
|
29609
|
+
});
|
|
29610
|
+
var PreconditionSchema = exports_external.object({
|
|
29611
|
+
code: exports_external.literal("precondition_failed"),
|
|
29612
|
+
current_updated_at: exports_external.string()
|
|
29613
|
+
});
|
|
29614
|
+
function baseHeaders(pat) {
|
|
29615
|
+
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
29171
29616
|
}
|
|
29172
|
-
function
|
|
29173
|
-
const {
|
|
29174
|
-
const
|
|
29175
|
-
|
|
29176
|
-
|
|
29177
|
-
|
|
29178
|
-
|
|
29179
|
-
|
|
29180
|
-
|
|
29181
|
-
|
|
29182
|
-
|
|
29183
|
-
|
|
29184
|
-
|
|
29185
|
-
|
|
29186
|
-
|
|
29187
|
-
currentVersion: "",
|
|
29188
|
-
generated,
|
|
29189
|
-
force,
|
|
29190
|
-
normalise: normaliseGeneratedAt
|
|
29191
|
-
});
|
|
29192
|
-
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
29193
|
-
const bakRel = `${CLAUDE_MD}.bak-${backupStamp3(now())}`;
|
|
29194
|
-
writeContextFile(workspaceSlug, bakRel, decision.backup);
|
|
29195
|
-
out.warn(`${CLAUDE_MD} had no recognisable BEGIN/END markers; backed your copy ` + `up to ${bakRel} before regenerating.`);
|
|
29617
|
+
async function syncFilesPage(args) {
|
|
29618
|
+
const { baseUrl, pat, cursor, limit, fetchImpl = fetch } = args;
|
|
29619
|
+
const params = new URLSearchParams;
|
|
29620
|
+
if (cursor)
|
|
29621
|
+
params.set("since", cursor);
|
|
29622
|
+
if (limit)
|
|
29623
|
+
params.set("limit", String(limit));
|
|
29624
|
+
const qs = params.toString();
|
|
29625
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files` + (qs.length > 0 ? `?${qs}` : "");
|
|
29626
|
+
let res;
|
|
29627
|
+
try {
|
|
29628
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
29629
|
+
} catch (err) {
|
|
29630
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29631
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
29196
29632
|
}
|
|
29197
|
-
if (
|
|
29198
|
-
|
|
29633
|
+
if (res.status === 401) {
|
|
29634
|
+
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
29199
29635
|
}
|
|
29200
|
-
if (
|
|
29201
|
-
|
|
29636
|
+
if (res.status === 429) {
|
|
29637
|
+
throw new ApiError(429, "Rate limited by the server. Wait a few seconds and retry.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29202
29638
|
}
|
|
29203
|
-
|
|
29204
|
-
|
|
29205
|
-
const resolved = resolvePlinthBinary();
|
|
29206
|
-
reportResolution(out, resolved);
|
|
29207
|
-
if (ensureSessionHook(workspaceSlug, "", out, resolved) === "skipped") {
|
|
29208
|
-
deps.onHookSkipped?.();
|
|
29639
|
+
if (!res.ok) {
|
|
29640
|
+
throw new ApiError(res.status, `sync files failed with HTTP ${res.status}`);
|
|
29209
29641
|
}
|
|
29210
|
-
|
|
29211
|
-
|
|
29212
|
-
|
|
29213
|
-
|
|
29642
|
+
const body = await res.json();
|
|
29643
|
+
const parsed = FilesPageSchema.safeParse(body);
|
|
29644
|
+
if (!parsed.success) {
|
|
29645
|
+
throw new ApiError(res.status, `sync files response did not match the expected shape: ${parsed.error.message}`);
|
|
29214
29646
|
}
|
|
29647
|
+
return { rows: parsed.data.files, next_cursor: parsed.data.next_cursor };
|
|
29215
29648
|
}
|
|
29216
|
-
function
|
|
29217
|
-
const
|
|
29218
|
-
const
|
|
29219
|
-
|
|
29220
|
-
|
|
29221
|
-
|
|
29222
|
-
|
|
29223
|
-
|
|
29224
|
-
`)
|
|
29225
|
-
`;
|
|
29226
|
-
if (existing === null || existing === "") {
|
|
29227
|
-
writeContextFile(workspaceSlug, GITIGNORE, block);
|
|
29228
|
-
return;
|
|
29649
|
+
async function getFile(args) {
|
|
29650
|
+
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
29651
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}`;
|
|
29652
|
+
let res;
|
|
29653
|
+
try {
|
|
29654
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
29655
|
+
} catch (err) {
|
|
29656
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29657
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
29229
29658
|
}
|
|
29230
|
-
|
|
29231
|
-
|
|
29232
|
-
|
|
29233
|
-
|
|
29234
|
-
}
|
|
29235
|
-
function backupStamp3(d) {
|
|
29236
|
-
return d.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
29237
|
-
}
|
|
29238
|
-
|
|
29239
|
-
// src/commands/refresh-context.ts
|
|
29240
|
-
init_output();
|
|
29241
|
-
function runRefreshContext(workspaceSlug, deps = {}) {
|
|
29242
|
-
const out = new Output({ sink: deps.sink, noColor: deps.noColor });
|
|
29243
|
-
const now = deps.now ?? (() => new Date);
|
|
29244
|
-
const config2 = loadConfig();
|
|
29245
|
-
if (!config2) {
|
|
29246
|
-
out.error("No workspace configured. Run `plinth login --workspace <slug>` first.");
|
|
29247
|
-
return 1;
|
|
29659
|
+
if (res.status === 404)
|
|
29660
|
+
return { ok: false, code: 404 };
|
|
29661
|
+
if (res.status === 401) {
|
|
29662
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29248
29663
|
}
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
if (!entry2) {
|
|
29252
|
-
out.error(`Workspace "${slug}" not found. Run \`plinth login --workspace ${slug}\` first.`);
|
|
29253
|
-
return 1;
|
|
29664
|
+
if (res.status === 429) {
|
|
29665
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29254
29666
|
}
|
|
29255
|
-
|
|
29256
|
-
|
|
29257
|
-
|
|
29258
|
-
|
|
29667
|
+
if (!res.ok) {
|
|
29668
|
+
throw new ApiError(res.status, `get file failed with HTTP ${res.status}`);
|
|
29669
|
+
}
|
|
29670
|
+
const body = await res.json();
|
|
29671
|
+
const parsed = FileWithDownloadSchema.safeParse(body);
|
|
29672
|
+
if (!parsed.success) {
|
|
29673
|
+
throw new ApiError(res.status, `get file response did not match the expected shape`);
|
|
29674
|
+
}
|
|
29675
|
+
return { ok: true, row: parsed.data };
|
|
29676
|
+
}
|
|
29677
|
+
async function getFileText(args) {
|
|
29678
|
+
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
29679
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}/text`;
|
|
29680
|
+
let res;
|
|
29259
29681
|
try {
|
|
29260
|
-
|
|
29261
|
-
workspaceSlug: slug,
|
|
29262
|
-
workspaceName: entry2.workspace_name ?? slug,
|
|
29263
|
-
force: true,
|
|
29264
|
-
out,
|
|
29265
|
-
now,
|
|
29266
|
-
onHookSkipped
|
|
29267
|
-
});
|
|
29268
|
-
ensurePerArtistContext({
|
|
29269
|
-
workspaceSlug: slug,
|
|
29270
|
-
force: true,
|
|
29271
|
-
out,
|
|
29272
|
-
now,
|
|
29273
|
-
onHookSkipped
|
|
29274
|
-
});
|
|
29682
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
29275
29683
|
} catch (err) {
|
|
29276
|
-
const msg = err instanceof Error ? err.message : "
|
|
29277
|
-
|
|
29278
|
-
return 1;
|
|
29684
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29685
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
29279
29686
|
}
|
|
29280
|
-
if (
|
|
29281
|
-
|
|
29282
|
-
|
|
29687
|
+
if (res.status === 404)
|
|
29688
|
+
return { ok: false, code: 404 };
|
|
29689
|
+
if (res.status === 401) {
|
|
29690
|
+
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
29283
29691
|
}
|
|
29284
|
-
|
|
29285
|
-
|
|
29692
|
+
if (res.status === 429) {
|
|
29693
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29694
|
+
}
|
|
29695
|
+
if (!res.ok) {
|
|
29696
|
+
throw new ApiError(res.status, `get file text failed with HTTP ${res.status}`);
|
|
29697
|
+
}
|
|
29698
|
+
const body = await res.json();
|
|
29699
|
+
const parsed = FileTextSchema.safeParse(body);
|
|
29700
|
+
if (!parsed.success) {
|
|
29701
|
+
throw new ApiError(res.status, `get file text response did not match the expected shape: ${parsed.error.message}`);
|
|
29702
|
+
}
|
|
29703
|
+
return { ok: true, row: parsed.data };
|
|
29286
29704
|
}
|
|
29287
|
-
|
|
29705
|
+
async function downloadBytes(downloadUrl, fetchImpl = fetch) {
|
|
29706
|
+
let res;
|
|
29707
|
+
try {
|
|
29708
|
+
res = await fetchImpl(downloadUrl, { method: "GET" });
|
|
29709
|
+
} catch (err) {
|
|
29710
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29711
|
+
throw new ApiError(0, `Could not download bytes: ${msg}`);
|
|
29712
|
+
}
|
|
29713
|
+
if (!res.ok) {
|
|
29714
|
+
throw new ApiError(res.status, `download failed with HTTP ${res.status}`);
|
|
29715
|
+
}
|
|
29716
|
+
const ab = await res.arrayBuffer();
|
|
29717
|
+
return Buffer.from(ab);
|
|
29718
|
+
}
|
|
29719
|
+
async function postFile(args) {
|
|
29720
|
+
const {
|
|
29721
|
+
baseUrl,
|
|
29722
|
+
pat,
|
|
29723
|
+
relPath,
|
|
29724
|
+
bytes,
|
|
29725
|
+
contentHash,
|
|
29726
|
+
mimeType,
|
|
29727
|
+
expectedUpdatedAt,
|
|
29728
|
+
fetchImpl = fetch
|
|
29729
|
+
} = args;
|
|
29730
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files`;
|
|
29731
|
+
const headers = {
|
|
29732
|
+
Authorization: `Bearer ${pat}`,
|
|
29733
|
+
Accept: "application/json",
|
|
29734
|
+
"Content-Type": mimeType,
|
|
29735
|
+
"X-Plinth-File-Path": encodeURIComponent(relPath),
|
|
29736
|
+
"X-Plinth-Content-Hash": contentHash
|
|
29737
|
+
};
|
|
29738
|
+
if (expectedUpdatedAt)
|
|
29739
|
+
headers["X-Plinth-Expected-Updated-At"] = expectedUpdatedAt;
|
|
29740
|
+
let res;
|
|
29741
|
+
try {
|
|
29742
|
+
res = await fetchImpl(url2, { method: "POST", headers, body: bytes });
|
|
29743
|
+
} catch (err) {
|
|
29744
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29745
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
29746
|
+
}
|
|
29747
|
+
if (res.status === 200 || res.status === 201) {
|
|
29748
|
+
const body = await res.json();
|
|
29749
|
+
const parsed = FileLiveSchema.safeParse(body);
|
|
29750
|
+
if (!parsed.success) {
|
|
29751
|
+
throw new ApiError(res.status, `post file response did not match the expected shape`);
|
|
29752
|
+
}
|
|
29753
|
+
return { ok: true, row: parsed.data };
|
|
29754
|
+
}
|
|
29755
|
+
if (res.status === 413) {
|
|
29756
|
+
let maxBytes = null;
|
|
29757
|
+
try {
|
|
29758
|
+
const json2 = await res.json();
|
|
29759
|
+
if (typeof json2.max_bytes === "number")
|
|
29760
|
+
maxBytes = json2.max_bytes;
|
|
29761
|
+
} catch {}
|
|
29762
|
+
return { ok: false, code: 413, maxBytes };
|
|
29763
|
+
}
|
|
29764
|
+
if (res.status === 409) {
|
|
29765
|
+
const json2 = await res.json();
|
|
29766
|
+
const parsed = PreconditionSchema.safeParse(json2);
|
|
29767
|
+
if (!parsed.success) {
|
|
29768
|
+
throw new ApiError(409, `post file 409 body did not match precondition_failed shape`);
|
|
29769
|
+
}
|
|
29770
|
+
return { ok: false, code: 409, currentUpdatedAt: parsed.data.current_updated_at };
|
|
29771
|
+
}
|
|
29772
|
+
if (res.status === 400) {
|
|
29773
|
+
let code;
|
|
29774
|
+
try {
|
|
29775
|
+
const json2 = await res.json();
|
|
29776
|
+
code = json2.code;
|
|
29777
|
+
} catch {}
|
|
29778
|
+
if (code === "hash_mismatch")
|
|
29779
|
+
return { ok: false, code: 400, reason: "hash_mismatch" };
|
|
29780
|
+
throw new ApiError(400, "post file rejected as bad request");
|
|
29781
|
+
}
|
|
29782
|
+
if (res.status === 401) {
|
|
29783
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29784
|
+
}
|
|
29785
|
+
if (res.status === 429) {
|
|
29786
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29787
|
+
}
|
|
29788
|
+
throw new ApiError(res.status, `post file failed with HTTP ${res.status}`);
|
|
29789
|
+
}
|
|
29790
|
+
async function deleteFile(args) {
|
|
29791
|
+
const { baseUrl, pat, id, expectedUpdatedAt, fetchImpl = fetch } = args;
|
|
29792
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}`;
|
|
29793
|
+
let res;
|
|
29794
|
+
try {
|
|
29795
|
+
res = await fetchImpl(url2, {
|
|
29796
|
+
method: "DELETE",
|
|
29797
|
+
headers: { ...baseHeaders(pat), "X-Plinth-Expected-Updated-At": expectedUpdatedAt }
|
|
29798
|
+
});
|
|
29799
|
+
} catch (err) {
|
|
29800
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
29801
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
29802
|
+
}
|
|
29803
|
+
if (res.status === 200) {
|
|
29804
|
+
const body = await res.json();
|
|
29805
|
+
const parsed = FileTombstoneSchema.safeParse(body);
|
|
29806
|
+
if (!parsed.success) {
|
|
29807
|
+
throw new ApiError(200, `delete file response did not match the expected tombstone shape`);
|
|
29808
|
+
}
|
|
29809
|
+
return { ok: true, tombstone: parsed.data };
|
|
29810
|
+
}
|
|
29811
|
+
if (res.status === 404)
|
|
29812
|
+
return { ok: false, code: 404 };
|
|
29813
|
+
if (res.status === 409) {
|
|
29814
|
+
const json2 = await res.json();
|
|
29815
|
+
const parsed = PreconditionSchema.safeParse(json2);
|
|
29816
|
+
if (!parsed.success) {
|
|
29817
|
+
throw new ApiError(409, `delete file 409 body did not match precondition_failed shape`);
|
|
29818
|
+
}
|
|
29819
|
+
return { ok: false, code: 409, currentUpdatedAt: parsed.data.current_updated_at };
|
|
29820
|
+
}
|
|
29821
|
+
if (res.status === 401) {
|
|
29822
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29823
|
+
}
|
|
29824
|
+
if (res.status === 429) {
|
|
29825
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29826
|
+
}
|
|
29827
|
+
throw new ApiError(res.status, `delete file failed with HTTP ${res.status}`);
|
|
29828
|
+
}
|
|
29829
|
+
|
|
29830
|
+
// src/lib/rejected.ts
|
|
29831
|
+
import { relative as relative3 } from "node:path";
|
|
29832
|
+
|
|
29833
|
+
// src/lib/log-tokens.ts
|
|
29834
|
+
function categorizeError(err) {
|
|
29835
|
+
if (err instanceof FrontmatterValidationError) {
|
|
29836
|
+
return { status: 422, reason: "invalid_frontmatter", detail: err.detail };
|
|
29837
|
+
}
|
|
29838
|
+
if (err instanceof ApiError) {
|
|
29839
|
+
const { status } = err;
|
|
29840
|
+
if (status === 0)
|
|
29841
|
+
return { status, reason: "network" };
|
|
29842
|
+
if (status === 401 || status === 403)
|
|
29843
|
+
return { status, reason: "auth" };
|
|
29844
|
+
if (status === 429)
|
|
29845
|
+
return { status, reason: "rate_limit" };
|
|
29846
|
+
if (status === 400 || status === 422) {
|
|
29847
|
+
const serverMessage = sanitizeServerMessage(err.message);
|
|
29848
|
+
return serverMessage ? { status, reason: "bad_request", serverMessage } : { status, reason: "bad_request" };
|
|
29849
|
+
}
|
|
29850
|
+
if (status >= 500 && status <= 599)
|
|
29851
|
+
return { status, reason: "server" };
|
|
29852
|
+
return { status, reason: "http" };
|
|
29853
|
+
}
|
|
29854
|
+
return { status: 0, reason: "unexpected" };
|
|
29855
|
+
}
|
|
29856
|
+
function sanitizeServerMessage(message) {
|
|
29857
|
+
const oneLine2 = message.replace(/\s+/g, " ").trim();
|
|
29858
|
+
if (oneLine2.length === 0)
|
|
29859
|
+
return "";
|
|
29860
|
+
return oneLine2.length > 200 ? `${oneLine2.slice(0, 197)}...` : oneLine2;
|
|
29861
|
+
}
|
|
29862
|
+
function pushFailedLine(f) {
|
|
29863
|
+
const idPart = f.id ? ` id=${f.id}` : "";
|
|
29864
|
+
const detailPart = f.detail ? ` detail=${f.detail}` : "";
|
|
29865
|
+
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
29866
|
+
return `[PUSH_FAILED entityType=${f.entityType} slug=${f.slug}${idPart} status=${f.status} reason=${f.reason}${detailPart}${serverPart}]`;
|
|
29867
|
+
}
|
|
29868
|
+
function pullFailedLine(f) {
|
|
29869
|
+
return `[PULL_FAILED status=${f.status} reason=${f.reason}]`;
|
|
29870
|
+
}
|
|
29871
|
+
function contextDocConflictLine(f) {
|
|
29872
|
+
const localIdPart = f.localId ? ` localId=${f.localId}` : "";
|
|
29873
|
+
const reasonPart = f.reason ? ` reason=${f.reason}` : "";
|
|
29874
|
+
const stashPart = f.stash ? ` stash=${f.stash}` : "";
|
|
29875
|
+
return `[CONTEXT_DOC_CONFLICT entityType=${f.entityType} localSlug=${f.localSlug}${localIdPart} ` + `existingSlug=${f.existingSlug} existingId=${f.existingId} scope=${f.scope} ` + `action=${f.action}${reasonPart}${stashPart}]`;
|
|
29876
|
+
}
|
|
29877
|
+
function syncErrorLine(f) {
|
|
29878
|
+
return `[SYNC_ERROR entityType=${f.entityType} slug=${f.slug} id=${f.id} ` + `fields=${f.fields.join(",")} pushed=${f.pushed}]`;
|
|
29879
|
+
}
|
|
29880
|
+
function filePushLine(f) {
|
|
29881
|
+
return `[FILE_PUSH path=${f.path} id=${f.id} action=${f.action}]`;
|
|
29882
|
+
}
|
|
29883
|
+
function fileDeleteLine(f) {
|
|
29884
|
+
const notePart = f.note ? ` note=${f.note}` : "";
|
|
29885
|
+
return `[FILE_DELETE path=${f.path} id=${f.id}${notePart}]`;
|
|
29886
|
+
}
|
|
29887
|
+
function fileDeleteSuppressedLine(f) {
|
|
29888
|
+
return `[FILE_DELETE_SUPPRESSED path=${f.path} reason=${f.reason}]`;
|
|
29889
|
+
}
|
|
29890
|
+
function entityDeleteSuppressedLine(f) {
|
|
29891
|
+
return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
|
|
29892
|
+
}
|
|
29893
|
+
function fileOverCapLine(f) {
|
|
29894
|
+
return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
|
|
29895
|
+
}
|
|
29896
|
+
function fileConflictLine(f) {
|
|
29897
|
+
return `[FILE_CONFLICT path=${f.path} stash=${f.stash}]`;
|
|
29898
|
+
}
|
|
29899
|
+
function conflictsViewFailedLine(f) {
|
|
29900
|
+
const msg = f.message === undefined ? "" : sanitizeServerMessage(f.message);
|
|
29901
|
+
const msgPart = msg === "" ? "" : ` message="${msg.replace(/"/g, "'")}"`;
|
|
29902
|
+
return `[CONFLICTS_VIEW_FAILED path=${f.path} reason=${f.reason}${msgPart}]`;
|
|
29903
|
+
}
|
|
29904
|
+
function fileFailedLine(f) {
|
|
29905
|
+
const detailPart = f.detail ? ` detail=${f.detail}` : "";
|
|
29906
|
+
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
29907
|
+
return `[FILE_PUSH_FAILED path=${f.path} status=${f.status} reason=${f.reason}${detailPart}${serverPart}]`;
|
|
29908
|
+
}
|
|
29909
|
+
function frontmatterIdBackfillLine(r) {
|
|
29910
|
+
return `[FRONTMATTER_ID_BACKFILL scanned=${r.scanned} written=${r.written} ` + `blocks_created=${r.blocksCreated} ` + `inodes_restamped=${r.inodesRestamped} failed=${r.failed} ` + `skipped_frozen=${r.skipped.frozen} ` + `skipped_foreign_id=${r.skipped.foreignId} ` + `skipped_unwritable=${r.skipped.unwritable} ` + `skipped_slug_collision=${r.skipped.slugCollision} ` + `skipped_diverged=${r.skipped.diverged}]`;
|
|
29911
|
+
}
|
|
29912
|
+
function pullNoticeLine(kind) {
|
|
29913
|
+
const token = kind === "auth" ? "PULL_PAUSED" : "PULL_DEGRADED";
|
|
29914
|
+
return `[${token} reason=${kind}]`;
|
|
29915
|
+
}
|
|
29916
|
+
function calendarProjectedLine(f) {
|
|
29917
|
+
return `[CALENDAR_PROJECTED written=${f.written} pruned=${f.pruned} ` + `calendars=${f.calendars} not_read=${f.notRead}]`;
|
|
29918
|
+
}
|
|
29919
|
+
function calendarFailedLine(f) {
|
|
29920
|
+
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
29921
|
+
return `[CALENDAR_FAILED status=${f.status} reason=${f.reason} staled=${f.staled}${serverPart}]`;
|
|
29922
|
+
}
|
|
29923
|
+
function calendarNoticeLine(kind) {
|
|
29924
|
+
const token = kind === "auth" ? "CALENDAR_PAUSED" : kind === "unsupported" ? "CALENDAR_UNSUPPORTED" : "CALENDAR_DEGRADED";
|
|
29925
|
+
return `[${token} reason=${kind}]`;
|
|
29926
|
+
}
|
|
29927
|
+
|
|
29928
|
+
// src/lib/rejected.ts
|
|
29929
|
+
var PERMANENT_REJECT_REASONS = new Set(["bad_request", "invalid_frontmatter"]);
|
|
29930
|
+
function isPermanentReject(err) {
|
|
29931
|
+
return PERMANENT_REJECT_REASONS.has(categorizeError(err).reason);
|
|
29932
|
+
}
|
|
29933
|
+
function persist(state, next) {
|
|
29934
|
+
if (!next.changed)
|
|
29935
|
+
return state;
|
|
29936
|
+
return commitState(state, next.state);
|
|
29937
|
+
}
|
|
29938
|
+
function recordEntityRejected(args) {
|
|
29939
|
+
const cat = categorizeError(args.err);
|
|
29940
|
+
let path3;
|
|
29941
|
+
try {
|
|
29942
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
29943
|
+
} catch {
|
|
29944
|
+
path3 = args.absolutePath;
|
|
29945
|
+
}
|
|
29946
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
29947
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
29948
|
+
target: "entity",
|
|
29949
|
+
op: args.op,
|
|
29950
|
+
path: path3,
|
|
29951
|
+
status: cat.status,
|
|
29952
|
+
reason: cat.reason,
|
|
29953
|
+
detail: cat.detail,
|
|
29954
|
+
message: cat.serverMessage,
|
|
29955
|
+
rejected_at: args.now().toISOString()
|
|
29956
|
+
}));
|
|
29957
|
+
}
|
|
29958
|
+
function recordFileRejected(args) {
|
|
29959
|
+
const cat = categorizeError(args.err);
|
|
29960
|
+
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
29961
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
29962
|
+
target: "file",
|
|
29963
|
+
op: args.op,
|
|
29964
|
+
path: args.relPath,
|
|
29965
|
+
status: cat.status,
|
|
29966
|
+
reason: cat.reason,
|
|
29967
|
+
detail: cat.detail,
|
|
29968
|
+
message: cat.serverMessage,
|
|
29969
|
+
rejected_at: args.now().toISOString()
|
|
29970
|
+
}));
|
|
29971
|
+
}
|
|
29972
|
+
var CLOUD_ROW_DELETED_REASON = "cloud_row_deleted";
|
|
29973
|
+
function recordEntityCloudRowDeleted(args) {
|
|
29974
|
+
let path3;
|
|
29975
|
+
try {
|
|
29976
|
+
path3 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
29977
|
+
} catch {
|
|
29978
|
+
path3 = args.absolutePath;
|
|
29979
|
+
}
|
|
29980
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
29981
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
29982
|
+
target: "entity",
|
|
29983
|
+
op: "upsert",
|
|
29984
|
+
path: path3,
|
|
29985
|
+
status: 404,
|
|
29986
|
+
reason: CLOUD_ROW_DELETED_REASON,
|
|
29987
|
+
message: "This was deleted in Plinth, and your copy has changes the cloud never " + "saw. Nothing can push it back — Plinth has no row for it. Keep the " + "file as your own, or delete it. To put the text into Plinth again, " + "copy it into a NEW file with a different name and no `id:`, `slug:` " + "or `revision:` lines.",
|
|
29988
|
+
rejected_at: args.now().toISOString()
|
|
29989
|
+
}));
|
|
29990
|
+
}
|
|
29991
|
+
function clearEntityCloudRowDeleted(state, path3) {
|
|
29992
|
+
const list = state.rejected ?? [];
|
|
29993
|
+
const next = list.filter((o) => !(o.target === "entity" && o.path === path3 && o.reason === CLOUD_ROW_DELETED_REASON));
|
|
29994
|
+
if (next.length === list.length)
|
|
29995
|
+
return state;
|
|
29996
|
+
return commitState(state, { ...state, rejected: next });
|
|
29997
|
+
}
|
|
29998
|
+
function humanBytes(bytes) {
|
|
29999
|
+
if (bytes >= 1e6)
|
|
30000
|
+
return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
30001
|
+
if (bytes >= 1000)
|
|
30002
|
+
return `${Math.round(bytes / 1000)} KB`;
|
|
30003
|
+
return `${bytes} bytes`;
|
|
30004
|
+
}
|
|
30005
|
+
function recordFileOverCap(args) {
|
|
30006
|
+
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30007
|
+
const ceiling = args.source === "server" ? `the ${humanBytes(args.max)} server limit` : `the ${humanBytes(args.max)} upload ceiling (measured platform limit)`;
|
|
30008
|
+
const message = args.tracked ? `Your local change to this file (${humanBytes(args.size)}) exceeds ${ceiling}, ` + `so it cannot be pushed. The copy in Plinth is unchanged; your edit is on disk here only.` : `${humanBytes(args.size)} exceeds ${ceiling}. The file is still on disk ` + `here but is not in Plinth; shrink or split it to sync it.`;
|
|
30009
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30010
|
+
target: "file",
|
|
30011
|
+
op: "upsert",
|
|
30012
|
+
path: args.relPath,
|
|
30013
|
+
status: 413,
|
|
30014
|
+
reason: "over_cap",
|
|
30015
|
+
message,
|
|
30016
|
+
rejected_at: args.now().toISOString()
|
|
30017
|
+
}));
|
|
30018
|
+
}
|
|
30019
|
+
function recordFileSymlinkRefused(args) {
|
|
30020
|
+
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30021
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30022
|
+
target: "file",
|
|
30023
|
+
op: args.op,
|
|
30024
|
+
path: args.relPath,
|
|
30025
|
+
status: 0,
|
|
30026
|
+
reason: "symlink",
|
|
30027
|
+
message: `A symlink on the path to files/${args.relPath} means syncing it would ` + `read or write outside your Plinth folder, so it was left alone. ` + `Replace the link with a real folder or file to sync it.`,
|
|
30028
|
+
rejected_at: args.now().toISOString()
|
|
30029
|
+
}));
|
|
30030
|
+
}
|
|
30031
|
+
function recordEntitySymlinkRefused(args) {
|
|
30032
|
+
const path3 = args.mirrorRelativePath;
|
|
30033
|
+
const withoutStale = removeUnsynced(args.state, "entity", path3).state;
|
|
30034
|
+
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30035
|
+
target: "entity",
|
|
30036
|
+
op: args.op,
|
|
30037
|
+
path: path3,
|
|
30038
|
+
status: 0,
|
|
30039
|
+
reason: "symlink",
|
|
30040
|
+
message: `A symlink on the path to ${path3} means syncing it would read or write ` + `outside your Plinth folder, so it was left alone. Replace the link ` + `with a real folder or file to sync it.`,
|
|
30041
|
+
rejected_at: args.now().toISOString()
|
|
30042
|
+
}));
|
|
30043
|
+
}
|
|
30044
|
+
function clearSymlinkRefused(state, target, path3) {
|
|
30045
|
+
const list = state.rejected ?? [];
|
|
30046
|
+
const next = list.filter((o) => !(o.target === target && o.path === path3 && o.reason === "symlink"));
|
|
30047
|
+
if (next.length === list.length)
|
|
30048
|
+
return state;
|
|
30049
|
+
return commitState(state, { ...state, rejected: next });
|
|
30050
|
+
}
|
|
30051
|
+
function clearEntityRejected(state, workspaceSlug, absolutePath) {
|
|
30052
|
+
let path3;
|
|
30053
|
+
try {
|
|
30054
|
+
path3 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30055
|
+
} catch {
|
|
30056
|
+
path3 = absolutePath;
|
|
30057
|
+
}
|
|
30058
|
+
const next = removeRejected(state, "entity", path3);
|
|
30059
|
+
if (!next.changed)
|
|
30060
|
+
return state;
|
|
30061
|
+
return commitState(state, next.state);
|
|
30062
|
+
}
|
|
30063
|
+
function clearFileRejected(state, relPath) {
|
|
30064
|
+
const next = removeRejected(state, "file", relPath);
|
|
30065
|
+
if (!next.changed)
|
|
30066
|
+
return state;
|
|
30067
|
+
return commitState(state, next.state);
|
|
30068
|
+
}
|
|
30069
|
+
|
|
30070
|
+
// src/lib/context/workspace-claude-md.ts
|
|
30071
|
+
var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
|
|
30072
|
+
function rosterBlock(roster, activeProjectCountBySlug) {
|
|
30073
|
+
if (roster.length === 0)
|
|
30074
|
+
return "_No artists synced yet._";
|
|
30075
|
+
return roster.map((a) => {
|
|
30076
|
+
const statusPart = a.status ? `, ${a.status}` : "";
|
|
30077
|
+
const n = activeProjectCountBySlug.get(a.slug) ?? 0;
|
|
30078
|
+
const projects = `${n} active project${n === 1 ? "" : "s"}`;
|
|
30079
|
+
return `- **${a.name}** (\`${a.slug}\`${statusPart}) · ${projects} · master context → \`artists/${a.slug}/_artist.md\``;
|
|
30080
|
+
}).join(`
|
|
30081
|
+
`);
|
|
30082
|
+
}
|
|
30083
|
+
function emptyNote(folders, names) {
|
|
30084
|
+
if (folders === undefined)
|
|
30085
|
+
return "";
|
|
30086
|
+
const byName = new Map(folders.map((f) => [f.name, f.empty]));
|
|
30087
|
+
const empty = names.filter((n) => byName.get(n) === true);
|
|
30088
|
+
if (empty.length === 0)
|
|
30089
|
+
return "";
|
|
30090
|
+
const list = empty.map((n) => `\`${n}/\``).join(" ");
|
|
30091
|
+
return ` · empty so far: ${list}`;
|
|
30092
|
+
}
|
|
30093
|
+
function buildWorkspaceClaudeMd(input) {
|
|
30094
|
+
const {
|
|
30095
|
+
workspaceName,
|
|
30096
|
+
workspaceSlug,
|
|
30097
|
+
roster,
|
|
30098
|
+
activeProjectCountBySlug,
|
|
30099
|
+
schemaVersion,
|
|
30100
|
+
folders
|
|
30101
|
+
} = input;
|
|
30102
|
+
const entityLines = ENTITY_CATALOG.map((e) => `- **${e.label}**: ${e.blurb}${e.file ? "" : " _(MCP only)_"}`);
|
|
30103
|
+
const skillLines = SKILL_LOCATIONS.map((s) => `- ${s.label}: ${s.detail}`);
|
|
30104
|
+
const skillCount = SKILL_LOCATIONS.length;
|
|
30105
|
+
const lines = [
|
|
30106
|
+
`<!-- plinth-schema-version: ${schemaVersion} -->`,
|
|
30107
|
+
`<!-- Auto-generated by Plinth. ${GENERATED_AT_PREFIX}${GENERATED_AT_PLACEHOLDER}; ` + `that stamp moves only when this file's content changes, so an unchanged file keeps an older one ` + `and is still re-checked against Plinth on every sync. Run \`plinth status\` for when the mirror last synced. ` + `Edit only between the BEGIN/END markers below; everything else is overwritten on sync. -->`,
|
|
30108
|
+
``,
|
|
30109
|
+
`# ${workspaceName} · Plinth workspace`,
|
|
30110
|
+
``,
|
|
30111
|
+
`Local mirror of the **${workspaceName}** workspace (\`${workspaceSlug}\`). ${SECURITY_NOTE}`,
|
|
30112
|
+
``,
|
|
30113
|
+
`> **Session grounding**: the current date, an entity-resolution rule, and a write-confirmation rule are injected at every session start by the Plinth hook in \`.claude/settings.json\` (and each \`artists/<slug>/.claude/settings.json\`).`,
|
|
30114
|
+
``,
|
|
30115
|
+
`> **Filing a file the user hands you - BOTH halves, and the first one is unconditional**: put a copy in \`files/\`, which is how Plinth gets the text (folder map below) - do this even if everything else here fails. **A file over ${humanBytes(MAX_WORKSPACE_FILE_BYTES)} is refused and never reaches Plinth at all**, so its copy stays on this machine only and \`plinth status\` lists it as rejected; that ceiling decides whether the file syncs, and is not the separate limit on how much text is extracted from one. Over it, the second half below stops being the tidy option and becomes the only record that survives. Then put the **original** in the user's own file storage with whatever tool you have for that, and write down where it went in a memory file, routed by the scope rule in your session grounding. A pointer that exists only in this conversation is lost when it ends. If you have no tool that reaches their file storage, SAY SO - do not leave them believing the original was filed.`,
|
|
30116
|
+
``,
|
|
30117
|
+
`## Folder map`,
|
|
30118
|
+
`- \`artists/<slug>/\` one folder per artist, the working dir for artist-scoped sessions · \`threads/<slug>/_thread.md\` one folder per thread${emptyNote(folders, ["artists", "threads"])}`,
|
|
30119
|
+
`- \`projects/\` \`tasks/\` \`documents/\` \`meetings/\` workspace-root flat entity files (one \`.md\` per entity, NOT nested under \`artists/\`, so one artist's rows are found by matching \`artist_id:\` in their frontmatter against that artist's own \`id:\` in \`artists/<slug>/_artist.md\`, never by matching the artist's name: a name match returned 16 of one artist's 46 projects and read as "no such project")${emptyNote(folders, ["projects", "tasks", "documents", "meetings"])}`,
|
|
30120
|
+
`- \`files/\` free-form document store: PDFs, executed agreements, spreadsheets, anything that is a FILE rather than an entity. Mirrored byte-for-byte both ways, folders and all${emptyNote(folders, [FILES_SUBTREE_DIR])}`,
|
|
30121
|
+
`- \`${extractedTextRelPath("<path>")}\` where \`<path>\` is the file's path under \`files/\`: the text Plinth extracted from that file, so a PDF's contents can be searched. Not every file has one - text files are already their own text, and extraction can fail. Where it produced junk the file exists and says so instead of carrying it, so read what is there before trusting a silence. Derived: edits here are overwritten and never reach Plinth`,
|
|
30122
|
+
`- \`.claude/commands/\` local skills, incl. generated \`${START_COMMAND_INVOCATION}\` + \`${CLOSE_COMMAND_INVOCATION}\` + \`${ONBOARD_COMMAND_INVOCATION}\` · at the mirror root: \`workspace-memory.md\` \`workspace-rules.md\` (workspace-wide) + \`user-memory.md\` (yours), synced to Plinth · \`voice.md\` (yours: how you write, read by the agent, edited only by you) · \`CLAUDE.local.md\` private, gitignored`,
|
|
30123
|
+
``,
|
|
30124
|
+
`## Where key context lives: look here before hunting`,
|
|
30125
|
+
`- Each artist's **master context** (stable profile + current state) → \`artists/<slug>/_artist.md\`; edit its body and the daemon syncs both ways. Blocks labelled "rendered from Plinth" are the exception: they are overwritten on the next sync`,
|
|
30126
|
+
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
30127
|
+
`- Workspace-wide **notes, briefs, bios & context docs** → \`documents/\` (one markdown file per doc, shared across artists)`,
|
|
30128
|
+
`- Per-artist long-term facts → \`artists/<slug>/memory.md\`, which syncs to that artist's Plinth record; most have none yet, and creating the file is how you start one · what a session did → a dated section under \`## Session log\` in \`artists/<slug>/_artist.md\`, which syncs too · which fact belongs in which file is stated once in your session grounding · \`artists/<slug>/daily-log.md\` is local scratch, never synced and no longer created; an older one may hold real history, and nothing writes there now`,
|
|
30129
|
+
`- ${MCP_AVAILABILITY_POINTER}`,
|
|
30130
|
+
``,
|
|
30131
|
+
`## Navigating the corpus`,
|
|
30132
|
+
`- **What points at X?** → \`plinth backlinks "<name>"\`, a mirror path, or a wikilink target as written. Render-only blocks, code spans and comments are narrowed out; \`--unfiltered\` shows them. A bare name several files answer to is refused with the candidates listed, so re-run with the path it names`,
|
|
30133
|
+
`- **What does this file link to?** → grep \`[[\` in it`,
|
|
30134
|
+
`- **Find a file by name** → glob under \`files/\` \`documents/\` \`artists/\` · **text inside a PDF** → \`${EXTRACTED_TEXT_DIR}\` (folder map above) · **write to a row you have the mirror file for** → pass its frontmatter \`id:\` to the \`mcp__plinth__*\` write tool; if you do not have the file, find it with \`search_entities\` / \`list_*\``,
|
|
30135
|
+
``,
|
|
30136
|
+
`## Entity model: ${MCP_INTROSPECTION_POINTER}`,
|
|
30137
|
+
...entityLines,
|
|
30138
|
+
``,
|
|
30139
|
+
`## Roster (${roster.length})`,
|
|
30140
|
+
rosterBlock(roster, activeProjectCountBySlug),
|
|
30141
|
+
``,
|
|
30142
|
+
`## Skills: ${skillCount} discoverable location${skillCount === 1 ? "" : "s"}`,
|
|
30143
|
+
...skillLines,
|
|
30144
|
+
``,
|
|
30145
|
+
USER_BLOCK_BEGIN,
|
|
30146
|
+
USER_BLOCK_PLACEHOLDER,
|
|
30147
|
+
USER_BLOCK_END
|
|
30148
|
+
];
|
|
30149
|
+
return lines.join(`
|
|
30150
|
+
`) + `
|
|
30151
|
+
`;
|
|
30152
|
+
}
|
|
30153
|
+
|
|
30154
|
+
// src/lib/context/generate.ts
|
|
30155
|
+
var CLAUDE_MD = "CLAUDE.md";
|
|
30156
|
+
var CLAUDE_LOCAL_MD = "CLAUDE.local.md";
|
|
30157
|
+
var GITIGNORE = ".gitignore";
|
|
30158
|
+
var GITIGNORE_ENTRIES = [
|
|
30159
|
+
CLAUDE_LOCAL_MD,
|
|
30160
|
+
"**/.claude/settings.json",
|
|
30161
|
+
"**/.claude/settings.local.json"
|
|
30162
|
+
];
|
|
30163
|
+
var CLAUDE_LOCAL_STUB = "<!-- Per-user overrides for this workspace. Private, gitignored, and " + `never overwritten by Plinth. Run /onboard to populate, or edit freely. -->
|
|
30164
|
+
`;
|
|
30165
|
+
function currentSchemaVersion() {
|
|
30166
|
+
return computeSchemaVersion({
|
|
30167
|
+
templateVersion: TEMPLATE_VERSION,
|
|
30168
|
+
entityCatalog: ENTITY_CATALOG,
|
|
30169
|
+
skillLocations: SKILL_LOCATIONS,
|
|
30170
|
+
securityNote: SECURITY_NOTE,
|
|
30171
|
+
mcpAvailabilityPointer: MCP_AVAILABILITY_POINTER
|
|
30172
|
+
});
|
|
30173
|
+
}
|
|
30174
|
+
function ensureWorkspaceContext(deps) {
|
|
30175
|
+
const { workspaceSlug, workspaceName, force, out, now } = deps;
|
|
30176
|
+
const version2 = currentSchemaVersion();
|
|
30177
|
+
const roster = readRoster(workspaceSlug);
|
|
30178
|
+
const activeProjectCountBySlug = activeProjectCountsBySlug(workspaceSlug, loadState(workspaceSlug));
|
|
30179
|
+
const generated = buildWorkspaceClaudeMd({
|
|
30180
|
+
workspaceName,
|
|
30181
|
+
workspaceSlug,
|
|
30182
|
+
roster,
|
|
30183
|
+
activeProjectCountBySlug,
|
|
30184
|
+
schemaVersion: version2,
|
|
30185
|
+
folders: observeMirrorFolders(workspaceSlug, [
|
|
30186
|
+
...allEntityTypes(),
|
|
30187
|
+
FILES_SUBTREE_DIR
|
|
30188
|
+
])
|
|
30189
|
+
});
|
|
30190
|
+
const existing = readContextFile(workspaceSlug, CLAUDE_MD);
|
|
30191
|
+
const decision = decideRegen({
|
|
30192
|
+
existing,
|
|
30193
|
+
currentVersion: "",
|
|
30194
|
+
generated,
|
|
30195
|
+
force,
|
|
30196
|
+
normalise: normaliseGeneratedAt
|
|
30197
|
+
});
|
|
30198
|
+
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
30199
|
+
const bakRel = `${CLAUDE_MD}.bak-${backupStamp3(now())}`;
|
|
30200
|
+
writeContextFile(workspaceSlug, bakRel, decision.backup);
|
|
30201
|
+
out.warn(`${CLAUDE_MD} had no recognisable BEGIN/END markers; backed your copy ` + `up to ${bakRel} before regenerating.`);
|
|
30202
|
+
}
|
|
30203
|
+
if (decision.content !== null) {
|
|
30204
|
+
writeContextFile(workspaceSlug, CLAUDE_MD, renderGeneratedAt(decision.content, now()));
|
|
30205
|
+
}
|
|
30206
|
+
if (!existsSync2(contextFilePath(workspaceSlug, CLAUDE_LOCAL_MD))) {
|
|
30207
|
+
writeContextFile(workspaceSlug, CLAUDE_LOCAL_MD, CLAUDE_LOCAL_STUB);
|
|
30208
|
+
}
|
|
30209
|
+
ensureWorkspaceCommands({ workspaceSlug, force, out, now });
|
|
30210
|
+
ensureGitignore(workspaceSlug);
|
|
30211
|
+
const resolved = resolvePlinthBinary();
|
|
30212
|
+
reportResolution(out, resolved);
|
|
30213
|
+
if (ensureSessionHook(workspaceSlug, "", out, resolved) === "skipped") {
|
|
30214
|
+
deps.onHookSkipped?.();
|
|
30215
|
+
}
|
|
30216
|
+
if (decision.action === "create") {
|
|
30217
|
+
out.info(`Generated ${CLAUDE_MD} (${roster.length} artists).`);
|
|
30218
|
+
} else if (decision.action === "regen" || decision.action === "backup-regen") {
|
|
30219
|
+
out.info(`Refreshed ${CLAUDE_MD} (your edits preserved).`);
|
|
30220
|
+
}
|
|
30221
|
+
}
|
|
30222
|
+
function ensureGitignore(workspaceSlug) {
|
|
30223
|
+
const existing = readContextFile(workspaceSlug, GITIGNORE);
|
|
30224
|
+
const present2 = new Set((existing ?? "").split(`
|
|
30225
|
+
`).map((l) => l.trim()).filter(Boolean));
|
|
30226
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !present2.has(e));
|
|
30227
|
+
if (missing.length === 0)
|
|
30228
|
+
return;
|
|
30229
|
+
const block = `${missing.join(`
|
|
30230
|
+
`)}
|
|
30231
|
+
`;
|
|
30232
|
+
if (existing === null || existing === "") {
|
|
30233
|
+
writeContextFile(workspaceSlug, GITIGNORE, block);
|
|
30234
|
+
return;
|
|
30235
|
+
}
|
|
30236
|
+
const lead = existing.endsWith(`
|
|
30237
|
+
`) ? "" : `
|
|
30238
|
+
`;
|
|
30239
|
+
writeContextFile(workspaceSlug, GITIGNORE, `${existing}${lead}${block}`);
|
|
30240
|
+
}
|
|
30241
|
+
function backupStamp3(d) {
|
|
30242
|
+
return d.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
30243
|
+
}
|
|
30244
|
+
|
|
30245
|
+
// src/commands/refresh-context.ts
|
|
30246
|
+
init_output();
|
|
30247
|
+
function runRefreshContext(workspaceSlug, deps = {}) {
|
|
30248
|
+
const out = new Output({ sink: deps.sink, noColor: deps.noColor });
|
|
30249
|
+
const now = deps.now ?? (() => new Date);
|
|
30250
|
+
const config2 = loadConfig();
|
|
30251
|
+
if (!config2) {
|
|
30252
|
+
out.error("No workspace configured. Run `plinth login --workspace <slug>` first.");
|
|
30253
|
+
return 1;
|
|
30254
|
+
}
|
|
30255
|
+
const slug = workspaceSlug ?? config2.active_workspace;
|
|
30256
|
+
const entry2 = config2.workspaces[slug];
|
|
30257
|
+
if (!entry2) {
|
|
30258
|
+
out.error(`Workspace "${slug}" not found. Run \`plinth login --workspace ${slug}\` first.`);
|
|
30259
|
+
return 1;
|
|
30260
|
+
}
|
|
30261
|
+
let skipped = 0;
|
|
30262
|
+
const onHookSkipped = () => {
|
|
30263
|
+
skipped++;
|
|
30264
|
+
};
|
|
30265
|
+
try {
|
|
30266
|
+
ensureWorkspaceContext({
|
|
30267
|
+
workspaceSlug: slug,
|
|
30268
|
+
workspaceName: entry2.workspace_name ?? slug,
|
|
30269
|
+
force: true,
|
|
30270
|
+
out,
|
|
30271
|
+
now,
|
|
30272
|
+
onHookSkipped
|
|
30273
|
+
});
|
|
30274
|
+
ensurePerArtistContext({
|
|
30275
|
+
workspaceSlug: slug,
|
|
30276
|
+
force: true,
|
|
30277
|
+
out,
|
|
30278
|
+
now,
|
|
30279
|
+
onHookSkipped
|
|
30280
|
+
});
|
|
30281
|
+
} catch (err) {
|
|
30282
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
30283
|
+
out.error(`Could not refresh context: ${msg}`);
|
|
30284
|
+
return 1;
|
|
30285
|
+
}
|
|
30286
|
+
if (skipped > 0) {
|
|
30287
|
+
out.error(`Context refreshed, but ${skipped} Claude Code hook mount${skipped === 1 ? " was" : "s were"} ` + `SKIPPED — ${skipped === 1 ? "that location is" : "those locations are"} ungrounded and ` + `agent-originated drafts there are UNGATED. See the errors above.`);
|
|
30288
|
+
return 1;
|
|
30289
|
+
}
|
|
30290
|
+
out.success(`Context up to date for ${slug}.`);
|
|
30291
|
+
return 0;
|
|
30292
|
+
}
|
|
30293
|
+
var refreshContextCommand = defineCommand({
|
|
29288
30294
|
meta: {
|
|
29289
30295
|
name: "refresh-context",
|
|
29290
30296
|
description: "Regenerate CLAUDE.md from the current Plinth schema + workspace"
|
|
@@ -29930,8 +30936,8 @@ var sessionCommand = defineCommand({
|
|
|
29930
30936
|
});
|
|
29931
30937
|
|
|
29932
30938
|
// src/commands/start.ts
|
|
29933
|
-
var
|
|
29934
|
-
import { readFileSync as
|
|
30939
|
+
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
30940
|
+
import { existsSync as existsSync8, readFileSync as readFileSync24, statSync as statSync16 } from "node:fs";
|
|
29935
30941
|
|
|
29936
30942
|
// src/lib/daemon-log.ts
|
|
29937
30943
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -29998,7 +31004,7 @@ class DaemonLog {
|
|
|
29998
31004
|
|
|
29999
31005
|
// src/lib/destructive-gate.ts
|
|
30000
31006
|
var import_gray_matter7 = __toESM(require_gray_matter(), 1);
|
|
30001
|
-
import { readFileSync as
|
|
31007
|
+
import { readFileSync as readFileSync13, statSync as statSync5 } from "node:fs";
|
|
30002
31008
|
import { basename as basename3 } from "node:path";
|
|
30003
31009
|
var GUTTING_RATIO_DEFAULT = 0.5;
|
|
30004
31010
|
var GUTTING_MIN_PRIOR_BYTES = 200;
|
|
@@ -30042,7 +31048,7 @@ function classifyDestructive(event, entityType, state, deps = {}) {
|
|
|
30042
31048
|
absolutePath: event.absolutePath
|
|
30043
31049
|
};
|
|
30044
31050
|
}
|
|
30045
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
31051
|
+
const readFile = deps.readFile ?? ((p) => readFileSync13(p, "utf8"));
|
|
30046
31052
|
const statInode = deps.statInode ?? ((p) => {
|
|
30047
31053
|
try {
|
|
30048
31054
|
return statSync5(p).ino;
|
|
@@ -30081,670 +31087,186 @@ function classifyDestructive(event, entityType, state, deps = {}) {
|
|
|
30081
31087
|
newLen
|
|
30082
31088
|
};
|
|
30083
31089
|
}
|
|
30084
|
-
function defaultThresholds() {
|
|
30085
|
-
return {
|
|
30086
|
-
absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
|
|
30087
|
-
propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
|
|
30088
|
-
propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5)
|
|
30089
|
-
};
|
|
30090
|
-
}
|
|
30091
|
-
function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
|
|
30092
|
-
const count = ops.length;
|
|
30093
|
-
if (count === 0)
|
|
30094
|
-
return { tripped: false };
|
|
30095
|
-
if (count >= thresholds.absCount) {
|
|
30096
|
-
return { tripped: true, reason: { kind: "count", count } };
|
|
30097
|
-
}
|
|
30098
|
-
const byType = new Map;
|
|
30099
|
-
for (const op of ops) {
|
|
30100
|
-
byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
|
|
30101
|
-
}
|
|
30102
|
-
for (const [entityType, typeCount] of byType) {
|
|
30103
|
-
const typePopulation = typePopulations[entityType] ?? typeCount;
|
|
30104
|
-
if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
|
|
30105
|
-
return {
|
|
30106
|
-
tripped: true,
|
|
30107
|
-
reason: {
|
|
30108
|
-
kind: "proportion",
|
|
30109
|
-
count,
|
|
30110
|
-
entityType,
|
|
30111
|
-
typeCount,
|
|
30112
|
-
typePopulation,
|
|
30113
|
-
pct: typeCount / typePopulation
|
|
30114
|
-
}
|
|
30115
|
-
};
|
|
30116
|
-
}
|
|
30117
|
-
}
|
|
30118
|
-
return { tripped: false };
|
|
30119
|
-
}
|
|
30120
|
-
function dedupeOps(ops) {
|
|
30121
|
-
const byKey = new Map;
|
|
30122
|
-
for (const op of ops)
|
|
30123
|
-
byKey.set(`${op.entityType}:${op.id}`, op);
|
|
30124
|
-
return Array.from(byKey.values());
|
|
30125
|
-
}
|
|
30126
|
-
var DEFAULT_SETTLE_MS = 600;
|
|
30127
|
-
var DEFAULT_MAX_BATCH = 500;
|
|
30128
|
-
|
|
30129
|
-
class DestructiveGate {
|
|
30130
|
-
pending = [];
|
|
30131
|
-
timer = null;
|
|
30132
|
-
settleMs;
|
|
30133
|
-
maxBatch;
|
|
30134
|
-
onSettle;
|
|
30135
|
-
onError;
|
|
30136
|
-
constructor(opts) {
|
|
30137
|
-
this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
30138
|
-
this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
30139
|
-
this.onSettle = opts.onSettle;
|
|
30140
|
-
this.onError = opts.onError;
|
|
30141
|
-
}
|
|
30142
|
-
consider(op) {
|
|
30143
|
-
this.pending.push(op);
|
|
30144
|
-
if (this.pending.length >= this.maxBatch) {
|
|
30145
|
-
this.flush();
|
|
30146
|
-
return;
|
|
30147
|
-
}
|
|
30148
|
-
if (this.timer)
|
|
30149
|
-
clearTimeout(this.timer);
|
|
30150
|
-
this.timer = setTimeout(() => {
|
|
30151
|
-
this.flush();
|
|
30152
|
-
}, this.settleMs);
|
|
30153
|
-
}
|
|
30154
|
-
async drain() {
|
|
30155
|
-
await this.flush();
|
|
30156
|
-
}
|
|
30157
|
-
size() {
|
|
30158
|
-
return this.pending.length;
|
|
30159
|
-
}
|
|
30160
|
-
async flush() {
|
|
30161
|
-
if (this.timer) {
|
|
30162
|
-
clearTimeout(this.timer);
|
|
30163
|
-
this.timer = null;
|
|
30164
|
-
}
|
|
30165
|
-
if (this.pending.length === 0)
|
|
30166
|
-
return;
|
|
30167
|
-
const batch = this.pending;
|
|
30168
|
-
this.pending = [];
|
|
30169
|
-
try {
|
|
30170
|
-
await this.onSettle(batch);
|
|
30171
|
-
} catch (err) {
|
|
30172
|
-
this.onError?.(err);
|
|
30173
|
-
}
|
|
30174
|
-
}
|
|
30175
|
-
}
|
|
30176
|
-
|
|
30177
|
-
// src/lib/resolve-pending.ts
|
|
30178
|
-
import { readFileSync as readFileSync16 } from "node:fs";
|
|
30179
|
-
|
|
30180
|
-
// src/lib/conflicts.ts
|
|
30181
|
-
import { mkdirSync as mkdirSync10, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30182
|
-
import { dirname as dirname7, join as join11 } from "node:path";
|
|
30183
|
-
function conflictsRoot(workspaceSlug) {
|
|
30184
|
-
return join11(plinthHome(), "_conflicts", workspaceSlug);
|
|
30185
|
-
}
|
|
30186
|
-
function conflictFilePath(args) {
|
|
30187
|
-
const { workspaceSlug, entityType, entitySlug, timestamp } = args;
|
|
30188
|
-
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
30189
|
-
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.md`);
|
|
30190
|
-
}
|
|
30191
|
-
function stashConflict(args) {
|
|
30192
|
-
const target = conflictFilePath(args);
|
|
30193
|
-
mkdirSync10(dirname7(target), { recursive: true, mode: 448 });
|
|
30194
|
-
writeFileSync7(target, args.contents, { mode: 384 });
|
|
30195
|
-
return target;
|
|
30196
|
-
}
|
|
30197
|
-
function fileConflictPath(args) {
|
|
30198
|
-
const { workspaceSlug, relPath, timestamp } = args;
|
|
30199
|
-
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
30200
|
-
const safe = relPath.replace(/[/\\]/g, "__");
|
|
30201
|
-
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}`);
|
|
30202
|
-
}
|
|
30203
|
-
function stashFileConflict(args) {
|
|
30204
|
-
const target = fileConflictPath(args);
|
|
30205
|
-
mkdirSync10(dirname7(target), { recursive: true, mode: 448 });
|
|
30206
|
-
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
30207
|
-
return target;
|
|
30208
|
-
}
|
|
30209
|
-
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(\.md)?$/;
|
|
30210
|
-
function parseStamp(stamp) {
|
|
30211
|
-
const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
30212
|
-
const d = new Date(iso);
|
|
30213
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
30214
|
-
}
|
|
30215
|
-
function labelFor(file2) {
|
|
30216
|
-
const m = STAMP_RE2.exec(file2);
|
|
30217
|
-
if (!m)
|
|
30218
|
-
return file2;
|
|
30219
|
-
const head = file2.slice(0, m.index);
|
|
30220
|
-
if (head.startsWith("files.")) {
|
|
30221
|
-
return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
|
|
30222
|
-
}
|
|
30223
|
-
const dot = head.indexOf(".");
|
|
30224
|
-
if (dot === -1)
|
|
30225
|
-
return head;
|
|
30226
|
-
return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
|
|
30227
|
-
}
|
|
30228
|
-
function listConflictStashes(workspaceSlug) {
|
|
30229
|
-
const dir = conflictsRoot(workspaceSlug);
|
|
30230
|
-
let names;
|
|
30231
|
-
try {
|
|
30232
|
-
names = readdirSync5(dir);
|
|
30233
|
-
} catch {
|
|
30234
|
-
return [];
|
|
30235
|
-
}
|
|
30236
|
-
const out = [];
|
|
30237
|
-
for (const file2 of names) {
|
|
30238
|
-
const absPath = join11(dir, file2);
|
|
30239
|
-
let st;
|
|
30240
|
-
try {
|
|
30241
|
-
st = statSync6(absPath);
|
|
30242
|
-
} catch {
|
|
30243
|
-
continue;
|
|
30244
|
-
}
|
|
30245
|
-
if (!st.isFile())
|
|
30246
|
-
continue;
|
|
30247
|
-
if (file2.startsWith("."))
|
|
30248
|
-
continue;
|
|
30249
|
-
const m = STAMP_RE2.exec(file2);
|
|
30250
|
-
out.push({
|
|
30251
|
-
file: file2,
|
|
30252
|
-
absPath,
|
|
30253
|
-
label: labelFor(file2),
|
|
30254
|
-
stashedAt: m ? parseStamp(m[1]) : null,
|
|
30255
|
-
mtimeMs: st.mtimeMs
|
|
30256
|
-
});
|
|
30257
|
-
}
|
|
30258
|
-
return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
|
|
30259
|
-
}
|
|
30260
|
-
|
|
30261
|
-
// src/lib/files/push.ts
|
|
30262
|
-
import { readFileSync as readFileSync13, statSync as statSync7 } from "node:fs";
|
|
30263
|
-
|
|
30264
|
-
// src/lib/log-tokens.ts
|
|
30265
|
-
function categorizeError(err) {
|
|
30266
|
-
if (err instanceof FrontmatterValidationError) {
|
|
30267
|
-
return { status: 422, reason: "invalid_frontmatter", detail: err.detail };
|
|
30268
|
-
}
|
|
30269
|
-
if (err instanceof ApiError) {
|
|
30270
|
-
const { status } = err;
|
|
30271
|
-
if (status === 0)
|
|
30272
|
-
return { status, reason: "network" };
|
|
30273
|
-
if (status === 401 || status === 403)
|
|
30274
|
-
return { status, reason: "auth" };
|
|
30275
|
-
if (status === 429)
|
|
30276
|
-
return { status, reason: "rate_limit" };
|
|
30277
|
-
if (status === 400 || status === 422) {
|
|
30278
|
-
const serverMessage = sanitizeServerMessage(err.message);
|
|
30279
|
-
return serverMessage ? { status, reason: "bad_request", serverMessage } : { status, reason: "bad_request" };
|
|
30280
|
-
}
|
|
30281
|
-
if (status >= 500 && status <= 599)
|
|
30282
|
-
return { status, reason: "server" };
|
|
30283
|
-
return { status, reason: "http" };
|
|
30284
|
-
}
|
|
30285
|
-
return { status: 0, reason: "unexpected" };
|
|
30286
|
-
}
|
|
30287
|
-
function sanitizeServerMessage(message) {
|
|
30288
|
-
const oneLine = message.replace(/\s+/g, " ").trim();
|
|
30289
|
-
if (oneLine.length === 0)
|
|
30290
|
-
return "";
|
|
30291
|
-
return oneLine.length > 200 ? `${oneLine.slice(0, 197)}...` : oneLine;
|
|
30292
|
-
}
|
|
30293
|
-
function pushFailedLine(f) {
|
|
30294
|
-
const idPart = f.id ? ` id=${f.id}` : "";
|
|
30295
|
-
const detailPart = f.detail ? ` detail=${f.detail}` : "";
|
|
30296
|
-
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
30297
|
-
return `[PUSH_FAILED entityType=${f.entityType} slug=${f.slug}${idPart} status=${f.status} reason=${f.reason}${detailPart}${serverPart}]`;
|
|
30298
|
-
}
|
|
30299
|
-
function pullFailedLine(f) {
|
|
30300
|
-
return `[PULL_FAILED status=${f.status} reason=${f.reason}]`;
|
|
30301
|
-
}
|
|
30302
|
-
function contextDocConflictLine(f) {
|
|
30303
|
-
const localIdPart = f.localId ? ` localId=${f.localId}` : "";
|
|
30304
|
-
const reasonPart = f.reason ? ` reason=${f.reason}` : "";
|
|
30305
|
-
const stashPart = f.stash ? ` stash=${f.stash}` : "";
|
|
30306
|
-
return `[CONTEXT_DOC_CONFLICT entityType=${f.entityType} localSlug=${f.localSlug}${localIdPart} ` + `existingSlug=${f.existingSlug} existingId=${f.existingId} scope=${f.scope} ` + `action=${f.action}${reasonPart}${stashPart}]`;
|
|
30307
|
-
}
|
|
30308
|
-
function syncErrorLine(f) {
|
|
30309
|
-
return `[SYNC_ERROR entityType=${f.entityType} slug=${f.slug} id=${f.id} ` + `fields=${f.fields.join(",")} pushed=${f.pushed}]`;
|
|
30310
|
-
}
|
|
30311
|
-
function filePushLine(f) {
|
|
30312
|
-
return `[FILE_PUSH path=${f.path} id=${f.id} action=${f.action}]`;
|
|
30313
|
-
}
|
|
30314
|
-
function fileDeleteLine(f) {
|
|
30315
|
-
const notePart = f.note ? ` note=${f.note}` : "";
|
|
30316
|
-
return `[FILE_DELETE path=${f.path} id=${f.id}${notePart}]`;
|
|
30317
|
-
}
|
|
30318
|
-
function fileOverCapLine(f) {
|
|
30319
|
-
return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
|
|
30320
|
-
}
|
|
30321
|
-
function fileConflictLine(f) {
|
|
30322
|
-
return `[FILE_CONFLICT path=${f.path} stash=${f.stash}]`;
|
|
30323
|
-
}
|
|
30324
|
-
function conflictsViewFailedLine(f) {
|
|
30325
|
-
const msg = f.message === undefined ? "" : sanitizeServerMessage(f.message);
|
|
30326
|
-
const msgPart = msg === "" ? "" : ` message="${msg.replace(/"/g, "'")}"`;
|
|
30327
|
-
return `[CONFLICTS_VIEW_FAILED path=${f.path} reason=${f.reason}${msgPart}]`;
|
|
30328
|
-
}
|
|
30329
|
-
function fileFailedLine(f) {
|
|
30330
|
-
const detailPart = f.detail ? ` detail=${f.detail}` : "";
|
|
30331
|
-
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
30332
|
-
return `[FILE_PUSH_FAILED path=${f.path} status=${f.status} reason=${f.reason}${detailPart}${serverPart}]`;
|
|
30333
|
-
}
|
|
30334
|
-
function frontmatterIdBackfillLine(r) {
|
|
30335
|
-
return `[FRONTMATTER_ID_BACKFILL scanned=${r.scanned} written=${r.written} ` + `inodes_restamped=${r.inodesRestamped} failed=${r.failed} ` + `skipped_frozen=${r.skipped.frozen} ` + `skipped_foreign_id=${r.skipped.foreignId} ` + `skipped_no_frontmatter=${r.skipped.noFrontmatter} ` + `skipped_slug_collision=${r.skipped.slugCollision} ` + `skipped_diverged=${r.skipped.diverged}]`;
|
|
30336
|
-
}
|
|
30337
|
-
function pullNoticeLine(kind) {
|
|
30338
|
-
const token = kind === "auth" ? "PULL_PAUSED" : "PULL_DEGRADED";
|
|
30339
|
-
return `[${token} reason=${kind}]`;
|
|
30340
|
-
}
|
|
30341
|
-
function calendarProjectedLine(f) {
|
|
30342
|
-
return `[CALENDAR_PROJECTED written=${f.written} pruned=${f.pruned} ` + `calendars=${f.calendars} not_read=${f.notRead}]`;
|
|
30343
|
-
}
|
|
30344
|
-
function calendarFailedLine(f) {
|
|
30345
|
-
const serverPart = f.serverMessage ? ` server_msg="${f.serverMessage}"` : "";
|
|
30346
|
-
return `[CALENDAR_FAILED status=${f.status} reason=${f.reason} staled=${f.staled}${serverPart}]`;
|
|
30347
|
-
}
|
|
30348
|
-
function calendarNoticeLine(kind) {
|
|
30349
|
-
const token = kind === "auth" ? "CALENDAR_PAUSED" : kind === "unsupported" ? "CALENDAR_UNSUPPORTED" : "CALENDAR_DEGRADED";
|
|
30350
|
-
return `[${token} reason=${kind}]`;
|
|
30351
|
-
}
|
|
30352
|
-
|
|
30353
|
-
// src/lib/rejected.ts
|
|
30354
|
-
import { relative as relative3 } from "node:path";
|
|
30355
|
-
var PERMANENT_REJECT_REASONS = new Set(["bad_request", "invalid_frontmatter"]);
|
|
30356
|
-
function isPermanentReject(err) {
|
|
30357
|
-
return PERMANENT_REJECT_REASONS.has(categorizeError(err).reason);
|
|
30358
|
-
}
|
|
30359
|
-
function persist(state, next) {
|
|
30360
|
-
if (!next.changed)
|
|
30361
|
-
return state;
|
|
30362
|
-
return commitState(state, next.state);
|
|
30363
|
-
}
|
|
30364
|
-
function recordEntityRejected(args) {
|
|
30365
|
-
const cat = categorizeError(args.err);
|
|
30366
|
-
let path5;
|
|
30367
|
-
try {
|
|
30368
|
-
path5 = relative3(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
30369
|
-
} catch {
|
|
30370
|
-
path5 = args.absolutePath;
|
|
30371
|
-
}
|
|
30372
|
-
const withoutStale = removeUnsynced(args.state, "entity", path5).state;
|
|
30373
|
-
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30374
|
-
target: "entity",
|
|
30375
|
-
op: args.op,
|
|
30376
|
-
path: path5,
|
|
30377
|
-
status: cat.status,
|
|
30378
|
-
reason: cat.reason,
|
|
30379
|
-
detail: cat.detail,
|
|
30380
|
-
message: cat.serverMessage,
|
|
30381
|
-
rejected_at: args.now().toISOString()
|
|
30382
|
-
}));
|
|
30383
|
-
}
|
|
30384
|
-
function recordFileRejected(args) {
|
|
30385
|
-
const cat = categorizeError(args.err);
|
|
30386
|
-
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30387
|
-
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30388
|
-
target: "file",
|
|
30389
|
-
op: args.op,
|
|
30390
|
-
path: args.relPath,
|
|
30391
|
-
status: cat.status,
|
|
30392
|
-
reason: cat.reason,
|
|
30393
|
-
detail: cat.detail,
|
|
30394
|
-
message: cat.serverMessage,
|
|
30395
|
-
rejected_at: args.now().toISOString()
|
|
30396
|
-
}));
|
|
30397
|
-
}
|
|
30398
|
-
function humanBytes(bytes) {
|
|
30399
|
-
if (bytes >= 1e6)
|
|
30400
|
-
return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
30401
|
-
if (bytes >= 1000)
|
|
30402
|
-
return `${Math.round(bytes / 1000)} KB`;
|
|
30403
|
-
return `${bytes} bytes`;
|
|
30404
|
-
}
|
|
30405
|
-
function recordFileOverCap(args) {
|
|
30406
|
-
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30407
|
-
const ceiling = args.source === "server" ? `the ${humanBytes(args.max)} server limit` : `the ${humanBytes(args.max)} upload ceiling (measured platform limit)`;
|
|
30408
|
-
const message = args.tracked ? `Your local change to this file (${humanBytes(args.size)}) exceeds ${ceiling}, ` + `so it cannot be pushed. The copy in Plinth is unchanged; your edit is on disk here only.` : `${humanBytes(args.size)} exceeds ${ceiling}. The file is still on disk ` + `here but is not in Plinth; shrink or split it to sync it.`;
|
|
30409
|
-
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30410
|
-
target: "file",
|
|
30411
|
-
op: "upsert",
|
|
30412
|
-
path: args.relPath,
|
|
30413
|
-
status: 413,
|
|
30414
|
-
reason: "over_cap",
|
|
30415
|
-
message,
|
|
30416
|
-
rejected_at: args.now().toISOString()
|
|
30417
|
-
}));
|
|
30418
|
-
}
|
|
30419
|
-
function recordFileSymlinkRefused(args) {
|
|
30420
|
-
const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
|
|
30421
|
-
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30422
|
-
target: "file",
|
|
30423
|
-
op: args.op,
|
|
30424
|
-
path: args.relPath,
|
|
30425
|
-
status: 0,
|
|
30426
|
-
reason: "symlink",
|
|
30427
|
-
message: `A symlink on the path to files/${args.relPath} means syncing it would ` + `read or write outside your Plinth folder, so it was left alone. ` + `Replace the link with a real folder or file to sync it.`,
|
|
30428
|
-
rejected_at: args.now().toISOString()
|
|
30429
|
-
}));
|
|
30430
|
-
}
|
|
30431
|
-
function recordEntitySymlinkRefused(args) {
|
|
30432
|
-
const path5 = args.mirrorRelativePath;
|
|
30433
|
-
const withoutStale = removeUnsynced(args.state, "entity", path5).state;
|
|
30434
|
-
return persist(withoutStale, upsertRejected(withoutStale, {
|
|
30435
|
-
target: "entity",
|
|
30436
|
-
op: args.op,
|
|
30437
|
-
path: path5,
|
|
30438
|
-
status: 0,
|
|
30439
|
-
reason: "symlink",
|
|
30440
|
-
message: `A symlink on the path to ${path5} means syncing it would read or write ` + `outside your Plinth folder, so it was left alone. Replace the link ` + `with a real folder or file to sync it.`,
|
|
30441
|
-
rejected_at: args.now().toISOString()
|
|
30442
|
-
}));
|
|
30443
|
-
}
|
|
30444
|
-
function clearSymlinkRefused(state, target, path5) {
|
|
30445
|
-
const list = state.rejected ?? [];
|
|
30446
|
-
const next = list.filter((o) => !(o.target === target && o.path === path5 && o.reason === "symlink"));
|
|
30447
|
-
if (next.length === list.length)
|
|
30448
|
-
return state;
|
|
30449
|
-
return commitState(state, { ...state, rejected: next });
|
|
30450
|
-
}
|
|
30451
|
-
function clearEntityRejected(state, workspaceSlug, absolutePath) {
|
|
30452
|
-
let path5;
|
|
30453
|
-
try {
|
|
30454
|
-
path5 = relative3(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30455
|
-
} catch {
|
|
30456
|
-
path5 = absolutePath;
|
|
30457
|
-
}
|
|
30458
|
-
const next = removeRejected(state, "entity", path5);
|
|
30459
|
-
if (!next.changed)
|
|
30460
|
-
return state;
|
|
30461
|
-
return commitState(state, next.state);
|
|
30462
|
-
}
|
|
30463
|
-
function clearFileRejected(state, relPath) {
|
|
30464
|
-
const next = removeRejected(state, "file", relPath);
|
|
30465
|
-
if (!next.changed)
|
|
30466
|
-
return state;
|
|
30467
|
-
return commitState(state, next.state);
|
|
30468
|
-
}
|
|
30469
|
-
|
|
30470
|
-
// src/lib/files/api.ts
|
|
30471
|
-
var MAX_WORKSPACE_FILE_BYTES = 4000000;
|
|
30472
|
-
var HASH_RE = /^[0-9a-f]{64}$/;
|
|
30473
|
-
var TextQualitySchema = exports_external.enum(["ok", "partial", "garbled"]);
|
|
30474
|
-
var FileLiveSchema = exports_external.object({
|
|
30475
|
-
id: exports_external.string().uuid(),
|
|
30476
|
-
path: exports_external.string().min(1),
|
|
30477
|
-
mime_type: exports_external.string(),
|
|
30478
|
-
size_bytes: exports_external.number().int().nonnegative(),
|
|
30479
|
-
content_hash: exports_external.string().regex(HASH_RE),
|
|
30480
|
-
artist_id: exports_external.string().uuid().nullable(),
|
|
30481
|
-
project_id: exports_external.string().uuid().nullable(),
|
|
30482
|
-
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30483
|
-
deleted_at: exports_external.null(),
|
|
30484
|
-
last_edited_by: exports_external.string().uuid().nullable(),
|
|
30485
|
-
content_quality: TextQualitySchema.nullable().optional().default(null).catch(null),
|
|
30486
|
-
has_extracted_text: exports_external.boolean().optional().catch(undefined),
|
|
30487
|
-
has_stored_bytes: exports_external.boolean().optional().catch(undefined)
|
|
30488
|
-
});
|
|
30489
|
-
function storedBytesSignal(row) {
|
|
30490
|
-
if (row.has_stored_bytes === true)
|
|
30491
|
-
return "present";
|
|
30492
|
-
if (row.has_stored_bytes === false)
|
|
30493
|
-
return "absent";
|
|
30494
|
-
return "unknown";
|
|
30495
|
-
}
|
|
30496
|
-
function extractedTextSignal(row) {
|
|
30497
|
-
if (row.has_extracted_text === true)
|
|
30498
|
-
return "present";
|
|
30499
|
-
if (row.has_extracted_text === false)
|
|
30500
|
-
return "absent";
|
|
30501
|
-
return "unknown";
|
|
30502
|
-
}
|
|
30503
|
-
var FileTombstoneSchema = exports_external.object({
|
|
30504
|
-
id: exports_external.string().uuid(),
|
|
30505
|
-
path: exports_external.string().min(1),
|
|
30506
|
-
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30507
|
-
deleted_at: exports_external.string().datetime({ offset: true })
|
|
30508
|
-
});
|
|
30509
|
-
var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
|
|
30510
|
-
var FilesPageSchema = exports_external.object({
|
|
30511
|
-
files: exports_external.array(FileRowSchema),
|
|
30512
|
-
next_cursor: exports_external.string().nullable()
|
|
30513
|
-
});
|
|
30514
|
-
var FileWithDownloadSchema = FileLiveSchema.extend({
|
|
30515
|
-
download_url: exports_external.string().url().nullable()
|
|
30516
|
-
});
|
|
30517
|
-
var FileTextSchema = exports_external.object({
|
|
30518
|
-
id: exports_external.string().uuid(),
|
|
30519
|
-
path: exports_external.string().min(1),
|
|
30520
|
-
mime_type: exports_external.string(),
|
|
30521
|
-
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30522
|
-
content_quality: TextQualitySchema.nullable(),
|
|
30523
|
-
source_uri: exports_external.string().nullable(),
|
|
30524
|
-
text: exports_external.string().nullable(),
|
|
30525
|
-
chars: exports_external.number().int().nonnegative(),
|
|
30526
|
-
withheld: exports_external.enum(["bytes_are_text", "no_text"]).nullable()
|
|
30527
|
-
});
|
|
30528
|
-
var PreconditionSchema = exports_external.object({
|
|
30529
|
-
code: exports_external.literal("precondition_failed"),
|
|
30530
|
-
current_updated_at: exports_external.string()
|
|
30531
|
-
});
|
|
30532
|
-
function baseHeaders(pat) {
|
|
30533
|
-
return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
|
|
30534
|
-
}
|
|
30535
|
-
async function syncFilesPage(args) {
|
|
30536
|
-
const { baseUrl, pat, cursor, limit, fetchImpl = fetch } = args;
|
|
30537
|
-
const params = new URLSearchParams;
|
|
30538
|
-
if (cursor)
|
|
30539
|
-
params.set("since", cursor);
|
|
30540
|
-
if (limit)
|
|
30541
|
-
params.set("limit", String(limit));
|
|
30542
|
-
const qs = params.toString();
|
|
30543
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files` + (qs.length > 0 ? `?${qs}` : "");
|
|
30544
|
-
let res;
|
|
30545
|
-
try {
|
|
30546
|
-
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
30547
|
-
} catch (err) {
|
|
30548
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30549
|
-
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
30550
|
-
}
|
|
30551
|
-
if (res.status === 401) {
|
|
30552
|
-
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
30553
|
-
}
|
|
30554
|
-
if (res.status === 429) {
|
|
30555
|
-
throw new ApiError(429, "Rate limited by the server. Wait a few seconds and retry.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
30556
|
-
}
|
|
30557
|
-
if (!res.ok) {
|
|
30558
|
-
throw new ApiError(res.status, `sync files failed with HTTP ${res.status}`);
|
|
30559
|
-
}
|
|
30560
|
-
const body = await res.json();
|
|
30561
|
-
const parsed = FilesPageSchema.safeParse(body);
|
|
30562
|
-
if (!parsed.success) {
|
|
30563
|
-
throw new ApiError(res.status, `sync files response did not match the expected shape: ${parsed.error.message}`);
|
|
30564
|
-
}
|
|
30565
|
-
return { rows: parsed.data.files, next_cursor: parsed.data.next_cursor };
|
|
30566
|
-
}
|
|
30567
|
-
async function getFile(args) {
|
|
30568
|
-
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
30569
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}`;
|
|
30570
|
-
let res;
|
|
30571
|
-
try {
|
|
30572
|
-
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
30573
|
-
} catch (err) {
|
|
30574
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30575
|
-
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
30576
|
-
}
|
|
30577
|
-
if (res.status === 404)
|
|
30578
|
-
return { ok: false, code: 404 };
|
|
30579
|
-
if (res.status === 401) {
|
|
30580
|
-
throw new ApiError(401, "Token was rejected by the server.");
|
|
30581
|
-
}
|
|
30582
|
-
if (res.status === 429) {
|
|
30583
|
-
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
30584
|
-
}
|
|
30585
|
-
if (!res.ok) {
|
|
30586
|
-
throw new ApiError(res.status, `get file failed with HTTP ${res.status}`);
|
|
30587
|
-
}
|
|
30588
|
-
const body = await res.json();
|
|
30589
|
-
const parsed = FileWithDownloadSchema.safeParse(body);
|
|
30590
|
-
if (!parsed.success) {
|
|
30591
|
-
throw new ApiError(res.status, `get file response did not match the expected shape`);
|
|
30592
|
-
}
|
|
30593
|
-
return { ok: true, row: parsed.data };
|
|
30594
|
-
}
|
|
30595
|
-
async function getFileText(args) {
|
|
30596
|
-
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
30597
|
-
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}/text`;
|
|
30598
|
-
let res;
|
|
30599
|
-
try {
|
|
30600
|
-
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
30601
|
-
} catch (err) {
|
|
30602
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30603
|
-
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
30604
|
-
}
|
|
30605
|
-
if (res.status === 404)
|
|
30606
|
-
return { ok: false, code: 404 };
|
|
30607
|
-
if (res.status === 401) {
|
|
30608
|
-
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
30609
|
-
}
|
|
30610
|
-
if (res.status === 429) {
|
|
30611
|
-
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
31090
|
+
function defaultThresholds() {
|
|
31091
|
+
return {
|
|
31092
|
+
absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
|
|
31093
|
+
propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
|
|
31094
|
+
propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5)
|
|
31095
|
+
};
|
|
31096
|
+
}
|
|
31097
|
+
function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
|
|
31098
|
+
const count = ops.length;
|
|
31099
|
+
if (count === 0)
|
|
31100
|
+
return { tripped: false };
|
|
31101
|
+
if (count >= thresholds.absCount) {
|
|
31102
|
+
return { tripped: true, reason: { kind: "count", count } };
|
|
30612
31103
|
}
|
|
30613
|
-
|
|
30614
|
-
|
|
31104
|
+
const byType = new Map;
|
|
31105
|
+
for (const op of ops) {
|
|
31106
|
+
byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
|
|
30615
31107
|
}
|
|
30616
|
-
const
|
|
30617
|
-
|
|
30618
|
-
|
|
30619
|
-
|
|
31108
|
+
for (const [entityType, typeCount] of byType) {
|
|
31109
|
+
const typePopulation = typePopulations[entityType] ?? typeCount;
|
|
31110
|
+
if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
|
|
31111
|
+
return {
|
|
31112
|
+
tripped: true,
|
|
31113
|
+
reason: {
|
|
31114
|
+
kind: "proportion",
|
|
31115
|
+
count,
|
|
31116
|
+
entityType,
|
|
31117
|
+
typeCount,
|
|
31118
|
+
typePopulation,
|
|
31119
|
+
pct: typeCount / typePopulation
|
|
31120
|
+
}
|
|
31121
|
+
};
|
|
31122
|
+
}
|
|
30620
31123
|
}
|
|
30621
|
-
return {
|
|
31124
|
+
return { tripped: false };
|
|
30622
31125
|
}
|
|
30623
|
-
|
|
30624
|
-
|
|
30625
|
-
|
|
30626
|
-
|
|
30627
|
-
|
|
30628
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30629
|
-
throw new ApiError(0, `Could not download bytes: ${msg}`);
|
|
30630
|
-
}
|
|
30631
|
-
if (!res.ok) {
|
|
30632
|
-
throw new ApiError(res.status, `download failed with HTTP ${res.status}`);
|
|
30633
|
-
}
|
|
30634
|
-
const ab = await res.arrayBuffer();
|
|
30635
|
-
return Buffer.from(ab);
|
|
31126
|
+
function dedupeOps(ops) {
|
|
31127
|
+
const byKey = new Map;
|
|
31128
|
+
for (const op of ops)
|
|
31129
|
+
byKey.set(`${op.entityType}:${op.id}`, op);
|
|
31130
|
+
return Array.from(byKey.values());
|
|
30636
31131
|
}
|
|
30637
|
-
|
|
30638
|
-
|
|
30639
|
-
|
|
30640
|
-
|
|
30641
|
-
|
|
30642
|
-
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
|
|
30646
|
-
|
|
30647
|
-
|
|
30648
|
-
|
|
30649
|
-
|
|
30650
|
-
|
|
30651
|
-
|
|
30652
|
-
"Content-Type": mimeType,
|
|
30653
|
-
"X-Plinth-File-Path": encodeURIComponent(relPath),
|
|
30654
|
-
"X-Plinth-Content-Hash": contentHash
|
|
30655
|
-
};
|
|
30656
|
-
if (expectedUpdatedAt)
|
|
30657
|
-
headers["X-Plinth-Expected-Updated-At"] = expectedUpdatedAt;
|
|
30658
|
-
let res;
|
|
30659
|
-
try {
|
|
30660
|
-
res = await fetchImpl(url2, { method: "POST", headers, body: bytes });
|
|
30661
|
-
} catch (err) {
|
|
30662
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30663
|
-
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
31132
|
+
var DEFAULT_SETTLE_MS = 600;
|
|
31133
|
+
var DEFAULT_MAX_BATCH = 500;
|
|
31134
|
+
|
|
31135
|
+
class DestructiveGate {
|
|
31136
|
+
pending = [];
|
|
31137
|
+
timer = null;
|
|
31138
|
+
settleMs;
|
|
31139
|
+
maxBatch;
|
|
31140
|
+
onSettle;
|
|
31141
|
+
onError;
|
|
31142
|
+
constructor(opts) {
|
|
31143
|
+
this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
31144
|
+
this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
31145
|
+
this.onSettle = opts.onSettle;
|
|
31146
|
+
this.onError = opts.onError;
|
|
30664
31147
|
}
|
|
30665
|
-
|
|
30666
|
-
|
|
30667
|
-
|
|
30668
|
-
|
|
30669
|
-
|
|
31148
|
+
consider(op) {
|
|
31149
|
+
this.pending.push(op);
|
|
31150
|
+
if (this.pending.length >= this.maxBatch) {
|
|
31151
|
+
this.flush();
|
|
31152
|
+
return;
|
|
30670
31153
|
}
|
|
30671
|
-
|
|
31154
|
+
if (this.timer)
|
|
31155
|
+
clearTimeout(this.timer);
|
|
31156
|
+
this.timer = setTimeout(() => {
|
|
31157
|
+
this.flush();
|
|
31158
|
+
}, this.settleMs);
|
|
30672
31159
|
}
|
|
30673
|
-
|
|
30674
|
-
|
|
30675
|
-
try {
|
|
30676
|
-
const json2 = await res.json();
|
|
30677
|
-
if (typeof json2.max_bytes === "number")
|
|
30678
|
-
maxBytes = json2.max_bytes;
|
|
30679
|
-
} catch {}
|
|
30680
|
-
return { ok: false, code: 413, maxBytes };
|
|
31160
|
+
async drain() {
|
|
31161
|
+
await this.flush();
|
|
30681
31162
|
}
|
|
30682
|
-
|
|
30683
|
-
|
|
30684
|
-
const parsed = PreconditionSchema.safeParse(json2);
|
|
30685
|
-
if (!parsed.success) {
|
|
30686
|
-
throw new ApiError(409, `post file 409 body did not match precondition_failed shape`);
|
|
30687
|
-
}
|
|
30688
|
-
return { ok: false, code: 409, currentUpdatedAt: parsed.data.current_updated_at };
|
|
31163
|
+
size() {
|
|
31164
|
+
return this.pending.length;
|
|
30689
31165
|
}
|
|
30690
|
-
|
|
30691
|
-
|
|
31166
|
+
async flush() {
|
|
31167
|
+
if (this.timer) {
|
|
31168
|
+
clearTimeout(this.timer);
|
|
31169
|
+
this.timer = null;
|
|
31170
|
+
}
|
|
31171
|
+
if (this.pending.length === 0)
|
|
31172
|
+
return;
|
|
31173
|
+
const batch = this.pending;
|
|
31174
|
+
this.pending = [];
|
|
30692
31175
|
try {
|
|
30693
|
-
|
|
30694
|
-
|
|
30695
|
-
|
|
30696
|
-
|
|
30697
|
-
return { ok: false, code: 400, reason: "hash_mismatch" };
|
|
30698
|
-
throw new ApiError(400, "post file rejected as bad request");
|
|
30699
|
-
}
|
|
30700
|
-
if (res.status === 401) {
|
|
30701
|
-
throw new ApiError(401, "Token was rejected by the server.");
|
|
31176
|
+
await this.onSettle(batch);
|
|
31177
|
+
} catch (err) {
|
|
31178
|
+
this.onError?.(err);
|
|
31179
|
+
}
|
|
30702
31180
|
}
|
|
30703
|
-
|
|
30704
|
-
|
|
31181
|
+
}
|
|
31182
|
+
|
|
31183
|
+
// src/lib/resolve-pending.ts
|
|
31184
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
31185
|
+
|
|
31186
|
+
// src/lib/conflicts.ts
|
|
31187
|
+
import { mkdirSync as mkdirSync10, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
31188
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
31189
|
+
function conflictsRoot(workspaceSlug) {
|
|
31190
|
+
return join11(plinthHome(), "_conflicts", workspaceSlug);
|
|
31191
|
+
}
|
|
31192
|
+
function conflictFilePath(args) {
|
|
31193
|
+
const { workspaceSlug, entityType, entitySlug, timestamp } = args;
|
|
31194
|
+
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31195
|
+
return join11(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.md`);
|
|
31196
|
+
}
|
|
31197
|
+
function stashConflict(args) {
|
|
31198
|
+
const target = conflictFilePath(args);
|
|
31199
|
+
mkdirSync10(dirname7(target), { recursive: true, mode: 448 });
|
|
31200
|
+
writeFileSync7(target, args.contents, { mode: 384 });
|
|
31201
|
+
return target;
|
|
31202
|
+
}
|
|
31203
|
+
function fileConflictPath(args) {
|
|
31204
|
+
const { workspaceSlug, relPath, timestamp } = args;
|
|
31205
|
+
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
31206
|
+
const safe = relPath.replace(/[/\\]/g, "__");
|
|
31207
|
+
return join11(conflictsRoot(workspaceSlug), `files.${safe}.${iso}`);
|
|
31208
|
+
}
|
|
31209
|
+
function stashFileConflict(args) {
|
|
31210
|
+
const target = fileConflictPath(args);
|
|
31211
|
+
mkdirSync10(dirname7(target), { recursive: true, mode: 448 });
|
|
31212
|
+
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
31213
|
+
return target;
|
|
31214
|
+
}
|
|
31215
|
+
var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(\.md)?$/;
|
|
31216
|
+
function parseStamp(stamp) {
|
|
31217
|
+
const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
|
|
31218
|
+
const d = new Date(iso);
|
|
31219
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
31220
|
+
}
|
|
31221
|
+
function labelFor(file2) {
|
|
31222
|
+
const m = STAMP_RE2.exec(file2);
|
|
31223
|
+
if (!m)
|
|
31224
|
+
return file2;
|
|
31225
|
+
const head = file2.slice(0, m.index);
|
|
31226
|
+
if (head.startsWith("files.")) {
|
|
31227
|
+
return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
|
|
30705
31228
|
}
|
|
30706
|
-
|
|
31229
|
+
const dot = head.indexOf(".");
|
|
31230
|
+
if (dot === -1)
|
|
31231
|
+
return head;
|
|
31232
|
+
return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
|
|
30707
31233
|
}
|
|
30708
|
-
|
|
30709
|
-
const
|
|
30710
|
-
|
|
30711
|
-
let res;
|
|
31234
|
+
function listConflictStashes(workspaceSlug) {
|
|
31235
|
+
const dir = conflictsRoot(workspaceSlug);
|
|
31236
|
+
let names;
|
|
30712
31237
|
try {
|
|
30713
|
-
|
|
30714
|
-
|
|
30715
|
-
|
|
30716
|
-
});
|
|
30717
|
-
} catch (err) {
|
|
30718
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
30719
|
-
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
30720
|
-
}
|
|
30721
|
-
if (res.status === 200) {
|
|
30722
|
-
const body = await res.json();
|
|
30723
|
-
const parsed = FileTombstoneSchema.safeParse(body);
|
|
30724
|
-
if (!parsed.success) {
|
|
30725
|
-
throw new ApiError(200, `delete file response did not match the expected tombstone shape`);
|
|
30726
|
-
}
|
|
30727
|
-
return { ok: true, tombstone: parsed.data };
|
|
31238
|
+
names = readdirSync5(dir);
|
|
31239
|
+
} catch {
|
|
31240
|
+
return [];
|
|
30728
31241
|
}
|
|
30729
|
-
|
|
30730
|
-
|
|
30731
|
-
|
|
30732
|
-
|
|
30733
|
-
|
|
30734
|
-
|
|
30735
|
-
|
|
31242
|
+
const out = [];
|
|
31243
|
+
for (const file2 of names) {
|
|
31244
|
+
const absPath = join11(dir, file2);
|
|
31245
|
+
let st;
|
|
31246
|
+
try {
|
|
31247
|
+
st = statSync6(absPath);
|
|
31248
|
+
} catch {
|
|
31249
|
+
continue;
|
|
30736
31250
|
}
|
|
30737
|
-
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30741
|
-
|
|
30742
|
-
|
|
30743
|
-
|
|
31251
|
+
if (!st.isFile())
|
|
31252
|
+
continue;
|
|
31253
|
+
if (file2.startsWith("."))
|
|
31254
|
+
continue;
|
|
31255
|
+
const m = STAMP_RE2.exec(file2);
|
|
31256
|
+
out.push({
|
|
31257
|
+
file: file2,
|
|
31258
|
+
absPath,
|
|
31259
|
+
label: labelFor(file2),
|
|
31260
|
+
stashedAt: m ? parseStamp(m[1]) : null,
|
|
31261
|
+
mtimeMs: st.mtimeMs
|
|
31262
|
+
});
|
|
30744
31263
|
}
|
|
30745
|
-
|
|
31264
|
+
return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
|
|
30746
31265
|
}
|
|
30747
31266
|
|
|
31267
|
+
// src/lib/files/push.ts
|
|
31268
|
+
import { readFileSync as readFileSync14, statSync as statSync7 } from "node:fs";
|
|
31269
|
+
|
|
30748
31270
|
// src/lib/files/content-hash.ts
|
|
30749
31271
|
import { createHash as createHash2 } from "node:crypto";
|
|
30750
31272
|
function sha256Bytes(bytes) {
|
|
@@ -30899,7 +31421,7 @@ async function pushFilesUpsert(args) {
|
|
|
30899
31421
|
state: next
|
|
30900
31422
|
};
|
|
30901
31423
|
}
|
|
30902
|
-
bytes =
|
|
31424
|
+
bytes = readFileSync14(absolutePath);
|
|
30903
31425
|
hash2 = sha256Bytes(bytes);
|
|
30904
31426
|
inode = st.ino;
|
|
30905
31427
|
}
|
|
@@ -31067,15 +31589,60 @@ class PerIdMutex {
|
|
|
31067
31589
|
}
|
|
31068
31590
|
|
|
31069
31591
|
// src/lib/push.ts
|
|
31070
|
-
var
|
|
31071
|
-
import { readFileSync as
|
|
31592
|
+
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
31593
|
+
import { readFileSync as readFileSync16, statSync as statSync8 } from "node:fs";
|
|
31072
31594
|
import { basename as basename4, relative as relative4 } from "node:path";
|
|
31073
31595
|
|
|
31074
|
-
// src/lib/
|
|
31596
|
+
// src/lib/frontmatter-id.ts
|
|
31075
31597
|
var import_gray_matter8 = __toESM(require_gray_matter(), 1);
|
|
31076
|
-
|
|
31598
|
+
function eolOf(content) {
|
|
31599
|
+
return /\r\n/.test(content) ? `\r
|
|
31600
|
+
` : `
|
|
31601
|
+
`;
|
|
31602
|
+
}
|
|
31603
|
+
function spliceFrontmatterId(content, id) {
|
|
31604
|
+
const eol = /^---\r\n/.test(content) ? `\r
|
|
31605
|
+
` : `
|
|
31606
|
+
`;
|
|
31607
|
+
if (!content.startsWith(`---${eol}`))
|
|
31608
|
+
return null;
|
|
31609
|
+
const closeAt = content.indexOf(`${eol}---`, 3);
|
|
31610
|
+
if (closeAt === -1)
|
|
31611
|
+
return null;
|
|
31612
|
+
return `${content.slice(0, closeAt)}${eol}id: ${id}${content.slice(closeAt)}`;
|
|
31613
|
+
}
|
|
31614
|
+
function prependFrontmatterBlock(content, id) {
|
|
31615
|
+
const eol = eolOf(content);
|
|
31616
|
+
return `---${eol}id: ${id}${eol}---${eol}${content}`;
|
|
31617
|
+
}
|
|
31618
|
+
function isFrontmatterless(raw, parsed) {
|
|
31619
|
+
return parsed.content === raw;
|
|
31620
|
+
}
|
|
31621
|
+
function withFrontmatterId(raw, parsed, id) {
|
|
31622
|
+
if (parsed.data?.["id"] !== undefined) {
|
|
31623
|
+
return null;
|
|
31624
|
+
}
|
|
31625
|
+
const spliced = spliceFrontmatterId(raw, id);
|
|
31626
|
+
if (spliced !== null)
|
|
31627
|
+
return { content: spliced, blockCreated: false };
|
|
31628
|
+
if (!isFrontmatterless(raw, parsed))
|
|
31629
|
+
return null;
|
|
31630
|
+
return { content: prependFrontmatterBlock(raw, id), blockCreated: true };
|
|
31631
|
+
}
|
|
31632
|
+
function frontmatterIdWriteIsSound(next, id, expectedBody) {
|
|
31633
|
+
try {
|
|
31634
|
+
const check2 = import_gray_matter8.default(next);
|
|
31635
|
+
return check2.data?.["id"] === id && check2.content === expectedBody;
|
|
31636
|
+
} catch {
|
|
31637
|
+
return false;
|
|
31638
|
+
}
|
|
31639
|
+
}
|
|
31640
|
+
|
|
31641
|
+
// src/lib/pull-write.ts
|
|
31642
|
+
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
31643
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
31077
31644
|
function computeMirrorHashes(strategy, content) {
|
|
31078
|
-
const parsed =
|
|
31645
|
+
const parsed = import_gray_matter9.default(content);
|
|
31079
31646
|
const fm = parsed.data ?? {};
|
|
31080
31647
|
const lifted = splitUserFrontmatter(strategy, fm);
|
|
31081
31648
|
const userFm = userFrontmatter(strategy, lifted.frontmatter);
|
|
@@ -31083,7 +31650,8 @@ function computeMirrorHashes(strategy, content) {
|
|
|
31083
31650
|
return {
|
|
31084
31651
|
bodyHash: stableHash(hashBody),
|
|
31085
31652
|
frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
|
|
31086
|
-
bodyLen: hashBody.length
|
|
31653
|
+
bodyLen: hashBody.length,
|
|
31654
|
+
frontmatterId: fm["id"]
|
|
31087
31655
|
};
|
|
31088
31656
|
}
|
|
31089
31657
|
function hashableBody(strategy, body) {
|
|
@@ -31095,15 +31663,15 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor)
|
|
|
31095
31663
|
const target = entityFilePath(workspaceSlug, strategy.type, doc2.slug);
|
|
31096
31664
|
let existing = null;
|
|
31097
31665
|
try {
|
|
31098
|
-
existing =
|
|
31666
|
+
existing = readFileSync15(target, "utf8");
|
|
31099
31667
|
} catch (err) {
|
|
31100
31668
|
if (err?.code !== "ENOENT")
|
|
31101
31669
|
throw err;
|
|
31102
31670
|
}
|
|
31103
31671
|
if (existing !== null) {
|
|
31104
|
-
const existingParsed =
|
|
31105
|
-
const cloudParsed =
|
|
31106
|
-
const stitched =
|
|
31672
|
+
const existingParsed = import_gray_matter9.default(existing);
|
|
31673
|
+
const cloudParsed = import_gray_matter9.default(content);
|
|
31674
|
+
const stitched = import_gray_matter9.default.stringify(existingParsed.content, cloudParsed.data);
|
|
31107
31675
|
content = stitched.endsWith(`
|
|
31108
31676
|
`) ? stitched : stitched + `
|
|
31109
31677
|
`;
|
|
@@ -31222,8 +31790,8 @@ function cloudMatchesAcked(strategy, cloud, acked) {
|
|
|
31222
31790
|
function stampStateFromMirror(args) {
|
|
31223
31791
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
31224
31792
|
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, doc2.slug);
|
|
31225
|
-
const mirrorRaw =
|
|
31226
|
-
const mirrorParsed =
|
|
31793
|
+
const mirrorRaw = readFileSync16(mirrorPath, "utf8");
|
|
31794
|
+
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
31227
31795
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
31228
31796
|
const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
|
|
31229
31797
|
const mirrorUserFm = userFrontmatter(strategy, mirrorLifted.frontmatter);
|
|
@@ -31275,6 +31843,7 @@ async function pushFile(args) {
|
|
|
31275
31843
|
const now = deps?.now ?? (() => new Date);
|
|
31276
31844
|
const mutex = deps?.mutex ?? new PerIdMutex;
|
|
31277
31845
|
const output = deps?.output;
|
|
31846
|
+
const suppressor = deps?.suppressor;
|
|
31278
31847
|
const strategy = getStrategy(entityType);
|
|
31279
31848
|
if (!strategy) {
|
|
31280
31849
|
return {
|
|
@@ -31302,7 +31871,7 @@ async function pushFile(args) {
|
|
|
31302
31871
|
}
|
|
31303
31872
|
let raw;
|
|
31304
31873
|
try {
|
|
31305
|
-
raw =
|
|
31874
|
+
raw = readFileSync16(absolutePath, "utf8");
|
|
31306
31875
|
} catch (err) {
|
|
31307
31876
|
if (err?.code === "ENOENT") {
|
|
31308
31877
|
return runPushDelete({
|
|
@@ -31322,7 +31891,7 @@ async function pushFile(args) {
|
|
|
31322
31891
|
}
|
|
31323
31892
|
let parsed;
|
|
31324
31893
|
try {
|
|
31325
|
-
parsed =
|
|
31894
|
+
parsed = import_gray_matter10.default(raw);
|
|
31326
31895
|
} catch {
|
|
31327
31896
|
return {
|
|
31328
31897
|
outcome: { kind: "ignored", absolutePath, reason: "frontmatter parse error" },
|
|
@@ -31350,14 +31919,14 @@ async function pushFile(args) {
|
|
|
31350
31919
|
if (strategy.classifyFieldErrors) {
|
|
31351
31920
|
const desiredBody = spliceSyncErrorBlock(parsed.content, fieldErrors);
|
|
31352
31921
|
if (desiredBody.replace(/\s+$/, "") !== parsed.content.replace(/\s+$/, "")) {
|
|
31353
|
-
let rewritten =
|
|
31922
|
+
let rewritten = import_gray_matter10.default.stringify(desiredBody, parsed.data ?? {});
|
|
31354
31923
|
if (!rewritten.endsWith(`
|
|
31355
31924
|
`))
|
|
31356
31925
|
rewritten += `
|
|
31357
31926
|
`;
|
|
31358
31927
|
writeMirrorFile(workspaceSlug, entityType, slug, rewritten);
|
|
31359
31928
|
raw = rewritten;
|
|
31360
|
-
parsed =
|
|
31929
|
+
parsed = import_gray_matter10.default(rewritten);
|
|
31361
31930
|
}
|
|
31362
31931
|
}
|
|
31363
31932
|
const liveInode = safeInode2(absolutePath);
|
|
@@ -31379,7 +31948,9 @@ async function pushFile(args) {
|
|
|
31379
31948
|
now,
|
|
31380
31949
|
output,
|
|
31381
31950
|
quarantinedKeys,
|
|
31382
|
-
identity
|
|
31951
|
+
identity,
|
|
31952
|
+
suppressor,
|
|
31953
|
+
writeBackReadBack: deps?.writeBackReadBack
|
|
31383
31954
|
});
|
|
31384
31955
|
}
|
|
31385
31956
|
return runPushUpsert({
|
|
@@ -31604,6 +32175,15 @@ async function runPushUpsert(args) {
|
|
|
31604
32175
|
const liveEntry = getEntry(args.state, strategy.type, args.stateEntry.id) ?? args.stateEntry;
|
|
31605
32176
|
const bodyChanged = newBodyHash !== liveEntry.body_hash;
|
|
31606
32177
|
const frontmatterChanged = newFrontmatterHash !== liveEntry.frontmatter_hash;
|
|
32178
|
+
let workingState = args.state;
|
|
32179
|
+
if (fm["id"] === undefined && workingState.frontmatter_id_backfill_version !== undefined) {
|
|
32180
|
+
const cleared = clearFrontmatterIdBackfillMarker(workingState);
|
|
32181
|
+
if (cleared.changed) {
|
|
32182
|
+
try {
|
|
32183
|
+
workingState = commitState(workingState, cleared.state);
|
|
32184
|
+
} catch {}
|
|
32185
|
+
}
|
|
32186
|
+
}
|
|
31607
32187
|
if (!bodyChanged && !frontmatterChanged) {
|
|
31608
32188
|
return {
|
|
31609
32189
|
outcome: {
|
|
@@ -31611,7 +32191,7 @@ async function runPushUpsert(args) {
|
|
|
31611
32191
|
entityType: strategy.type,
|
|
31612
32192
|
entitySlug: liveEntry.slug
|
|
31613
32193
|
},
|
|
31614
|
-
state:
|
|
32194
|
+
state: workingState
|
|
31615
32195
|
};
|
|
31616
32196
|
}
|
|
31617
32197
|
const wireBodyFields = bodyChanged ? strategy.bodyToWire(bodyMarkdown) : undefined;
|
|
@@ -31630,7 +32210,7 @@ async function runPushUpsert(args) {
|
|
|
31630
32210
|
entityType: strategy.type,
|
|
31631
32211
|
entitySlug: liveEntry.slug
|
|
31632
32212
|
},
|
|
31633
|
-
state: persistEntry(
|
|
32213
|
+
state: persistEntry(workingState, strategy.type, updated)
|
|
31634
32214
|
};
|
|
31635
32215
|
}
|
|
31636
32216
|
const mergedTopLevel = split || wireBodyFields?.topLevel ? {
|
|
@@ -31649,7 +32229,7 @@ async function runPushUpsert(args) {
|
|
|
31649
32229
|
inode: safeInode2(args.absolutePath),
|
|
31650
32230
|
body_len: hashBody.length
|
|
31651
32231
|
};
|
|
31652
|
-
const nextState2 = persistEntry(
|
|
32232
|
+
const nextState2 = persistEntry(workingState, strategy.type, parked);
|
|
31653
32233
|
const fields = [...quarantined];
|
|
31654
32234
|
args.output?.log(syncErrorLine({
|
|
31655
32235
|
entityType: strategy.type,
|
|
@@ -31720,7 +32300,7 @@ async function runPushUpsert(args) {
|
|
|
31720
32300
|
body_len: hashBody.length,
|
|
31721
32301
|
memory_hash: liveEntry.memory_hash
|
|
31722
32302
|
};
|
|
31723
|
-
const nextState2 = persistEntry(
|
|
32303
|
+
const nextState2 = persistEntry(workingState, strategy.type, updated);
|
|
31724
32304
|
args.output?.log(`[PATCH entityType=${strategy.type} slug=${updated.slug} id=${updated.id} revision=${updated.revision}]`);
|
|
31725
32305
|
if (quarantined.size > 0) {
|
|
31726
32306
|
const fields = [...quarantined];
|
|
@@ -31758,7 +32338,7 @@ async function runPushUpsert(args) {
|
|
|
31758
32338
|
entityType: strategy.type,
|
|
31759
32339
|
entitySlug: liveEntry.slug
|
|
31760
32340
|
},
|
|
31761
|
-
state:
|
|
32341
|
+
state: workingState
|
|
31762
32342
|
};
|
|
31763
32343
|
}
|
|
31764
32344
|
if (patchResult.reason === "slug_conflict") {
|
|
@@ -31776,7 +32356,7 @@ async function runPushUpsert(args) {
|
|
|
31776
32356
|
existing: patchResult.existing,
|
|
31777
32357
|
scope: patchResult.scope,
|
|
31778
32358
|
staleEntryId: liveEntry.id,
|
|
31779
|
-
state:
|
|
32359
|
+
state: workingState,
|
|
31780
32360
|
fetchImpl: args.fetchImpl,
|
|
31781
32361
|
now: args.now,
|
|
31782
32362
|
output: args.output
|
|
@@ -31793,7 +32373,7 @@ async function runPushUpsert(args) {
|
|
|
31793
32373
|
parsed: args.parsed,
|
|
31794
32374
|
liveEntry,
|
|
31795
32375
|
currentUpdatedAt: patchResult.currentUpdatedAt,
|
|
31796
|
-
state:
|
|
32376
|
+
state: workingState,
|
|
31797
32377
|
fetchImpl: args.fetchImpl,
|
|
31798
32378
|
now: args.now,
|
|
31799
32379
|
output: args.output
|
|
@@ -31812,7 +32392,7 @@ async function runPushUpsert(args) {
|
|
|
31812
32392
|
baseUrl: args.baseUrl,
|
|
31813
32393
|
pat: args.pat,
|
|
31814
32394
|
id: liveEntry.id,
|
|
31815
|
-
state:
|
|
32395
|
+
state: workingState,
|
|
31816
32396
|
fetchImpl: args.fetchImpl
|
|
31817
32397
|
});
|
|
31818
32398
|
return {
|
|
@@ -31928,6 +32508,24 @@ async function reconcileTaskConflict(args) {
|
|
|
31928
32508
|
state
|
|
31929
32509
|
};
|
|
31930
32510
|
}
|
|
32511
|
+
function writeCreatedIdBack(args) {
|
|
32512
|
+
const write = withFrontmatterId(args.raw, args.parsed, args.id);
|
|
32513
|
+
if (write === null)
|
|
32514
|
+
return;
|
|
32515
|
+
if (!frontmatterIdWriteIsSound(write.content, args.id, args.parsed.content)) {
|
|
32516
|
+
return;
|
|
32517
|
+
}
|
|
32518
|
+
try {
|
|
32519
|
+
if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
|
|
32520
|
+
return;
|
|
32521
|
+
}
|
|
32522
|
+
const readBack = args.readBack ?? ((p) => readFileSync16(p, "utf8"));
|
|
32523
|
+
if (readBack(args.absolutePath) !== args.raw)
|
|
32524
|
+
return;
|
|
32525
|
+
args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
|
|
32526
|
+
writeMirrorFile(args.workspaceSlug, args.strategy.type, args.slug, write.content);
|
|
32527
|
+
} catch {}
|
|
32528
|
+
}
|
|
31931
32529
|
async function runPushCreate(args) {
|
|
31932
32530
|
const strategy = args.strategy;
|
|
31933
32531
|
const key = `create:${strategy.type}:${args.slug}`;
|
|
@@ -31978,6 +32576,17 @@ async function runPushCreate(args) {
|
|
|
31978
32576
|
const createLifted = splitUserFrontmatter(strategy, createFm);
|
|
31979
32577
|
const userFm = userFrontmatter(strategy, createLifted.frontmatter);
|
|
31980
32578
|
const createHashBody = hashableBody(strategy, args.parsed.content);
|
|
32579
|
+
writeCreatedIdBack({
|
|
32580
|
+
strategy,
|
|
32581
|
+
workspaceSlug: args.workspaceSlug,
|
|
32582
|
+
absolutePath: args.absolutePath,
|
|
32583
|
+
raw: args.raw,
|
|
32584
|
+
parsed: args.parsed,
|
|
32585
|
+
slug: doc2.slug,
|
|
32586
|
+
id: doc2.id,
|
|
32587
|
+
suppressor: args.suppressor,
|
|
32588
|
+
readBack: args.writeBackReadBack
|
|
32589
|
+
});
|
|
31981
32590
|
const nextState2 = persistEntry(args.state, strategy.type, {
|
|
31982
32591
|
id: doc2.id,
|
|
31983
32592
|
slug: doc2.slug,
|
|
@@ -32225,8 +32834,8 @@ async function pushRename(args) {
|
|
|
32225
32834
|
};
|
|
32226
32835
|
if (!updated.body_hash || !updated.frontmatter_hash) {
|
|
32227
32836
|
try {
|
|
32228
|
-
const mirrorRaw =
|
|
32229
|
-
const mirrorParsed =
|
|
32837
|
+
const mirrorRaw = readFileSync16(toPath, "utf8");
|
|
32838
|
+
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
32230
32839
|
const renameFm = mirrorParsed.data ?? {};
|
|
32231
32840
|
const renameLifted = splitUserFrontmatter(strategy, renameFm);
|
|
32232
32841
|
const renameUserFm = userFrontmatter(strategy, renameLifted.frontmatter);
|
|
@@ -32260,7 +32869,7 @@ async function pushRename(args) {
|
|
|
32260
32869
|
if (patchResult.reason === "slug_conflict") {
|
|
32261
32870
|
let contents2 = "";
|
|
32262
32871
|
try {
|
|
32263
|
-
contents2 =
|
|
32872
|
+
contents2 = readFileSync16(toPath, "utf8");
|
|
32264
32873
|
} catch {}
|
|
32265
32874
|
const stashPath2 = stashConflict({
|
|
32266
32875
|
workspaceSlug,
|
|
@@ -32300,7 +32909,7 @@ async function pushRename(args) {
|
|
|
32300
32909
|
}
|
|
32301
32910
|
let contents = "";
|
|
32302
32911
|
try {
|
|
32303
|
-
contents =
|
|
32912
|
+
contents = readFileSync16(toPath, "utf8");
|
|
32304
32913
|
} catch {}
|
|
32305
32914
|
const stashPath = stashConflict({
|
|
32306
32915
|
workspaceSlug,
|
|
@@ -32421,7 +33030,7 @@ async function discardBatchOps(batch, deps) {
|
|
|
32421
33030
|
const path5 = opPath(deps.workspaceSlug, op);
|
|
32422
33031
|
let local = null;
|
|
32423
33032
|
try {
|
|
32424
|
-
local =
|
|
33033
|
+
local = readFileSync17(path5, "utf8");
|
|
32425
33034
|
} catch {
|
|
32426
33035
|
local = null;
|
|
32427
33036
|
}
|
|
@@ -32455,7 +33064,7 @@ async function discardFileOp(op, deps) {
|
|
|
32455
33064
|
const abs = filesSubtreePath(deps.workspaceSlug, op.path);
|
|
32456
33065
|
let local = null;
|
|
32457
33066
|
try {
|
|
32458
|
-
local =
|
|
33067
|
+
local = readFileSync17(abs);
|
|
32459
33068
|
} catch {
|
|
32460
33069
|
local = null;
|
|
32461
33070
|
}
|
|
@@ -32643,7 +33252,7 @@ class FileDestructiveGate {
|
|
|
32643
33252
|
}
|
|
32644
33253
|
|
|
32645
33254
|
// src/lib/files/reconcile.ts
|
|
32646
|
-
import { readFileSync as
|
|
33255
|
+
import { readFileSync as readFileSync18, statSync as statSync10 } from "node:fs";
|
|
32647
33256
|
|
|
32648
33257
|
// src/lib/unsynced.ts
|
|
32649
33258
|
import { join as join12, relative as relative6 } from "node:path";
|
|
@@ -34669,7 +35278,7 @@ function readBlobForPush(args) {
|
|
|
34669
35278
|
}
|
|
34670
35279
|
let bytes;
|
|
34671
35280
|
try {
|
|
34672
|
-
bytes =
|
|
35281
|
+
bytes = readFileSync18(absolutePath);
|
|
34673
35282
|
} catch (err) {
|
|
34674
35283
|
if (err?.code === "ENOENT")
|
|
34675
35284
|
return { kind: "gone" };
|
|
@@ -35530,7 +36139,7 @@ var FULL_BAND_DAYS = 14;
|
|
|
35530
36139
|
function yamlString(value) {
|
|
35531
36140
|
return JSON.stringify(value);
|
|
35532
36141
|
}
|
|
35533
|
-
function
|
|
36142
|
+
function oneLine2(value, maxLength = 200) {
|
|
35534
36143
|
const flattened = value.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/<!--/g, "<! --").replace(/\s+/g, " ").trim();
|
|
35535
36144
|
if (flattened.length === 0)
|
|
35536
36145
|
return "(untitled)";
|
|
@@ -35579,7 +36188,7 @@ function sortInstant(event) {
|
|
|
35579
36188
|
function describeWhen(event, referenceYear) {
|
|
35580
36189
|
const start = dateParts(event.start);
|
|
35581
36190
|
if (!start)
|
|
35582
|
-
return
|
|
36191
|
+
return oneLine2(event.start, 60);
|
|
35583
36192
|
if (event.all_day) {
|
|
35584
36193
|
const end2 = dateParts(event.end);
|
|
35585
36194
|
const last = end2 ? shiftDays(end2, -1) : start;
|
|
@@ -35605,19 +36214,19 @@ function fullLine(event, referenceYear, artistPrefix, includeUrl) {
|
|
|
35605
36214
|
const parts = [
|
|
35606
36215
|
`**${describeWhen(event, referenceYear)}**`,
|
|
35607
36216
|
artistPrefix,
|
|
35608
|
-
`${
|
|
36217
|
+
`${oneLine2(event.title)} (${event.booking_status})`
|
|
35609
36218
|
].filter((p) => p.length > 0);
|
|
35610
36219
|
if (event.location)
|
|
35611
|
-
parts.push(
|
|
36220
|
+
parts.push(oneLine2(event.location, 80));
|
|
35612
36221
|
if (includeUrl && event.url)
|
|
35613
|
-
parts.push(
|
|
36222
|
+
parts.push(oneLine2(event.url, 300));
|
|
35614
36223
|
return `- ${parts.join(" - ")}`;
|
|
35615
36224
|
}
|
|
35616
36225
|
function compactLine(event, referenceYear, artistPrefix) {
|
|
35617
36226
|
const parts = [
|
|
35618
36227
|
describeWhen(event, referenceYear),
|
|
35619
36228
|
artistPrefix,
|
|
35620
|
-
`${
|
|
36229
|
+
`${oneLine2(event.title)} (${event.booking_status})`
|
|
35621
36230
|
].filter((p) => p.length > 0);
|
|
35622
36231
|
return `- ${parts.join(" - ")}`;
|
|
35623
36232
|
}
|
|
@@ -35653,7 +36262,7 @@ function statusLine(args) {
|
|
|
35653
36262
|
case "token-expired":
|
|
35654
36263
|
return `**NOT READ - the Google connection for ${userLabel} has expired.** Plinth ` + `still holds a connection but could not refresh it, so nothing below is ` + `current. Reconnect Google in Plinth. Treat this calendar as unknown ` + `rather than empty.`;
|
|
35655
36264
|
case "google-error":
|
|
35656
|
-
return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ?
|
|
36265
|
+
return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ? oneLine2(error52.message, 300) : "No detail was returned."} ` + `A calendar that was unshared or deleted in Google reads exactly like this. ` + `Nothing below is current; treat this calendar as unknown rather than empty.`;
|
|
35657
36266
|
}
|
|
35658
36267
|
}
|
|
35659
36268
|
function shortStatus(row) {
|
|
@@ -35669,7 +36278,7 @@ function shortStatus(row) {
|
|
|
35669
36278
|
case "token-expired":
|
|
35670
36279
|
return "NOT READ - the Google connection has expired; unknown, not empty";
|
|
35671
36280
|
case "google-error":
|
|
35672
|
-
return `NOT READ - Google refused` + `${row.error ? ` (HTTP ${row.error.upstream_status}): ${
|
|
36281
|
+
return `NOT READ - Google refused` + `${row.error ? ` (HTTP ${row.error.upstream_status}): ${oneLine2(row.error.message, 120)}` : ""}` + `; unknown, not empty`;
|
|
35673
36282
|
}
|
|
35674
36283
|
}
|
|
35675
36284
|
function rowFileName(row, internalName) {
|
|
@@ -35677,8 +36286,8 @@ function rowFileName(row, internalName) {
|
|
|
35677
36286
|
}
|
|
35678
36287
|
function rowDisplayName(row) {
|
|
35679
36288
|
if (row.artist_name)
|
|
35680
|
-
return
|
|
35681
|
-
return `${
|
|
36289
|
+
return oneLine2(row.artist_name, 80);
|
|
36290
|
+
return `${oneLine2(row.calendar_name, 80)} (internal)`;
|
|
35682
36291
|
}
|
|
35683
36292
|
function splitBands(events, generatedAt) {
|
|
35684
36293
|
const base2 = Date.parse(generatedAt);
|
|
@@ -35816,8 +36425,8 @@ function renderUpcoming(args) {
|
|
|
35816
36425
|
"",
|
|
35817
36426
|
"## Calendar status",
|
|
35818
36427
|
"",
|
|
35819
|
-
...rows.map((row) => `- **${rowDisplayName(row)}**` + `${row.artist_slug ? ` (\`${
|
|
35820
|
-
...unmappedArtists.map((a) => `- **${
|
|
36428
|
+
...rows.map((row) => `- **${rowDisplayName(row)}**` + `${row.artist_slug ? ` (\`${oneLine2(row.artist_slug, 80)}\`)` : ""}` + ` - ${shortStatus(row)} - \`views/calendar/${rowFileName(row, internalName)}.md\``),
|
|
36429
|
+
...unmappedArtists.map((a) => `- **${oneLine2(a.name, 80)}** (\`${oneLine2(a.slug, 80)}\`) - NO CALENDAR MAPPED in Plinth, so there is nothing to read and no file for them. Map one in Plinth if they should have dates.`),
|
|
35821
36430
|
""
|
|
35822
36431
|
].join(`
|
|
35823
36432
|
`);
|
|
@@ -35886,14 +36495,14 @@ function renderStaleFile(existing, args) {
|
|
|
35886
36495
|
...carried,
|
|
35887
36496
|
"fetch_state: stale",
|
|
35888
36497
|
`last_refresh_attempt: ${yamlString(args.attemptedAt)}`,
|
|
35889
|
-
`last_refresh_error: ${yamlString(
|
|
36498
|
+
`last_refresh_error: ${yamlString(oneLine2(args.reason, 300))}`,
|
|
35890
36499
|
"---",
|
|
35891
36500
|
"",
|
|
35892
36501
|
OWNERSHIP_NOTE2,
|
|
35893
36502
|
"",
|
|
35894
36503
|
`# ${title}`,
|
|
35895
36504
|
"",
|
|
35896
|
-
`**STALE - the last refresh FAILED and nothing below was updated.** Attempted ` + `${humanStamp(args.attemptedAt)} and it did not succeed: ` + `${
|
|
36505
|
+
`**STALE - the last refresh FAILED and nothing below was updated.** Attempted ` + `${humanStamp(args.attemptedAt)} and it did not succeed: ` + `${oneLine2(args.reason, 300)}`,
|
|
35897
36506
|
"",
|
|
35898
36507
|
`**What is below was last read at ${stamp}** and has not moved since. Anything ` + `changed in Google after that is not here, and something cancelled after that ` + `is still listed. Say the age out loud rather than briefing it as current.`,
|
|
35899
36508
|
""
|
|
@@ -36075,8 +36684,8 @@ class PullSuppressor {
|
|
|
36075
36684
|
}
|
|
36076
36685
|
|
|
36077
36686
|
// src/lib/discover-entities.ts
|
|
36078
|
-
var
|
|
36079
|
-
import { readFileSync as
|
|
36687
|
+
var import_gray_matter11 = __toESM(require_gray_matter(), 1);
|
|
36688
|
+
import { readFileSync as readFileSync19, statSync as statSync11 } from "node:fs";
|
|
36080
36689
|
var DISCOVERY_WRITE_CAP_DEFAULT = 10;
|
|
36081
36690
|
function discoveryWriteCap() {
|
|
36082
36691
|
const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
|
|
@@ -36175,7 +36784,7 @@ function planDiscoveryWrites(args) {
|
|
|
36175
36784
|
counts.discovered++;
|
|
36176
36785
|
let frontmatter;
|
|
36177
36786
|
try {
|
|
36178
|
-
frontmatter =
|
|
36787
|
+
frontmatter = import_gray_matter11.default(readFileSync19(absolutePath, "utf8")).data ?? {};
|
|
36179
36788
|
} catch {
|
|
36180
36789
|
counts.discovered--;
|
|
36181
36790
|
continue;
|
|
@@ -36403,21 +37012,23 @@ function recordDiscoveryFailure(args) {
|
|
|
36403
37012
|
}
|
|
36404
37013
|
|
|
36405
37014
|
// src/lib/frontmatter-id-backfill.ts
|
|
36406
|
-
var
|
|
36407
|
-
import { readFileSync as
|
|
37015
|
+
var import_gray_matter12 = __toESM(require_gray_matter(), 1);
|
|
37016
|
+
import { readFileSync as readFileSync20, statSync as statSync12 } from "node:fs";
|
|
37017
|
+
var FRONTMATTER_ID_BACKFILL_VERSION = 1;
|
|
36408
37018
|
function backfillWorthReporting(r) {
|
|
36409
|
-
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0;
|
|
37019
|
+
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
|
|
36410
37020
|
}
|
|
36411
37021
|
function emptyResult() {
|
|
36412
37022
|
return {
|
|
36413
37023
|
scanned: 0,
|
|
36414
37024
|
written: 0,
|
|
37025
|
+
blocksCreated: 0,
|
|
36415
37026
|
alreadyPresent: 0,
|
|
36416
37027
|
missing: 0,
|
|
36417
37028
|
skipped: {
|
|
36418
37029
|
frozen: 0,
|
|
36419
37030
|
foreignId: 0,
|
|
36420
|
-
|
|
37031
|
+
unwritable: 0,
|
|
36421
37032
|
slugCollision: 0,
|
|
36422
37033
|
diverged: 0
|
|
36423
37034
|
},
|
|
@@ -36442,21 +37053,14 @@ function safeInode3(path5) {
|
|
|
36442
37053
|
return;
|
|
36443
37054
|
}
|
|
36444
37055
|
}
|
|
36445
|
-
function spliceFrontmatterId(content, id) {
|
|
36446
|
-
const eol = /^---\r\n/.test(content) ? `\r
|
|
36447
|
-
` : `
|
|
36448
|
-
`;
|
|
36449
|
-
if (!content.startsWith(`---${eol}`))
|
|
36450
|
-
return null;
|
|
36451
|
-
const closeAt = content.indexOf(`${eol}---`, 3);
|
|
36452
|
-
if (closeAt === -1)
|
|
36453
|
-
return null;
|
|
36454
|
-
return `${content.slice(0, closeAt)}${eol}id: ${id}${content.slice(closeAt)}`;
|
|
36455
|
-
}
|
|
36456
37056
|
function backfillFrontmatterIds(args) {
|
|
36457
37057
|
const { workspaceSlug, state } = args;
|
|
36458
|
-
const
|
|
37058
|
+
const compose = args.compose ?? withFrontmatterId;
|
|
37059
|
+
const readRaw = args.readFile ?? ((p) => readFileSync20(p, "utf8"));
|
|
36459
37060
|
const result = emptyResult();
|
|
37061
|
+
if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
|
|
37062
|
+
return result;
|
|
37063
|
+
}
|
|
36460
37064
|
let held;
|
|
36461
37065
|
try {
|
|
36462
37066
|
held = heldByAnyBatch(workspaceSlug);
|
|
@@ -36465,10 +37069,13 @@ function backfillFrontmatterIds(args) {
|
|
|
36465
37069
|
}
|
|
36466
37070
|
let nextState = state;
|
|
36467
37071
|
let stateChanged = false;
|
|
37072
|
+
let sawUnknownType = false;
|
|
36468
37073
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
36469
37074
|
const strategy = getStrategy(entityType);
|
|
36470
|
-
if (!strategy)
|
|
37075
|
+
if (!strategy) {
|
|
37076
|
+
sawUnknownType = true;
|
|
36471
37077
|
continue;
|
|
37078
|
+
}
|
|
36472
37079
|
const slugCounts = new Map;
|
|
36473
37080
|
for (const e of Object.values(bucket)) {
|
|
36474
37081
|
slugCounts.set(e.slug, (slugCounts.get(e.slug) ?? 0) + 1);
|
|
@@ -36485,7 +37092,7 @@ function backfillFrontmatterIds(args) {
|
|
|
36485
37092
|
}
|
|
36486
37093
|
let raw;
|
|
36487
37094
|
try {
|
|
36488
|
-
raw =
|
|
37095
|
+
raw = readRaw(entityFilePath(workspaceSlug, entityType, entry2.slug));
|
|
36489
37096
|
} catch (err) {
|
|
36490
37097
|
if (err?.code === "ENOENT") {
|
|
36491
37098
|
result.missing++;
|
|
@@ -36496,9 +37103,11 @@ function backfillFrontmatterIds(args) {
|
|
|
36496
37103
|
}
|
|
36497
37104
|
let existing;
|
|
36498
37105
|
let rawBody;
|
|
37106
|
+
let parsedData;
|
|
36499
37107
|
try {
|
|
36500
|
-
const parsed =
|
|
36501
|
-
|
|
37108
|
+
const parsed = import_gray_matter12.default(raw);
|
|
37109
|
+
parsedData = parsed.data ?? {};
|
|
37110
|
+
existing = parsedData["id"];
|
|
36502
37111
|
rawBody = parsed.content;
|
|
36503
37112
|
} catch {
|
|
36504
37113
|
result.failed++;
|
|
@@ -36516,30 +37125,27 @@ function backfillFrontmatterIds(args) {
|
|
|
36516
37125
|
result.skipped.diverged++;
|
|
36517
37126
|
continue;
|
|
36518
37127
|
}
|
|
36519
|
-
const
|
|
36520
|
-
if (
|
|
36521
|
-
result.skipped.
|
|
37128
|
+
const write = compose(raw, { data: parsedData, content: rawBody }, id);
|
|
37129
|
+
if (write === null) {
|
|
37130
|
+
result.skipped.unwritable++;
|
|
36522
37131
|
continue;
|
|
36523
37132
|
}
|
|
36524
|
-
|
|
36525
|
-
|
|
36526
|
-
if (check2.data?.["id"] !== id || check2.content !== rawBody) {
|
|
36527
|
-
result.failed++;
|
|
36528
|
-
continue;
|
|
36529
|
-
}
|
|
36530
|
-
} catch {
|
|
37133
|
+
const next = write.content;
|
|
37134
|
+
if (!frontmatterIdWriteIsSound(next, id, rawBody)) {
|
|
36531
37135
|
result.failed++;
|
|
36532
37136
|
continue;
|
|
36533
37137
|
}
|
|
36534
37138
|
let ino;
|
|
36535
37139
|
try {
|
|
36536
|
-
const stillRaw =
|
|
37140
|
+
const stillRaw = readRaw(entityFilePath(workspaceSlug, entityType, entry2.slug));
|
|
36537
37141
|
if (stillRaw !== raw) {
|
|
36538
37142
|
result.skipped.diverged++;
|
|
36539
37143
|
continue;
|
|
36540
37144
|
}
|
|
36541
37145
|
writeMirrorFile(workspaceSlug, entityType, entry2.slug, next);
|
|
36542
37146
|
result.written++;
|
|
37147
|
+
if (write.blockCreated)
|
|
37148
|
+
result.blocksCreated++;
|
|
36543
37149
|
ino = safeInode3(entityFilePath(workspaceSlug, entityType, entry2.slug));
|
|
36544
37150
|
} catch {
|
|
36545
37151
|
result.failed++;
|
|
@@ -36555,6 +37161,17 @@ function backfillFrontmatterIds(args) {
|
|
|
36555
37161
|
}
|
|
36556
37162
|
}
|
|
36557
37163
|
}
|
|
37164
|
+
const retryable = result.written + result.failed + result.skipped.frozen + result.skipped.diverged + result.skipped.unwritable + result.skipped.slugCollision;
|
|
37165
|
+
if (retryable === 0 && !sawUnknownType) {
|
|
37166
|
+
const stamped = {
|
|
37167
|
+
...nextState,
|
|
37168
|
+
frontmatter_id_backfill_version: FRONTMATTER_ID_BACKFILL_VERSION
|
|
37169
|
+
};
|
|
37170
|
+
if (stamped.frontmatter_id_backfill_version !== nextState.frontmatter_id_backfill_version) {
|
|
37171
|
+
nextState = stamped;
|
|
37172
|
+
stateChanged = true;
|
|
37173
|
+
}
|
|
37174
|
+
}
|
|
36558
37175
|
if (stateChanged) {
|
|
36559
37176
|
try {
|
|
36560
37177
|
commitState(state, nextState);
|
|
@@ -36567,7 +37184,7 @@ function backfillFrontmatterIds(args) {
|
|
|
36567
37184
|
}
|
|
36568
37185
|
|
|
36569
37186
|
// src/lib/reconcile.ts
|
|
36570
|
-
import { readFileSync as
|
|
37187
|
+
import { readFileSync as readFileSync21, statSync as statSync13 } from "node:fs";
|
|
36571
37188
|
async function reconcileDivergedRows(args) {
|
|
36572
37189
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
36573
37190
|
const now = args.now ?? (() => new Date);
|
|
@@ -36609,6 +37226,14 @@ async function reconcileDivergedRows(args) {
|
|
|
36609
37226
|
case "noop":
|
|
36610
37227
|
noop++;
|
|
36611
37228
|
break;
|
|
37229
|
+
case "not_found":
|
|
37230
|
+
state = recordEntityCloudRowDeleted({
|
|
37231
|
+
state,
|
|
37232
|
+
workspaceSlug,
|
|
37233
|
+
absolutePath,
|
|
37234
|
+
now
|
|
37235
|
+
});
|
|
37236
|
+
break;
|
|
36612
37237
|
default:
|
|
36613
37238
|
break;
|
|
36614
37239
|
}
|
|
@@ -36635,6 +37260,7 @@ async function reconcileDivergedRows(args) {
|
|
|
36635
37260
|
}
|
|
36636
37261
|
};
|
|
36637
37262
|
const guttingCandidates = [];
|
|
37263
|
+
let sawTrackedFileWithoutId = false;
|
|
36638
37264
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
36639
37265
|
const strategy = getStrategy(entityType);
|
|
36640
37266
|
if (!strategy)
|
|
@@ -36651,15 +37277,32 @@ async function reconcileDivergedRows(args) {
|
|
|
36651
37277
|
continue;
|
|
36652
37278
|
throw err;
|
|
36653
37279
|
}
|
|
37280
|
+
let liveInode;
|
|
37281
|
+
try {
|
|
37282
|
+
liveInode = statSync13(absolutePath).ino;
|
|
37283
|
+
} catch {
|
|
37284
|
+
liveInode = undefined;
|
|
37285
|
+
}
|
|
37286
|
+
if (liveInode !== undefined && entry2.inode !== liveInode) {
|
|
37287
|
+
const trueOwner = getEntryByInode(state, entityType, liveInode);
|
|
37288
|
+
if (trueOwner) {
|
|
37289
|
+
output?.log(`[RECONCILE_SKIP entityType=${entityType} slug=${entry2.slug} ` + `id=${entry2.id} owner_id=${trueOwner.id} reason=path_owned_by_other_row]`);
|
|
37290
|
+
continue;
|
|
37291
|
+
}
|
|
37292
|
+
}
|
|
36654
37293
|
let content;
|
|
36655
37294
|
try {
|
|
36656
|
-
content =
|
|
37295
|
+
content = readFileSync21(absolutePath, "utf8");
|
|
36657
37296
|
} catch (err) {
|
|
36658
|
-
if (err?.code === "ENOENT")
|
|
37297
|
+
if (err?.code === "ENOENT") {
|
|
37298
|
+
state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
|
|
36659
37299
|
continue;
|
|
37300
|
+
}
|
|
36660
37301
|
throw err;
|
|
36661
37302
|
}
|
|
36662
37303
|
const hashes = computeMirrorHashes(strategy, content);
|
|
37304
|
+
if (hashes.frontmatterId === undefined)
|
|
37305
|
+
sawTrackedFileWithoutId = true;
|
|
36663
37306
|
const isDiverged = hashes.bodyHash !== entry2.body_hash || hashes.frontmatterHash !== entry2.frontmatter_hash;
|
|
36664
37307
|
if (!isDiverged)
|
|
36665
37308
|
continue;
|
|
@@ -36726,6 +37369,14 @@ async function reconcileDivergedRows(args) {
|
|
|
36726
37369
|
isFrozen
|
|
36727
37370
|
});
|
|
36728
37371
|
state = discovery.state;
|
|
37372
|
+
if (sawTrackedFileWithoutId) {
|
|
37373
|
+
const cleared = clearFrontmatterIdBackfillMarker(state);
|
|
37374
|
+
if (cleared.changed) {
|
|
37375
|
+
try {
|
|
37376
|
+
state = commitState(state, cleared.state);
|
|
37377
|
+
} catch {}
|
|
37378
|
+
}
|
|
37379
|
+
}
|
|
36729
37380
|
return {
|
|
36730
37381
|
state,
|
|
36731
37382
|
scanned,
|
|
@@ -37734,9 +38385,9 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
37734
38385
|
import {
|
|
37735
38386
|
existsSync as existsSync7,
|
|
37736
38387
|
readdirSync as readdirSync6,
|
|
37737
|
-
readFileSync as
|
|
37738
|
-
statSync as
|
|
37739
|
-
unlinkSync as
|
|
38388
|
+
readFileSync as readFileSync23,
|
|
38389
|
+
statSync as statSync15,
|
|
38390
|
+
unlinkSync as unlinkSync8
|
|
37740
38391
|
} from "node:fs";
|
|
37741
38392
|
import { join as join16 } from "node:path";
|
|
37742
38393
|
|
|
@@ -37861,7 +38512,7 @@ function refreshConflictsView(workspaceSlug) {
|
|
|
37861
38512
|
}
|
|
37862
38513
|
|
|
37863
38514
|
// src/lib/files/pull.ts
|
|
37864
|
-
import { existsSync as existsSync6, readFileSync as
|
|
38515
|
+
import { existsSync as existsSync6, readFileSync as readFileSync22, statSync as statSync14 } from "node:fs";
|
|
37865
38516
|
|
|
37866
38517
|
// src/lib/files/extracted-text.ts
|
|
37867
38518
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -38132,7 +38783,7 @@ async function applyFileRow(args) {
|
|
|
38132
38783
|
}
|
|
38133
38784
|
let localBytes;
|
|
38134
38785
|
try {
|
|
38135
|
-
localBytes =
|
|
38786
|
+
localBytes = readFileSync22(abs);
|
|
38136
38787
|
} catch {
|
|
38137
38788
|
localBytes = undefined;
|
|
38138
38789
|
}
|
|
@@ -38229,7 +38880,7 @@ async function applyFileRow(args) {
|
|
|
38229
38880
|
}
|
|
38230
38881
|
if (!suppressor && existing && existing.content_hash !== row.content_hash && existsSync6(abs)) {
|
|
38231
38882
|
try {
|
|
38232
|
-
const localBytes =
|
|
38883
|
+
const localBytes = readFileSync22(abs);
|
|
38233
38884
|
if (sha256Bytes(localBytes) !== existing.content_hash) {
|
|
38234
38885
|
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes });
|
|
38235
38886
|
output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
@@ -38270,7 +38921,7 @@ async function applyFileRow(args) {
|
|
|
38270
38921
|
}
|
|
38271
38922
|
let inode;
|
|
38272
38923
|
try {
|
|
38273
|
-
inode =
|
|
38924
|
+
inode = statSync14(abs).ino;
|
|
38274
38925
|
} catch {
|
|
38275
38926
|
inode = undefined;
|
|
38276
38927
|
}
|
|
@@ -38339,6 +38990,8 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
38339
38990
|
|
|
38340
38991
|
// src/commands/sync.ts
|
|
38341
38992
|
init_output();
|
|
38993
|
+
var memberRefreshFailedAt = new Map;
|
|
38994
|
+
var MEMBER_REFRESH_RETRY_AFTER_MS = 60 * 60 * 1000;
|
|
38342
38995
|
function classifyApiError(err) {
|
|
38343
38996
|
if (err.status === 401 || err.status === 403) {
|
|
38344
38997
|
return { kind: "auth", status: err.status };
|
|
@@ -38367,31 +39020,50 @@ function hasExistingContent(workspacePath) {
|
|
|
38367
39020
|
return false;
|
|
38368
39021
|
}
|
|
38369
39022
|
}
|
|
38370
|
-
function
|
|
39023
|
+
function otherEntryAtSlug(state, entityType, slug, exceptId) {
|
|
39024
|
+
const bucket = state.entries[entityType];
|
|
39025
|
+
if (!bucket)
|
|
39026
|
+
return;
|
|
39027
|
+
for (const candidate of Object.values(bucket)) {
|
|
39028
|
+
if (candidate.id !== exceptId && candidate.slug === slug)
|
|
39029
|
+
return candidate;
|
|
39030
|
+
}
|
|
39031
|
+
return;
|
|
39032
|
+
}
|
|
39033
|
+
function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
38371
39034
|
let target;
|
|
38372
39035
|
try {
|
|
38373
|
-
target = entityFilePath(workspaceSlug,
|
|
39036
|
+
target = entityFilePath(workspaceSlug, strategy.type, entry2.slug);
|
|
38374
39037
|
} catch (err) {
|
|
38375
39038
|
if (err instanceof MirrorPathError)
|
|
38376
39039
|
return { didUnlink: false };
|
|
38377
39040
|
throw err;
|
|
38378
39041
|
}
|
|
38379
|
-
let
|
|
39042
|
+
let raw;
|
|
39043
|
+
try {
|
|
39044
|
+
raw = readFileSync23(target, "utf8");
|
|
39045
|
+
} catch (err) {
|
|
39046
|
+
if (err?.code === "ENOENT") {
|
|
39047
|
+
return { didUnlink: false };
|
|
39048
|
+
}
|
|
39049
|
+
return { didUnlink: false, refusedReason: "unreadable" };
|
|
39050
|
+
}
|
|
39051
|
+
let hashes;
|
|
38380
39052
|
try {
|
|
38381
|
-
|
|
39053
|
+
hashes = computeMirrorHashes(strategy, raw);
|
|
38382
39054
|
} catch {
|
|
38383
|
-
return { didUnlink: false,
|
|
39055
|
+
return { didUnlink: false, refusedReason: "unreadable" };
|
|
38384
39056
|
}
|
|
38385
|
-
if (
|
|
38386
|
-
return { didUnlink: false,
|
|
39057
|
+
if (hashes.bodyHash !== entry2.body_hash || hashes.frontmatterHash !== entry2.frontmatter_hash) {
|
|
39058
|
+
return { didUnlink: false, refusedReason: "diverged" };
|
|
38387
39059
|
}
|
|
38388
39060
|
try {
|
|
38389
39061
|
suppressor?.registerDelete(target);
|
|
38390
|
-
|
|
39062
|
+
unlinkSync8(target);
|
|
38391
39063
|
return { didUnlink: true };
|
|
38392
39064
|
} catch (err) {
|
|
38393
39065
|
if (err?.code === "ENOENT") {
|
|
38394
|
-
return { didUnlink: false
|
|
39066
|
+
return { didUnlink: false };
|
|
38395
39067
|
}
|
|
38396
39068
|
throw err;
|
|
38397
39069
|
}
|
|
@@ -38475,7 +39147,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
38475
39147
|
ctr.fetched++;
|
|
38476
39148
|
let outcome;
|
|
38477
39149
|
try {
|
|
38478
|
-
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen);
|
|
39150
|
+
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
|
|
38479
39151
|
} catch (err) {
|
|
38480
39152
|
if (!(err instanceof MirrorSymlinkError))
|
|
38481
39153
|
throw err;
|
|
@@ -38633,6 +39305,18 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
38633
39305
|
out.error(`Sync state could not be persisted: ${msg}`);
|
|
38634
39306
|
return { exitCode: 1 };
|
|
38635
39307
|
}
|
|
39308
|
+
try {
|
|
39309
|
+
ensureFilesSubtree(slug);
|
|
39310
|
+
} catch (err) {
|
|
39311
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
39312
|
+
out.warn(`Could not create the files/ folder in this workspace mirror: ${msg}`);
|
|
39313
|
+
}
|
|
39314
|
+
try {
|
|
39315
|
+
ensureEntityFolders(slug);
|
|
39316
|
+
} catch (err) {
|
|
39317
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
39318
|
+
out.warn(`Could not scaffold the workspace entity folders: ${msg}`);
|
|
39319
|
+
}
|
|
38636
39320
|
const generateContext = deps.generateContext ?? ensureWorkspaceContext;
|
|
38637
39321
|
try {
|
|
38638
39322
|
generateContext({
|
|
@@ -38646,6 +39330,17 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
38646
39330
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
38647
39331
|
out.warn(`Context generation skipped: ${msg}`);
|
|
38648
39332
|
}
|
|
39333
|
+
const lastFailure = memberRefreshFailedAt.get(slug);
|
|
39334
|
+
const failureCooling = lastFailure !== undefined && now().getTime() - lastFailure < MEMBER_REFRESH_RETRY_AFTER_MS;
|
|
39335
|
+
if (!failureCooling && memberNeedsRefresh(entry2, now)) {
|
|
39336
|
+
try {
|
|
39337
|
+
const principal = await whoami(entry2.api_base_url, pat, fetchImpl);
|
|
39338
|
+
setMemberPrincipal(slug, memberPrincipalFrom(principal, now));
|
|
39339
|
+
memberRefreshFailedAt.delete(slug);
|
|
39340
|
+
} catch {
|
|
39341
|
+
memberRefreshFailedAt.set(slug, now().getTime());
|
|
39342
|
+
}
|
|
39343
|
+
}
|
|
38649
39344
|
const generatePerArtistContext = deps.generatePerArtistContext ?? ensurePerArtistContext;
|
|
38650
39345
|
try {
|
|
38651
39346
|
generatePerArtistContext({ workspaceSlug: slug, force: false, out, now });
|
|
@@ -38674,48 +39369,58 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
38674
39369
|
out.success(`Done in ${elapsed}s.`);
|
|
38675
39370
|
return { exitCode: 0, applied: totalWritten + totalDeleted };
|
|
38676
39371
|
}
|
|
38677
|
-
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen) {
|
|
39372
|
+
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips) {
|
|
38678
39373
|
if (isFrozen?.(strategy.type, row.id)) {
|
|
38679
39374
|
return { state, action: "skipped" };
|
|
38680
39375
|
}
|
|
38681
39376
|
if (row.deleted_at !== null) {
|
|
38682
39377
|
const bucket = state.entries[strategy.type];
|
|
38683
39378
|
const entry2 = bucket?.[row.id];
|
|
38684
|
-
|
|
38685
|
-
|
|
38686
|
-
|
|
38687
|
-
|
|
38688
|
-
|
|
38689
|
-
|
|
38690
|
-
|
|
38691
|
-
|
|
39379
|
+
const targetSlug = entry2?.slug ?? row.slug;
|
|
39380
|
+
const dropEntry2 = () => {
|
|
39381
|
+
if (!entry2)
|
|
39382
|
+
return state;
|
|
39383
|
+
const nextBucket = { ...bucket };
|
|
39384
|
+
delete nextBucket[row.id];
|
|
39385
|
+
const cleared = clearEntityCloudRowDeleted({ ...state, entries: { ...state.entries, [strategy.type]: nextBucket } }, strategy.mirrorRelativePath(entry2.slug));
|
|
39386
|
+
return cleared;
|
|
39387
|
+
};
|
|
39388
|
+
const logOnce = (line, key) => {
|
|
39389
|
+
if (loggedTombstoneSkips) {
|
|
39390
|
+
if (loggedTombstoneSkips.has(key))
|
|
39391
|
+
return;
|
|
39392
|
+
loggedTombstoneSkips.add(key);
|
|
38692
39393
|
}
|
|
38693
|
-
|
|
39394
|
+
out.log(line);
|
|
39395
|
+
};
|
|
39396
|
+
const owner = otherEntryAtSlug(state, strategy.type, targetSlug, row.id);
|
|
39397
|
+
if (owner) {
|
|
39398
|
+
logOnce(`[TOMBSTONE_SKIP entityType=${strategy.type} slug=${targetSlug} id=${row.id} owner_id=${owner.id} reason=slug_owned_by_other_row]`, `entity:${strategy.type}:${row.id}:${owner.id}:${targetSlug}`);
|
|
39399
|
+
const next = dropEntry2();
|
|
39400
|
+
return { state: next, action: "tombstone_noop" };
|
|
39401
|
+
}
|
|
39402
|
+
if (!entry2) {
|
|
39403
|
+
let present2 = false;
|
|
38694
39404
|
try {
|
|
38695
|
-
|
|
38696
|
-
suppressor?.registerDelete(target);
|
|
38697
|
-
didUnlink = deleteMirrorFile(workspaceSlug, strategy.type, row.slug);
|
|
39405
|
+
present2 = existsSync7(entityFilePath(workspaceSlug, strategy.type, row.slug));
|
|
38698
39406
|
} catch (err) {
|
|
38699
39407
|
if (!(err instanceof MirrorPathError))
|
|
38700
39408
|
throw err;
|
|
38701
39409
|
}
|
|
39410
|
+
if (present2) {
|
|
39411
|
+
logOnce(`[TOMBSTONE_KEEP entityType=${strategy.type} slug=${row.slug} id=${row.id} reason=untracked]`, `entity:${strategy.type}:${row.id}:untracked:${row.slug}`);
|
|
39412
|
+
}
|
|
39413
|
+
return { state, action: "tombstone_noop" };
|
|
38702
39414
|
}
|
|
38703
|
-
const
|
|
38704
|
-
if (
|
|
39415
|
+
const tombstone = unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor);
|
|
39416
|
+
if (tombstone.refusedReason) {
|
|
39417
|
+
logOnce(`[TOMBSTONE_KEEP entityType=${strategy.type} slug=${entry2.slug} id=${row.id} reason=${tombstone.refusedReason}]`, `entity:${strategy.type}:${row.id}:${tombstone.refusedReason}:${entry2.slug}`);
|
|
38705
39418
|
return { state, action: "tombstone_noop" };
|
|
38706
39419
|
}
|
|
38707
|
-
if (
|
|
38708
|
-
|
|
39420
|
+
if (tombstone.didUnlink) {
|
|
39421
|
+
out.log(`[TOMBSTONE_UNLINK entityType=${strategy.type} slug=${entry2.slug} id=${row.id}]`);
|
|
38709
39422
|
}
|
|
38710
|
-
|
|
38711
|
-
delete nextBucket[row.id];
|
|
38712
|
-
return {
|
|
38713
|
-
state: {
|
|
38714
|
-
...state,
|
|
38715
|
-
entries: { ...state.entries, [strategy.type]: nextBucket }
|
|
38716
|
-
},
|
|
38717
|
-
action: "deleted"
|
|
38718
|
-
};
|
|
39423
|
+
return { state: dropEntry2(), action: "deleted" };
|
|
38719
39424
|
}
|
|
38720
39425
|
const live = row;
|
|
38721
39426
|
const existingEntry = state.entries[strategy.type]?.[live.id];
|
|
@@ -38741,7 +39446,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
38741
39446
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
38742
39447
|
let localRaw = null;
|
|
38743
39448
|
try {
|
|
38744
|
-
localRaw =
|
|
39449
|
+
localRaw = readFileSync23(entityFilePath(workspaceSlug, strategy.type, live.slug), "utf8");
|
|
38745
39450
|
} catch {
|
|
38746
39451
|
localRaw = null;
|
|
38747
39452
|
}
|
|
@@ -38764,7 +39469,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
38764
39469
|
const hashes = computeMirrorHashes(strategy, content);
|
|
38765
39470
|
let inode;
|
|
38766
39471
|
try {
|
|
38767
|
-
inode =
|
|
39472
|
+
inode = statSync15(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
38768
39473
|
} catch {
|
|
38769
39474
|
inode = undefined;
|
|
38770
39475
|
}
|
|
@@ -38857,7 +39562,7 @@ var syncCommand = defineCommand({
|
|
|
38857
39562
|
// src/commands/start.ts
|
|
38858
39563
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
38859
39564
|
var PULL_HEARTBEAT_MS = 900000;
|
|
38860
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
39565
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync16(p)) {
|
|
38861
39566
|
let changed = false;
|
|
38862
39567
|
const nextEntries = {};
|
|
38863
39568
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -38890,7 +39595,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync15(p)
|
|
|
38890
39595
|
return { state, changed: false };
|
|
38891
39596
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
38892
39597
|
}
|
|
38893
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
39598
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync24(p, "utf8")) {
|
|
38894
39599
|
let changed = false;
|
|
38895
39600
|
const nextEntries = {};
|
|
38896
39601
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -38902,7 +39607,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync2
|
|
|
38902
39607
|
}
|
|
38903
39608
|
try {
|
|
38904
39609
|
const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
|
|
38905
|
-
const bodyLen =
|
|
39610
|
+
const bodyLen = import_gray_matter13.default(readFn(path5)).content.length;
|
|
38906
39611
|
nextBucket[id] = { ...entry2, body_len: bodyLen };
|
|
38907
39612
|
changed = true;
|
|
38908
39613
|
} catch {
|
|
@@ -38953,8 +39658,14 @@ function makeWatchHandler(deps) {
|
|
|
38953
39658
|
return;
|
|
38954
39659
|
}
|
|
38955
39660
|
if (!exists) {
|
|
38956
|
-
if (suppressor?.shouldSuppressDelete(absolutePath))
|
|
39661
|
+
if (suppressor?.shouldSuppressDelete(absolutePath)) {
|
|
39662
|
+
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "pull_echo" }));
|
|
39663
|
+
return;
|
|
39664
|
+
}
|
|
39665
|
+
if (existsSync8(absolutePath)) {
|
|
39666
|
+
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "path_exists" }));
|
|
38957
39667
|
return;
|
|
39668
|
+
}
|
|
38958
39669
|
if (fileGate) {
|
|
38959
39670
|
const op = classifyFileDestructive({ relPath, absolutePath, exists: false }, getState());
|
|
38960
39671
|
if (op) {
|
|
@@ -38978,7 +39689,7 @@ function makeWatchHandler(deps) {
|
|
|
38978
39689
|
}
|
|
38979
39690
|
let st;
|
|
38980
39691
|
try {
|
|
38981
|
-
st =
|
|
39692
|
+
st = statSync16(absolutePath);
|
|
38982
39693
|
} catch (err) {
|
|
38983
39694
|
if (err?.code === "ENOENT")
|
|
38984
39695
|
return;
|
|
@@ -39009,7 +39720,7 @@ function makeWatchHandler(deps) {
|
|
|
39009
39720
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
39010
39721
|
return;
|
|
39011
39722
|
}
|
|
39012
|
-
const bytes =
|
|
39723
|
+
const bytes = readFileSync24(absolutePath);
|
|
39013
39724
|
const hash2 = sha256Bytes(bytes);
|
|
39014
39725
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
39015
39726
|
bodyHash: hash2,
|
|
@@ -39067,7 +39778,7 @@ function makeWatchHandler(deps) {
|
|
|
39067
39778
|
if (suppressor) {
|
|
39068
39779
|
let raw = null;
|
|
39069
39780
|
try {
|
|
39070
|
-
raw =
|
|
39781
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39071
39782
|
} catch {
|
|
39072
39783
|
raw = null;
|
|
39073
39784
|
}
|
|
@@ -39105,7 +39816,7 @@ function makeWatchHandler(deps) {
|
|
|
39105
39816
|
if (suppressor) {
|
|
39106
39817
|
let raw = null;
|
|
39107
39818
|
try {
|
|
39108
|
-
raw =
|
|
39819
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39109
39820
|
} catch {
|
|
39110
39821
|
raw = null;
|
|
39111
39822
|
}
|
|
@@ -39143,7 +39854,7 @@ function makeWatchHandler(deps) {
|
|
|
39143
39854
|
if (suppressor) {
|
|
39144
39855
|
let raw = null;
|
|
39145
39856
|
try {
|
|
39146
|
-
raw =
|
|
39857
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39147
39858
|
} catch {
|
|
39148
39859
|
raw = null;
|
|
39149
39860
|
}
|
|
@@ -39181,7 +39892,7 @@ function makeWatchHandler(deps) {
|
|
|
39181
39892
|
if (suppressor) {
|
|
39182
39893
|
let raw = null;
|
|
39183
39894
|
try {
|
|
39184
|
-
raw =
|
|
39895
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39185
39896
|
} catch {
|
|
39186
39897
|
raw = null;
|
|
39187
39898
|
}
|
|
@@ -39219,7 +39930,7 @@ function makeWatchHandler(deps) {
|
|
|
39219
39930
|
if (suppressor) {
|
|
39220
39931
|
let raw = null;
|
|
39221
39932
|
try {
|
|
39222
|
-
raw =
|
|
39933
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39223
39934
|
} catch {
|
|
39224
39935
|
raw = null;
|
|
39225
39936
|
}
|
|
@@ -39251,7 +39962,7 @@ function makeWatchHandler(deps) {
|
|
|
39251
39962
|
spinner.warn(`Push failed for artists/${slug}/memory.md: ${msg}`);
|
|
39252
39963
|
}
|
|
39253
39964
|
};
|
|
39254
|
-
|
|
39965
|
+
const handle = async (event) => {
|
|
39255
39966
|
try {
|
|
39256
39967
|
if (event.kind === "renamed") {
|
|
39257
39968
|
const classifiedFrom = classifyMirrorPath({
|
|
@@ -39321,14 +40032,20 @@ function makeWatchHandler(deps) {
|
|
|
39321
40032
|
}
|
|
39322
40033
|
if (suppressor) {
|
|
39323
40034
|
if (event.kind === "unlink") {
|
|
39324
|
-
if (suppressor.shouldSuppressDelete(absolutePath))
|
|
40035
|
+
if (suppressor.shouldSuppressDelete(absolutePath)) {
|
|
40036
|
+
output?.log(entityDeleteSuppressedLine({
|
|
40037
|
+
entityType: classified.entityType,
|
|
40038
|
+
slug: classified.entitySlug,
|
|
40039
|
+
reason: "pull_echo"
|
|
40040
|
+
}));
|
|
39325
40041
|
return;
|
|
40042
|
+
}
|
|
39326
40043
|
} else if (event.kind === "upsert") {
|
|
39327
40044
|
const strategy = getStrategy(classified.entityType);
|
|
39328
40045
|
if (strategy) {
|
|
39329
40046
|
let raw = null;
|
|
39330
40047
|
try {
|
|
39331
|
-
raw =
|
|
40048
|
+
raw = readFileSync24(absolutePath, "utf8");
|
|
39332
40049
|
} catch {
|
|
39333
40050
|
raw = null;
|
|
39334
40051
|
}
|
|
@@ -39338,6 +40055,15 @@ function makeWatchHandler(deps) {
|
|
|
39338
40055
|
}
|
|
39339
40056
|
}
|
|
39340
40057
|
}
|
|
40058
|
+
if (event.kind === "unlink" && existsSync8(absolutePath)) {
|
|
40059
|
+
output?.log(entityDeleteSuppressedLine({
|
|
40060
|
+
entityType: classified.entityType,
|
|
40061
|
+
slug: classified.entitySlug,
|
|
40062
|
+
reason: "path_exists"
|
|
40063
|
+
}));
|
|
40064
|
+
await handle({ kind: "upsert", absolutePath });
|
|
40065
|
+
return;
|
|
40066
|
+
}
|
|
39341
40067
|
if (event.kind === "unlink" && event.renameFallback) {
|
|
39342
40068
|
const slugHint = event.stateEntry?.slug ? `${classified.entityType}/${event.stateEntry.slug}` : absolutePath;
|
|
39343
40069
|
spinner.warn(`Rename pair miss for ${slugHint} — no inode in state, treated as delete (classifier degraded to create+delete). ` + `If you just ran \`mv\`, the cloud row was likely tombstoned; check ~/.plinth/_conflicts/ or restore from cloud.`);
|
|
@@ -39364,7 +40090,7 @@ function makeWatchHandler(deps) {
|
|
|
39364
40090
|
slug: event.stateEntry.slug,
|
|
39365
40091
|
updated_at: event.stateEntry.updated_at
|
|
39366
40092
|
} : null,
|
|
39367
|
-
deps: { fetchImpl, now, mutex, output }
|
|
40093
|
+
deps: { fetchImpl, now, mutex, output, suppressor }
|
|
39368
40094
|
});
|
|
39369
40095
|
setState(clearEntityRejected(result.state, workspaceSlug, absolutePath));
|
|
39370
40096
|
reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
|
|
@@ -39403,6 +40129,7 @@ function makeWatchHandler(deps) {
|
|
|
39403
40129
|
spinner.warn(`Push failed for ${path5}: ${msg}`);
|
|
39404
40130
|
}
|
|
39405
40131
|
};
|
|
40132
|
+
return handle;
|
|
39406
40133
|
}
|
|
39407
40134
|
function discardSink() {
|
|
39408
40135
|
const stream = { write: () => true };
|
|
@@ -40459,11 +41186,101 @@ var startCommand = defineCommand({
|
|
|
40459
41186
|
});
|
|
40460
41187
|
|
|
40461
41188
|
// src/commands/status.ts
|
|
40462
|
-
import { statSync as
|
|
41189
|
+
import { statSync as statSync18 } from "node:fs";
|
|
41190
|
+
|
|
41191
|
+
// src/lib/context/memory-bars.ts
|
|
41192
|
+
var RULE_FILE_BAR_BYTES = 6000;
|
|
41193
|
+
var FACT_STORE_BAR_BYTES = 12000;
|
|
41194
|
+
var MAX_NAMED_FACT_STORES = 5;
|
|
41195
|
+
function countRuleShapedLines(body) {
|
|
41196
|
+
let count = 0;
|
|
41197
|
+
for (const line of body.split(`
|
|
41198
|
+
`)) {
|
|
41199
|
+
const trimmed = line.trim();
|
|
41200
|
+
if (/^[-*+]\s/.test(trimmed))
|
|
41201
|
+
count += 1;
|
|
41202
|
+
else if (/^\*\*[^*]+\*\*/.test(trimmed))
|
|
41203
|
+
count += 1;
|
|
41204
|
+
}
|
|
41205
|
+
return count;
|
|
41206
|
+
}
|
|
41207
|
+
function factBody(raw) {
|
|
41208
|
+
return raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*$/, "").split(`
|
|
41209
|
+
`).filter((line) => !/^\s*#{1,6}\s/.test(line)).join(`
|
|
41210
|
+
`).trim();
|
|
41211
|
+
}
|
|
41212
|
+
var RULE_FILES = [
|
|
41213
|
+
{ filename: VOICE_FILENAME, agentMayEdit: false, memberOwned: true, ridesInjection: false },
|
|
41214
|
+
{ filename: WORKSPACE_RULES_FILENAME, agentMayEdit: true, memberOwned: false, ridesInjection: true }
|
|
41215
|
+
];
|
|
41216
|
+
function assessMemoryBars(workspaceSlug, deps = {}) {
|
|
41217
|
+
const readContext = deps.readContext ?? readContextFile;
|
|
41218
|
+
const readArtistMemory = deps.readArtistMemory ?? readArtistMemoryFile;
|
|
41219
|
+
const rosterOf = deps.readRosterFn ?? readRoster;
|
|
41220
|
+
const over = [];
|
|
41221
|
+
for (const spec of RULE_FILES) {
|
|
41222
|
+
let raw = null;
|
|
41223
|
+
try {
|
|
41224
|
+
raw = readContext(workspaceSlug, spec.filename);
|
|
41225
|
+
} catch {
|
|
41226
|
+
raw = null;
|
|
41227
|
+
}
|
|
41228
|
+
if (raw === null)
|
|
41229
|
+
continue;
|
|
41230
|
+
const bytes = Buffer.byteLength(raw);
|
|
41231
|
+
if (bytes <= RULE_FILE_BAR_BYTES)
|
|
41232
|
+
continue;
|
|
41233
|
+
over.push({
|
|
41234
|
+
path: spec.filename,
|
|
41235
|
+
kind: "rule",
|
|
41236
|
+
bytes,
|
|
41237
|
+
bar: RULE_FILE_BAR_BYTES,
|
|
41238
|
+
rules: countRuleShapedLines(raw),
|
|
41239
|
+
facts: groupFacts(factBody(raw)).length,
|
|
41240
|
+
agentMayEdit: spec.agentMayEdit,
|
|
41241
|
+
memberOwned: spec.memberOwned,
|
|
41242
|
+
buys: !spec.ridesInjection ? "per-read" : Buffer.byteLength(extractWorkspaceRules(raw) ?? "") > 0 ? "injection" : "nothing"
|
|
41243
|
+
});
|
|
41244
|
+
}
|
|
41245
|
+
let roster;
|
|
41246
|
+
try {
|
|
41247
|
+
roster = rosterOf(workspaceSlug);
|
|
41248
|
+
} catch {
|
|
41249
|
+
roster = [];
|
|
41250
|
+
}
|
|
41251
|
+
const stores = [];
|
|
41252
|
+
for (const artist of roster) {
|
|
41253
|
+
let raw = null;
|
|
41254
|
+
try {
|
|
41255
|
+
raw = readArtistMemory(workspaceSlug, artist.slug);
|
|
41256
|
+
} catch {
|
|
41257
|
+
raw = null;
|
|
41258
|
+
}
|
|
41259
|
+
if (raw === null)
|
|
41260
|
+
continue;
|
|
41261
|
+
const bytes = Buffer.byteLength(raw);
|
|
41262
|
+
if (bytes <= FACT_STORE_BAR_BYTES)
|
|
41263
|
+
continue;
|
|
41264
|
+
stores.push({
|
|
41265
|
+
path: `artists/${artist.slug}/memory.md`,
|
|
41266
|
+
kind: "fact",
|
|
41267
|
+
bytes,
|
|
41268
|
+
bar: FACT_STORE_BAR_BYTES,
|
|
41269
|
+
rules: countRuleShapedLines(raw),
|
|
41270
|
+
facts: groupFacts(factBody(raw)).length,
|
|
41271
|
+
agentMayEdit: true,
|
|
41272
|
+
memberOwned: true,
|
|
41273
|
+
buys: "per-read"
|
|
41274
|
+
});
|
|
41275
|
+
}
|
|
41276
|
+
stores.sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path));
|
|
41277
|
+
const named = stores.slice(0, MAX_NAMED_FACT_STORES);
|
|
41278
|
+
return { over: [...over, ...named], notNamed: stores.length - named.length };
|
|
41279
|
+
}
|
|
40463
41280
|
|
|
40464
41281
|
// src/lib/build-stamp.ts
|
|
40465
41282
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
40466
|
-
import { existsSync as
|
|
41283
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync25, writeFileSync as writeFileSync8 } from "node:fs";
|
|
40467
41284
|
import { dirname as dirname10, join as join17 } from "node:path";
|
|
40468
41285
|
import { fileURLToPath } from "node:url";
|
|
40469
41286
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
@@ -40487,7 +41304,7 @@ function readHead(repoDir) {
|
|
|
40487
41304
|
}
|
|
40488
41305
|
function readBuildStamp(distDir) {
|
|
40489
41306
|
try {
|
|
40490
|
-
const parsed = JSON.parse(
|
|
41307
|
+
const parsed = JSON.parse(readFileSync25(stampPath(distDir), "utf8"));
|
|
40491
41308
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
40492
41309
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
40493
41310
|
}
|
|
@@ -40499,7 +41316,7 @@ function readBuildStamp(distDir) {
|
|
|
40499
41316
|
function findRepoRoot(fromDir) {
|
|
40500
41317
|
let dir = fromDir;
|
|
40501
41318
|
for (;; ) {
|
|
40502
|
-
if (
|
|
41319
|
+
if (existsSync9(join17(dir, ".git")))
|
|
40503
41320
|
return dir;
|
|
40504
41321
|
const parent = dirname10(dir);
|
|
40505
41322
|
if (parent === dir)
|
|
@@ -40547,13 +41364,59 @@ function checkBuildStaleness(deps = {}) {
|
|
|
40547
41364
|
return { kind: "fresh", stamp, head };
|
|
40548
41365
|
}
|
|
40549
41366
|
|
|
41367
|
+
// src/lib/memory-links.ts
|
|
41368
|
+
import { readFileSync as readFileSync26 } from "node:fs";
|
|
41369
|
+
import { relative as relative13, sep as sep5 } from "node:path";
|
|
41370
|
+
var MEMORY_FILE_RE = /^(workspace-memory\.md|artists\/[^/]+\/memory\.md)$/i;
|
|
41371
|
+
function scanMemoryLinks(deps) {
|
|
41372
|
+
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
41373
|
+
const readFile = deps.readFile ?? ((p) => readFileSync26(p, "utf8"));
|
|
41374
|
+
let root;
|
|
41375
|
+
let absPaths;
|
|
41376
|
+
try {
|
|
41377
|
+
root = workspaceMirrorPath(deps.workspaceSlug);
|
|
41378
|
+
absPaths = listFiles(root);
|
|
41379
|
+
} catch {
|
|
41380
|
+
return { scanned: 0, unreadable: [], dead: [] };
|
|
41381
|
+
}
|
|
41382
|
+
const relPaths = absPaths.map((p) => relative13(root, p).split(sep5).join("/"));
|
|
41383
|
+
const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
|
|
41384
|
+
const dead = [];
|
|
41385
|
+
const unreadable = [];
|
|
41386
|
+
let scanned = 0;
|
|
41387
|
+
for (let i = 0;i < relPaths.length; i++) {
|
|
41388
|
+
const rel = relPaths[i];
|
|
41389
|
+
if (!MEMORY_FILE_RE.test(rel))
|
|
41390
|
+
continue;
|
|
41391
|
+
let body;
|
|
41392
|
+
try {
|
|
41393
|
+
body = readFile(absPaths[i]);
|
|
41394
|
+
} catch {
|
|
41395
|
+
unreadable.push(rel);
|
|
41396
|
+
continue;
|
|
41397
|
+
}
|
|
41398
|
+
scanned++;
|
|
41399
|
+
for (const link of parseMirrorLinks(body)) {
|
|
41400
|
+
if (link.kind !== "prose")
|
|
41401
|
+
continue;
|
|
41402
|
+
const resolution = resolveTarget(link.matchTarget, relPaths, artists);
|
|
41403
|
+
if (resolution.kind !== "unresolved")
|
|
41404
|
+
continue;
|
|
41405
|
+
dead.push({ file: rel, line: link.line, target: link.target });
|
|
41406
|
+
}
|
|
41407
|
+
}
|
|
41408
|
+
dead.sort((a, b) => a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file));
|
|
41409
|
+
unreadable.sort();
|
|
41410
|
+
return { scanned, unreadable, dead };
|
|
41411
|
+
}
|
|
41412
|
+
|
|
40550
41413
|
// src/commands/status.ts
|
|
40551
41414
|
init_output();
|
|
40552
41415
|
|
|
40553
41416
|
// src/lib/pending-writes.ts
|
|
40554
|
-
import { readFileSync as
|
|
40555
|
-
import { relative as
|
|
40556
|
-
var
|
|
41417
|
+
import { readFileSync as readFileSync27, statSync as statSync17 } from "node:fs";
|
|
41418
|
+
import { relative as relative14 } from "node:path";
|
|
41419
|
+
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
40557
41420
|
var EMPTY2 = { writes: [], count: 0 };
|
|
40558
41421
|
function reportedElsewhere(state) {
|
|
40559
41422
|
const entities = new Set;
|
|
@@ -40581,7 +41444,7 @@ function scanPendingWrites(args) {
|
|
|
40581
41444
|
const writes = [];
|
|
40582
41445
|
const discovered = [];
|
|
40583
41446
|
for (const absolutePath of walkSubtreeFiles(mirrorRootPath, ignored)) {
|
|
40584
|
-
const relPath =
|
|
41447
|
+
const relPath = relative14(mirrorRootPath, absolutePath);
|
|
40585
41448
|
const fileMatch = classifyFilesPath({ workspaceSlug, absolutePath });
|
|
40586
41449
|
if (fileMatch) {
|
|
40587
41450
|
if (frozenFiles.has(fileMatch.relPath))
|
|
@@ -40591,7 +41454,7 @@ function scanPendingWrites(args) {
|
|
|
40591
41454
|
const entry3 = getFileEntry(state, fileMatch.relPath);
|
|
40592
41455
|
let size;
|
|
40593
41456
|
try {
|
|
40594
|
-
size =
|
|
41457
|
+
size = statSync17(absolutePath).size;
|
|
40595
41458
|
} catch {
|
|
40596
41459
|
continue;
|
|
40597
41460
|
}
|
|
@@ -40599,7 +41462,7 @@ function scanPendingWrites(args) {
|
|
|
40599
41462
|
continue;
|
|
40600
41463
|
let hash2;
|
|
40601
41464
|
try {
|
|
40602
|
-
hash2 = sha256Bytes(
|
|
41465
|
+
hash2 = sha256Bytes(readFileSync27(absolutePath));
|
|
40603
41466
|
} catch {
|
|
40604
41467
|
continue;
|
|
40605
41468
|
}
|
|
@@ -40620,7 +41483,7 @@ function scanPendingWrites(args) {
|
|
|
40620
41483
|
continue;
|
|
40621
41484
|
let liveInode;
|
|
40622
41485
|
try {
|
|
40623
|
-
liveInode =
|
|
41486
|
+
liveInode = statSync17(absolutePath).ino;
|
|
40624
41487
|
} catch {
|
|
40625
41488
|
liveInode = undefined;
|
|
40626
41489
|
}
|
|
@@ -40630,14 +41493,14 @@ function scanPendingWrites(args) {
|
|
|
40630
41493
|
continue;
|
|
40631
41494
|
let content;
|
|
40632
41495
|
try {
|
|
40633
|
-
content =
|
|
41496
|
+
content = readFileSync27(absolutePath, "utf8");
|
|
40634
41497
|
} catch {
|
|
40635
41498
|
continue;
|
|
40636
41499
|
}
|
|
40637
41500
|
if (!entry2) {
|
|
40638
41501
|
let frontmatter;
|
|
40639
41502
|
try {
|
|
40640
|
-
frontmatter =
|
|
41503
|
+
frontmatter = import_gray_matter14.default(content).data ?? {};
|
|
40641
41504
|
} catch {
|
|
40642
41505
|
continue;
|
|
40643
41506
|
}
|
|
@@ -40799,6 +41662,15 @@ function formatAbsolute(date5) {
|
|
|
40799
41662
|
const pad3 = (n) => String(n).padStart(2, "0");
|
|
40800
41663
|
return `${date5.getFullYear()}-${pad3(date5.getMonth() + 1)}-${pad3(date5.getDate())} ` + `${pad3(date5.getHours())}:${pad3(date5.getMinutes())}:${pad3(date5.getSeconds())}`;
|
|
40801
41664
|
}
|
|
41665
|
+
function describeTokens(tokens) {
|
|
41666
|
+
const versions2 = new Set(tokens.map((t) => t.version));
|
|
41667
|
+
const names = tokens.map((t) => versions2.size === 1 ? `\`${t.token}\`` : `\`${t.token}\` (${t.version})`);
|
|
41668
|
+
const joined = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
41669
|
+
return {
|
|
41670
|
+
joined,
|
|
41671
|
+
inVersion: versions2.size === 1 ? ` in ${tokens[0].version}` : ""
|
|
41672
|
+
};
|
|
41673
|
+
}
|
|
40802
41674
|
function short(sha) {
|
|
40803
41675
|
return sha.slice(0, 7);
|
|
40804
41676
|
}
|
|
@@ -40820,7 +41692,7 @@ function renderBuild(out, staleness) {
|
|
|
40820
41692
|
}
|
|
40821
41693
|
function lastActivity(now) {
|
|
40822
41694
|
try {
|
|
40823
|
-
const mtimeMs =
|
|
41695
|
+
const mtimeMs = statSync18(daemonLogPath()).mtimeMs;
|
|
40824
41696
|
return formatRelative(now.getTime() - mtimeMs);
|
|
40825
41697
|
} catch {
|
|
40826
41698
|
return null;
|
|
@@ -41003,17 +41875,60 @@ async function runStatus(args, deps = {}) {
|
|
|
41003
41875
|
out.info(cont(`Full list with restore commands: ${CONFLICTS_VIEW_REL_PATH} (written by the sync, so this line is the fresher of the two)`));
|
|
41004
41876
|
}
|
|
41005
41877
|
out.info("");
|
|
41006
|
-
const
|
|
41878
|
+
const links = scanMemoryLinks({ workspaceSlug: slug });
|
|
41879
|
+
const unreadableLinkLines = () => {
|
|
41880
|
+
const n = links.unreadable.length;
|
|
41881
|
+
return [
|
|
41882
|
+
`${n} memory file${n === 1 ? "" : "s"} could not be read, so ${n === 1 ? "its" : "their"} pointers are unchecked.`,
|
|
41883
|
+
...links.unreadable
|
|
41884
|
+
];
|
|
41885
|
+
};
|
|
41886
|
+
if (links.dead.length > 0) {
|
|
41887
|
+
const n = links.dead.length;
|
|
41888
|
+
out.info(row("Links", `${n} memory pointer${n === 1 ? "" : "s"} resolve${n === 1 ? "s" : ""} to nothing.`));
|
|
41889
|
+
const MAX_LINKS_SHOWN = 10;
|
|
41890
|
+
for (const d of links.dead.slice(0, MAX_LINKS_SHOWN)) {
|
|
41891
|
+
out.info(cont(`${d.file}:${d.line} — [[${d.target}]]`));
|
|
41892
|
+
}
|
|
41893
|
+
if (n > MAX_LINKS_SHOWN) {
|
|
41894
|
+
out.info(cont(`…and ${n - MAX_LINKS_SHOWN} more.`));
|
|
41895
|
+
}
|
|
41896
|
+
out.info(cont("The fact's pointer is its source. Restore the document, repoint the link, or fold what the pointer was carrying into the fact itself."));
|
|
41897
|
+
if (links.unreadable.length > 0) {
|
|
41898
|
+
for (const line of unreadableLinkLines())
|
|
41899
|
+
out.info(cont(line));
|
|
41900
|
+
}
|
|
41901
|
+
} else if (links.unreadable.length > 0) {
|
|
41902
|
+
const [head, ...rest] = unreadableLinkLines();
|
|
41903
|
+
out.info(row("Links", head));
|
|
41904
|
+
for (const line of rest)
|
|
41905
|
+
out.info(cont(line));
|
|
41906
|
+
} else if (links.scanned === 0) {
|
|
41907
|
+
out.info(row("Links", "No memory files yet. They arrive on the next sync."));
|
|
41908
|
+
} else {
|
|
41909
|
+
out.info(row("Links", "Every memory pointer resolves."));
|
|
41910
|
+
}
|
|
41911
|
+
out.info("");
|
|
41912
|
+
const commandScan = scanStaleUserBlocks({
|
|
41913
|
+
workspaceSlug: slug,
|
|
41914
|
+
retired: deps.retiredTokens
|
|
41915
|
+
});
|
|
41007
41916
|
const unchecked = commandScan.unchecked;
|
|
41008
41917
|
if (commandScan.stale.length > 0) {
|
|
41009
41918
|
const n = commandScan.stale.length;
|
|
41010
41919
|
out.info(row("Commands", n === 1 ? `1 customisable block names machinery ${commandScan.stale[0].invocation} no longer uses.` : `${n} customisable blocks name machinery their commands no longer use.`));
|
|
41011
41920
|
for (const f of commandScan.stale) {
|
|
41012
|
-
const
|
|
41013
|
-
const
|
|
41014
|
-
|
|
41015
|
-
|
|
41016
|
-
|
|
41921
|
+
const stillLive = f.tokens.filter((t) => t.stillACommand);
|
|
41922
|
+
const gone = f.tokens.filter((t) => !t.stillACommand);
|
|
41923
|
+
if (gone.length > 0) {
|
|
41924
|
+
const { joined, inVersion } = describeTokens(gone);
|
|
41925
|
+
out.info(cont(`${f.commandRel} names ${joined}, retired from ${f.invocation}` + `${inVersion}. Review it.`));
|
|
41926
|
+
}
|
|
41927
|
+
if (stillLive.length > 0) {
|
|
41928
|
+
const { joined, inVersion } = describeTokens(stillLive);
|
|
41929
|
+
const noun = stillLive.length === 1 ? "Still a working command" : "Still working commands";
|
|
41930
|
+
out.info(cont(`${f.commandRel} names ${joined}, which ${f.invocation} stopped ` + `running${inVersion}. ${noun} — keep the step if you want it.`));
|
|
41931
|
+
}
|
|
41017
41932
|
}
|
|
41018
41933
|
if (unchecked.length > 0) {
|
|
41019
41934
|
for (const line of uncheckedLines(unchecked))
|
|
@@ -41054,7 +41969,7 @@ async function runStatus(args, deps = {}) {
|
|
|
41054
41969
|
out.info("");
|
|
41055
41970
|
let report = null;
|
|
41056
41971
|
try {
|
|
41057
|
-
report = runGroundingReport({ workspaceSlug: slug });
|
|
41972
|
+
report = (deps.groundingReport ?? (() => runGroundingReport({ workspaceSlug: slug })))();
|
|
41058
41973
|
} catch {
|
|
41059
41974
|
report = null;
|
|
41060
41975
|
}
|
|
@@ -41065,6 +41980,16 @@ async function runStatus(args, deps = {}) {
|
|
|
41065
41980
|
const bytes = Buffer.byteLength(injected);
|
|
41066
41981
|
const overBudget = bytes > GROUNDING_BUDGET_BYTES;
|
|
41067
41982
|
out.info(row("Injection", `${bytes.toLocaleString("en-GB")} bytes at every session start` + (overBudget ? "" : ` (budget ${GROUNDING_BUDGET_BYTES.toLocaleString("en-GB")})`)));
|
|
41983
|
+
const split = report.split;
|
|
41984
|
+
const spent = GROUNDING_CATEGORY_ORDER.filter((c) => split[c] > 0);
|
|
41985
|
+
if (spent.length > 0) {
|
|
41986
|
+
out.info(cont("Where those bytes go:"));
|
|
41987
|
+
const width = Math.max(...spent.map((c) => GROUNDING_CATEGORY_LABELS[c].length));
|
|
41988
|
+
for (const c of spent) {
|
|
41989
|
+
out.info(cont(` ${GROUNDING_CATEGORY_LABELS[c].padEnd(width)} ` + `${split[c].toLocaleString("en-GB").padStart(6)}` + (MEMBER_EDITABLE_CATEGORIES.includes(c) ? " *" : "")));
|
|
41990
|
+
}
|
|
41991
|
+
out.info(cont(`A * marks what is yours to change; the rest is Plinth's own ` + `grounding and is the same on every workspace.`));
|
|
41992
|
+
}
|
|
41068
41993
|
const contributors = [];
|
|
41069
41994
|
const rides = (name, reduce) => {
|
|
41070
41995
|
let raw = null;
|
|
@@ -41085,8 +42010,8 @@ async function runStatus(args, deps = {}) {
|
|
|
41085
42010
|
{ name: WORKSPACE_MEMORY_FILENAME, pick: report.picks.wmPick },
|
|
41086
42011
|
{ name: USER_MEMORY_FILENAME, pick: report.picks.umPick }
|
|
41087
42012
|
].flatMap((i) => i.pick === null ? [] : [{ name: i.name, ...i.pick }]);
|
|
41088
|
-
const dropped = indexes.filter((i) => i.kept === 0 && i.total > 0);
|
|
41089
|
-
const partial2 = indexes.filter((i) => i.kept > 0 && i.kept < i.total);
|
|
42013
|
+
const dropped = indexes.filter((i) => i.kept === 0 && i.named === 0 && i.total > 0);
|
|
42014
|
+
const partial2 = indexes.filter((i) => i.kept + i.named > 0 && i.kept < i.total);
|
|
41090
42015
|
if (overBudget) {
|
|
41091
42016
|
out.info(cont(`Over the ${GROUNDING_BUDGET_BYTES.toLocaleString("en-GB")}-byte budget.` + (indexes.length > 0 ? " Your memory indexes are already trimmed as far as they go, so the rest is Plinth's own grounding." : " There is no memory index to trim, so all of it is Plinth's own grounding and this workspace's rules.") + " Report it."));
|
|
41092
42017
|
} else if (dropped.length > 0 || partial2.length > 0) {
|
|
@@ -41094,7 +42019,10 @@ async function runStatus(args, deps = {}) {
|
|
|
41094
42019
|
out.info(cont(`NONE of ${i.name} fits in the injection: all ${i.total} facts are read from the file on demand, not recalled.`));
|
|
41095
42020
|
}
|
|
41096
42021
|
for (const i of partial2) {
|
|
41097
|
-
|
|
42022
|
+
const unnamed = i.total - i.kept - i.named;
|
|
42023
|
+
const head = i.kept === 0 ? `No fact in ${i.name} fits whole, but ${i.named} of ${i.total} ride as their opening line, so the agent knows they exist.` : `Only ${i.kept} of ${i.total} facts in ${i.name} fit in the injection.` + (i.named > 0 ? ` Another ${i.named} ride as their opening line, so the agent knows they exist.` : ``);
|
|
42024
|
+
const tail = unnamed > 0 ? ` The remaining ${unnamed} stay in the file and are read on demand.` : ` The full text of all of them stays in the file.`;
|
|
42025
|
+
out.info(cont(`${head}${tail}`));
|
|
41098
42026
|
}
|
|
41099
42027
|
if (largest !== undefined) {
|
|
41100
42028
|
out.info(cont(`${largest.name} is the largest of these at ${largest.bytes.toLocaleString("en-GB")} bytes. Shorter lines fit more facts: the convention is one line per fact, a hook and a [[pointer]], not a paragraph.`));
|
|
@@ -41103,6 +42031,45 @@ async function runStatus(args, deps = {}) {
|
|
|
41103
42031
|
out.info(cont("Every memory fact fits; nothing is being trimmed."));
|
|
41104
42032
|
}
|
|
41105
42033
|
}
|
|
42034
|
+
let bars = { over: [], notNamed: 0 };
|
|
42035
|
+
try {
|
|
42036
|
+
bars = assessMemoryBars(slug);
|
|
42037
|
+
} catch {
|
|
42038
|
+
bars = { over: [], notNamed: 0 };
|
|
42039
|
+
}
|
|
42040
|
+
if (bars.over.length > 0) {
|
|
42041
|
+
const overCount = bars.over.length;
|
|
42042
|
+
out.info(row("Too long", `${overCount} file${overCount === 1 ? " is" : "s are"} past the size ` + `${overCount === 1 ? "it is" : "they are"} useful at.`));
|
|
42043
|
+
const size = (n) => n.toLocaleString("en-GB");
|
|
42044
|
+
const buys = (f) => {
|
|
42045
|
+
if (f.buys === "injection") {
|
|
42046
|
+
return " It rides the injection and is never trimmed, so those bytes come off your memory facts.";
|
|
42047
|
+
}
|
|
42048
|
+
if (f.buys === "nothing") {
|
|
42049
|
+
return " None of it rides the injection today: it reduces to comments and headings, so trimming it buys nothing until it holds a rule.";
|
|
42050
|
+
}
|
|
42051
|
+
return "";
|
|
42052
|
+
};
|
|
42053
|
+
for (const f of bars.over.filter((o) => o.kind === "rule")) {
|
|
42054
|
+
const tail = !f.agentMayEdit ? " Yours to edit; the agent is denied." : f.memberOwned ? "" : " It belongs to the workspace, so a person trims it and no session close does.";
|
|
42055
|
+
out.info(cont(`${f.path} is ${size(f.bytes)} bytes against a ${size(f.bar)}-byte bar, ` + `about ${f.rules} rules.${buys(f)}${tail}`));
|
|
42056
|
+
}
|
|
42057
|
+
if (bars.over.some((o) => o.kind === "rule")) {
|
|
42058
|
+
out.info(cont("A rule file gets the tighter bar because it spends the instruction " + "budget: a model follows about 150 reliably and the harness already " + "takes 50. Facts cost room, not instructions."));
|
|
42059
|
+
}
|
|
42060
|
+
for (const f of bars.over.filter((o) => o.kind === "fact")) {
|
|
42061
|
+
out.info(cont(`${f.path} is ${size(f.bytes)} bytes against a ${size(f.bar)}-byte bar, ` + `${f.facts} facts.${buys(f)}`));
|
|
42062
|
+
}
|
|
42063
|
+
if (bars.notNamed > 0) {
|
|
42064
|
+
out.info(cont(`And ${bars.notNamed} more artist ${bars.notNamed === 1 ? "store" : "stores"} past the bar, not named here.`));
|
|
42065
|
+
}
|
|
42066
|
+
if (bars.over.some((o) => o.buys === "per-read")) {
|
|
42067
|
+
out.info(cont((bars.over.some((o) => o.buys !== "per-read") ? "The others are" : "These are") + " read whole when something opens them rather than injected, so" + " trimming them buys context on every read and no injection room."));
|
|
42068
|
+
}
|
|
42069
|
+
if (bars.over.some((o) => o.kind === "fact")) {
|
|
42070
|
+
out.info(cont(`Ending a session with ${CLOSE_COMMAND_INVOCATION} offers to propose a trim for the artist stores.`));
|
|
42071
|
+
}
|
|
42072
|
+
}
|
|
41106
42073
|
out.info("");
|
|
41107
42074
|
return { exitCode: 0 };
|
|
41108
42075
|
}
|
|
@@ -41143,7 +42110,7 @@ var versionCommand = defineCommand({
|
|
|
41143
42110
|
});
|
|
41144
42111
|
|
|
41145
42112
|
// src/commands/voice-gate.ts
|
|
41146
|
-
import { readFileSync as
|
|
42113
|
+
import { readFileSync as readFileSync30 } from "node:fs";
|
|
41147
42114
|
|
|
41148
42115
|
// src/lib/voice-gate/api.ts
|
|
41149
42116
|
var VoiceGateSurfaces = [
|
|
@@ -41248,9 +42215,9 @@ async function callVoiceGate(req) {
|
|
|
41248
42215
|
import { createHash as createHash4 } from "node:crypto";
|
|
41249
42216
|
import {
|
|
41250
42217
|
mkdirSync as mkdirSync12,
|
|
41251
|
-
readFileSync as
|
|
42218
|
+
readFileSync as readFileSync28,
|
|
41252
42219
|
renameSync as renameSync8,
|
|
41253
|
-
unlinkSync as
|
|
42220
|
+
unlinkSync as unlinkSync9,
|
|
41254
42221
|
writeFileSync as writeFileSync9
|
|
41255
42222
|
} from "node:fs";
|
|
41256
42223
|
import { join as join18 } from "node:path";
|
|
@@ -41279,7 +42246,7 @@ function emptyFile() {
|
|
|
41279
42246
|
}
|
|
41280
42247
|
function load2(workspaceSlug) {
|
|
41281
42248
|
try {
|
|
41282
|
-
const raw =
|
|
42249
|
+
const raw = readFileSync28(attemptsPath(workspaceSlug), "utf8");
|
|
41283
42250
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
41284
42251
|
return parsed.success ? parsed.data : emptyFile();
|
|
41285
42252
|
} catch {
|
|
@@ -41306,7 +42273,7 @@ function save(workspaceSlug, file2) {
|
|
|
41306
42273
|
renameSync8(tmp, target);
|
|
41307
42274
|
} catch (err) {
|
|
41308
42275
|
try {
|
|
41309
|
-
|
|
42276
|
+
unlinkSync9(tmp);
|
|
41310
42277
|
} catch {}
|
|
41311
42278
|
throw err;
|
|
41312
42279
|
}
|
|
@@ -41442,7 +42409,7 @@ function denyUngated(detail) {
|
|
|
41442
42409
|
}
|
|
41443
42410
|
|
|
41444
42411
|
// src/lib/voice-gate/payload.ts
|
|
41445
|
-
import { readFileSync as
|
|
42412
|
+
import { readFileSync as readFileSync29, statSync as statSync19 } from "node:fs";
|
|
41446
42413
|
import { homedir as homedir2 } from "node:os";
|
|
41447
42414
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
41448
42415
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -41599,11 +42566,11 @@ function isHelperInvocation(tokens) {
|
|
|
41599
42566
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
41600
42567
|
}
|
|
41601
42568
|
function defaultReadBodyFile(path5) {
|
|
41602
|
-
const stat4 =
|
|
42569
|
+
const stat4 = statSync19(path5);
|
|
41603
42570
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
41604
42571
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
41605
42572
|
}
|
|
41606
|
-
return
|
|
42573
|
+
return readFileSync29(path5, "utf8");
|
|
41607
42574
|
}
|
|
41608
42575
|
function parseRecipients(...raw) {
|
|
41609
42576
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -41963,7 +42930,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
41963
42930
|
let text;
|
|
41964
42931
|
if (args.file) {
|
|
41965
42932
|
try {
|
|
41966
|
-
text =
|
|
42933
|
+
text = readFileSync30(args.file, "utf8");
|
|
41967
42934
|
} catch (err) {
|
|
41968
42935
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
41969
42936
|
out.error(`Could not read ${args.file}: ${msg}`);
|
|
@@ -42023,12 +42990,12 @@ async function runVoiceGateCommand(args, deps = {}) {
|
|
|
42023
42990
|
var voiceGateCommand = defineCommand({
|
|
42024
42991
|
meta: {
|
|
42025
42992
|
name: "voice-gate",
|
|
42026
|
-
description: "
|
|
42993
|
+
description: "Check agent-written copy against this workspace's voice rubric, on demand. Nothing gates drafting automatically: Plinth stopped mounting this as a hook and un-mounts the entry it used to write, so applying the rubric is the agent's own job."
|
|
42027
42994
|
},
|
|
42028
42995
|
args: {
|
|
42029
42996
|
hook: {
|
|
42030
42997
|
type: "boolean",
|
|
42031
|
-
description: "PreToolUse hook mode: read
|
|
42998
|
+
description: "PreToolUse hook mode: read a hook payload on stdin and block non-compliant Gmail drafts. Plinth does not mount this and un-mounts the entry it used to write; one you add by hand is left alone and still runs.",
|
|
42032
42999
|
required: false
|
|
42033
43000
|
},
|
|
42034
43001
|
workspace: {
|
|
@@ -42083,6 +43050,24 @@ var voiceGateCommand = defineCommand({
|
|
|
42083
43050
|
}
|
|
42084
43051
|
});
|
|
42085
43052
|
|
|
43053
|
+
// src/commands/registry.ts
|
|
43054
|
+
var subCommands = {
|
|
43055
|
+
login: loginCommand,
|
|
43056
|
+
sync: syncCommand,
|
|
43057
|
+
start: startCommand,
|
|
43058
|
+
status: statusCommand,
|
|
43059
|
+
confirm: confirmCommand,
|
|
43060
|
+
version: versionCommand,
|
|
43061
|
+
"refresh-context": refreshContextCommand,
|
|
43062
|
+
grounding: groundingCommand,
|
|
43063
|
+
"voice-gate": voiceGateCommand,
|
|
43064
|
+
backlinks: backlinksCommand,
|
|
43065
|
+
declare: declareCommand,
|
|
43066
|
+
"review-tier": reviewTierCommand,
|
|
43067
|
+
session: sessionCommand,
|
|
43068
|
+
"memory-gate": memoryGateCommand
|
|
43069
|
+
};
|
|
43070
|
+
|
|
42086
43071
|
// src/cli.ts
|
|
42087
43072
|
var argv = process.argv.slice(2);
|
|
42088
43073
|
var main = defineCommand({
|
|
@@ -42091,22 +43076,7 @@ var main = defineCommand({
|
|
|
42091
43076
|
version: package_default.version,
|
|
42092
43077
|
description: "Plinth capstone sync daemon and CLI"
|
|
42093
43078
|
},
|
|
42094
|
-
subCommands
|
|
42095
|
-
login: loginCommand,
|
|
42096
|
-
sync: syncCommand,
|
|
42097
|
-
start: startCommand,
|
|
42098
|
-
status: statusCommand,
|
|
42099
|
-
confirm: confirmCommand,
|
|
42100
|
-
version: versionCommand,
|
|
42101
|
-
"refresh-context": refreshContextCommand,
|
|
42102
|
-
grounding: groundingCommand,
|
|
42103
|
-
"voice-gate": voiceGateCommand,
|
|
42104
|
-
backlinks: backlinksCommand,
|
|
42105
|
-
declare: declareCommand,
|
|
42106
|
-
"review-tier": reviewTierCommand,
|
|
42107
|
-
session: sessionCommand,
|
|
42108
|
-
"memory-gate": memoryGateCommand
|
|
42109
|
-
}
|
|
43079
|
+
subCommands
|
|
42110
43080
|
});
|
|
42111
43081
|
var globalSlotEnd = 0;
|
|
42112
43082
|
while (globalSlotEnd < argv.length && argv[globalSlotEnd].startsWith("-"))
|