@plinth-music/cli 0.5.0 → 0.6.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 +76 -0
- package/README.md +46 -3
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1959 -428
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7714,6 +7714,7 @@ var init_ora = __esm(() => {
|
|
|
7714
7714
|
var exports_output = {};
|
|
7715
7715
|
__export(exports_output, {
|
|
7716
7716
|
spinnerEnabled: () => spinnerEnabled,
|
|
7717
|
+
noColorFromArgs: () => noColorFromArgs,
|
|
7717
7718
|
createSpinner: () => createSpinner,
|
|
7718
7719
|
Output: () => Output
|
|
7719
7720
|
});
|
|
@@ -7743,6 +7744,9 @@ function createSpinner(opts = {}) {
|
|
|
7743
7744
|
}
|
|
7744
7745
|
};
|
|
7745
7746
|
}
|
|
7747
|
+
function noColorFromArgs(args) {
|
|
7748
|
+
return args.color === false || args["no-color"] === true;
|
|
7749
|
+
}
|
|
7746
7750
|
function selectChalk(noColor) {
|
|
7747
7751
|
if (noColor)
|
|
7748
7752
|
return new Chalk({ level: 0 });
|
|
@@ -9189,7 +9193,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
9189
9193
|
// package.json
|
|
9190
9194
|
var package_default = {
|
|
9191
9195
|
name: "@plinth-music/cli",
|
|
9192
|
-
version: "0.
|
|
9196
|
+
version: "0.6.0",
|
|
9193
9197
|
description: "Plinth capstone sync daemon and CLI",
|
|
9194
9198
|
type: "module",
|
|
9195
9199
|
private: false,
|
|
@@ -24832,7 +24836,9 @@ var ThreadLiveSchema = exports_external.object({
|
|
|
24832
24836
|
scope: exports_external.enum(["internal", "artist"]),
|
|
24833
24837
|
frontmatter: exports_external.record(exports_external.string(), exports_external.unknown()),
|
|
24834
24838
|
last_edited_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
24835
|
-
last_edited_by: exports_external.string().uuid().nullable()
|
|
24839
|
+
last_edited_by: exports_external.string().uuid().nullable(),
|
|
24840
|
+
message_count: exports_external.number().int().nonnegative(),
|
|
24841
|
+
last_message_at: exports_external.string().datetime({ offset: true }).nullable()
|
|
24836
24842
|
});
|
|
24837
24843
|
var ThreadsPageResponseSchema = makePageResponseSchema("threads", ThreadLiveSchema);
|
|
24838
24844
|
var SERVER_AUTHORITATIVE_KEYS6 = new Set([
|
|
@@ -24842,7 +24848,9 @@ var SERVER_AUTHORITATIVE_KEYS6 = new Set([
|
|
|
24842
24848
|
"last_edited_by",
|
|
24843
24849
|
"scope",
|
|
24844
24850
|
"name",
|
|
24845
|
-
"artist_id"
|
|
24851
|
+
"artist_id",
|
|
24852
|
+
"message_count",
|
|
24853
|
+
"last_message_at"
|
|
24846
24854
|
]);
|
|
24847
24855
|
var WIRE_EDITABLE_TOP_LEVEL_KEYS6 = new Set([
|
|
24848
24856
|
"name",
|
|
@@ -24854,7 +24862,9 @@ var PROJECTION_KEYS6 = [
|
|
|
24854
24862
|
"artist_id",
|
|
24855
24863
|
"scope",
|
|
24856
24864
|
"revision",
|
|
24857
|
-
"last_edited_at"
|
|
24865
|
+
"last_edited_at",
|
|
24866
|
+
"message_count",
|
|
24867
|
+
"last_message_at"
|
|
24858
24868
|
];
|
|
24859
24869
|
function buildCreatePayload6(args) {
|
|
24860
24870
|
const fm = args.parsed.data ?? {};
|
|
@@ -25174,6 +25184,59 @@ function deleteFilesFile(workspaceSlug, relPath) {
|
|
|
25174
25184
|
throw err;
|
|
25175
25185
|
}
|
|
25176
25186
|
}
|
|
25187
|
+
var VIEWS_SUBTREE = "views";
|
|
25188
|
+
var EXTRACTED_VIEW_DIR = "extracted";
|
|
25189
|
+
function extractedTextRelPath(relPath) {
|
|
25190
|
+
return `${VIEWS_SUBTREE}/${EXTRACTED_VIEW_DIR}/${relPath}.md`;
|
|
25191
|
+
}
|
|
25192
|
+
function extractedTextPath(workspaceSlug, relPath) {
|
|
25193
|
+
if (validateFilesRelPath(relPath) === null) {
|
|
25194
|
+
throw new MirrorPathError(`Invalid files sub-path for extracted text: ${JSON.stringify(relPath)}`);
|
|
25195
|
+
}
|
|
25196
|
+
const ws = workspaceMirrorPath(workspaceSlug);
|
|
25197
|
+
const posix = extractedTextRelPath(relPath);
|
|
25198
|
+
const native = sep2 === "/" ? posix : posix.split("/").join(sep2);
|
|
25199
|
+
const target = resolve(join2(ws, native));
|
|
25200
|
+
const prefix = join2(ws, VIEWS_SUBTREE, EXTRACTED_VIEW_DIR) + sep2;
|
|
25201
|
+
if (!target.startsWith(prefix)) {
|
|
25202
|
+
throw new MirrorPathError(`Resolved extracted-text path escapes the views/extracted subtree`);
|
|
25203
|
+
}
|
|
25204
|
+
return target;
|
|
25205
|
+
}
|
|
25206
|
+
function writeExtractedText(workspaceSlug, relPath, content) {
|
|
25207
|
+
const target = extractedTextPath(workspaceSlug, relPath);
|
|
25208
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
25209
|
+
mkdirSync(dirname(target), { recursive: true, mode: 448 });
|
|
25210
|
+
writeFileSync(tmp, content, { mode: 384 });
|
|
25211
|
+
try {
|
|
25212
|
+
renameSync(tmp, target);
|
|
25213
|
+
} catch (err) {
|
|
25214
|
+
try {
|
|
25215
|
+
unlinkSync(tmp);
|
|
25216
|
+
} catch {}
|
|
25217
|
+
throw err;
|
|
25218
|
+
}
|
|
25219
|
+
}
|
|
25220
|
+
function readExtractedText(workspaceSlug, relPath) {
|
|
25221
|
+
try {
|
|
25222
|
+
return readFileSync(extractedTextPath(workspaceSlug, relPath), "utf8");
|
|
25223
|
+
} catch (err) {
|
|
25224
|
+
if (err?.code === "ENOENT")
|
|
25225
|
+
return null;
|
|
25226
|
+
throw err;
|
|
25227
|
+
}
|
|
25228
|
+
}
|
|
25229
|
+
function deleteExtractedText(workspaceSlug, relPath) {
|
|
25230
|
+
const target = extractedTextPath(workspaceSlug, relPath);
|
|
25231
|
+
try {
|
|
25232
|
+
unlinkSync(target);
|
|
25233
|
+
return true;
|
|
25234
|
+
} catch (err) {
|
|
25235
|
+
if (err?.code === "ENOENT")
|
|
25236
|
+
return false;
|
|
25237
|
+
throw err;
|
|
25238
|
+
}
|
|
25239
|
+
}
|
|
25177
25240
|
function contextFilePath(workspaceSlug, relativePath) {
|
|
25178
25241
|
const ws = workspaceMirrorPath(workspaceSlug);
|
|
25179
25242
|
const resolved = resolve(join2(ws, relativePath));
|
|
@@ -26772,9 +26835,15 @@ function buildGroundingBlock(today, workspaceRules = null, workspaceMemory = nul
|
|
|
26772
26835
|
``,
|
|
26773
26836
|
`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. (Native dry_run support in the Plinth MCP tools ` + `ships separately; until then this is a behavioural rule.)`,
|
|
26774
26837
|
``,
|
|
26775
|
-
`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 one
|
|
26838
|
+
`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.`,
|
|
26839
|
+
``,
|
|
26840
|
+
`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. Never create ` + `or update a key date: do not call \`create_key_date\` or \`update_key_date\` (they ` + `write to the retired table and are being removed from the tool surface). Reading ` + `dates is fine (\`list_key_dates\`, \`list_artist_calendars\`).`,
|
|
26776
26841
|
``,
|
|
26777
|
-
`
|
|
26842
|
+
`Read the calendar, and hand a date over rather than writing it yourself. An ` + `artist's dates live on their mapped Google Calendar and reading them is yours: ` + `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 it is not yours. When a date needs ` + `adding or correcting (a show, a hold, a release, a deadline), say plainly what ` + `the change is, say that it goes in through Plinth's calendar view, and say that ` + `a person makes that change rather than you. Say the reason too, because a date ` + `left looking handled is worse than one openly outstanding: a calendar write ` + `goes out to everyone invited the moment it lands, and it cannot be recalled. ` + `This is not a puzzle to route around. Do not reach for a shell command line to ` + `do it, the Google Workspace one included, and do not go looking for another ` + `tool that writes a calendar. Those tools are denied to you wherever Plinth has ` + `configured this workspace, and where a deny has not reached, the rule is the ` + `same and you are the only thing holding it.`,
|
|
26843
|
+
``,
|
|
26844
|
+
`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, and neither is any shell command line ` + `that does the same thing. Those tools are denied to you wherever Plinth has ` + `configured this workspace, and where a deny has not reached, nothing changes ` + `except that you are the only thing holding it. 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.`,
|
|
26845
|
+
``,
|
|
26846
|
+
`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 the Voice section of your own \`user-memory.md\` holds the personal ` + `part, 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.`
|
|
26778
26847
|
];
|
|
26779
26848
|
if (workspaceRules !== null) {
|
|
26780
26849
|
block.push(``, `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:
|
|
@@ -26851,7 +26920,8 @@ var FileStateEntrySchema = exports_external.object({
|
|
|
26851
26920
|
size_bytes: exports_external.number().int().nonnegative(),
|
|
26852
26921
|
mime_type: exports_external.string().min(1),
|
|
26853
26922
|
updated_at: exports_external.string().datetime({ offset: true }),
|
|
26854
|
-
inode: exports_external.number().int().nonnegative().optional()
|
|
26923
|
+
inode: exports_external.number().int().nonnegative().optional(),
|
|
26924
|
+
text_hash: exports_external.string().min(1).optional()
|
|
26855
26925
|
});
|
|
26856
26926
|
var FileEntriesSchema = exports_external.record(exports_external.string(), FileStateEntrySchema);
|
|
26857
26927
|
var WorkspaceRulesStateSchema = exports_external.object({
|
|
@@ -26895,7 +26965,8 @@ var WorkspaceStateV2Schema = exports_external.object({
|
|
|
26895
26965
|
rejected: exports_external.array(RejectedWriteSchema).optional(),
|
|
26896
26966
|
workspace_rules: WorkspaceRulesStateSchema.optional(),
|
|
26897
26967
|
workspace_memory: WorkspaceMemoryStateSchema.optional(),
|
|
26898
|
-
user_memory: UserMemoryStateSchema.optional()
|
|
26968
|
+
user_memory: UserMemoryStateSchema.optional(),
|
|
26969
|
+
derived_text_version: exports_external.number().int().nonnegative().optional()
|
|
26899
26970
|
});
|
|
26900
26971
|
var WorkspaceStateV1Schema = exports_external.object({
|
|
26901
26972
|
version: exports_external.literal(1),
|
|
@@ -27745,148 +27816,787 @@ var loginCommand = defineCommand({
|
|
|
27745
27816
|
}
|
|
27746
27817
|
});
|
|
27747
27818
|
|
|
27748
|
-
// src/lib/
|
|
27749
|
-
import {
|
|
27819
|
+
// src/lib/memory-gate/log.ts
|
|
27820
|
+
import {
|
|
27821
|
+
appendFileSync,
|
|
27822
|
+
closeSync,
|
|
27823
|
+
mkdirSync as mkdirSync7,
|
|
27824
|
+
openSync,
|
|
27825
|
+
readFileSync as readFileSync8,
|
|
27826
|
+
readSync,
|
|
27827
|
+
statSync as statSync2
|
|
27828
|
+
} from "node:fs";
|
|
27829
|
+
import { join as join7 } from "node:path";
|
|
27750
27830
|
|
|
27751
|
-
// src/lib/
|
|
27752
|
-
|
|
27753
|
-
|
|
27754
|
-
|
|
27755
|
-
|
|
27756
|
-
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
{
|
|
27764
|
-
label: "threads + thread_messages",
|
|
27765
|
-
blurb: "threaded discussions, one folder per thread anchored at " + "`threads/<slug>/_thread.md`; messages live in Plinth, not the mirror, and " + "the anchor's own body is a local scratchpad that never pushes",
|
|
27766
|
-
file: true
|
|
27767
|
-
},
|
|
27768
|
-
{ label: "key_dates", blurb: "calendar milestones and release dates", file: false },
|
|
27769
|
-
{ label: "comments", blurb: "comments anchored to any entity", file: false },
|
|
27770
|
-
{ label: "tracks", blurb: "track and release metadata", file: false },
|
|
27771
|
-
{ label: "social_posts", blurb: "planned and published social content", file: false },
|
|
27772
|
-
{ label: "useful_links", blurb: "bookmarked links per artist or workspace", file: false },
|
|
27773
|
-
{ label: "financial_entries", blurb: "income and cost ledger rows", file: false },
|
|
27774
|
-
{ label: "commissions", blurb: "agent, sync, streaming, publishing & other statements", file: false }
|
|
27775
|
-
];
|
|
27776
|
-
var MCP_INTROSPECTION_POINTER = "ask the `mcp__plinth__*` tools for exact fields & enum values (the " + "platform is the schema source of truth)";
|
|
27777
|
-
var MCP_AVAILABILITY_POINTER = "Other MCP servers may be connected on this machine: `claude mcp list` " + "names them and their connection status. Plinth neither configures nor " + "ships them.";
|
|
27778
|
-
var SKILL_LOCATIONS = [
|
|
27779
|
-
{
|
|
27780
|
-
label: "workspace",
|
|
27781
|
-
detail: "`.claude/commands/` in this folder; this machine only, the daemon does not sync it"
|
|
27782
|
-
},
|
|
27783
|
-
{ label: "personal", detail: "`~/.claude/commands/`; your own, available everywhere" }
|
|
27784
|
-
];
|
|
27785
|
-
var SECURITY_NOTE = "Scoped to a single Plinth workspace: do not read or write across " + "workspace folders; each has its own token and isolation boundary.";
|
|
27831
|
+
// src/lib/sessions/registry.ts
|
|
27832
|
+
import {
|
|
27833
|
+
mkdirSync as mkdirSync6,
|
|
27834
|
+
readdirSync as readdirSync3,
|
|
27835
|
+
readFileSync as readFileSync7,
|
|
27836
|
+
renameSync as renameSync6,
|
|
27837
|
+
statSync,
|
|
27838
|
+
unlinkSync as unlinkSync6,
|
|
27839
|
+
writeFileSync as writeFileSync6
|
|
27840
|
+
} from "node:fs";
|
|
27841
|
+
import { execFileSync } from "node:child_process";
|
|
27842
|
+
import { basename, dirname as dirname5, join as join6 } from "node:path";
|
|
27786
27843
|
|
|
27787
|
-
// src/lib/
|
|
27788
|
-
|
|
27789
|
-
|
|
27790
|
-
var
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
27844
|
+
// src/lib/daemon-pid.ts
|
|
27845
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27846
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
27847
|
+
var DaemonPidSchema = exports_external.object({
|
|
27848
|
+
version: exports_external.literal(1),
|
|
27849
|
+
pid: exports_external.number().int().positive(),
|
|
27850
|
+
workspace_slug: exports_external.string().min(1),
|
|
27851
|
+
started_at: exports_external.string().min(1)
|
|
27852
|
+
});
|
|
27853
|
+
function daemonPidPath(workspaceSlug) {
|
|
27854
|
+
return join5(plinthHome(), `daemon-${workspaceSlug}.pid`);
|
|
27855
|
+
}
|
|
27856
|
+
function daemonPidTmpPath(workspaceSlug) {
|
|
27857
|
+
return join5(plinthHome(), `daemon-${workspaceSlug}.pid.tmp`);
|
|
27858
|
+
}
|
|
27859
|
+
function writeDaemonPid(workspaceSlug, pid, startedAt) {
|
|
27860
|
+
const file2 = {
|
|
27861
|
+
version: 1,
|
|
27862
|
+
pid,
|
|
27863
|
+
workspace_slug: workspaceSlug,
|
|
27864
|
+
started_at: startedAt.toISOString()
|
|
27865
|
+
};
|
|
27866
|
+
const target = daemonPidPath(workspaceSlug);
|
|
27867
|
+
const tmp = daemonPidTmpPath(workspaceSlug);
|
|
27868
|
+
mkdirSync5(dirname4(target), { recursive: true, mode: 448 });
|
|
27869
|
+
writeFileSync5(tmp, JSON.stringify(file2, null, 2) + `
|
|
27870
|
+
`, { mode: 384 });
|
|
27871
|
+
try {
|
|
27872
|
+
renameSync5(tmp, target);
|
|
27873
|
+
} catch (err) {
|
|
27874
|
+
try {
|
|
27875
|
+
unlinkSync5(tmp);
|
|
27876
|
+
} catch {}
|
|
27877
|
+
throw err;
|
|
27878
|
+
}
|
|
27879
|
+
}
|
|
27880
|
+
function readDaemonPid(workspaceSlug) {
|
|
27881
|
+
let raw;
|
|
27882
|
+
try {
|
|
27883
|
+
raw = readFileSync6(daemonPidPath(workspaceSlug), "utf8");
|
|
27884
|
+
} catch (err) {
|
|
27885
|
+
if (err?.code === "ENOENT")
|
|
27886
|
+
return null;
|
|
27887
|
+
throw err;
|
|
27888
|
+
}
|
|
27889
|
+
let json2;
|
|
27890
|
+
try {
|
|
27891
|
+
json2 = JSON.parse(raw);
|
|
27892
|
+
} catch {
|
|
27798
27893
|
return null;
|
|
27799
|
-
|
|
27894
|
+
}
|
|
27895
|
+
const parsed = DaemonPidSchema.safeParse(json2);
|
|
27896
|
+
return parsed.success ? parsed.data : null;
|
|
27800
27897
|
}
|
|
27801
|
-
function
|
|
27802
|
-
|
|
27803
|
-
|
|
27804
|
-
|
|
27898
|
+
function clearDaemonPid(workspaceSlug) {
|
|
27899
|
+
try {
|
|
27900
|
+
unlinkSync5(daemonPidPath(workspaceSlug));
|
|
27901
|
+
} catch (err) {
|
|
27902
|
+
if (err?.code === "ENOENT")
|
|
27903
|
+
return;
|
|
27904
|
+
throw err;
|
|
27805
27905
|
}
|
|
27806
|
-
|
|
27807
|
-
|
|
27808
|
-
|
|
27809
|
-
|
|
27906
|
+
}
|
|
27907
|
+
function isPidAlive(pid) {
|
|
27908
|
+
try {
|
|
27909
|
+
process.kill(pid, 0);
|
|
27910
|
+
return true;
|
|
27911
|
+
} catch (err) {
|
|
27912
|
+
return err?.code === "EPERM";
|
|
27810
27913
|
}
|
|
27811
|
-
|
|
27914
|
+
}
|
|
27915
|
+
function readLiveDaemon(workspaceSlug, aliveFn = isPidAlive) {
|
|
27916
|
+
const pidFile = readDaemonPid(workspaceSlug);
|
|
27917
|
+
if (!pidFile)
|
|
27918
|
+
return null;
|
|
27919
|
+
if (aliveFn(pidFile.pid))
|
|
27920
|
+
return pidFile;
|
|
27921
|
+
clearDaemonPid(workspaceSlug);
|
|
27922
|
+
return null;
|
|
27812
27923
|
}
|
|
27813
27924
|
|
|
27814
|
-
// src/lib/
|
|
27815
|
-
var
|
|
27816
|
-
|
|
27817
|
-
|
|
27818
|
-
|
|
27819
|
-
|
|
27820
|
-
|
|
27821
|
-
|
|
27822
|
-
|
|
27823
|
-
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
|
|
27925
|
+
// src/lib/sessions/registry.ts
|
|
27926
|
+
var SessionRecordSchema = exports_external.object({
|
|
27927
|
+
version: exports_external.literal(1),
|
|
27928
|
+
session_id: exports_external.string().min(1),
|
|
27929
|
+
pid: exports_external.number().int().positive(),
|
|
27930
|
+
pid_start_time: exports_external.string().min(1),
|
|
27931
|
+
cwd: exports_external.string().min(1),
|
|
27932
|
+
workspace_slug: exports_external.string().min(1),
|
|
27933
|
+
started_at: exports_external.string().min(1),
|
|
27934
|
+
transcript_path: exports_external.string().min(1).nullable(),
|
|
27935
|
+
tab_id: exports_external.string().min(1).nullable().default(null),
|
|
27936
|
+
ended_at: exports_external.string().min(1).nullable().default(null)
|
|
27937
|
+
});
|
|
27938
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
27939
|
+
function isSafeSegment(value) {
|
|
27940
|
+
return SAFE_SEGMENT.test(value) && !value.includes("..");
|
|
27828
27941
|
}
|
|
27829
|
-
function
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
lines.push(`<!-- Auto-generated by Plinth. Edit only between the BEGIN/END markers below; everything else is overwritten on sync. -->`);
|
|
27833
|
-
lines.push("");
|
|
27834
|
-
lines.push(`# ${name} · Plinth artist`);
|
|
27835
|
-
lines.push("");
|
|
27836
|
-
const statusPart = status ? `, status ${status}` : "";
|
|
27837
|
-
const bioPart = profile.bio ? ` ${profile.bio}` : "";
|
|
27838
|
-
lines.push(`${name} (artist \`${slug}\`${statusPart}).${bioPart}`);
|
|
27839
|
-
lines.push(`Working dir for ${name}-scoped AI sessions. ${SECURITY_NOTE}`);
|
|
27840
|
-
lines.push("");
|
|
27841
|
-
const links = buildLinks(profile);
|
|
27842
|
-
if (links) {
|
|
27843
|
-
lines.push(`## Links`);
|
|
27844
|
-
lines.push(links);
|
|
27845
|
-
lines.push("");
|
|
27942
|
+
function sessionsDir(workspaceSlug) {
|
|
27943
|
+
if (!isSafeSegment(workspaceSlug)) {
|
|
27944
|
+
throw new Error(`Refusing unsafe workspace slug: ${workspaceSlug}`);
|
|
27846
27945
|
}
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
if (
|
|
27851
|
-
|
|
27852
|
-
} else {
|
|
27853
|
-
for (const p of sortedProjects) {
|
|
27854
|
-
const meta3 = [p.status, formatDateRange(p.startDate, p.endDate)].filter((s2) => s2 !== null && s2.length > 0).join(", ");
|
|
27855
|
-
lines.push(`- **${p.name}** (project \`${p.id}\`)${meta3 ? ` · ${meta3}` : ""}`);
|
|
27856
|
-
}
|
|
27946
|
+
return join6(plinthHome(), workspaceSlug, "sessions");
|
|
27947
|
+
}
|
|
27948
|
+
function sessionRecordPath(workspaceSlug, sessionId) {
|
|
27949
|
+
if (!isSafeSegment(sessionId)) {
|
|
27950
|
+
throw new Error(`Refusing unsafe session id: ${sessionId}`);
|
|
27857
27951
|
}
|
|
27858
|
-
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
lines.push(`- _+${remaining} more via \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}_`);
|
|
27873
|
-
}
|
|
27952
|
+
return join6(sessionsDir(workspaceSlug), `${sessionId}.json`);
|
|
27953
|
+
}
|
|
27954
|
+
function readPidStartTime(pid) {
|
|
27955
|
+
try {
|
|
27956
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
27957
|
+
encoding: "utf8",
|
|
27958
|
+
timeout: 5000,
|
|
27959
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27960
|
+
env: { ...process.env, TZ: "UTC" }
|
|
27961
|
+
});
|
|
27962
|
+
const collapsed = out.trim().replace(/\s+/g, " ");
|
|
27963
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
27964
|
+
} catch {
|
|
27965
|
+
return null;
|
|
27874
27966
|
}
|
|
27875
|
-
lines.push("");
|
|
27876
|
-
lines.push(`## More & companions`);
|
|
27877
|
-
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`);
|
|
27878
|
-
lines.push(`- Closed/archived, meetings & full lists: \`mcp__plinth__list_projects\` / \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}`);
|
|
27879
|
-
lines.push(`- Schema: ${MCP_INTROSPECTION_POINTER}`);
|
|
27880
|
-
lines.push(`- Long-term facts → \`./memory.md\` · daily activity → \`./daily-log.md\``);
|
|
27881
|
-
lines.push("");
|
|
27882
|
-
lines.push(USER_BLOCK_BEGIN);
|
|
27883
|
-
lines.push(USER_BLOCK_PLACEHOLDER);
|
|
27884
|
-
lines.push(USER_BLOCK_END);
|
|
27885
|
-
return lines.join(`
|
|
27886
|
-
`) + `
|
|
27887
|
-
`;
|
|
27888
27967
|
}
|
|
27889
|
-
function
|
|
27968
|
+
function registerSession(input) {
|
|
27969
|
+
const startTime = (input.startTimeFn ?? readPidStartTime)(input.pid);
|
|
27970
|
+
if (startTime === null)
|
|
27971
|
+
return null;
|
|
27972
|
+
const record2 = {
|
|
27973
|
+
version: 1,
|
|
27974
|
+
session_id: input.sessionId,
|
|
27975
|
+
pid: input.pid,
|
|
27976
|
+
pid_start_time: startTime,
|
|
27977
|
+
cwd: input.cwd,
|
|
27978
|
+
workspace_slug: input.workspaceSlug,
|
|
27979
|
+
started_at: (input.now ?? new Date).toISOString(),
|
|
27980
|
+
transcript_path: input.transcriptPath,
|
|
27981
|
+
tab_id: input.tabId ?? null,
|
|
27982
|
+
ended_at: null
|
|
27983
|
+
};
|
|
27984
|
+
writeRecordAtomic(sessionRecordPath(input.workspaceSlug, input.sessionId), record2);
|
|
27985
|
+
return record2;
|
|
27986
|
+
}
|
|
27987
|
+
var tmpCounter = 0;
|
|
27988
|
+
function writeRecordAtomic(target, record2) {
|
|
27989
|
+
const dir = dirname5(target);
|
|
27990
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
27991
|
+
const tmp = `${target}.${process.pid}.${tmpCounter++}.tmp`;
|
|
27992
|
+
sweepDeadTmpFiles(dir, target);
|
|
27993
|
+
writeFileSync6(tmp, JSON.stringify(record2, null, 2) + `
|
|
27994
|
+
`, { mode: 384 });
|
|
27995
|
+
try {
|
|
27996
|
+
renameSync6(tmp, target);
|
|
27997
|
+
} catch (err) {
|
|
27998
|
+
try {
|
|
27999
|
+
unlinkSync6(tmp);
|
|
28000
|
+
} catch {}
|
|
28001
|
+
throw err;
|
|
28002
|
+
}
|
|
28003
|
+
}
|
|
28004
|
+
function sweepDeadTmpFiles(dir, target) {
|
|
28005
|
+
const prefix = `${basename(target)}.`;
|
|
28006
|
+
let names;
|
|
28007
|
+
try {
|
|
28008
|
+
names = readdirSync3(dir);
|
|
28009
|
+
} catch {
|
|
28010
|
+
return;
|
|
28011
|
+
}
|
|
28012
|
+
for (const name of names) {
|
|
28013
|
+
if (!name.startsWith(prefix) || !name.endsWith(".tmp"))
|
|
28014
|
+
continue;
|
|
28015
|
+
const parts = name.slice(0, -".tmp".length).split(".");
|
|
28016
|
+
const pid = Number.parseInt(parts[parts.length - 2] ?? "", 10);
|
|
28017
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
28018
|
+
continue;
|
|
28019
|
+
if (pid === process.pid || isPidAlive(pid))
|
|
28020
|
+
continue;
|
|
28021
|
+
try {
|
|
28022
|
+
unlinkSync6(join6(dir, name));
|
|
28023
|
+
} catch {}
|
|
28024
|
+
}
|
|
28025
|
+
}
|
|
28026
|
+
function endSession(workspaceSlug, sessionId, now = new Date, deps = {}) {
|
|
28027
|
+
const read = deps.readFn ?? readSessionRecord;
|
|
28028
|
+
const path3 = sessionRecordPath(workspaceSlug, sessionId);
|
|
28029
|
+
const current = read(path3);
|
|
28030
|
+
if (current === null)
|
|
28031
|
+
return null;
|
|
28032
|
+
const latest = read(path3);
|
|
28033
|
+
if (latest === null || latest.pid !== current.pid || latest.pid_start_time !== current.pid_start_time) {
|
|
28034
|
+
return null;
|
|
28035
|
+
}
|
|
28036
|
+
const record2 = { ...latest, ended_at: now.toISOString() };
|
|
28037
|
+
writeRecordAtomic(path3, record2);
|
|
28038
|
+
return record2;
|
|
28039
|
+
}
|
|
28040
|
+
function readSessionRecord(path3) {
|
|
28041
|
+
let raw;
|
|
28042
|
+
try {
|
|
28043
|
+
raw = readFileSync7(path3, "utf8");
|
|
28044
|
+
} catch {
|
|
28045
|
+
return null;
|
|
28046
|
+
}
|
|
28047
|
+
let json2;
|
|
28048
|
+
try {
|
|
28049
|
+
json2 = JSON.parse(raw);
|
|
28050
|
+
} catch {
|
|
28051
|
+
return null;
|
|
28052
|
+
}
|
|
28053
|
+
const parsed = SessionRecordSchema.safeParse(json2);
|
|
28054
|
+
return parsed.success ? parsed.data : null;
|
|
28055
|
+
}
|
|
28056
|
+
function sessionLiveness(record2, deps = {}) {
|
|
28057
|
+
if (!(deps.aliveFn ?? isPidAlive)(record2.pid))
|
|
28058
|
+
return "dead";
|
|
28059
|
+
const startTime = (deps.startTimeFn ?? readPidStartTime)(record2.pid);
|
|
28060
|
+
if (startTime === null)
|
|
28061
|
+
return "unknown";
|
|
28062
|
+
return startTime === record2.pid_start_time ? "live" : "dead";
|
|
28063
|
+
}
|
|
28064
|
+
function transcriptMtime(path3) {
|
|
28065
|
+
try {
|
|
28066
|
+
return statSync(path3).mtimeMs;
|
|
28067
|
+
} catch {
|
|
28068
|
+
return null;
|
|
28069
|
+
}
|
|
28070
|
+
}
|
|
28071
|
+
function listSessions(workspaceSlug, deps = {}) {
|
|
28072
|
+
let names;
|
|
28073
|
+
try {
|
|
28074
|
+
names = readdirSync3(sessionsDir(workspaceSlug));
|
|
28075
|
+
} catch {
|
|
28076
|
+
return { live: [], reaped: 0, unknown: 0, ended: 0 };
|
|
28077
|
+
}
|
|
28078
|
+
const live = [];
|
|
28079
|
+
let reaped = 0;
|
|
28080
|
+
let unknown2 = 0;
|
|
28081
|
+
let ended = 0;
|
|
28082
|
+
for (const name of names) {
|
|
28083
|
+
if (!name.endsWith(".json"))
|
|
28084
|
+
continue;
|
|
28085
|
+
const path3 = join6(sessionsDir(workspaceSlug), name);
|
|
28086
|
+
const record2 = readSessionRecord(path3);
|
|
28087
|
+
if (!record2) {
|
|
28088
|
+
continue;
|
|
28089
|
+
}
|
|
28090
|
+
const verdict = sessionLiveness(record2, deps);
|
|
28091
|
+
if (verdict === "unknown") {
|
|
28092
|
+
unknown2 += 1;
|
|
28093
|
+
continue;
|
|
28094
|
+
}
|
|
28095
|
+
if (verdict === "dead") {
|
|
28096
|
+
const current = readSessionRecord(path3);
|
|
28097
|
+
const sameRecord = current !== null && current.pid === record2.pid && current.pid_start_time === record2.pid_start_time;
|
|
28098
|
+
if (sameRecord) {
|
|
28099
|
+
try {
|
|
28100
|
+
unlinkSync6(path3);
|
|
28101
|
+
reaped += 1;
|
|
28102
|
+
} catch {}
|
|
28103
|
+
}
|
|
28104
|
+
continue;
|
|
28105
|
+
}
|
|
28106
|
+
if (record2.ended_at !== null) {
|
|
28107
|
+
ended += 1;
|
|
28108
|
+
continue;
|
|
28109
|
+
}
|
|
28110
|
+
const mtime = record2.transcript_path ? (deps.mtimeFn ?? transcriptMtime)(record2.transcript_path) : null;
|
|
28111
|
+
live.push({ record: record2, lastActivityMs: mtime });
|
|
28112
|
+
}
|
|
28113
|
+
live.sort((a2, b2) => (b2.lastActivityMs ?? -1) - (a2.lastActivityMs ?? -1));
|
|
28114
|
+
return { live, reaped, unknown: unknown2, ended };
|
|
28115
|
+
}
|
|
28116
|
+
|
|
28117
|
+
// src/lib/memory-gate/log.ts
|
|
28118
|
+
var MEMORY_GATE_DECISIONS = ["accept", "reject", "edit"];
|
|
28119
|
+
var Common = {
|
|
28120
|
+
version: exports_external.literal(1),
|
|
28121
|
+
at: exports_external.string().min(1),
|
|
28122
|
+
session_id: exports_external.string().min(1).nullable(),
|
|
28123
|
+
member: exports_external.string().min(1).nullable()
|
|
28124
|
+
};
|
|
28125
|
+
var DecisionRecordSchema = exports_external.object({
|
|
28126
|
+
...Common,
|
|
28127
|
+
kind: exports_external.literal("decision"),
|
|
28128
|
+
proposed: exports_external.string().min(1),
|
|
28129
|
+
decision: exports_external.enum(MEMORY_GATE_DECISIONS),
|
|
28130
|
+
final: exports_external.string().min(1).nullable()
|
|
28131
|
+
});
|
|
28132
|
+
var NothingProposedRecordSchema = exports_external.object({
|
|
28133
|
+
...Common,
|
|
28134
|
+
kind: exports_external.literal("nothing-proposed")
|
|
28135
|
+
});
|
|
28136
|
+
var MemoryGateRecordSchema = exports_external.discriminatedUnion("kind", [
|
|
28137
|
+
DecisionRecordSchema,
|
|
28138
|
+
NothingProposedRecordSchema
|
|
28139
|
+
]);
|
|
28140
|
+
function memoryGateDir(workspaceSlug) {
|
|
28141
|
+
if (!isSafeSegment(workspaceSlug)) {
|
|
28142
|
+
throw new Error(`Refusing unsafe workspace slug: ${workspaceSlug}`);
|
|
28143
|
+
}
|
|
28144
|
+
return join7(plinthHome(), "_memory_gate", workspaceSlug);
|
|
28145
|
+
}
|
|
28146
|
+
function memoryGateLogPath(workspaceSlug) {
|
|
28147
|
+
return join7(memoryGateDir(workspaceSlug), "log.jsonl");
|
|
28148
|
+
}
|
|
28149
|
+
function buildDecisionRecord(input) {
|
|
28150
|
+
const final = input.decision === "accept" ? input.proposed : input.decision === "reject" ? null : input.final ?? null;
|
|
28151
|
+
return {
|
|
28152
|
+
version: 1,
|
|
28153
|
+
kind: "decision",
|
|
28154
|
+
at: (input.now ?? new Date).toISOString(),
|
|
28155
|
+
session_id: input.sessionId,
|
|
28156
|
+
member: input.member,
|
|
28157
|
+
proposed: input.proposed,
|
|
28158
|
+
decision: input.decision,
|
|
28159
|
+
final
|
|
28160
|
+
};
|
|
28161
|
+
}
|
|
28162
|
+
function buildNothingProposedRecord(input) {
|
|
28163
|
+
return {
|
|
28164
|
+
version: 1,
|
|
28165
|
+
kind: "nothing-proposed",
|
|
28166
|
+
at: (input.now ?? new Date).toISOString(),
|
|
28167
|
+
session_id: input.sessionId,
|
|
28168
|
+
member: input.member
|
|
28169
|
+
};
|
|
28170
|
+
}
|
|
28171
|
+
function endsMidLine(path3) {
|
|
28172
|
+
let size;
|
|
28173
|
+
try {
|
|
28174
|
+
size = statSync2(path3).size;
|
|
28175
|
+
} catch {
|
|
28176
|
+
return false;
|
|
28177
|
+
}
|
|
28178
|
+
if (size === 0)
|
|
28179
|
+
return false;
|
|
28180
|
+
const fd = openSync(path3, "r");
|
|
28181
|
+
try {
|
|
28182
|
+
const last = Buffer.alloc(1);
|
|
28183
|
+
readSync(fd, last, 0, 1, size - 1);
|
|
28184
|
+
return last[0] !== 10;
|
|
28185
|
+
} finally {
|
|
28186
|
+
closeSync(fd);
|
|
28187
|
+
}
|
|
28188
|
+
}
|
|
28189
|
+
function appendMemoryGateRecord(workspaceSlug, record2) {
|
|
28190
|
+
const checked = MemoryGateRecordSchema.parse(record2);
|
|
28191
|
+
const path3 = memoryGateLogPath(workspaceSlug);
|
|
28192
|
+
mkdirSync7(memoryGateDir(workspaceSlug), { recursive: true, mode: 448 });
|
|
28193
|
+
const prefix = endsMidLine(path3) ? `
|
|
28194
|
+
` : "";
|
|
28195
|
+
appendFileSync(path3, `${prefix}${JSON.stringify(checked)}
|
|
28196
|
+
`, { mode: 384 });
|
|
28197
|
+
}
|
|
28198
|
+
function readMemoryGateLog(workspaceSlug) {
|
|
28199
|
+
let raw;
|
|
28200
|
+
try {
|
|
28201
|
+
raw = readFileSync8(memoryGateLogPath(workspaceSlug), "utf8");
|
|
28202
|
+
} catch {
|
|
28203
|
+
return { records: [], skipped: 0 };
|
|
28204
|
+
}
|
|
28205
|
+
const records = [];
|
|
28206
|
+
let skipped = 0;
|
|
28207
|
+
for (const line of raw.split(`
|
|
28208
|
+
`)) {
|
|
28209
|
+
if (line.trim().length === 0)
|
|
28210
|
+
continue;
|
|
28211
|
+
let json2;
|
|
28212
|
+
try {
|
|
28213
|
+
json2 = JSON.parse(line);
|
|
28214
|
+
} catch {
|
|
28215
|
+
skipped += 1;
|
|
28216
|
+
continue;
|
|
28217
|
+
}
|
|
28218
|
+
const parsed = MemoryGateRecordSchema.safeParse(json2);
|
|
28219
|
+
if (parsed.success)
|
|
28220
|
+
records.push(parsed.data);
|
|
28221
|
+
else
|
|
28222
|
+
skipped += 1;
|
|
28223
|
+
}
|
|
28224
|
+
return { records, skipped };
|
|
28225
|
+
}
|
|
28226
|
+
|
|
28227
|
+
// src/commands/memory-gate.ts
|
|
28228
|
+
init_output();
|
|
28229
|
+
function resolveTarget2(explicitSlug, explicitMember, loadConfigFn) {
|
|
28230
|
+
let config2;
|
|
28231
|
+
try {
|
|
28232
|
+
config2 = loadConfigFn();
|
|
28233
|
+
} catch {
|
|
28234
|
+
return { ok: false, message: "Config at ~/.plinth/config.json could not be read. Run `plinth login --workspace <slug>` first." };
|
|
28235
|
+
}
|
|
28236
|
+
if (!config2) {
|
|
28237
|
+
return { ok: false, message: "No workspace configured. Run `plinth login --workspace <slug>` first." };
|
|
28238
|
+
}
|
|
28239
|
+
const slug = explicitSlug ?? config2.active_workspace;
|
|
28240
|
+
const entry2 = config2.workspaces[slug];
|
|
28241
|
+
if (!entry2) {
|
|
28242
|
+
return {
|
|
28243
|
+
ok: false,
|
|
28244
|
+
message: explicitSlug === undefined ? "No workspace configured. Run `plinth login --workspace <slug>` first." : `Workspace "${slug}" not found. Run \`plinth login --workspace ${slug}\` first.`
|
|
28245
|
+
};
|
|
28246
|
+
}
|
|
28247
|
+
return { ok: true, slug, member: explicitMember ?? entry2.user_id ?? null };
|
|
28248
|
+
}
|
|
28249
|
+
function isDecision(value) {
|
|
28250
|
+
return MEMORY_GATE_DECISIONS.includes(value);
|
|
28251
|
+
}
|
|
28252
|
+
async function runMemoryGateLog(args, deps = {}) {
|
|
28253
|
+
const out = new Output({ sink: deps.sink, noColor: deps.noColor ?? args.noColor });
|
|
28254
|
+
const env3 = deps.env ?? process.env;
|
|
28255
|
+
const now = (deps.now ?? (() => new Date))();
|
|
28256
|
+
const nothing = args.nothingProposed === true;
|
|
28257
|
+
const hasDecisionArgs = args.proposed !== undefined || args.decision !== undefined;
|
|
28258
|
+
if (nothing && hasDecisionArgs) {
|
|
28259
|
+
out.error("--nothing-proposed records a close that proposed nothing; it cannot be combined with --proposed/--decision.");
|
|
28260
|
+
return { exitCode: 1 };
|
|
28261
|
+
}
|
|
28262
|
+
if (!nothing && !hasDecisionArgs) {
|
|
28263
|
+
out.error("Nothing to log. Pass --proposed and --decision for one decision, or --nothing-proposed for a close that had nothing to bank.");
|
|
28264
|
+
return { exitCode: 1 };
|
|
28265
|
+
}
|
|
28266
|
+
const target = resolveTarget2(args.workspace, args.member, deps.loadConfigFn ?? loadConfig);
|
|
28267
|
+
if (!target.ok) {
|
|
28268
|
+
out.error(target.message);
|
|
28269
|
+
return { exitCode: 1 };
|
|
28270
|
+
}
|
|
28271
|
+
const sessionId = typeof env3.CLAUDE_CODE_SESSION_ID === "string" && env3.CLAUDE_CODE_SESSION_ID.length > 0 ? env3.CLAUDE_CODE_SESSION_ID : null;
|
|
28272
|
+
const append = deps.appendFn ?? appendMemoryGateRecord;
|
|
28273
|
+
if (nothing) {
|
|
28274
|
+
try {
|
|
28275
|
+
append(target.slug, buildNothingProposedRecord({ sessionId, member: target.member, now }));
|
|
28276
|
+
} catch (err) {
|
|
28277
|
+
out.error(`Could not write the memory-gate log: ${err.message}`);
|
|
28278
|
+
return { exitCode: 1 };
|
|
28279
|
+
}
|
|
28280
|
+
out.info("Logged: this close proposed nothing for shared memory.");
|
|
28281
|
+
return { exitCode: 0 };
|
|
28282
|
+
}
|
|
28283
|
+
const proposed = args.proposed?.trim() ?? "";
|
|
28284
|
+
if (proposed.length === 0) {
|
|
28285
|
+
out.error("--proposed must carry the line as it was put to the user.");
|
|
28286
|
+
return { exitCode: 1 };
|
|
28287
|
+
}
|
|
28288
|
+
const decisionRaw = args.decision?.trim() ?? "";
|
|
28289
|
+
if (!isDecision(decisionRaw)) {
|
|
28290
|
+
out.error(`--decision must be one of ${MEMORY_GATE_DECISIONS.join(", ")} (got ${decisionRaw === "" ? "nothing" : `"${decisionRaw}"`}).`);
|
|
28291
|
+
return { exitCode: 1 };
|
|
28292
|
+
}
|
|
28293
|
+
const finalArg = args.final?.trim() ?? "";
|
|
28294
|
+
if (decisionRaw === "edit" && finalArg.length === 0) {
|
|
28295
|
+
out.error("--decision edit needs --final: the user's rewritten line is the record.");
|
|
28296
|
+
return { exitCode: 1 };
|
|
28297
|
+
}
|
|
28298
|
+
if (decisionRaw !== "edit" && finalArg.length > 0) {
|
|
28299
|
+
out.error(`--final applies to --decision edit only (accept records the proposed line, reject records none).`);
|
|
28300
|
+
return { exitCode: 1 };
|
|
28301
|
+
}
|
|
28302
|
+
try {
|
|
28303
|
+
append(target.slug, buildDecisionRecord({
|
|
28304
|
+
proposed,
|
|
28305
|
+
decision: decisionRaw,
|
|
28306
|
+
final: finalArg.length > 0 ? finalArg : null,
|
|
28307
|
+
sessionId,
|
|
28308
|
+
member: target.member,
|
|
28309
|
+
now
|
|
28310
|
+
}));
|
|
28311
|
+
} catch (err) {
|
|
28312
|
+
out.error(`Could not write the memory-gate log: ${err.message}`);
|
|
28313
|
+
return { exitCode: 1 };
|
|
28314
|
+
}
|
|
28315
|
+
out.info(`Logged ${decisionRaw} for shared memory (${target.slug}).`);
|
|
28316
|
+
return { exitCode: 0 };
|
|
28317
|
+
}
|
|
28318
|
+
async function runMemoryGateShow(args, deps = {}) {
|
|
28319
|
+
const out = new Output({ sink: deps.sink, noColor: deps.noColor ?? args.noColor });
|
|
28320
|
+
const target = resolveTarget2(args.workspace, args.member, deps.loadConfigFn ?? loadConfig);
|
|
28321
|
+
if (!target.ok) {
|
|
28322
|
+
out.error(target.message);
|
|
28323
|
+
return { exitCode: 1 };
|
|
28324
|
+
}
|
|
28325
|
+
const { records, skipped } = (deps.readFn ?? readMemoryGateLog)(target.slug);
|
|
28326
|
+
const limit = args.limit !== undefined && args.limit > 0 ? args.limit : undefined;
|
|
28327
|
+
const shown = limit === undefined ? records : records.slice(-limit);
|
|
28328
|
+
if (args.json) {
|
|
28329
|
+
out.info(JSON.stringify({
|
|
28330
|
+
workspace_slug: target.slug,
|
|
28331
|
+
path: memoryGateLogPath(target.slug),
|
|
28332
|
+
records: shown,
|
|
28333
|
+
withheld: records.length - shown.length,
|
|
28334
|
+
unreadable: skipped
|
|
28335
|
+
}, null, 2));
|
|
28336
|
+
return { exitCode: 0 };
|
|
28337
|
+
}
|
|
28338
|
+
if (shown.length === 0) {
|
|
28339
|
+
out.info(`No memory-gate records for ${target.slug}.`);
|
|
28340
|
+
}
|
|
28341
|
+
for (const record2 of shown) {
|
|
28342
|
+
if (record2.kind === "nothing-proposed") {
|
|
28343
|
+
out.info(`${record2.at} · nothing proposed`);
|
|
28344
|
+
continue;
|
|
28345
|
+
}
|
|
28346
|
+
const tail = record2.final === null ? "" : ` → ${record2.final}`;
|
|
28347
|
+
out.info(`${record2.at} · ${record2.decision} · ${record2.proposed}${tail}`);
|
|
28348
|
+
}
|
|
28349
|
+
if (records.length - shown.length > 0) {
|
|
28350
|
+
out.info(`${records.length - shown.length} earlier record(s) not shown (--limit).`);
|
|
28351
|
+
}
|
|
28352
|
+
if (skipped > 0) {
|
|
28353
|
+
out.info(`${skipped} line(s) could not be read and are not counted above.`);
|
|
28354
|
+
}
|
|
28355
|
+
return { exitCode: 0 };
|
|
28356
|
+
}
|
|
28357
|
+
var memoryGateCommand = defineCommand({
|
|
28358
|
+
meta: {
|
|
28359
|
+
name: "memory-gate",
|
|
28360
|
+
description: "Record what a /close ritual proposed for shared memory, and what the user decided"
|
|
28361
|
+
},
|
|
28362
|
+
subCommands: {
|
|
28363
|
+
log: defineCommand({
|
|
28364
|
+
meta: {
|
|
28365
|
+
name: "log",
|
|
28366
|
+
description: "Append one decision (or a nothing-proposed close) to the local memory-gate log. Machine-local: nothing under ~/.plinth syncs."
|
|
28367
|
+
},
|
|
28368
|
+
args: {
|
|
28369
|
+
workspace: { type: "string", description: "Workspace slug" },
|
|
28370
|
+
proposed: { type: "string", description: "The line as it was put to the user" },
|
|
28371
|
+
decision: { type: "string", description: `One of ${MEMORY_GATE_DECISIONS.join(", ")}` },
|
|
28372
|
+
final: { type: "string", description: "The user's rewritten line (--decision edit only)" },
|
|
28373
|
+
"nothing-proposed": {
|
|
28374
|
+
type: "boolean",
|
|
28375
|
+
description: "Record a close that proposed nothing for shared memory"
|
|
28376
|
+
},
|
|
28377
|
+
member: { type: "string", description: "Override the member id (defaults to this login's user)" },
|
|
28378
|
+
"no-color": { type: "boolean", description: "Disable colour" }
|
|
28379
|
+
},
|
|
28380
|
+
async run({ args }) {
|
|
28381
|
+
const result = await runMemoryGateLog({
|
|
28382
|
+
workspace: args.workspace,
|
|
28383
|
+
proposed: args.proposed,
|
|
28384
|
+
decision: args.decision,
|
|
28385
|
+
final: args.final,
|
|
28386
|
+
nothingProposed: args["nothing-proposed"],
|
|
28387
|
+
member: args.member
|
|
28388
|
+
}, { noColor: noColorFromArgs(args) });
|
|
28389
|
+
process.exitCode = result.exitCode;
|
|
28390
|
+
}
|
|
28391
|
+
}),
|
|
28392
|
+
show: defineCommand({
|
|
28393
|
+
meta: {
|
|
28394
|
+
name: "show",
|
|
28395
|
+
description: "Read the local memory-gate log back. Machine-local: nothing under ~/.plinth syncs."
|
|
28396
|
+
},
|
|
28397
|
+
args: {
|
|
28398
|
+
workspace: { type: "string", description: "Workspace slug" },
|
|
28399
|
+
json: { type: "boolean", description: "Machine-readable output" },
|
|
28400
|
+
limit: { type: "string", description: "Show only the most recent N records" },
|
|
28401
|
+
"no-color": { type: "boolean", description: "Disable colour" }
|
|
28402
|
+
},
|
|
28403
|
+
async run({ args }) {
|
|
28404
|
+
const rawLimit = args.limit;
|
|
28405
|
+
const parsed = rawLimit === undefined ? undefined : Number.parseInt(rawLimit, 10);
|
|
28406
|
+
const result = await runMemoryGateShow({
|
|
28407
|
+
workspace: args.workspace,
|
|
28408
|
+
json: args.json,
|
|
28409
|
+
limit: parsed !== undefined && Number.isInteger(parsed) ? parsed : undefined
|
|
28410
|
+
}, { noColor: noColorFromArgs(args) });
|
|
28411
|
+
process.exitCode = result.exitCode;
|
|
28412
|
+
}
|
|
28413
|
+
})
|
|
28414
|
+
}
|
|
28415
|
+
});
|
|
28416
|
+
|
|
28417
|
+
// src/lib/context/artist-context.ts
|
|
28418
|
+
import { existsSync } from "node:fs";
|
|
28419
|
+
|
|
28420
|
+
// src/lib/context/command-paths.ts
|
|
28421
|
+
var WORKSPACE_COMMANDS_DIR = ".claude/commands/plinth";
|
|
28422
|
+
var WORKSPACE_COMMANDS_SUBFOLDER = WORKSPACE_COMMANDS_DIR.split("/").pop() ?? "";
|
|
28423
|
+
var START_COMMAND_REL = `${WORKSPACE_COMMANDS_DIR}/start.md`;
|
|
28424
|
+
var CLOSE_COMMAND_REL = `${WORKSPACE_COMMANDS_DIR}/close.md`;
|
|
28425
|
+
function invocationFor(relPath) {
|
|
28426
|
+
const parts = relPath.split("/");
|
|
28427
|
+
const name = (parts.pop() ?? "").replace(/\.md$/, "");
|
|
28428
|
+
const namespace = parts.pop() ?? "";
|
|
28429
|
+
return `/${namespace}:${name}`;
|
|
28430
|
+
}
|
|
28431
|
+
var START_COMMAND_INVOCATION = invocationFor(START_COMMAND_REL);
|
|
28432
|
+
var CLOSE_COMMAND_INVOCATION = invocationFor(CLOSE_COMMAND_REL);
|
|
28433
|
+
|
|
28434
|
+
// src/lib/context/entity-catalog.ts
|
|
28435
|
+
var ENTITY_CATALOG = [
|
|
28436
|
+
{ label: "artists", blurb: "roster acts; one folder per artist under `artists/<slug>/`", file: true },
|
|
28437
|
+
{ label: "projects", blurb: "campaigns, releases, and workstreams", file: true },
|
|
28438
|
+
{ label: "tasks", blurb: "actionable to-dos, optionally tied to an artist or project", file: true },
|
|
28439
|
+
{ label: "meetings + meeting_notes", blurb: "meeting records and their attached notes", file: true },
|
|
28440
|
+
{ label: "documents", blurb: "notes, briefs, bios, transcripts, context docs (markdown bodies, not attachments)", file: true },
|
|
28441
|
+
{
|
|
28442
|
+
label: "workspace_files",
|
|
28443
|
+
blurb: "the free-form document store: PDFs, executed agreements, spreadsheets, " + "anything that is a FILE rather than an entity. Mirrored byte-for-byte " + "both ways under `files/`, folders and all. Look here for contracts",
|
|
28444
|
+
file: true
|
|
28445
|
+
},
|
|
28446
|
+
{
|
|
28447
|
+
label: "threads + thread_messages",
|
|
28448
|
+
blurb: "threaded discussions, one folder per thread anchored at " + "`threads/<slug>/_thread.md`; messages live in Plinth, not the mirror, and " + "the anchor's own body is a local scratchpad that never pushes",
|
|
28449
|
+
file: true
|
|
28450
|
+
},
|
|
28451
|
+
{ label: "key_dates", blurb: "calendar milestones and release dates", file: false },
|
|
28452
|
+
{ label: "comments", blurb: "comments anchored to any entity", file: false },
|
|
28453
|
+
{ label: "tracks", blurb: "track and release metadata", file: false },
|
|
28454
|
+
{ label: "social_posts", blurb: "planned and published social content", file: false },
|
|
28455
|
+
{ label: "useful_links", blurb: "bookmarked links per artist or workspace", file: false },
|
|
28456
|
+
{ label: "financial_entries", blurb: "income and cost ledger rows", file: false },
|
|
28457
|
+
{ label: "commissions", blurb: "agent, sync, streaming, publishing & other statements", file: false }
|
|
28458
|
+
];
|
|
28459
|
+
var MCP_INTROSPECTION_POINTER = "ask the `mcp__plinth__*` tools for exact fields & enum values (the " + "platform is the schema source of truth)";
|
|
28460
|
+
var MCP_AVAILABILITY_POINTER = "Other MCP servers may be connected on this machine: `claude mcp list` " + "names them and their connection status, though that command is blocked " + "inside the Plinth cockpit (it denies `Bash(claude:*)`), so in the cockpit " + "ask the user instead of concluding there are none. Plinth neither " + "configures nor ships them.";
|
|
28461
|
+
var SKILL_LOCATIONS = [
|
|
28462
|
+
{
|
|
28463
|
+
label: "workspace",
|
|
28464
|
+
detail: "`.claude/commands/` in this folder; this machine only, the daemon does not sync it. " + `Plinth generates two into \`${WORKSPACE_COMMANDS_SUBFOLDER}/\` on every sync: ` + `**${START_COMMAND_INVOCATION}** to open a session and **${CLOSE_COMMAND_INVOCATION}** to ` + "bank what it learned before the context is gone. Anything else in the folder is yours and is never touched"
|
|
28465
|
+
},
|
|
28466
|
+
{ label: "personal", detail: "`~/.claude/commands/`; your own, available everywhere" }
|
|
28467
|
+
];
|
|
28468
|
+
var SECURITY_NOTE = "Scoped to a single Plinth workspace: do not read or write across " + "workspace folders; each has its own token and isolation boundary.";
|
|
28469
|
+
|
|
28470
|
+
// src/lib/context/generated-at.ts
|
|
28471
|
+
var GENERATED_AT_PLACEHOLDER = "{{plinth-generated-at}}";
|
|
28472
|
+
var GENERATED_AT_PREFIX = "Content built ";
|
|
28473
|
+
var STAMP_RE = /\d{4}-\d{2}-\d{2} \d{2}:\d{2} [+-]\d{2}:\d{2}/;
|
|
28474
|
+
function escapeRegExp(literal2) {
|
|
28475
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28476
|
+
}
|
|
28477
|
+
var STAMP_SLOT_RE = new RegExp(escapeRegExp(GENERATED_AT_PREFIX) + "(?:" + STAMP_RE.source + "|" + escapeRegExp(GENERATED_AT_PLACEHOLDER) + ")");
|
|
28478
|
+
function pad2(n2) {
|
|
28479
|
+
return String(n2).padStart(2, "0");
|
|
28480
|
+
}
|
|
28481
|
+
function formatGeneratedAt(d2) {
|
|
28482
|
+
const date5 = `${d2.getFullYear()}-${pad2(d2.getMonth() + 1)}-${pad2(d2.getDate())}`;
|
|
28483
|
+
const time3 = `${pad2(d2.getHours())}:${pad2(d2.getMinutes())}`;
|
|
28484
|
+
const offsetMinutes = -d2.getTimezoneOffset();
|
|
28485
|
+
const sign = offsetMinutes < 0 ? "-" : "+";
|
|
28486
|
+
const abs = Math.abs(offsetMinutes);
|
|
28487
|
+
return `${date5} ${time3} ${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
|
|
28488
|
+
}
|
|
28489
|
+
function renderGeneratedAt(content, at) {
|
|
28490
|
+
return content.replace(GENERATED_AT_PLACEHOLDER, formatGeneratedAt(at));
|
|
28491
|
+
}
|
|
28492
|
+
function normaliseGeneratedAt(content) {
|
|
28493
|
+
return content.replace(STAMP_SLOT_RE, GENERATED_AT_PREFIX + GENERATED_AT_PLACEHOLDER);
|
|
28494
|
+
}
|
|
28495
|
+
|
|
28496
|
+
// src/lib/context/markers.ts
|
|
28497
|
+
var USER_BLOCK_BEGIN = "<!-- BEGIN: user-customisable -->";
|
|
28498
|
+
var USER_BLOCK_END = "<!-- END -->";
|
|
28499
|
+
var USER_BLOCK_PLACEHOLDER = "_Add workspace-specific guidance here. This block is the only part of " + "the file Plinth preserves across regenerations._";
|
|
28500
|
+
function extractUserBlock(content) {
|
|
28501
|
+
const beginIdx = content.indexOf(USER_BLOCK_BEGIN);
|
|
28502
|
+
if (beginIdx === -1)
|
|
28503
|
+
return null;
|
|
28504
|
+
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28505
|
+
const endIdx = content.indexOf(USER_BLOCK_END, afterBegin);
|
|
28506
|
+
if (endIdx === -1)
|
|
28507
|
+
return null;
|
|
28508
|
+
return content.slice(afterBegin, endIdx);
|
|
28509
|
+
}
|
|
28510
|
+
function injectUserBlock(generated, preserved) {
|
|
28511
|
+
const beginIdx = generated.indexOf(USER_BLOCK_BEGIN);
|
|
28512
|
+
if (beginIdx === -1) {
|
|
28513
|
+
throw new Error("injectUserBlock: generated content has no BEGIN marker");
|
|
28514
|
+
}
|
|
28515
|
+
const afterBegin = beginIdx + USER_BLOCK_BEGIN.length;
|
|
28516
|
+
const endIdx = generated.indexOf(USER_BLOCK_END, afterBegin);
|
|
28517
|
+
if (endIdx === -1) {
|
|
28518
|
+
throw new Error("injectUserBlock: generated content has no END marker");
|
|
28519
|
+
}
|
|
28520
|
+
return generated.slice(0, afterBegin) + preserved + generated.slice(endIdx);
|
|
28521
|
+
}
|
|
28522
|
+
|
|
28523
|
+
// src/lib/context/artist-claude-md.ts
|
|
28524
|
+
var CONTEXT_VERSION_HEADER = "plinth-context-version";
|
|
28525
|
+
var TASK_DISPLAY_CAP = 10;
|
|
28526
|
+
var PRIORITY_RANK = {
|
|
28527
|
+
urgent: 0,
|
|
28528
|
+
high: 1,
|
|
28529
|
+
medium: 2,
|
|
28530
|
+
low: 3
|
|
28531
|
+
};
|
|
28532
|
+
function buildArtistClaudeMd(input) {
|
|
28533
|
+
const body = buildBody(input);
|
|
28534
|
+
const version2 = stableHash(body).slice(0, 12);
|
|
28535
|
+
return `<!-- ${CONTEXT_VERSION_HEADER}: ${version2} -->
|
|
28536
|
+
${body}`;
|
|
28537
|
+
}
|
|
28538
|
+
function buildBody(input) {
|
|
28539
|
+
const { slug, name, status, uuid: uuid3, profile, projects, tasks } = input;
|
|
28540
|
+
const lines = [];
|
|
28541
|
+
lines.push(`<!-- 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. -->`);
|
|
28542
|
+
lines.push("");
|
|
28543
|
+
lines.push(`# ${name} · Plinth artist`);
|
|
28544
|
+
lines.push("");
|
|
28545
|
+
const statusPart = status ? `, status ${status}` : "";
|
|
28546
|
+
const bioPart = profile.bio ? ` ${profile.bio}` : "";
|
|
28547
|
+
lines.push(`${name} (artist \`${slug}\`${statusPart}).${bioPart}`);
|
|
28548
|
+
lines.push(`Working dir for ${name}-scoped AI sessions. ${SECURITY_NOTE}`);
|
|
28549
|
+
lines.push("");
|
|
28550
|
+
const links = buildLinks(profile);
|
|
28551
|
+
if (links) {
|
|
28552
|
+
lines.push(`## Links`);
|
|
28553
|
+
lines.push(links);
|
|
28554
|
+
lines.push("");
|
|
28555
|
+
}
|
|
28556
|
+
const sortedProjects = sortProjects(projects);
|
|
28557
|
+
const names = sortedProjects.map((p) => p.name).join(", ");
|
|
28558
|
+
lines.push(`## Active projects (${sortedProjects.length})${names ? `: ${names}` : ""}`);
|
|
28559
|
+
if (sortedProjects.length === 0) {
|
|
28560
|
+
lines.push(`_None active._`);
|
|
28561
|
+
} else {
|
|
28562
|
+
for (const p of sortedProjects) {
|
|
28563
|
+
const meta3 = [p.status, formatDateRange(p.startDate, p.endDate)].filter((s2) => s2 !== null && s2.length > 0).join(", ");
|
|
28564
|
+
lines.push(`- **${p.name}** (project \`${p.id}\`)${meta3 ? ` · ${meta3}` : ""}`);
|
|
28565
|
+
}
|
|
28566
|
+
}
|
|
28567
|
+
lines.push("");
|
|
28568
|
+
const sortedTasks = sortTasks(tasks);
|
|
28569
|
+
const shown = sortedTasks.slice(0, TASK_DISPLAY_CAP);
|
|
28570
|
+
lines.push(`## Open tasks (${shown.length}/${sortedTasks.length})`);
|
|
28571
|
+
if (sortedTasks.length === 0) {
|
|
28572
|
+
lines.push(`_None open._`);
|
|
28573
|
+
} else {
|
|
28574
|
+
for (const t2 of shown) {
|
|
28575
|
+
const due = t2.dueDate ? `due ${datePart(t2.dueDate)}` : "no due date";
|
|
28576
|
+
const pri = t2.priority ? ` (${t2.priority})` : "";
|
|
28577
|
+
lines.push(`- ${t2.name} · ${due}${pri}`);
|
|
28578
|
+
}
|
|
28579
|
+
const remaining = sortedTasks.length - shown.length;
|
|
28580
|
+
if (remaining > 0) {
|
|
28581
|
+
lines.push(`- _+${remaining} more via \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}_`);
|
|
28582
|
+
}
|
|
28583
|
+
}
|
|
28584
|
+
lines.push("");
|
|
28585
|
+
lines.push(`## More & companions`);
|
|
28586
|
+
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`);
|
|
28587
|
+
lines.push(`- Closed/archived, meetings & full lists: \`mcp__plinth__list_projects\` / \`mcp__plinth__list_tasks\`${artistIdHint(uuid3)}`);
|
|
28588
|
+
lines.push(`- Schema: ${MCP_INTROSPECTION_POINTER}`);
|
|
28589
|
+
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. One line per fact with its source and an absolute date; when a fact goes wrong, edit that line rather than adding a second`);
|
|
28590
|
+
lines.push(`- Rolling day-to-day activity → \`./daily-log.md\`, local to this machine and never synced. Durable facts do not belong here; they belong in \`./memory.md\` above`);
|
|
28591
|
+
lines.push("");
|
|
28592
|
+
lines.push(USER_BLOCK_BEGIN);
|
|
28593
|
+
lines.push(USER_BLOCK_PLACEHOLDER);
|
|
28594
|
+
lines.push(USER_BLOCK_END);
|
|
28595
|
+
return lines.join(`
|
|
28596
|
+
`) + `
|
|
28597
|
+
`;
|
|
28598
|
+
}
|
|
28599
|
+
function artistIdHint(uuid3) {
|
|
27890
28600
|
return uuid3 ? ` (artist_id \`${uuid3}\`)` : "";
|
|
27891
28601
|
}
|
|
27892
28602
|
function buildLinks(p) {
|
|
@@ -27950,7 +28660,7 @@ function datePart(value) {
|
|
|
27950
28660
|
|
|
27951
28661
|
// src/lib/context/artist-data.ts
|
|
27952
28662
|
var import_gray_matter5 = __toESM(require_gray_matter(), 1);
|
|
27953
|
-
import { readFileSync as
|
|
28663
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
27954
28664
|
function artistUuidBySlug(state) {
|
|
27955
28665
|
const map2 = new Map;
|
|
27956
28666
|
for (const entry2 of Object.values(state.entries.artists ?? {})) {
|
|
@@ -28062,7 +28772,7 @@ function readEntityFrontmatter(workspaceSlug, entityType, slug) {
|
|
|
28062
28772
|
}
|
|
28063
28773
|
let raw;
|
|
28064
28774
|
try {
|
|
28065
|
-
raw =
|
|
28775
|
+
raw = readFileSync9(path3, "utf8");
|
|
28066
28776
|
} catch {
|
|
28067
28777
|
return null;
|
|
28068
28778
|
}
|
|
@@ -28090,6 +28800,7 @@ function readSchemaVersion(content) {
|
|
|
28090
28800
|
// src/lib/context/regen.ts
|
|
28091
28801
|
function decideRegen(input) {
|
|
28092
28802
|
const { existing, currentVersion, generated, force } = input;
|
|
28803
|
+
const normalise = input.normalise ?? ((c3) => c3);
|
|
28093
28804
|
if (existing === null || existing.trim() === "") {
|
|
28094
28805
|
return { action: "create", content: generated };
|
|
28095
28806
|
}
|
|
@@ -28103,7 +28814,7 @@ function decideRegen(input) {
|
|
|
28103
28814
|
return { action: "backup-regen", content: generated, backup: existing };
|
|
28104
28815
|
}
|
|
28105
28816
|
const merged = injectUserBlock(generated, preserved);
|
|
28106
|
-
if (merged === existing) {
|
|
28817
|
+
if (normalise(merged) === normalise(existing)) {
|
|
28107
28818
|
return { action: "skip", content: null };
|
|
28108
28819
|
}
|
|
28109
28820
|
return { action: "regen", content: merged };
|
|
@@ -28111,18 +28822,18 @@ function decideRegen(input) {
|
|
|
28111
28822
|
|
|
28112
28823
|
// src/lib/context/roster.ts
|
|
28113
28824
|
var import_gray_matter6 = __toESM(require_gray_matter(), 1);
|
|
28114
|
-
import { readdirSync as
|
|
28115
|
-
import { join as
|
|
28825
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
28826
|
+
import { join as join8 } from "node:path";
|
|
28116
28827
|
function readRoster(workspaceSlug) {
|
|
28117
28828
|
let artistsDir;
|
|
28118
28829
|
try {
|
|
28119
|
-
artistsDir =
|
|
28830
|
+
artistsDir = join8(workspaceMirrorPath(workspaceSlug), "artists");
|
|
28120
28831
|
} catch {
|
|
28121
28832
|
return [];
|
|
28122
28833
|
}
|
|
28123
28834
|
let slugs;
|
|
28124
28835
|
try {
|
|
28125
|
-
slugs =
|
|
28836
|
+
slugs = readdirSync4(artistsDir, { withFileTypes: true }).filter((d2) => d2.isDirectory()).map((d2) => d2.name).sort();
|
|
28126
28837
|
} catch {
|
|
28127
28838
|
return [];
|
|
28128
28839
|
}
|
|
@@ -28130,7 +28841,7 @@ function readRoster(workspaceSlug) {
|
|
|
28130
28841
|
for (const slug of slugs) {
|
|
28131
28842
|
let raw;
|
|
28132
28843
|
try {
|
|
28133
|
-
raw =
|
|
28844
|
+
raw = readFileSync10(join8(artistsDir, slug, "_artist.md"), "utf8");
|
|
28134
28845
|
} catch {
|
|
28135
28846
|
continue;
|
|
28136
28847
|
}
|
|
@@ -28147,9 +28858,9 @@ function readRoster(workspaceSlug) {
|
|
|
28147
28858
|
}
|
|
28148
28859
|
|
|
28149
28860
|
// src/lib/context/session-hook.ts
|
|
28150
|
-
import { execFileSync } from "node:child_process";
|
|
28151
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
28152
|
-
import { basename } from "node:path";
|
|
28861
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28862
|
+
import { accessSync, constants, realpathSync, statSync as statSync3 } from "node:fs";
|
|
28863
|
+
import { basename as basename2 } from "node:path";
|
|
28153
28864
|
var SETTINGS_REL = ".claude/settings.json";
|
|
28154
28865
|
var MANAGED_KEY = "_plinth";
|
|
28155
28866
|
var COMMENT_SENTINEL = 'Managed by plinth-cli. Entries tagged "_plinth" are REGENERATED on every sync:';
|
|
@@ -28157,8 +28868,32 @@ var MANAGED_COMMENT = [
|
|
|
28157
28868
|
COMMENT_SENTINEL,
|
|
28158
28869
|
"edits to them are overwritten without warning. Add your own hooks as separate",
|
|
28159
28870
|
"entries in this file (they are preserved verbatim, including their matchers)",
|
|
28160
|
-
"or in .claude/settings.local.json."
|
|
28871
|
+
"or in .claude/settings.local.json.",
|
|
28872
|
+
"The permissions.deny entries below are managed the same way. They stop an agent",
|
|
28873
|
+
"in this workspace sending, replying to or forwarding mail and writing a calendar;",
|
|
28874
|
+
"drafting and reading stay allowed. Your own allow list is untouched, and your own",
|
|
28875
|
+
"deny rules are kept and listed first. One exception, stated because it is",
|
|
28876
|
+
"visible: if you write a deny rule whose text is identical to one of ours, it is",
|
|
28877
|
+
"kept once, in our section, rather than twice. A deny always beats an allow,",
|
|
28878
|
+
"including your own."
|
|
28161
28879
|
];
|
|
28880
|
+
var MANAGED_DENY = [
|
|
28881
|
+
"mcp__claude_ai_Gmail__send_message",
|
|
28882
|
+
"mcp__claude_ai_Gmail__reply",
|
|
28883
|
+
"mcp__claude_ai_Gmail__forward",
|
|
28884
|
+
"mcp__claude_ai_Google_Calendar__create_event",
|
|
28885
|
+
"mcp__claude_ai_Google_Calendar__update_event",
|
|
28886
|
+
"mcp__claude_ai_Google_Calendar__delete_event",
|
|
28887
|
+
"mcp__claude_ai_Google_Calendar__respond_to_event",
|
|
28888
|
+
"mcp__*__send_message",
|
|
28889
|
+
"mcp__*__reply",
|
|
28890
|
+
"mcp__*__forward",
|
|
28891
|
+
"mcp__*__create_event",
|
|
28892
|
+
"mcp__*__update_event",
|
|
28893
|
+
"mcp__*__delete_event",
|
|
28894
|
+
"mcp__*__respond_to_event"
|
|
28895
|
+
];
|
|
28896
|
+
var RETIRED_DENY = [];
|
|
28162
28897
|
var BIN_SENTINEL = "__PLINTH_BIN__";
|
|
28163
28898
|
var ALL_SENTINEL = "__PLINTH_ALL__";
|
|
28164
28899
|
var RESOLVE_SCRIPT = `printf '%s
|
|
@@ -28189,7 +28924,7 @@ function resolveOverride(override, deps) {
|
|
|
28189
28924
|
}
|
|
28190
28925
|
function resolveUncached(deps) {
|
|
28191
28926
|
const shell = deps.shell ?? process.env.SHELL ?? "/bin/zsh";
|
|
28192
|
-
const exec = deps.exec ?? ((sh, args) =>
|
|
28927
|
+
const exec = deps.exec ?? ((sh, args) => execFileSync2(sh, args, {
|
|
28193
28928
|
encoding: "utf8",
|
|
28194
28929
|
timeout: 5000,
|
|
28195
28930
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -28252,13 +28987,13 @@ function probe(run, usable) {
|
|
|
28252
28987
|
function isPlinthBinary(candidate, usable) {
|
|
28253
28988
|
if (!candidate.startsWith("/"))
|
|
28254
28989
|
return false;
|
|
28255
|
-
if (
|
|
28990
|
+
if (basename2(candidate) !== "plinth")
|
|
28256
28991
|
return false;
|
|
28257
28992
|
return usable(candidate);
|
|
28258
28993
|
}
|
|
28259
28994
|
function defaultIsExecutableFile(path3) {
|
|
28260
28995
|
try {
|
|
28261
|
-
if (!
|
|
28996
|
+
if (!statSync3(path3).isFile())
|
|
28262
28997
|
return false;
|
|
28263
28998
|
accessSync(path3, constants.X_OK);
|
|
28264
28999
|
return true;
|
|
@@ -28269,10 +29004,6 @@ function defaultIsExecutableFile(path3) {
|
|
|
28269
29004
|
function shellQuote(value) {
|
|
28270
29005
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
28271
29006
|
}
|
|
28272
|
-
var VOICE_GATE_MATCHERS = [
|
|
28273
|
-
"Bash",
|
|
28274
|
-
"mcp__.*[Gg]mail.*__(create|update)_draft"
|
|
28275
|
-
];
|
|
28276
29007
|
function managedEntry(id, invocation, subcommand, workspaceSlug) {
|
|
28277
29008
|
return {
|
|
28278
29009
|
type: "command",
|
|
@@ -28284,24 +29015,41 @@ function buildManagedBlocks(invocation, workspaceSlug) {
|
|
|
28284
29015
|
return {
|
|
28285
29016
|
SessionStart: [
|
|
28286
29017
|
{
|
|
28287
|
-
hooks: [
|
|
29018
|
+
hooks: [
|
|
29019
|
+
managedEntry("grounding", invocation, "grounding", workspaceSlug),
|
|
29020
|
+
managedEntry("sessions", invocation, "session register", workspaceSlug)
|
|
29021
|
+
]
|
|
28288
29022
|
}
|
|
28289
|
-
]
|
|
28290
|
-
PreToolUse: VOICE_GATE_MATCHERS.map((matcher) => ({
|
|
28291
|
-
matcher,
|
|
28292
|
-
hooks: [
|
|
28293
|
-
managedEntry("voice-gate", invocation, "voice-gate --hook", workspaceSlug)
|
|
28294
|
-
]
|
|
28295
|
-
}))
|
|
29023
|
+
]
|
|
28296
29024
|
};
|
|
28297
29025
|
}
|
|
29026
|
+
function mergePermissions(existing) {
|
|
29027
|
+
if (existing !== undefined && existing !== null && !isEntry(existing)) {
|
|
29028
|
+
throw new Error("`permissions` is not a JSON object");
|
|
29029
|
+
}
|
|
29030
|
+
const base2 = existing ? { ...existing } : {};
|
|
29031
|
+
const currentDeny = base2.deny;
|
|
29032
|
+
if (currentDeny !== undefined && currentDeny !== null && !Array.isArray(currentDeny)) {
|
|
29033
|
+
throw new Error("`permissions.deny` is not an array");
|
|
29034
|
+
}
|
|
29035
|
+
const ours = new Set([...MANAGED_DENY, ...RETIRED_DENY]);
|
|
29036
|
+
const kept = (Array.isArray(currentDeny) ? currentDeny : []).filter((rule) => typeof rule !== "string" || !ours.has(rule));
|
|
29037
|
+
base2.deny = [...kept, ...MANAGED_DENY];
|
|
29038
|
+
return base2;
|
|
29039
|
+
}
|
|
28298
29040
|
var LEGACY_GROUNDING = /^'[^']*'\s+'[^']*'\s+grounding(\s|$)/;
|
|
29041
|
+
var OWNED_EVENTS = {
|
|
29042
|
+
grounding: "SessionStart",
|
|
29043
|
+
sessions: "SessionStart",
|
|
29044
|
+
"voice-gate": "PreToolUse"
|
|
29045
|
+
};
|
|
28299
29046
|
function isManaged(entry2, event) {
|
|
28300
29047
|
if (!isEntry(entry2))
|
|
28301
29048
|
return false;
|
|
28302
29049
|
const marker = entry2[MANAGED_KEY];
|
|
28303
|
-
if (marker === "
|
|
29050
|
+
if (typeof marker === "string" && OWNED_EVENTS[marker] === event) {
|
|
28304
29051
|
return true;
|
|
29052
|
+
}
|
|
28305
29053
|
if (event === "SessionStart" && typeof entry2.command === "string") {
|
|
28306
29054
|
return LEGACY_GROUNDING.test(entry2.command);
|
|
28307
29055
|
}
|
|
@@ -28317,17 +29065,18 @@ function mergeSettings(existing, managed) {
|
|
|
28317
29065
|
const base2 = existing ? { ...existing } : {};
|
|
28318
29066
|
const existingHooks = typeof base2.hooks === "object" && base2.hooks !== null && !Array.isArray(base2.hooks) ? base2.hooks : {};
|
|
28319
29067
|
const hooks = {};
|
|
28320
|
-
|
|
28321
|
-
|
|
28322
|
-
|
|
28323
|
-
for (const [event, managedBlocks] of Object.entries(managed)) {
|
|
28324
|
-
const kept = asArray(hooks[event]).filter(isEntry).map((block) => ({
|
|
29068
|
+
const events = new Set([...Object.keys(existingHooks), ...Object.keys(managed)]);
|
|
29069
|
+
for (const event of events) {
|
|
29070
|
+
const kept = asArray(existingHooks[event]).filter(isEntry).map((block) => ({
|
|
28325
29071
|
...block,
|
|
28326
29072
|
hooks: asArray(block.hooks).filter((entry2) => isEntry(entry2) && !isManaged(entry2, event))
|
|
28327
29073
|
})).filter((block) => block.hooks.length > 0);
|
|
28328
|
-
|
|
29074
|
+
const merged = [...kept, ...managed[event] ?? []];
|
|
29075
|
+
if (merged.length > 0)
|
|
29076
|
+
hooks[event] = merged;
|
|
28329
29077
|
}
|
|
28330
29078
|
base2.hooks = hooks;
|
|
29079
|
+
base2.permissions = mergePermissions(base2.permissions);
|
|
28331
29080
|
const current = base2.$comment;
|
|
28332
29081
|
const isOurs = Array.isArray(current) && current[0] === COMMENT_SENTINEL;
|
|
28333
29082
|
if (current === undefined || isOurs) {
|
|
@@ -28355,7 +29104,7 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28355
29104
|
parsed = raw;
|
|
28356
29105
|
} catch (err) {
|
|
28357
29106
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28358
|
-
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `
|
|
29107
|
+
out.error(`${rel} could not be parsed (${msg}), so the Plinth session-grounding ` + `hook, the session-visibility hook and the permissions.deny list were ` + `NOT installed there. Fix or remove the file and re-run — until then ` + `this workspace is ungrounded and an agent here can SEND mail and ` + `write calendars.`);
|
|
28359
29108
|
return "skipped";
|
|
28360
29109
|
}
|
|
28361
29110
|
}
|
|
@@ -28364,16 +29113,16 @@ function ensureSessionHook(workspaceSlug, baseRelDir, out, resolved) {
|
|
|
28364
29113
|
generated = buildSettingsJson(resolved.invocation, workspaceSlug, parsed);
|
|
28365
29114
|
} catch (err) {
|
|
28366
29115
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
28367
|
-
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding
|
|
29116
|
+
out.error(`${rel} has a structure the generator could not merge into (${msg}), so ` + `the Plinth session-grounding hook, the session-visibility hook and the ` + `permissions.deny list were NOT installed there. This workspace is ` + `ungrounded and an agent here can SEND mail and write calendars. Fix or ` + `remove the file and re-run.`);
|
|
28368
29117
|
return "skipped";
|
|
28369
29118
|
}
|
|
28370
29119
|
if (existing === generated)
|
|
28371
29120
|
return "unchanged";
|
|
28372
29121
|
writeContextFile(workspaceSlug, rel, generated);
|
|
28373
29122
|
if (existing === null) {
|
|
28374
|
-
out.info(`Generated Plinth session
|
|
29123
|
+
out.info(`Generated Plinth session settings (${rel}).`);
|
|
28375
29124
|
} else {
|
|
28376
|
-
out.info(`Refreshed the Plinth session
|
|
29125
|
+
out.info(`Refreshed the Plinth session settings in ${rel} (your entries kept).`);
|
|
28377
29126
|
}
|
|
28378
29127
|
return "written";
|
|
28379
29128
|
}
|
|
@@ -28390,7 +29139,7 @@ function reportResolution(out, resolved) {
|
|
|
28390
29139
|
}
|
|
28391
29140
|
}
|
|
28392
29141
|
function warnUnresolvedBinary(out) {
|
|
28393
|
-
out.error("Could not locate an installed `plinth` binary, so no Claude Code
|
|
29142
|
+
out.error("Could not locate an installed `plinth` binary, so no Claude Code settings " + "were written for this workspace — sessions will be UNGROUNDED and an " + "agent here can SEND mail and write calendars. Install the CLI globally " + "(`npm i -g @plinth-music/cli`) so `command -v plinth` resolves, then " + "re-run `plinth refresh-context`.");
|
|
28394
29143
|
}
|
|
28395
29144
|
|
|
28396
29145
|
// src/lib/context/artist-context.ts
|
|
@@ -28441,14 +29190,20 @@ function ensurePerArtistContext(deps) {
|
|
|
28441
29190
|
function regenerateClaudeMd(workspaceSlug, artistSlug, generated, force, out, now) {
|
|
28442
29191
|
const claudeRel = `artists/${artistSlug}/${ARTIST_CLAUDE_MD}`;
|
|
28443
29192
|
const existing = readContextFile(workspaceSlug, claudeRel);
|
|
28444
|
-
const decision = decideRegen({
|
|
29193
|
+
const decision = decideRegen({
|
|
29194
|
+
existing,
|
|
29195
|
+
currentVersion: "",
|
|
29196
|
+
generated,
|
|
29197
|
+
force,
|
|
29198
|
+
normalise: normaliseGeneratedAt
|
|
29199
|
+
});
|
|
28445
29200
|
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
28446
29201
|
const bakRel = `${claudeRel}.bak-${backupStamp(now())}`;
|
|
28447
29202
|
writeContextFile(workspaceSlug, bakRel, decision.backup);
|
|
28448
29203
|
out.warn(`artists/${artistSlug}/${ARTIST_CLAUDE_MD} had no recognisable BEGIN/END ` + `markers; backed your copy up to ${bakRel} before regenerating.`);
|
|
28449
29204
|
}
|
|
28450
29205
|
if (decision.content !== null) {
|
|
28451
|
-
writeContextFile(workspaceSlug, claudeRel, decision.content);
|
|
29206
|
+
writeContextFile(workspaceSlug, claudeRel, renderGeneratedAt(decision.content, now()));
|
|
28452
29207
|
}
|
|
28453
29208
|
return decision.action === "create";
|
|
28454
29209
|
}
|
|
@@ -28461,6 +29216,7 @@ function dailyLogStub(name) {
|
|
|
28461
29216
|
return `# ${name} - Daily log
|
|
28462
29217
|
|
|
28463
29218
|
` + `<!-- Rolling activity, newest first; edit freely, Plinth never overwrites this file. -->
|
|
29219
|
+
` + `<!-- Durable facts do not belong here - they go in ./memory.md, which syncs to this artist's Plinth record. -->
|
|
28464
29220
|
`;
|
|
28465
29221
|
}
|
|
28466
29222
|
function backupStamp(d2) {
|
|
@@ -28470,6 +29226,199 @@ function backupStamp(d2) {
|
|
|
28470
29226
|
// src/lib/context/generate.ts
|
|
28471
29227
|
import { existsSync as existsSync2 } from "node:fs";
|
|
28472
29228
|
|
|
29229
|
+
// src/lib/context/skill-commands.ts
|
|
29230
|
+
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._";
|
|
29231
|
+
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. -->";
|
|
29232
|
+
function withUserBlock(lines) {
|
|
29233
|
+
return [...lines, "", USER_BLOCK_BEGIN, COMMAND_USER_BLOCK_PLACEHOLDER, USER_BLOCK_END].join(`
|
|
29234
|
+
`) + `
|
|
29235
|
+
`;
|
|
29236
|
+
}
|
|
29237
|
+
function buildStartCommandMd() {
|
|
29238
|
+
return withUserBlock([
|
|
29239
|
+
"---",
|
|
29240
|
+
"description: Open a Plinth session in this workspace. Reads both memory files, pulls what is unread, gives a three-line brief, then asks what you want to work on.",
|
|
29241
|
+
"---",
|
|
29242
|
+
"",
|
|
29243
|
+
OWNERSHIP_NOTE,
|
|
29244
|
+
"",
|
|
29245
|
+
"# Start",
|
|
29246
|
+
"",
|
|
29247
|
+
"Get oriented in this Plinth workspace in a few seconds, then load context for whatever the user names.",
|
|
29248
|
+
"",
|
|
29249
|
+
"**Do not re-run `date`, and do not restate the grounding rules.** The Plinth SessionStart hook has already injected today's date, the entity-resolution rule and the write-confirmation rule, and the workspace `CLAUDE.md` (roster and folder map) is already in context. Repeating them spends the budget this command exists to protect.",
|
|
29250
|
+
"",
|
|
29251
|
+
"## Step 1: use the two memory indexes you were already given",
|
|
29252
|
+
"",
|
|
29253
|
+
"The SessionStart hook injects **both memory indexes in full**, above: the workspace index (durable facts and conventions that hold for anyone here) and your personal index (facts true of **this member**: their email address, how they write, how they work). **Do not re-read `workspace-memory.md` or `user-memory.md`.** They are already in this context window, and re-reading them is the single most expensive thing this command could do for nothing.",
|
|
29254
|
+
"",
|
|
29255
|
+
"**Use the second one. It is the one that gets skipped**, and it is the one carrying the member's own identity, including the email address that every task assignment needs. An agent that ignores it asks the user for something the workspace already told it.",
|
|
29256
|
+
"",
|
|
29257
|
+
"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.",
|
|
29258
|
+
"",
|
|
29259
|
+
"## Step 2: what has landed",
|
|
29260
|
+
"",
|
|
29261
|
+
"Pull **unread notifications** from the Plinth MCP (`list_notifications`). If the Plinth MCP is not connected, say so in one line and carry on from the injected indexes rather than failing.",
|
|
29262
|
+
"",
|
|
29263
|
+
"**That one call is the whole budget, and the list below is exhaustive rather than indicative.** Do not sweep tasks, do not list projects, do not open a calendar, do not read an artist folder, and do not re-read the memory files. If a fact is not in the two injected indexes or in the notifications, it does not belong in this brief.",
|
|
29264
|
+
"",
|
|
29265
|
+
"## Step 3: brief, in three lines",
|
|
29266
|
+
"",
|
|
29267
|
+
"Then stop reading and report. **Three lines, and not a summary of what you read:**",
|
|
29268
|
+
"",
|
|
29269
|
+
"1. Today, and anything with a clock on it that the injected indexes or the notifications **have already named**. Do not go looking for more.",
|
|
29270
|
+
"2. The unread count, and the one or two that actually matter.",
|
|
29271
|
+
"3. Anything blocked or waiting on someone.",
|
|
29272
|
+
"",
|
|
29273
|
+
'Then ask: **"What do you want to work on?"** If the user already said so in the same message, skip the question and load that context now.',
|
|
29274
|
+
"",
|
|
29275
|
+
"## Step 4: offer the sweep, do not run it",
|
|
29276
|
+
"",
|
|
29277
|
+
`A full roster sweep, every artist's overdue and upcoming work plus calendars and project status, is useful and it is **not** part of opening a session. **Offer it in one clause ("want the roster sweep?") and stop there.** Reporting a sweep you ran anyway is the failure this step exists to prevent: it is the same cost the user did not ask for, with the offer bolted on afterwards.`,
|
|
29278
|
+
"",
|
|
29279
|
+
"**The budget is the one call in Step 2, and it is a ceiling rather than a target.** Every new session pays this command before the user has asked for anything, so a second call needs a reason you would defend out loud. An opener that takes half a minute gets turned off, and an opener nobody runs banks nothing at the other end.",
|
|
29280
|
+
"",
|
|
29281
|
+
"## Loading area context",
|
|
29282
|
+
"",
|
|
29283
|
+
"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`, their durable facts are `artists/<slug>/memory.md`, their recent activity is `artists/<slug>/daily-log.md`. Brief on what is live, not on what you opened.",
|
|
29284
|
+
"",
|
|
29285
|
+
"If nothing resolves, say so plainly and ask. Do not invent an entity.",
|
|
29286
|
+
"",
|
|
29287
|
+
"## Write discipline",
|
|
29288
|
+
"",
|
|
29289
|
+
"This command is read-only orientation. If the session moves into changes, follow the injected grounding: prose and context through the mirror file, structured fields through the Plinth MCP, the exact write stated before the first mutation, and never the database directly.",
|
|
29290
|
+
"",
|
|
29291
|
+
`When the session is finished, \`${CLOSE_COMMAND_INVOCATION}\` is the other half of this one: it banks what was learned before the context is gone.`
|
|
29292
|
+
]);
|
|
29293
|
+
}
|
|
29294
|
+
function buildCloseCommandMd() {
|
|
29295
|
+
return withUserBlock([
|
|
29296
|
+
"---",
|
|
29297
|
+
"description: Close a Plinth session. Verifies what you wrote landed, routes follow-ups to a durable surface, asks before anything reaches shared memory, and logs what was decided.",
|
|
29298
|
+
"---",
|
|
29299
|
+
"",
|
|
29300
|
+
OWNERSHIP_NOTE,
|
|
29301
|
+
"",
|
|
29302
|
+
"# Close",
|
|
29303
|
+
"",
|
|
29304
|
+
"End a working session cleanly: everything done this session is on a durable surface, nothing is half-written, and the next session can pick up cold.",
|
|
29305
|
+
"",
|
|
29306
|
+
"Trust the date the session hook injected. Do not re-derive it.",
|
|
29307
|
+
"",
|
|
29308
|
+
"## Step 1: reconstruct the session",
|
|
29309
|
+
"",
|
|
29310
|
+
"From the **transcript**, not from your memory of it. Which entities were touched, which writes were made, what was decided, what was learned. If the session was pure reading with no changes, say so and go to Step 4 anyway: a read-only close still logs, and the `--nothing-proposed` rule below is why.",
|
|
29311
|
+
"",
|
|
29312
|
+
"## Step 2: verify each write landed",
|
|
29313
|
+
"",
|
|
29314
|
+
"For every intended write, confirm it actually happened: the tool returned success, or the file on disk reflects it. Re-read the file if you are unsure. **A change that exists only in this conversation is lost the moment it ends.**",
|
|
29315
|
+
"",
|
|
29316
|
+
"Flag anything left in flight: an unsent draft, a quarantined edit awaiting `plinth confirm`, a task created with no owner or due date, something agreed out loud and written nowhere.",
|
|
29317
|
+
"",
|
|
29318
|
+
"## Step 3: route the follow-ups",
|
|
29319
|
+
"",
|
|
29320
|
+
"Each loose end goes to the surface that holds it, and nowhere else:",
|
|
29321
|
+
"",
|
|
29322
|
+
"- **Something to do** goes to a Plinth task, scoped to its artist or project, with an owner and a real due date where one exists. Fold it into the existing umbrella task rather than adding a second.",
|
|
29323
|
+
"- **State or context that changed** goes to the artist's master context, the body of `artists/<slug>/_artist.md`, or to the project's summary. Prose goes through the mirror file.",
|
|
29324
|
+
"- **What happened today** goes to `artists/<slug>/daily-log.md`, a dated entry per artist touched. Local-only and rolling, and never the place for a durable fact.",
|
|
29325
|
+
"",
|
|
29326
|
+
"## Step 4: memory, in two piles",
|
|
29327
|
+
"",
|
|
29328
|
+
"The two piles are treated differently on purpose, and the difference is not politeness. A wrong line in **shared** memory reaches every member on every machine and is injected into every future session. A wrong line in a member's **own** memory is theirs to prune.",
|
|
29329
|
+
"",
|
|
29330
|
+
"### Private: `user-memory.md` at the mirror root. Write it directly.",
|
|
29331
|
+
"",
|
|
29332
|
+
"Facts true of this member: how they write, how they sign off, what they prefer, their own identifiers. No gate. **List what you wrote in the closing summary**, so it is visible without opening the file.",
|
|
29333
|
+
"",
|
|
29334
|
+
"### Shared: `workspace-memory.md` at the mirror root. Propose. Never write unasked.",
|
|
29335
|
+
"",
|
|
29336
|
+
"Draft the candidate lines first: durable facts about this workspace that would change how a future session behaves, not this session's detail.",
|
|
29337
|
+
"",
|
|
29338
|
+
"**Each line has a required shape, and it is the file's own, not this command's.** `workspace-memory.md` is an INDEX, and the grounding block tells every future session that each line is a one-line hook pointing via a `[[wikilink]]` to the warm store where the full detail lives. So a line carries: the hook, a `[[wikilink]]` to where the detail actually is, an **absolute** date (turn \"last week\" into a real one), and what taught you it. **A line with no warm store behind it makes the grounding block lie to every session that loads it.** Read the file's header comment and follow the conventions there; where it and this command disagree, the file wins.",
|
|
29339
|
+
"",
|
|
29340
|
+
"Then put the candidates to the user with **`AskUserQuestion`**:",
|
|
29341
|
+
"",
|
|
29342
|
+
"- **One multi-select question, one option per proposed line.** Selected means bank it. Unselected means rejected.",
|
|
29343
|
+
'- **The free-text "Other" choice is how the user rewrites a line in their own words.** Their wording wins over yours, always.',
|
|
29344
|
+
"- The tool takes **four options per question and four questions**. If you have more than four candidate lines, you are banking session detail rather than durable facts: **say so and cut the list. Do not paginate.**",
|
|
29345
|
+
"",
|
|
29346
|
+
"**An accepted line that contradicts one already in the file supersedes it in place.** Edit the wrong line. Never leave two lines disagreeing.",
|
|
29347
|
+
"",
|
|
29348
|
+
"## Step 5: log every decision, then write the file",
|
|
29349
|
+
"",
|
|
29350
|
+
"**In that order.** The log is the record of what was decided; the file is the consequence.",
|
|
29351
|
+
"",
|
|
29352
|
+
"```sh",
|
|
29353
|
+
'plinth memory-gate log --proposed "<the line as you put it>" --decision accept',
|
|
29354
|
+
'plinth memory-gate log --proposed "<the line as you put it>" --decision reject',
|
|
29355
|
+
'plinth memory-gate log --proposed "<the line as you put it>" --decision edit --final "<their rewrite>"',
|
|
29356
|
+
"```",
|
|
29357
|
+
"",
|
|
29358
|
+
"A close that proposed nothing still logs, because *gated and proposed nothing* has to stay distinguishable from *never gated*:",
|
|
29359
|
+
"",
|
|
29360
|
+
"```sh",
|
|
29361
|
+
"plinth memory-gate log --nothing-proposed",
|
|
29362
|
+
"```",
|
|
29363
|
+
"",
|
|
29364
|
+
"`plinth memory-gate show [--json]` reads the log back.",
|
|
29365
|
+
"",
|
|
29366
|
+
"**Four rules. Break one and a decision is lost:**",
|
|
29367
|
+
"",
|
|
29368
|
+
"1. `--decision edit` **requires** `--final`. The rewrite is the record: it is the user's own words for their own standard, and it is the most valuable row in the log.",
|
|
29369
|
+
"2. `--final` is **refused** on `accept` and `reject`. An accept records the proposed line; a reject records none.",
|
|
29370
|
+
"3. `--nothing-proposed` **cannot** be combined with `--proposed` or `--decision`.",
|
|
29371
|
+
'4. **`memory-gate` fails LOUD**: exit 1, with a message on stderr. **Surface it. Never swallow it.** A silently dropped decision produces exactly the "never gated" state the log exists to make visible, while looking like a clean close. If it fails, tell the user, and do not report the line as banked.',
|
|
29372
|
+
"",
|
|
29373
|
+
"**Never write this log by hand.** The CLI owns the format so it can still be read back in a year.",
|
|
29374
|
+
"",
|
|
29375
|
+
"Only once the decisions are logged, write the accepted lines into `workspace-memory.md`.",
|
|
29376
|
+
"",
|
|
29377
|
+
"## Step 6: summarise, then end the session",
|
|
29378
|
+
"",
|
|
29379
|
+
"**Four to six lines**, and no file-by-file dump: what now lives on a durable surface, what is still in flight and on whom, what was banked to memory (private and shared), and the obvious next move.",
|
|
29380
|
+
"",
|
|
29381
|
+
"Then, as the **very last step**:",
|
|
29382
|
+
"",
|
|
29383
|
+
"```sh",
|
|
29384
|
+
'plinth session end --summary "<one line>"',
|
|
29385
|
+
"```",
|
|
29386
|
+
"",
|
|
29387
|
+
"This is the opposite of `memory-gate`: it **always exits 0 and always prints nothing**, whatever happens. It marks this session ended, and when it is running inside the Plinth desktop dock it tells the dock the session is finished so the tab can close itself. **Outside the dock it does nothing visible, and that is correct.** Nothing in this command depends on the dock; it runs the same in a bare terminal."
|
|
29388
|
+
]);
|
|
29389
|
+
}
|
|
29390
|
+
function ensureWorkspaceCommands(deps) {
|
|
29391
|
+
const { workspaceSlug, force, out, now } = deps;
|
|
29392
|
+
const created = [];
|
|
29393
|
+
const refreshed = [];
|
|
29394
|
+
const targets = [
|
|
29395
|
+
[START_COMMAND_REL, buildStartCommandMd()],
|
|
29396
|
+
[CLOSE_COMMAND_REL, buildCloseCommandMd()]
|
|
29397
|
+
];
|
|
29398
|
+
for (const [rel, generated] of targets) {
|
|
29399
|
+
const existing = readContextFile(workspaceSlug, rel);
|
|
29400
|
+
const decision = decideRegen({ existing, currentVersion: "", generated, force });
|
|
29401
|
+
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
29402
|
+
const bakRel = `${rel}.bak-${backupStamp2(now())}`;
|
|
29403
|
+
writeContextFile(workspaceSlug, bakRel, decision.backup);
|
|
29404
|
+
out.warn(`${rel} had no recognisable BEGIN/END markers; backed your copy up to ` + `${bakRel} before regenerating.`);
|
|
29405
|
+
}
|
|
29406
|
+
if (decision.content !== null) {
|
|
29407
|
+
writeContextFile(workspaceSlug, rel, decision.content);
|
|
29408
|
+
(decision.action === "create" ? created : refreshed).push(invocationFor(rel));
|
|
29409
|
+
}
|
|
29410
|
+
}
|
|
29411
|
+
if (created.length > 0) {
|
|
29412
|
+
out.info(`Generated ${created.join(" and ")}.`);
|
|
29413
|
+
}
|
|
29414
|
+
if (refreshed.length > 0) {
|
|
29415
|
+
out.info(`Refreshed ${refreshed.join(" and ")} (your edits preserved).`);
|
|
29416
|
+
}
|
|
29417
|
+
}
|
|
29418
|
+
function backupStamp2(d2) {
|
|
29419
|
+
return d2.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
29420
|
+
}
|
|
29421
|
+
|
|
28473
29422
|
// src/lib/context/workspace-claude-md.ts
|
|
28474
29423
|
function rosterBlock(roster, activeProjectCountBySlug) {
|
|
28475
29424
|
if (roster.length === 0)
|
|
@@ -28495,7 +29444,7 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
28495
29444
|
const skillCount = SKILL_LOCATIONS.length;
|
|
28496
29445
|
const lines = [
|
|
28497
29446
|
`<!-- plinth-schema-version: ${schemaVersion} -->`,
|
|
28498
|
-
`<!-- Auto-generated by Plinth. Edit only between the BEGIN/END markers below; everything else is overwritten on sync. -->`,
|
|
29447
|
+
`<!-- 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. -->`,
|
|
28499
29448
|
``,
|
|
28500
29449
|
`# ${workspaceName} · Plinth workspace`,
|
|
28501
29450
|
``,
|
|
@@ -28503,17 +29452,20 @@ function buildWorkspaceClaudeMd(input) {
|
|
|
28503
29452
|
``,
|
|
28504
29453
|
`> **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\`).`,
|
|
28505
29454
|
``,
|
|
29455
|
+
`> **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 \`workspace-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.`,
|
|
29456
|
+
``,
|
|
28506
29457
|
`## Folder map`,
|
|
28507
29458
|
`- \`artists/<slug>/\` one folder per artist, the working dir for artist-scoped sessions · \`threads/<slug>/_thread.md\` one folder per thread`,
|
|
28508
29459
|
`- \`projects/\` \`tasks/\` \`documents/\` \`meetings/\` workspace-root flat entity files (one \`.md\` per entity, NOT nested under \`artists/\`)`,
|
|
28509
29460
|
`- \`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`,
|
|
28510
|
-
`-
|
|
29461
|
+
`- \`${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`,
|
|
29462
|
+
`- \`.claude/commands/\` local skills, incl. generated \`${START_COMMAND_INVOCATION}\` + \`${CLOSE_COMMAND_INVOCATION}\` · at the mirror root: \`workspace-memory.md\` \`workspace-rules.md\` (workspace-wide) + \`user-memory.md\` (yours), synced to Plinth · \`CLAUDE.local.md\` private, gitignored`,
|
|
28511
29463
|
``,
|
|
28512
29464
|
`## Where key context lives: look here before hunting`,
|
|
28513
29465
|
`- 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`,
|
|
28514
29466
|
`- **Contracts, executed agreements & any PDF** → \`files/\` (search here first; \`documents/\` holds markdown bodies, not attachments)`,
|
|
28515
29467
|
`- Workspace-wide **notes, briefs, bios & context docs** → \`documents/\` (one markdown file per doc, shared across artists)`,
|
|
28516
|
-
`- Per-artist long-term facts → \`artists/<slug>/memory.md
|
|
29468
|
+
`- 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 · rolling activity → \`artists/<slug>/daily-log.md\`, local-only and never the place for a durable fact`,
|
|
28517
29469
|
`- ${MCP_AVAILABILITY_POINTER}`,
|
|
28518
29470
|
``,
|
|
28519
29471
|
`## Entity model: ${MCP_INTROSPECTION_POINTER}`,
|
|
@@ -28571,19 +29523,21 @@ function ensureWorkspaceContext(deps) {
|
|
|
28571
29523
|
existing,
|
|
28572
29524
|
currentVersion: "",
|
|
28573
29525
|
generated,
|
|
28574
|
-
force
|
|
29526
|
+
force,
|
|
29527
|
+
normalise: normaliseGeneratedAt
|
|
28575
29528
|
});
|
|
28576
29529
|
if (decision.action === "backup-regen" && decision.backup !== undefined) {
|
|
28577
|
-
const bakRel = `${CLAUDE_MD}.bak-${
|
|
29530
|
+
const bakRel = `${CLAUDE_MD}.bak-${backupStamp3(now())}`;
|
|
28578
29531
|
writeContextFile(workspaceSlug, bakRel, decision.backup);
|
|
28579
29532
|
out.warn(`${CLAUDE_MD} had no recognisable BEGIN/END markers; backed your copy ` + `up to ${bakRel} before regenerating.`);
|
|
28580
29533
|
}
|
|
28581
29534
|
if (decision.content !== null) {
|
|
28582
|
-
writeContextFile(workspaceSlug, CLAUDE_MD, decision.content);
|
|
29535
|
+
writeContextFile(workspaceSlug, CLAUDE_MD, renderGeneratedAt(decision.content, now()));
|
|
28583
29536
|
}
|
|
28584
29537
|
if (!existsSync2(contextFilePath(workspaceSlug, CLAUDE_LOCAL_MD))) {
|
|
28585
29538
|
writeContextFile(workspaceSlug, CLAUDE_LOCAL_MD, CLAUDE_LOCAL_STUB);
|
|
28586
29539
|
}
|
|
29540
|
+
ensureWorkspaceCommands({ workspaceSlug, force, out, now });
|
|
28587
29541
|
ensureGitignore(workspaceSlug);
|
|
28588
29542
|
const resolved = resolvePlinthBinary();
|
|
28589
29543
|
reportResolution(out, resolved);
|
|
@@ -28615,7 +29569,7 @@ function ensureGitignore(workspaceSlug) {
|
|
|
28615
29569
|
`;
|
|
28616
29570
|
writeContextFile(workspaceSlug, GITIGNORE, `${existing}${lead}${block}`);
|
|
28617
29571
|
}
|
|
28618
|
-
function
|
|
29572
|
+
function backupStamp3(d2) {
|
|
28619
29573
|
return d2.toISOString().replace(/[-:.]/g, "").replace("Z", "");
|
|
28620
29574
|
}
|
|
28621
29575
|
|
|
@@ -28824,12 +29778,12 @@ function classify(facts) {
|
|
|
28824
29778
|
}
|
|
28825
29779
|
|
|
28826
29780
|
// src/lib/review-tier/collect.ts
|
|
28827
|
-
import { execFileSync as
|
|
29781
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
28828
29782
|
import fs2 from "node:fs";
|
|
28829
29783
|
import path3 from "node:path";
|
|
28830
29784
|
function execGit(args, cwd) {
|
|
28831
29785
|
try {
|
|
28832
|
-
const stdout2 =
|
|
29786
|
+
const stdout2 = execFileSync3("git", args, {
|
|
28833
29787
|
cwd,
|
|
28834
29788
|
encoding: "utf8",
|
|
28835
29789
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -29047,16 +30001,280 @@ var reviewTierCommand = defineCommand({
|
|
|
29047
30001
|
}
|
|
29048
30002
|
});
|
|
29049
30003
|
|
|
30004
|
+
// src/commands/session.ts
|
|
30005
|
+
init_output();
|
|
30006
|
+
async function defaultReadStdin() {
|
|
30007
|
+
if (process.stdin.isTTY)
|
|
30008
|
+
return "";
|
|
30009
|
+
const chunks = [];
|
|
30010
|
+
for await (const chunk of process.stdin) {
|
|
30011
|
+
chunks.push(Buffer.from(chunk));
|
|
30012
|
+
}
|
|
30013
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
30014
|
+
}
|
|
30015
|
+
function asString(value) {
|
|
30016
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
30017
|
+
}
|
|
30018
|
+
function resolveSlug(explicit) {
|
|
30019
|
+
let config2;
|
|
30020
|
+
try {
|
|
30021
|
+
config2 = loadConfig();
|
|
30022
|
+
} catch {
|
|
30023
|
+
return { ok: false, reason: "unconfigured" };
|
|
30024
|
+
}
|
|
30025
|
+
if (!config2)
|
|
30026
|
+
return { ok: false, reason: "unconfigured" };
|
|
30027
|
+
const slug = explicit ?? config2.active_workspace;
|
|
30028
|
+
if (!config2.workspaces[slug]) {
|
|
30029
|
+
return explicit === undefined ? { ok: false, reason: "unconfigured" } : { ok: false, reason: "unknown-workspace", slug };
|
|
30030
|
+
}
|
|
30031
|
+
return { ok: true, slug };
|
|
30032
|
+
}
|
|
30033
|
+
async function runSessionRegister(args, deps = {}) {
|
|
30034
|
+
const env3 = deps.env ?? process.env;
|
|
30035
|
+
const raw = await (deps.readStdin ?? defaultReadStdin)().catch(() => "");
|
|
30036
|
+
let payload = {};
|
|
30037
|
+
if (raw.trim().length > 0) {
|
|
30038
|
+
try {
|
|
30039
|
+
const parsed = JSON.parse(raw);
|
|
30040
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
30041
|
+
payload = parsed;
|
|
30042
|
+
}
|
|
30043
|
+
} catch {}
|
|
30044
|
+
}
|
|
30045
|
+
const sessionId = asString(payload.session_id) ?? asString(env3.CLAUDE_CODE_SESSION_ID);
|
|
30046
|
+
const cwd = asString(payload.cwd) ?? (deps.cwd ?? process.cwd)();
|
|
30047
|
+
const transcriptPath = asString(payload.transcript_path);
|
|
30048
|
+
const tabId = asString(env3.PLINTH_DOCK_TAB_ID);
|
|
30049
|
+
const pidRaw = asString(env3.CLAUDE_PID);
|
|
30050
|
+
const pid = pidRaw !== null ? Number.parseInt(pidRaw, 10) : Number.NaN;
|
|
30051
|
+
if (sessionId === null || !isSafeSegment(sessionId))
|
|
30052
|
+
return { exitCode: 0 };
|
|
30053
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
30054
|
+
return { exitCode: 0 };
|
|
30055
|
+
const resolved = resolveSlug(args.workspace);
|
|
30056
|
+
if (!resolved.ok)
|
|
30057
|
+
return { exitCode: 0 };
|
|
30058
|
+
try {
|
|
30059
|
+
registerSession({
|
|
30060
|
+
sessionId,
|
|
30061
|
+
pid,
|
|
30062
|
+
cwd,
|
|
30063
|
+
workspaceSlug: resolved.slug,
|
|
30064
|
+
transcriptPath,
|
|
30065
|
+
tabId,
|
|
30066
|
+
now: (deps.now ?? (() => new Date))(),
|
|
30067
|
+
startTimeFn: deps.startTimeFn
|
|
30068
|
+
});
|
|
30069
|
+
} catch {}
|
|
30070
|
+
return { exitCode: 0 };
|
|
30071
|
+
}
|
|
30072
|
+
function buildSessionListJson(workspaceSlug, live, reaped, unchecked, selfPid, selfSessionId, ended = 0) {
|
|
30073
|
+
return {
|
|
30074
|
+
workspace_slug: workspaceSlug,
|
|
30075
|
+
self_pid: selfPid,
|
|
30076
|
+
self_session_id: selfSessionId,
|
|
30077
|
+
sessions: live.map((s2) => ({
|
|
30078
|
+
session_id: s2.record.session_id,
|
|
30079
|
+
pid: s2.record.pid,
|
|
30080
|
+
cwd: s2.record.cwd,
|
|
30081
|
+
started_at: s2.record.started_at,
|
|
30082
|
+
last_activity: s2.lastActivityMs === null ? null : new Date(s2.lastActivityMs).toISOString(),
|
|
30083
|
+
is_self: s2.record.session_id === selfSessionId || selfPid !== null && s2.record.pid === selfPid,
|
|
30084
|
+
tab_id: s2.record.tab_id
|
|
30085
|
+
})),
|
|
30086
|
+
reaped,
|
|
30087
|
+
unchecked,
|
|
30088
|
+
ended,
|
|
30089
|
+
scope: "machine-local"
|
|
30090
|
+
};
|
|
30091
|
+
}
|
|
30092
|
+
async function runSessionList(args, deps = {}) {
|
|
30093
|
+
const out = new Output({ sink: deps.sink, noColor: deps.noColor ?? args.noColor });
|
|
30094
|
+
const env3 = deps.env ?? process.env;
|
|
30095
|
+
const resolved = resolveSlug(args.workspace);
|
|
30096
|
+
if (!resolved.ok) {
|
|
30097
|
+
out.error(resolved.reason === "unknown-workspace" ? `Workspace "${resolved.slug}" not found. Run \`plinth login --workspace ${resolved.slug}\` first.` : "No workspace configured. Run `plinth login --workspace <slug>` first.");
|
|
30098
|
+
return { exitCode: 1 };
|
|
30099
|
+
}
|
|
30100
|
+
const slug = resolved.slug;
|
|
30101
|
+
const { live, reaped, unknown: unknown2, ended } = listSessions(slug, deps);
|
|
30102
|
+
const selfPidRaw = asString(env3.CLAUDE_PID);
|
|
30103
|
+
const selfPid = selfPidRaw !== null ? Number.parseInt(selfPidRaw, 10) : null;
|
|
30104
|
+
const selfSessionId = asString(env3.CLAUDE_CODE_SESSION_ID);
|
|
30105
|
+
if (args.json) {
|
|
30106
|
+
out.info(JSON.stringify(buildSessionListJson(slug, live, reaped, unknown2, Number.isInteger(selfPid) ? selfPid : null, selfSessionId, ended), null, 2));
|
|
30107
|
+
return { exitCode: 0 };
|
|
30108
|
+
}
|
|
30109
|
+
const now = (deps.now ?? (() => new Date))();
|
|
30110
|
+
for (const line of renderSessionLines(live, now, selfPid, selfSessionId)) {
|
|
30111
|
+
out.info(line);
|
|
30112
|
+
}
|
|
30113
|
+
for (const line of sessionCaveats(live.length, unknown2, ended))
|
|
30114
|
+
out.info(line);
|
|
30115
|
+
return { exitCode: 0 };
|
|
30116
|
+
}
|
|
30117
|
+
var DOCK_CLOSED_ROUTE = "/closed";
|
|
30118
|
+
var DOCK_SIGNAL_TIMEOUT_MS = 1000;
|
|
30119
|
+
var SUMMARY_MAX_CHARS = 2000;
|
|
30120
|
+
function readDockSignalEnv(env3) {
|
|
30121
|
+
const port = asString(env3.PLINTH_DOCK_SIGNAL_PORT);
|
|
30122
|
+
const token = asString(env3.PLINTH_DOCK_SIGNAL_TOKEN);
|
|
30123
|
+
const tabId = asString(env3.PLINTH_DOCK_TAB_ID);
|
|
30124
|
+
if (port === null || token === null || tabId === null)
|
|
30125
|
+
return null;
|
|
30126
|
+
if (!/^[0-9]{1,5}$/.test(port))
|
|
30127
|
+
return null;
|
|
30128
|
+
return { port, token, tabId };
|
|
30129
|
+
}
|
|
30130
|
+
function normaliseSummary(raw) {
|
|
30131
|
+
if (raw === undefined)
|
|
30132
|
+
return null;
|
|
30133
|
+
const folded = raw.replace(/\s+/g, " ").trim();
|
|
30134
|
+
if (folded.length === 0)
|
|
30135
|
+
return null;
|
|
30136
|
+
return folded.length > SUMMARY_MAX_CHARS ? folded.slice(0, SUMMARY_MAX_CHARS) : folded;
|
|
30137
|
+
}
|
|
30138
|
+
var defaultFetch = (url2, init2) => fetch(url2, init2);
|
|
30139
|
+
async function runSessionEnd(args, deps = {}) {
|
|
30140
|
+
const env3 = deps.env ?? process.env;
|
|
30141
|
+
const now = (deps.now ?? (() => new Date))();
|
|
30142
|
+
const sessionId = asString(env3.CLAUDE_CODE_SESSION_ID);
|
|
30143
|
+
if (sessionId !== null && isSafeSegment(sessionId)) {
|
|
30144
|
+
const resolved = resolveSlug(args.workspace);
|
|
30145
|
+
if (resolved.ok) {
|
|
30146
|
+
try {
|
|
30147
|
+
endSession(resolved.slug, sessionId, now);
|
|
30148
|
+
} catch {}
|
|
30149
|
+
}
|
|
30150
|
+
}
|
|
30151
|
+
const dock = readDockSignalEnv(env3);
|
|
30152
|
+
if (dock === null)
|
|
30153
|
+
return { exitCode: 0 };
|
|
30154
|
+
const body = JSON.stringify({ summary: normaliseSummary(args.summary) });
|
|
30155
|
+
try {
|
|
30156
|
+
await (deps.fetchFn ?? defaultFetch)(`http://127.0.0.1:${dock.port}${DOCK_CLOSED_ROUTE}`, {
|
|
30157
|
+
method: "POST",
|
|
30158
|
+
headers: {
|
|
30159
|
+
"content-type": "application/json",
|
|
30160
|
+
"x-plinth-token": dock.token,
|
|
30161
|
+
"x-plinth-tab": dock.tabId
|
|
30162
|
+
},
|
|
30163
|
+
body,
|
|
30164
|
+
signal: AbortSignal.timeout(DOCK_SIGNAL_TIMEOUT_MS)
|
|
30165
|
+
});
|
|
30166
|
+
} catch {}
|
|
30167
|
+
return { exitCode: 0 };
|
|
30168
|
+
}
|
|
30169
|
+
function renderSessionLines(live, now, selfPid, selfSessionId, formatRelative = defaultRelative) {
|
|
30170
|
+
if (live.length === 0)
|
|
30171
|
+
return ["No Claude sessions registered on this machine."];
|
|
30172
|
+
return live.map((s2) => {
|
|
30173
|
+
const isSelf = s2.record.session_id === selfSessionId || selfPid !== null && s2.record.pid === selfPid;
|
|
30174
|
+
const activity = s2.lastActivityMs === null ? "activity unknown" : `active ${formatRelative(now.getTime() - s2.lastActivityMs)}`;
|
|
30175
|
+
const tag = isSelf ? " (this session)" : "";
|
|
30176
|
+
const tab = s2.record.tab_id === null ? "" : ` · tab ${s2.record.tab_id}`;
|
|
30177
|
+
return `${s2.record.session_id.slice(0, 8)} · pid ${s2.record.pid}${tab} · ${activity} · ${s2.record.cwd}${tag}`;
|
|
30178
|
+
});
|
|
30179
|
+
}
|
|
30180
|
+
function sessionCaveats(liveCount, unknown2, ended = 0) {
|
|
30181
|
+
const lines = [];
|
|
30182
|
+
if (liveCount > 1) {
|
|
30183
|
+
lines.push("Visibility only - nothing here prevents a conflicting write.");
|
|
30184
|
+
}
|
|
30185
|
+
if (ended > 0) {
|
|
30186
|
+
lines.push(`${ended} session${ended === 1 ? " has" : "s have"} run \`plinth session end\` but ${ended === 1 ? "its process is" : "their processes are"} still alive - not listed above.`);
|
|
30187
|
+
}
|
|
30188
|
+
if (unknown2 > 0) {
|
|
30189
|
+
lines.push(`${unknown2} record${unknown2 === 1 ? "" : "s"} could not be checked and ${unknown2 === 1 ? "is" : "are"} not listed.`);
|
|
30190
|
+
}
|
|
30191
|
+
lines.push("Machine-local - a session on another machine is not counted.");
|
|
30192
|
+
return lines;
|
|
30193
|
+
}
|
|
30194
|
+
function defaultRelative(deltaMs) {
|
|
30195
|
+
const s2 = Math.floor(Math.max(0, deltaMs) / 1000);
|
|
30196
|
+
if (s2 < 60)
|
|
30197
|
+
return `${s2}s ago`;
|
|
30198
|
+
const m2 = Math.floor(s2 / 60);
|
|
30199
|
+
if (m2 < 60)
|
|
30200
|
+
return `${m2}m ago`;
|
|
30201
|
+
const h2 = Math.floor(m2 / 60);
|
|
30202
|
+
if (h2 < 24)
|
|
30203
|
+
return `${h2}h ago`;
|
|
30204
|
+
return `${Math.floor(h2 / 24)}d ago`;
|
|
30205
|
+
}
|
|
30206
|
+
var sessionCommand = defineCommand({
|
|
30207
|
+
meta: {
|
|
30208
|
+
name: "session",
|
|
30209
|
+
description: "Concurrent Claude session visibility for this workspace"
|
|
30210
|
+
},
|
|
30211
|
+
subCommands: {
|
|
30212
|
+
register: defineCommand({
|
|
30213
|
+
meta: {
|
|
30214
|
+
name: "register",
|
|
30215
|
+
description: "Record this Claude session (SessionStart hook; fail-quiet)"
|
|
30216
|
+
},
|
|
30217
|
+
args: {
|
|
30218
|
+
workspace: { type: "string", description: "Workspace slug" }
|
|
30219
|
+
},
|
|
30220
|
+
async run({ args }) {
|
|
30221
|
+
const result = await runSessionRegister({
|
|
30222
|
+
workspace: args.workspace
|
|
30223
|
+
});
|
|
30224
|
+
process.exitCode = result.exitCode;
|
|
30225
|
+
}
|
|
30226
|
+
}),
|
|
30227
|
+
list: defineCommand({
|
|
30228
|
+
meta: {
|
|
30229
|
+
name: "list",
|
|
30230
|
+
description: "List live Claude sessions writing this workspace"
|
|
30231
|
+
},
|
|
30232
|
+
args: {
|
|
30233
|
+
workspace: { type: "string", description: "Workspace slug" },
|
|
30234
|
+
json: { type: "boolean", description: "Machine-readable output" },
|
|
30235
|
+
"no-color": { type: "boolean", description: "Disable colour" }
|
|
30236
|
+
},
|
|
30237
|
+
async run({ args }) {
|
|
30238
|
+
const result = await runSessionList({
|
|
30239
|
+
workspace: args.workspace,
|
|
30240
|
+
json: args.json
|
|
30241
|
+
}, { noColor: noColorFromArgs(args) });
|
|
30242
|
+
process.exitCode = result.exitCode;
|
|
30243
|
+
}
|
|
30244
|
+
}),
|
|
30245
|
+
end: defineCommand({
|
|
30246
|
+
meta: {
|
|
30247
|
+
name: "end",
|
|
30248
|
+
description: "Mark this Claude session ended and, inside the desktop dock, tell the dock (last step of /close; fail-quiet)"
|
|
30249
|
+
},
|
|
30250
|
+
args: {
|
|
30251
|
+
workspace: { type: "string", description: "Workspace slug" },
|
|
30252
|
+
summary: {
|
|
30253
|
+
type: "string",
|
|
30254
|
+
description: "One-line summary of the session, forwarded to the dock"
|
|
30255
|
+
}
|
|
30256
|
+
},
|
|
30257
|
+
async run({ args }) {
|
|
30258
|
+
const result = await runSessionEnd({
|
|
30259
|
+
workspace: args.workspace,
|
|
30260
|
+
summary: args.summary
|
|
30261
|
+
});
|
|
30262
|
+
process.exitCode = result.exitCode;
|
|
30263
|
+
}
|
|
30264
|
+
})
|
|
30265
|
+
}
|
|
30266
|
+
});
|
|
30267
|
+
|
|
29050
30268
|
// src/commands/start.ts
|
|
29051
30269
|
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
29052
|
-
import { readFileSync as
|
|
30270
|
+
import { readFileSync as readFileSync20, statSync as statSync12 } from "node:fs";
|
|
29053
30271
|
|
|
29054
30272
|
// src/lib/daemon-log.ts
|
|
29055
|
-
import { appendFileSync, mkdirSync as
|
|
29056
|
-
import { dirname as
|
|
30273
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
30274
|
+
import { dirname as dirname6, join as join9 } from "node:path";
|
|
29057
30275
|
var DEFAULT_DAEMON_LOG_MAX_BYTES = 5000000;
|
|
29058
30276
|
function daemonLogPath() {
|
|
29059
|
-
return
|
|
30277
|
+
return join9(plinthHome(), "daemon.log");
|
|
29060
30278
|
}
|
|
29061
30279
|
function daemonLogMaxBytes() {
|
|
29062
30280
|
const raw = process.env.PLINTH_DAEMON_LOG_MAX_BYTES;
|
|
@@ -29078,10 +30296,10 @@ class DaemonLog {
|
|
|
29078
30296
|
this.now = opts.now ?? (() => new Date);
|
|
29079
30297
|
this.maxBytes = opts.maxBytes ?? daemonLogMaxBytes();
|
|
29080
30298
|
try {
|
|
29081
|
-
|
|
30299
|
+
mkdirSync8(dirname6(this.path), { recursive: true });
|
|
29082
30300
|
} catch {}
|
|
29083
30301
|
try {
|
|
29084
|
-
this.bytesWritten =
|
|
30302
|
+
this.bytesWritten = statSync4(this.path).size;
|
|
29085
30303
|
} catch {
|
|
29086
30304
|
this.bytesWritten = 0;
|
|
29087
30305
|
}
|
|
@@ -29094,7 +30312,7 @@ class DaemonLog {
|
|
|
29094
30312
|
const bytes = Buffer.byteLength(line);
|
|
29095
30313
|
try {
|
|
29096
30314
|
this.maybeRotate(bytes);
|
|
29097
|
-
|
|
30315
|
+
appendFileSync2(this.path, line);
|
|
29098
30316
|
this.bytesWritten += bytes;
|
|
29099
30317
|
} catch {
|
|
29100
30318
|
try {
|
|
@@ -29108,97 +30326,16 @@ class DaemonLog {
|
|
|
29108
30326
|
}
|
|
29109
30327
|
rotate() {
|
|
29110
30328
|
try {
|
|
29111
|
-
|
|
30329
|
+
renameSync7(this.path, `${this.path}.1`);
|
|
29112
30330
|
} catch {}
|
|
29113
30331
|
this.bytesWritten = 0;
|
|
29114
30332
|
}
|
|
29115
30333
|
}
|
|
29116
30334
|
|
|
29117
|
-
// src/lib/daemon-pid.ts
|
|
29118
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
29119
|
-
import { dirname as dirname5, join as join7 } from "node:path";
|
|
29120
|
-
var DaemonPidSchema = exports_external.object({
|
|
29121
|
-
version: exports_external.literal(1),
|
|
29122
|
-
pid: exports_external.number().int().positive(),
|
|
29123
|
-
workspace_slug: exports_external.string().min(1),
|
|
29124
|
-
started_at: exports_external.string().min(1)
|
|
29125
|
-
});
|
|
29126
|
-
function daemonPidPath(workspaceSlug) {
|
|
29127
|
-
return join7(plinthHome(), `daemon-${workspaceSlug}.pid`);
|
|
29128
|
-
}
|
|
29129
|
-
function daemonPidTmpPath(workspaceSlug) {
|
|
29130
|
-
return join7(plinthHome(), `daemon-${workspaceSlug}.pid.tmp`);
|
|
29131
|
-
}
|
|
29132
|
-
function writeDaemonPid(workspaceSlug, pid, startedAt) {
|
|
29133
|
-
const file2 = {
|
|
29134
|
-
version: 1,
|
|
29135
|
-
pid,
|
|
29136
|
-
workspace_slug: workspaceSlug,
|
|
29137
|
-
started_at: startedAt.toISOString()
|
|
29138
|
-
};
|
|
29139
|
-
const target = daemonPidPath(workspaceSlug);
|
|
29140
|
-
const tmp = daemonPidTmpPath(workspaceSlug);
|
|
29141
|
-
mkdirSync6(dirname5(target), { recursive: true, mode: 448 });
|
|
29142
|
-
writeFileSync5(tmp, JSON.stringify(file2, null, 2) + `
|
|
29143
|
-
`, { mode: 384 });
|
|
29144
|
-
try {
|
|
29145
|
-
renameSync6(tmp, target);
|
|
29146
|
-
} catch (err) {
|
|
29147
|
-
try {
|
|
29148
|
-
unlinkSync5(tmp);
|
|
29149
|
-
} catch {}
|
|
29150
|
-
throw err;
|
|
29151
|
-
}
|
|
29152
|
-
}
|
|
29153
|
-
function readDaemonPid(workspaceSlug) {
|
|
29154
|
-
let raw;
|
|
29155
|
-
try {
|
|
29156
|
-
raw = readFileSync8(daemonPidPath(workspaceSlug), "utf8");
|
|
29157
|
-
} catch (err) {
|
|
29158
|
-
if (err?.code === "ENOENT")
|
|
29159
|
-
return null;
|
|
29160
|
-
throw err;
|
|
29161
|
-
}
|
|
29162
|
-
let json2;
|
|
29163
|
-
try {
|
|
29164
|
-
json2 = JSON.parse(raw);
|
|
29165
|
-
} catch {
|
|
29166
|
-
return null;
|
|
29167
|
-
}
|
|
29168
|
-
const parsed = DaemonPidSchema.safeParse(json2);
|
|
29169
|
-
return parsed.success ? parsed.data : null;
|
|
29170
|
-
}
|
|
29171
|
-
function clearDaemonPid(workspaceSlug) {
|
|
29172
|
-
try {
|
|
29173
|
-
unlinkSync5(daemonPidPath(workspaceSlug));
|
|
29174
|
-
} catch (err) {
|
|
29175
|
-
if (err?.code === "ENOENT")
|
|
29176
|
-
return;
|
|
29177
|
-
throw err;
|
|
29178
|
-
}
|
|
29179
|
-
}
|
|
29180
|
-
function isPidAlive(pid) {
|
|
29181
|
-
try {
|
|
29182
|
-
process.kill(pid, 0);
|
|
29183
|
-
return true;
|
|
29184
|
-
} catch (err) {
|
|
29185
|
-
return err?.code === "EPERM";
|
|
29186
|
-
}
|
|
29187
|
-
}
|
|
29188
|
-
function readLiveDaemon(workspaceSlug, aliveFn = isPidAlive) {
|
|
29189
|
-
const pidFile = readDaemonPid(workspaceSlug);
|
|
29190
|
-
if (!pidFile)
|
|
29191
|
-
return null;
|
|
29192
|
-
if (aliveFn(pidFile.pid))
|
|
29193
|
-
return pidFile;
|
|
29194
|
-
clearDaemonPid(workspaceSlug);
|
|
29195
|
-
return null;
|
|
29196
|
-
}
|
|
29197
|
-
|
|
29198
30335
|
// src/lib/destructive-gate.ts
|
|
29199
30336
|
var import_gray_matter7 = __toESM(require_gray_matter(), 1);
|
|
29200
|
-
import { readFileSync as
|
|
29201
|
-
import { basename as
|
|
30337
|
+
import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
|
|
30338
|
+
import { basename as basename3 } from "node:path";
|
|
29202
30339
|
var GUTTING_RATIO_DEFAULT = 0.5;
|
|
29203
30340
|
var GUTTING_MIN_PRIOR_BYTES = 200;
|
|
29204
30341
|
function envFloat(name, def) {
|
|
@@ -29241,10 +30378,10 @@ function classifyDestructive(event, entityType, state, deps = {}) {
|
|
|
29241
30378
|
absolutePath: event.absolutePath
|
|
29242
30379
|
};
|
|
29243
30380
|
}
|
|
29244
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
30381
|
+
const readFile = deps.readFile ?? ((p) => readFileSync11(p, "utf8"));
|
|
29245
30382
|
const statInode = deps.statInode ?? ((p) => {
|
|
29246
30383
|
try {
|
|
29247
|
-
return
|
|
30384
|
+
return statSync5(p).ino;
|
|
29248
30385
|
} catch {
|
|
29249
30386
|
return;
|
|
29250
30387
|
}
|
|
@@ -29263,7 +30400,7 @@ function classifyDestructive(event, entityType, state, deps = {}) {
|
|
|
29263
30400
|
return null;
|
|
29264
30401
|
}
|
|
29265
30402
|
const liveInode = statInode(event.absolutePath);
|
|
29266
|
-
const slug =
|
|
30403
|
+
const slug = basename3(event.absolutePath, ".md");
|
|
29267
30404
|
const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
|
|
29268
30405
|
if (!entry2)
|
|
29269
30406
|
return null;
|
|
@@ -29368,40 +30505,40 @@ class DestructiveGate {
|
|
|
29368
30505
|
}
|
|
29369
30506
|
|
|
29370
30507
|
// src/lib/resolve-pending.ts
|
|
29371
|
-
import { readFileSync as
|
|
30508
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
29372
30509
|
|
|
29373
30510
|
// src/lib/conflicts.ts
|
|
29374
|
-
import { mkdirSync as
|
|
29375
|
-
import { dirname as
|
|
30511
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30512
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
29376
30513
|
function conflictsRoot(workspaceSlug) {
|
|
29377
|
-
return
|
|
30514
|
+
return join10(plinthHome(), "_conflicts", workspaceSlug);
|
|
29378
30515
|
}
|
|
29379
30516
|
function conflictFilePath(args) {
|
|
29380
30517
|
const { workspaceSlug, entityType, entitySlug, timestamp } = args;
|
|
29381
30518
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
29382
|
-
return
|
|
30519
|
+
return join10(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.md`);
|
|
29383
30520
|
}
|
|
29384
30521
|
function stashConflict(args) {
|
|
29385
30522
|
const target = conflictFilePath(args);
|
|
29386
|
-
|
|
29387
|
-
|
|
30523
|
+
mkdirSync9(dirname7(target), { recursive: true, mode: 448 });
|
|
30524
|
+
writeFileSync7(target, args.contents, { mode: 384 });
|
|
29388
30525
|
return target;
|
|
29389
30526
|
}
|
|
29390
30527
|
function fileConflictPath(args) {
|
|
29391
30528
|
const { workspaceSlug, relPath, timestamp } = args;
|
|
29392
30529
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
29393
30530
|
const safe = relPath.replace(/[/\\]/g, "__");
|
|
29394
|
-
return
|
|
30531
|
+
return join10(conflictsRoot(workspaceSlug), `files.${safe}.${iso}`);
|
|
29395
30532
|
}
|
|
29396
30533
|
function stashFileConflict(args) {
|
|
29397
30534
|
const target = fileConflictPath(args);
|
|
29398
|
-
|
|
29399
|
-
|
|
30535
|
+
mkdirSync9(dirname7(target), { recursive: true, mode: 448 });
|
|
30536
|
+
writeFileSync7(target, args.bytes, { mode: 384 });
|
|
29400
30537
|
return target;
|
|
29401
30538
|
}
|
|
29402
30539
|
|
|
29403
30540
|
// src/lib/files/push.ts
|
|
29404
|
-
import { readFileSync as
|
|
30541
|
+
import { readFileSync as readFileSync12, statSync as statSync6 } from "node:fs";
|
|
29405
30542
|
|
|
29406
30543
|
// src/lib/log-tokens.ts
|
|
29407
30544
|
function categorizeError(err) {
|
|
@@ -29561,6 +30698,7 @@ function clearFileRejected(state, relPath) {
|
|
|
29561
30698
|
// src/lib/files/api.ts
|
|
29562
30699
|
var MAX_WORKSPACE_FILE_BYTES = 4000000;
|
|
29563
30700
|
var HASH_RE = /^[0-9a-f]{64}$/;
|
|
30701
|
+
var TextQualitySchema = exports_external.enum(["ok", "partial", "garbled"]);
|
|
29564
30702
|
var FileLiveSchema = exports_external.object({
|
|
29565
30703
|
id: exports_external.string().uuid(),
|
|
29566
30704
|
path: exports_external.string().min(1),
|
|
@@ -29571,8 +30709,25 @@ var FileLiveSchema = exports_external.object({
|
|
|
29571
30709
|
project_id: exports_external.string().uuid().nullable(),
|
|
29572
30710
|
updated_at: exports_external.string().datetime({ offset: true }),
|
|
29573
30711
|
deleted_at: exports_external.null(),
|
|
29574
|
-
last_edited_by: exports_external.string().uuid().nullable()
|
|
29575
|
-
|
|
30712
|
+
last_edited_by: exports_external.string().uuid().nullable(),
|
|
30713
|
+
content_quality: TextQualitySchema.nullable().optional().default(null).catch(null),
|
|
30714
|
+
has_extracted_text: exports_external.boolean().optional().catch(undefined),
|
|
30715
|
+
has_stored_bytes: exports_external.boolean().optional().catch(undefined)
|
|
30716
|
+
});
|
|
30717
|
+
function storedBytesSignal(row) {
|
|
30718
|
+
if (row.has_stored_bytes === true)
|
|
30719
|
+
return "present";
|
|
30720
|
+
if (row.has_stored_bytes === false)
|
|
30721
|
+
return "absent";
|
|
30722
|
+
return "unknown";
|
|
30723
|
+
}
|
|
30724
|
+
function extractedTextSignal(row) {
|
|
30725
|
+
if (row.has_extracted_text === true)
|
|
30726
|
+
return "present";
|
|
30727
|
+
if (row.has_extracted_text === false)
|
|
30728
|
+
return "absent";
|
|
30729
|
+
return "unknown";
|
|
30730
|
+
}
|
|
29576
30731
|
var FileTombstoneSchema = exports_external.object({
|
|
29577
30732
|
id: exports_external.string().uuid(),
|
|
29578
30733
|
path: exports_external.string().min(1),
|
|
@@ -29585,7 +30740,18 @@ var FilesPageSchema = exports_external.object({
|
|
|
29585
30740
|
next_cursor: exports_external.string().nullable()
|
|
29586
30741
|
});
|
|
29587
30742
|
var FileWithDownloadSchema = FileLiveSchema.extend({
|
|
29588
|
-
download_url: exports_external.string().url()
|
|
30743
|
+
download_url: exports_external.string().url().nullable()
|
|
30744
|
+
});
|
|
30745
|
+
var FileTextSchema = exports_external.object({
|
|
30746
|
+
id: exports_external.string().uuid(),
|
|
30747
|
+
path: exports_external.string().min(1),
|
|
30748
|
+
mime_type: exports_external.string(),
|
|
30749
|
+
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30750
|
+
content_quality: TextQualitySchema.nullable(),
|
|
30751
|
+
source_uri: exports_external.string().nullable(),
|
|
30752
|
+
text: exports_external.string().nullable(),
|
|
30753
|
+
chars: exports_external.number().int().nonnegative(),
|
|
30754
|
+
withheld: exports_external.enum(["bytes_are_text", "no_text"]).nullable()
|
|
29589
30755
|
});
|
|
29590
30756
|
var PreconditionSchema = exports_external.object({
|
|
29591
30757
|
code: exports_external.literal("precondition_failed"),
|
|
@@ -29654,6 +30820,34 @@ async function getFile(args) {
|
|
|
29654
30820
|
}
|
|
29655
30821
|
return { ok: true, row: parsed.data };
|
|
29656
30822
|
}
|
|
30823
|
+
async function getFileText(args) {
|
|
30824
|
+
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
30825
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files/${id}/text`;
|
|
30826
|
+
let res;
|
|
30827
|
+
try {
|
|
30828
|
+
res = await fetchImpl(url2, { method: "GET", headers: baseHeaders(pat) });
|
|
30829
|
+
} catch (err) {
|
|
30830
|
+
const msg = err instanceof Error ? err.message : "network error";
|
|
30831
|
+
throw new ApiError(0, `Could not reach ${url2}: ${msg}`);
|
|
30832
|
+
}
|
|
30833
|
+
if (res.status === 404)
|
|
30834
|
+
return { ok: false, code: 404 };
|
|
30835
|
+
if (res.status === 401) {
|
|
30836
|
+
throw new ApiError(401, "Token was rejected by the server. Run `plinth login --workspace <slug>` to refresh.");
|
|
30837
|
+
}
|
|
30838
|
+
if (res.status === 429) {
|
|
30839
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
30840
|
+
}
|
|
30841
|
+
if (!res.ok) {
|
|
30842
|
+
throw new ApiError(res.status, `get file text failed with HTTP ${res.status}`);
|
|
30843
|
+
}
|
|
30844
|
+
const body = await res.json();
|
|
30845
|
+
const parsed = FileTextSchema.safeParse(body);
|
|
30846
|
+
if (!parsed.success) {
|
|
30847
|
+
throw new ApiError(res.status, `get file text response did not match the expected shape: ${parsed.error.message}`);
|
|
30848
|
+
}
|
|
30849
|
+
return { ok: true, row: parsed.data };
|
|
30850
|
+
}
|
|
29657
30851
|
async function downloadBytes(downloadUrl, fetchImpl = fetch) {
|
|
29658
30852
|
let res;
|
|
29659
30853
|
try {
|
|
@@ -29845,13 +31039,13 @@ function dropFiles(state, relPath) {
|
|
|
29845
31039
|
}
|
|
29846
31040
|
function safeInode(absolutePath) {
|
|
29847
31041
|
try {
|
|
29848
|
-
return
|
|
31042
|
+
return statSync6(absolutePath).ino;
|
|
29849
31043
|
} catch {
|
|
29850
31044
|
return;
|
|
29851
31045
|
}
|
|
29852
31046
|
}
|
|
29853
31047
|
async function restoreFileFromCloud(args) {
|
|
29854
|
-
const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor } = args;
|
|
31048
|
+
const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor, output } = args;
|
|
29855
31049
|
const got = await getFile({ baseUrl, pat, id, fetchImpl });
|
|
29856
31050
|
const abs = filesSubtreePath(workspaceSlug, relPath);
|
|
29857
31051
|
if (!got.ok) {
|
|
@@ -29859,12 +31053,17 @@ async function restoreFileFromCloud(args) {
|
|
|
29859
31053
|
deleteFilesFile(workspaceSlug, relPath);
|
|
29860
31054
|
return dropFiles(state, relPath);
|
|
29861
31055
|
}
|
|
31056
|
+
if (got.row.download_url === null) {
|
|
31057
|
+
output?.warn(`files/${relPath}: Plinth holds no bytes for this file, so there is nothing to restore from cloud — the local copy and its state were left exactly as they are.`);
|
|
31058
|
+
return state;
|
|
31059
|
+
}
|
|
29862
31060
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
29863
31061
|
suppressor?.registerWrite(abs, {
|
|
29864
31062
|
bodyHash: got.row.content_hash,
|
|
29865
31063
|
frontmatterHash: ""
|
|
29866
31064
|
});
|
|
29867
31065
|
writeFilesBytes(workspaceSlug, relPath, bytes);
|
|
31066
|
+
deleteExtractedText(workspaceSlug, relPath);
|
|
29868
31067
|
return persistFiles(state, {
|
|
29869
31068
|
id: got.row.id,
|
|
29870
31069
|
path: relPath,
|
|
@@ -29893,7 +31092,7 @@ async function pushFilesUpsert(args) {
|
|
|
29893
31092
|
} else {
|
|
29894
31093
|
let st;
|
|
29895
31094
|
try {
|
|
29896
|
-
st =
|
|
31095
|
+
st = statSync6(absolutePath);
|
|
29897
31096
|
} catch (err) {
|
|
29898
31097
|
if (err?.code === "ENOENT") {
|
|
29899
31098
|
return pushFilesDelete({ workspaceSlug, baseUrl, pat, relPath, state, deps });
|
|
@@ -29928,7 +31127,7 @@ async function pushFilesUpsert(args) {
|
|
|
29928
31127
|
state: next
|
|
29929
31128
|
};
|
|
29930
31129
|
}
|
|
29931
|
-
bytes =
|
|
31130
|
+
bytes = readFileSync12(absolutePath);
|
|
29932
31131
|
hash2 = sha256Bytes(bytes);
|
|
29933
31132
|
inode = st.ino;
|
|
29934
31133
|
}
|
|
@@ -29999,7 +31198,8 @@ async function pushFilesUpsert(args) {
|
|
|
29999
31198
|
relPath,
|
|
30000
31199
|
state,
|
|
30001
31200
|
fetchImpl,
|
|
30002
|
-
suppressor
|
|
31201
|
+
suppressor,
|
|
31202
|
+
output
|
|
30003
31203
|
});
|
|
30004
31204
|
}
|
|
30005
31205
|
return { outcome: { kind: "conflict", relPath, stashPath }, state: nextState };
|
|
@@ -30036,7 +31236,8 @@ async function pushFilesDelete(args) {
|
|
|
30036
31236
|
relPath,
|
|
30037
31237
|
state,
|
|
30038
31238
|
fetchImpl,
|
|
30039
|
-
suppressor
|
|
31239
|
+
suppressor,
|
|
31240
|
+
output
|
|
30040
31241
|
});
|
|
30041
31242
|
return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
|
|
30042
31243
|
}
|
|
@@ -30063,12 +31264,12 @@ class PerIdMutex {
|
|
|
30063
31264
|
|
|
30064
31265
|
// src/lib/push.ts
|
|
30065
31266
|
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
30066
|
-
import { readFileSync as
|
|
30067
|
-
import { basename as
|
|
31267
|
+
import { readFileSync as readFileSync14, statSync as statSync7 } from "node:fs";
|
|
31268
|
+
import { basename as basename4, relative as relative4 } from "node:path";
|
|
30068
31269
|
|
|
30069
31270
|
// src/lib/pull-write.ts
|
|
30070
31271
|
var import_gray_matter8 = __toESM(require_gray_matter(), 1);
|
|
30071
|
-
import { readFileSync as
|
|
31272
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
30072
31273
|
function computeMirrorHashes(strategy, content) {
|
|
30073
31274
|
const parsed = import_gray_matter8.default(content);
|
|
30074
31275
|
const fm = parsed.data ?? {};
|
|
@@ -30090,7 +31291,7 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor)
|
|
|
30090
31291
|
const target = entityFilePath(workspaceSlug, strategy.type, doc2.slug);
|
|
30091
31292
|
let existing = null;
|
|
30092
31293
|
try {
|
|
30093
|
-
existing =
|
|
31294
|
+
existing = readFileSync13(target, "utf8");
|
|
30094
31295
|
} catch (err) {
|
|
30095
31296
|
if (err?.code !== "ENOENT")
|
|
30096
31297
|
throw err;
|
|
@@ -30161,7 +31362,7 @@ function omitKeys(obj, keys) {
|
|
|
30161
31362
|
}
|
|
30162
31363
|
function safeInode2(absolutePath) {
|
|
30163
31364
|
try {
|
|
30164
|
-
return
|
|
31365
|
+
return statSync7(absolutePath).ino;
|
|
30165
31366
|
} catch {
|
|
30166
31367
|
return;
|
|
30167
31368
|
}
|
|
@@ -30181,7 +31382,7 @@ function dropEntry(state, entityType, id) {
|
|
|
30181
31382
|
function stampStateFromMirror(args) {
|
|
30182
31383
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
30183
31384
|
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, doc2.slug);
|
|
30184
|
-
const mirrorRaw =
|
|
31385
|
+
const mirrorRaw = readFileSync14(mirrorPath, "utf8");
|
|
30185
31386
|
const mirrorParsed = import_gray_matter9.default(mirrorRaw);
|
|
30186
31387
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
30187
31388
|
const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
|
|
@@ -30260,7 +31461,7 @@ async function pushFile(args) {
|
|
|
30260
31461
|
}
|
|
30261
31462
|
let raw;
|
|
30262
31463
|
try {
|
|
30263
|
-
raw =
|
|
31464
|
+
raw = readFileSync14(absolutePath, "utf8");
|
|
30264
31465
|
} catch (err) {
|
|
30265
31466
|
if (err?.code === "ENOENT") {
|
|
30266
31467
|
return runPushDelete({
|
|
@@ -30294,7 +31495,7 @@ async function pushFile(args) {
|
|
|
30294
31495
|
const rel = relative4(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30295
31496
|
parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
|
|
30296
31497
|
} catch {}
|
|
30297
|
-
const slugFromPath = parsedSlug ??
|
|
31498
|
+
const slugFromPath = parsedSlug ?? basename4(absolutePath, ".md");
|
|
30298
31499
|
const slug = slugFromFm ?? slugFromPath;
|
|
30299
31500
|
if (!slug) {
|
|
30300
31501
|
return {
|
|
@@ -30377,9 +31578,9 @@ async function reconcileContextDocConflict(args) {
|
|
|
30377
31578
|
let fileSlug;
|
|
30378
31579
|
try {
|
|
30379
31580
|
const rel = relative4(workspaceMirrorPath(workspaceSlug), absolutePath);
|
|
30380
|
-
fileSlug = strategy.parseMirrorPath(rel)?.slug ??
|
|
31581
|
+
fileSlug = strategy.parseMirrorPath(rel)?.slug ?? basename4(absolutePath, ".md");
|
|
30381
31582
|
} catch {
|
|
30382
|
-
fileSlug =
|
|
31583
|
+
fileSlug = basename4(absolutePath, ".md");
|
|
30383
31584
|
}
|
|
30384
31585
|
if (fileSlug !== existingSlug) {
|
|
30385
31586
|
const stashPath = stashConflict({
|
|
@@ -31006,7 +32207,7 @@ async function runPushDelete(args) {
|
|
|
31006
32207
|
const rel = relative4(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
|
|
31007
32208
|
parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
|
|
31008
32209
|
} catch {}
|
|
31009
|
-
const lookupSlug = parsedSlug ??
|
|
32210
|
+
const lookupSlug = parsedSlug ?? basename4(args.absolutePath, ".md");
|
|
31010
32211
|
const entry2 = getEntryBySlug(args.state, strategy.type, lookupSlug);
|
|
31011
32212
|
if (entry2) {
|
|
31012
32213
|
id = entry2.id;
|
|
@@ -31101,9 +32302,9 @@ async function pushRename(args) {
|
|
|
31101
32302
|
let toSlug;
|
|
31102
32303
|
try {
|
|
31103
32304
|
const rel = relative4(workspaceMirrorPath(workspaceSlug), toPath);
|
|
31104
|
-
toSlug = strategy.parseMirrorPath(rel)?.slug ??
|
|
32305
|
+
toSlug = strategy.parseMirrorPath(rel)?.slug ?? basename4(toPath, ".md");
|
|
31105
32306
|
} catch {
|
|
31106
|
-
toSlug =
|
|
32307
|
+
toSlug = basename4(toPath, ".md");
|
|
31107
32308
|
}
|
|
31108
32309
|
return mutex.run(id, async () => {
|
|
31109
32310
|
const live = getEntry(state, entityType, id);
|
|
@@ -31133,7 +32334,7 @@ async function pushRename(args) {
|
|
|
31133
32334
|
};
|
|
31134
32335
|
if (!updated.body_hash || !updated.frontmatter_hash) {
|
|
31135
32336
|
try {
|
|
31136
|
-
const mirrorRaw =
|
|
32337
|
+
const mirrorRaw = readFileSync14(toPath, "utf8");
|
|
31137
32338
|
const mirrorParsed = import_gray_matter9.default(mirrorRaw);
|
|
31138
32339
|
const renameFm = mirrorParsed.data ?? {};
|
|
31139
32340
|
const renameLifted = splitUserFrontmatter(strategy, renameFm);
|
|
@@ -31168,7 +32369,7 @@ async function pushRename(args) {
|
|
|
31168
32369
|
if (patchResult.reason === "slug_conflict") {
|
|
31169
32370
|
let contents2 = "";
|
|
31170
32371
|
try {
|
|
31171
|
-
contents2 =
|
|
32372
|
+
contents2 = readFileSync14(toPath, "utf8");
|
|
31172
32373
|
} catch {}
|
|
31173
32374
|
const stashPath2 = stashConflict({
|
|
31174
32375
|
workspaceSlug,
|
|
@@ -31208,7 +32409,7 @@ async function pushRename(args) {
|
|
|
31208
32409
|
}
|
|
31209
32410
|
let contents = "";
|
|
31210
32411
|
try {
|
|
31211
|
-
contents =
|
|
32412
|
+
contents = readFileSync14(toPath, "utf8");
|
|
31212
32413
|
} catch {}
|
|
31213
32414
|
const stashPath = stashConflict({
|
|
31214
32415
|
workspaceSlug,
|
|
@@ -31329,7 +32530,7 @@ async function discardBatchOps(batch, deps) {
|
|
|
31329
32530
|
const path5 = opPath(deps.workspaceSlug, op);
|
|
31330
32531
|
let local = null;
|
|
31331
32532
|
try {
|
|
31332
|
-
local =
|
|
32533
|
+
local = readFileSync15(path5, "utf8");
|
|
31333
32534
|
} catch {
|
|
31334
32535
|
local = null;
|
|
31335
32536
|
}
|
|
@@ -31363,7 +32564,7 @@ async function discardFileOp(op, deps) {
|
|
|
31363
32564
|
const abs = filesSubtreePath(deps.workspaceSlug, op.path);
|
|
31364
32565
|
let local = null;
|
|
31365
32566
|
try {
|
|
31366
|
-
local =
|
|
32567
|
+
local = readFileSync15(abs);
|
|
31367
32568
|
} catch {
|
|
31368
32569
|
local = null;
|
|
31369
32570
|
}
|
|
@@ -31382,7 +32583,8 @@ async function discardFileOp(op, deps) {
|
|
|
31382
32583
|
id: op.id,
|
|
31383
32584
|
relPath: op.path,
|
|
31384
32585
|
state: deps.getState(),
|
|
31385
|
-
fetchImpl: deps.fetchImpl
|
|
32586
|
+
fetchImpl: deps.fetchImpl,
|
|
32587
|
+
output: deps.output
|
|
31386
32588
|
});
|
|
31387
32589
|
deps.setState(next);
|
|
31388
32590
|
}
|
|
@@ -31544,10 +32746,10 @@ class FileDestructiveGate {
|
|
|
31544
32746
|
}
|
|
31545
32747
|
|
|
31546
32748
|
// src/lib/files/reconcile.ts
|
|
31547
|
-
import { readFileSync as
|
|
32749
|
+
import { readFileSync as readFileSync16, statSync as statSync9 } from "node:fs";
|
|
31548
32750
|
|
|
31549
32751
|
// src/lib/unsynced.ts
|
|
31550
|
-
import { join as
|
|
32752
|
+
import { join as join11, relative as relative6 } from "node:path";
|
|
31551
32753
|
var RECORDABLE_REASONS = new Set([
|
|
31552
32754
|
"rate_limit",
|
|
31553
32755
|
"server",
|
|
@@ -31610,7 +32812,7 @@ async function drainUnsyncedOps(args) {
|
|
|
31610
32812
|
attempted += 1;
|
|
31611
32813
|
try {
|
|
31612
32814
|
if (op.target === "entity") {
|
|
31613
|
-
const absolutePath =
|
|
32815
|
+
const absolutePath = join11(workspaceMirrorPath(workspaceSlug), op.path);
|
|
31614
32816
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
31615
32817
|
if (!classified) {
|
|
31616
32818
|
clear("entity", op.path);
|
|
@@ -31856,10 +33058,10 @@ class ReaddirpStream extends Readable {
|
|
|
31856
33058
|
}
|
|
31857
33059
|
async _formatEntry(dirent, path5) {
|
|
31858
33060
|
let entry2;
|
|
31859
|
-
const
|
|
33061
|
+
const basename5 = this._isDirent ? dirent.name : dirent;
|
|
31860
33062
|
try {
|
|
31861
|
-
const fullPath = presolve(pjoin(path5,
|
|
31862
|
-
entry2 = { path: prelative(this._root, fullPath), fullPath, basename:
|
|
33063
|
+
const fullPath = presolve(pjoin(path5, basename5));
|
|
33064
|
+
entry2 = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
|
|
31863
33065
|
entry2[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
|
|
31864
33066
|
} catch (err) {
|
|
31865
33067
|
this._onError(err);
|
|
@@ -32386,9 +33588,9 @@ class NodeFsHandler {
|
|
|
32386
33588
|
_watchWithNodeFs(path5, listener) {
|
|
32387
33589
|
const opts = this.fsw.options;
|
|
32388
33590
|
const directory = sp.dirname(path5);
|
|
32389
|
-
const
|
|
33591
|
+
const basename6 = sp.basename(path5);
|
|
32390
33592
|
const parent = this.fsw._getWatchedDir(directory);
|
|
32391
|
-
parent.add(
|
|
33593
|
+
parent.add(basename6);
|
|
32392
33594
|
const absolutePath = sp.resolve(path5);
|
|
32393
33595
|
const options2 = {
|
|
32394
33596
|
persistent: opts.persistent
|
|
@@ -32398,7 +33600,7 @@ class NodeFsHandler {
|
|
|
32398
33600
|
let closer;
|
|
32399
33601
|
if (opts.usePolling) {
|
|
32400
33602
|
const enableBin = opts.interval !== opts.binaryInterval;
|
|
32401
|
-
options2.interval = enableBin && isBinaryPath(
|
|
33603
|
+
options2.interval = enableBin && isBinaryPath(basename6) ? opts.binaryInterval : opts.interval;
|
|
32402
33604
|
closer = setFsWatchFileListener(path5, absolutePath, options2, {
|
|
32403
33605
|
listener,
|
|
32404
33606
|
rawEmitter: this.fsw._emitRaw
|
|
@@ -32416,11 +33618,11 @@ class NodeFsHandler {
|
|
|
32416
33618
|
if (this.fsw.closed) {
|
|
32417
33619
|
return;
|
|
32418
33620
|
}
|
|
32419
|
-
const
|
|
32420
|
-
const
|
|
32421
|
-
const parent = this.fsw._getWatchedDir(
|
|
33621
|
+
const dirname9 = sp.dirname(file2);
|
|
33622
|
+
const basename6 = sp.basename(file2);
|
|
33623
|
+
const parent = this.fsw._getWatchedDir(dirname9);
|
|
32422
33624
|
let prevStats = stats;
|
|
32423
|
-
if (parent.has(
|
|
33625
|
+
if (parent.has(basename6))
|
|
32424
33626
|
return;
|
|
32425
33627
|
const listener = async (path5, newStats) => {
|
|
32426
33628
|
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
|
|
@@ -32445,9 +33647,9 @@ class NodeFsHandler {
|
|
|
32445
33647
|
prevStats = newStats2;
|
|
32446
33648
|
}
|
|
32447
33649
|
} catch (error52) {
|
|
32448
|
-
this.fsw._remove(
|
|
33650
|
+
this.fsw._remove(dirname9, basename6);
|
|
32449
33651
|
}
|
|
32450
|
-
} else if (parent.has(
|
|
33652
|
+
} else if (parent.has(basename6)) {
|
|
32451
33653
|
const at = newStats.atimeMs;
|
|
32452
33654
|
const mt = newStats.mtimeMs;
|
|
32453
33655
|
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
@@ -33301,7 +34503,7 @@ function watch(paths, options2 = {}) {
|
|
|
33301
34503
|
var chokidar_default = { watch, FSWatcher };
|
|
33302
34504
|
|
|
33303
34505
|
// src/lib/watcher.ts
|
|
33304
|
-
import { statSync as
|
|
34506
|
+
import { statSync as statSync8 } from "node:fs";
|
|
33305
34507
|
import { sep as sep5 } from "node:path";
|
|
33306
34508
|
|
|
33307
34509
|
// src/lib/rename-classifier.ts
|
|
@@ -33517,7 +34719,7 @@ function startWatcher(opts, handler) {
|
|
|
33517
34719
|
pairingWindowMs: opts.renamePairingWindowMs ?? DEFAULT_DEBOUNCE_MS,
|
|
33518
34720
|
classifyPath: opts.classifyPath,
|
|
33519
34721
|
getStateEntry: opts.getStateEntry,
|
|
33520
|
-
statSync: opts.statSync ?? ((p) => ({ ino:
|
|
34722
|
+
statSync: opts.statSync ?? ((p) => ({ ino: statSync8(p).ino }))
|
|
33521
34723
|
});
|
|
33522
34724
|
const factory = opts.chokidarFactory ?? ((paths, watcherOpts) => chokidar_default.watch(paths, watcherOpts));
|
|
33523
34725
|
const watcher = factory(workspaceRoot, {
|
|
@@ -33552,7 +34754,7 @@ function readBlobForPush(args) {
|
|
|
33552
34754
|
const { absolutePath, relPath, output } = args;
|
|
33553
34755
|
let st;
|
|
33554
34756
|
try {
|
|
33555
|
-
st =
|
|
34757
|
+
st = statSync9(absolutePath);
|
|
33556
34758
|
} catch (err) {
|
|
33557
34759
|
if (err?.code === "ENOENT")
|
|
33558
34760
|
return { kind: "gone" };
|
|
@@ -33571,7 +34773,7 @@ function readBlobForPush(args) {
|
|
|
33571
34773
|
}
|
|
33572
34774
|
let bytes;
|
|
33573
34775
|
try {
|
|
33574
|
-
bytes =
|
|
34776
|
+
bytes = readFileSync16(absolutePath);
|
|
33575
34777
|
} catch (err) {
|
|
33576
34778
|
if (err?.code === "ENOENT")
|
|
33577
34779
|
return { kind: "gone" };
|
|
@@ -34230,7 +35432,7 @@ class PullSuppressor {
|
|
|
34230
35432
|
}
|
|
34231
35433
|
|
|
34232
35434
|
// src/lib/reconcile.ts
|
|
34233
|
-
import { readFileSync as
|
|
35435
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
34234
35436
|
async function reconcileDivergedRows(args) {
|
|
34235
35437
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
34236
35438
|
const now = args.now ?? (() => new Date);
|
|
@@ -34316,7 +35518,7 @@ async function reconcileDivergedRows(args) {
|
|
|
34316
35518
|
}
|
|
34317
35519
|
let content;
|
|
34318
35520
|
try {
|
|
34319
|
-
content =
|
|
35521
|
+
content = readFileSync17(absolutePath, "utf8");
|
|
34320
35522
|
} catch (err) {
|
|
34321
35523
|
if (err?.code === "ENOENT")
|
|
34322
35524
|
continue;
|
|
@@ -35259,25 +36461,111 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
35259
36461
|
// src/commands/sync.ts
|
|
35260
36462
|
import {
|
|
35261
36463
|
existsSync as existsSync6,
|
|
35262
|
-
readdirSync as
|
|
35263
|
-
readFileSync as
|
|
35264
|
-
statSync as
|
|
35265
|
-
unlinkSync as
|
|
36464
|
+
readdirSync as readdirSync5,
|
|
36465
|
+
readFileSync as readFileSync19,
|
|
36466
|
+
statSync as statSync11,
|
|
36467
|
+
unlinkSync as unlinkSync7
|
|
35266
36468
|
} from "node:fs";
|
|
35267
|
-
import { join as
|
|
36469
|
+
import { join as join14 } from "node:path";
|
|
36470
|
+
|
|
36471
|
+
// src/lib/files/pull.ts
|
|
36472
|
+
import { existsSync as existsSync5, readFileSync as readFileSync18, statSync as statSync10 } from "node:fs";
|
|
36473
|
+
|
|
36474
|
+
// src/lib/files/extracted-text.ts
|
|
36475
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
36476
|
+
var DERIVED_TEXT_VERSION = 2;
|
|
36477
|
+
function yamlString(value) {
|
|
36478
|
+
return JSON.stringify(value);
|
|
36479
|
+
}
|
|
36480
|
+
var PARTIAL_WARNING = [
|
|
36481
|
+
"> ⚠️ **PARTIAL EXTRACTION.** Some spans of this document came out as text and",
|
|
36482
|
+
"> some did not. Which spans is NOT recorded, so this could be a clean document",
|
|
36483
|
+
"> with mangled party names, or clean prose that degrades part-way through.",
|
|
36484
|
+
"> Verify anything load-bearing against the source file."
|
|
36485
|
+
].join(`
|
|
36486
|
+
`);
|
|
36487
|
+
var NO_LOCAL_COPY_NOTE = [
|
|
36488
|
+
"> ℹ️ **NO LOCAL COPY OF THIS FILE.** Plinth does not hold this file's bytes, so",
|
|
36489
|
+
"> nothing was written under `files/` for it and the text below is the only copy",
|
|
36490
|
+
"> in this mirror. The original lives wherever `source_uri` above points."
|
|
36491
|
+
].join(`
|
|
36492
|
+
`);
|
|
36493
|
+
function garbledBody(relPath, bytesInMirror) {
|
|
36494
|
+
return [
|
|
36495
|
+
"> \uD83D\uDD34 **NOT TEXT — body withheld.**",
|
|
36496
|
+
">",
|
|
36497
|
+
"> Extraction produced characters that are not words. The usual cause is a PDF",
|
|
36498
|
+
"> whose embedded fonts carry no ToUnicode map, so the glyph indices come out",
|
|
36499
|
+
"> where the letters should be: a page reading *“Dated this 10th day of",
|
|
36500
|
+
"> September”* extracts as `):-,:019`.",
|
|
36501
|
+
">",
|
|
36502
|
+
"> The characters are NOT reproduced here on purpose. They read as text to a",
|
|
36503
|
+
"> reader and to a search index, and they say nothing — writing them into the",
|
|
36504
|
+
"> mirror would put a document into your corpus that appears readable and is",
|
|
36505
|
+
"> not. They are excluded from Plinth's own search index for the same reason.",
|
|
36506
|
+
">",
|
|
36507
|
+
bytesInMirror ? `> **Read the original instead:** \`files/${relPath}\`` : "> **There is no local original to read** — Plinth holds no bytes for this file." + " Follow `source_uri` above to the copy that has them."
|
|
36508
|
+
].join(`
|
|
36509
|
+
`);
|
|
36510
|
+
}
|
|
36511
|
+
function renderExtractedText(doc2) {
|
|
36512
|
+
const withheld = doc2.quality === "garbled";
|
|
36513
|
+
const frontmatter = [
|
|
36514
|
+
"---",
|
|
36515
|
+
"plinth_generated: true",
|
|
36516
|
+
`source_file: ${doc2.bytesInMirror ? yamlString(`files/${doc2.relPath}`) : "null"}`,
|
|
36517
|
+
`source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" : yamlString(doc2.sourceUri)}`,
|
|
36518
|
+
`content_quality: ${doc2.quality ?? "unchecked"}`,
|
|
36519
|
+
`body_withheld: ${withheld}`,
|
|
36520
|
+
`extracted_chars: ${withheld ? 0 : doc2.text.length}`,
|
|
36521
|
+
"---",
|
|
36522
|
+
"",
|
|
36523
|
+
"<!-- Generated by Plinth from the source file above. This file is DERIVED:",
|
|
36524
|
+
" your edits here are overwritten on the next sync and are never uploaded.",
|
|
36525
|
+
" Edit the source file instead. -->",
|
|
36526
|
+
""
|
|
36527
|
+
].join(`
|
|
36528
|
+
`);
|
|
36529
|
+
if (withheld) {
|
|
36530
|
+
return `${frontmatter}${garbledBody(doc2.relPath, doc2.bytesInMirror)}
|
|
36531
|
+
`;
|
|
36532
|
+
}
|
|
36533
|
+
const notices = [
|
|
36534
|
+
...doc2.quality === "partial" ? [PARTIAL_WARNING] : [],
|
|
36535
|
+
...doc2.bytesInMirror ? [] : [NO_LOCAL_COPY_NOTE]
|
|
36536
|
+
];
|
|
36537
|
+
if (notices.length > 0) {
|
|
36538
|
+
return `${frontmatter}${notices.join(`
|
|
36539
|
+
|
|
36540
|
+
`)}
|
|
36541
|
+
|
|
36542
|
+
${doc2.text}
|
|
36543
|
+
`;
|
|
36544
|
+
}
|
|
36545
|
+
return `${frontmatter}${doc2.text}
|
|
36546
|
+
`;
|
|
36547
|
+
}
|
|
36548
|
+
function extractedTextHash(rendered) {
|
|
36549
|
+
return createHash3("sha256").update(rendered, "utf8").digest("hex");
|
|
36550
|
+
}
|
|
35268
36551
|
|
|
35269
36552
|
// src/lib/files/pull.ts
|
|
35270
|
-
import { existsSync as existsSync5, readFileSync as readFileSync16, statSync as statSync8 } from "node:fs";
|
|
35271
36553
|
async function pullFiles(args) {
|
|
35272
36554
|
const { workspaceSlug, baseUrl, pat, fetchImpl } = args;
|
|
35273
36555
|
let state = args.state;
|
|
35274
36556
|
let fetched = 0;
|
|
35275
36557
|
let written = 0;
|
|
35276
36558
|
let deleted = 0;
|
|
36559
|
+
let textWritten = 0;
|
|
36560
|
+
let textDeleted = 0;
|
|
36561
|
+
let textUnknown = 0;
|
|
36562
|
+
let pointerRows = 0;
|
|
35277
36563
|
let cursor = args.cursor;
|
|
35278
36564
|
let lastCursor = args.cursor;
|
|
36565
|
+
let cursorBlocked = false;
|
|
35279
36566
|
while (true) {
|
|
35280
36567
|
const page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
|
|
36568
|
+
let pageTextFailed = false;
|
|
35281
36569
|
for (const row of page.rows) {
|
|
35282
36570
|
fetched++;
|
|
35283
36571
|
const outcome = await applyFileRow({
|
|
@@ -35290,20 +36578,101 @@ async function pullFiles(args) {
|
|
|
35290
36578
|
now: args.now,
|
|
35291
36579
|
output: args.output,
|
|
35292
36580
|
suppressor: args.suppressor,
|
|
35293
|
-
isFileFrozen: args.isFileFrozen
|
|
36581
|
+
isFileFrozen: args.isFileFrozen,
|
|
36582
|
+
forceTextRewalk: args.forceTextRewalk
|
|
35294
36583
|
});
|
|
35295
36584
|
state = outcome.state;
|
|
35296
36585
|
if (outcome.action === "written")
|
|
35297
36586
|
written++;
|
|
35298
36587
|
else if (outcome.action === "deleted")
|
|
35299
36588
|
deleted++;
|
|
35300
|
-
|
|
36589
|
+
textWritten += outcome.textWritten ?? 0;
|
|
36590
|
+
textDeleted += outcome.textDeleted ?? 0;
|
|
36591
|
+
if (outcome.textUnknown)
|
|
36592
|
+
textUnknown++;
|
|
36593
|
+
if (outcome.pointer)
|
|
36594
|
+
pointerRows++;
|
|
36595
|
+
if (outcome.textFailed)
|
|
36596
|
+
pageTextFailed = true;
|
|
36597
|
+
}
|
|
36598
|
+
if (pageTextFailed)
|
|
36599
|
+
cursorBlocked = true;
|
|
35301
36600
|
if (page.next_cursor === null)
|
|
35302
36601
|
break;
|
|
35303
|
-
|
|
36602
|
+
if (!cursorBlocked)
|
|
36603
|
+
lastCursor = page.next_cursor;
|
|
35304
36604
|
cursor = page.next_cursor;
|
|
35305
36605
|
}
|
|
35306
|
-
|
|
36606
|
+
if (textUnknown > 0) {
|
|
36607
|
+
args.output?.warn(`${textUnknown} file ${textUnknown === 1 ? "row" : "rows"} arrived without a readable \`has_extracted_text\` — this server predates the extracted-text contract, or the field was dropped from the payload. Derived text under views/extracted/ was left untouched rather than deleted, and is not being updated either.`);
|
|
36608
|
+
}
|
|
36609
|
+
if (pointerRows > 0) {
|
|
36610
|
+
args.output?.info(`${pointerRows} file ${pointerRows === 1 ? "row holds" : "rows hold"} no bytes in Plinth — nothing was written under files/ for ${pointerRows === 1 ? "it" : "them"}; where there is extracted text, views/extracted/ holds the only local copy.`);
|
|
36611
|
+
}
|
|
36612
|
+
return {
|
|
36613
|
+
state,
|
|
36614
|
+
fetched,
|
|
36615
|
+
written,
|
|
36616
|
+
deleted,
|
|
36617
|
+
textWritten,
|
|
36618
|
+
textDeleted,
|
|
36619
|
+
textUnknown,
|
|
36620
|
+
pointerRows,
|
|
36621
|
+
lastCursor
|
|
36622
|
+
};
|
|
36623
|
+
}
|
|
36624
|
+
async function applyExtractedText(args) {
|
|
36625
|
+
try {
|
|
36626
|
+
return await materialiseExtractedText(args);
|
|
36627
|
+
} catch (err) {
|
|
36628
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36629
|
+
args.output?.warn(`files/${args.relPath}: could not materialise extracted text (${msg}) — mirror text left unwritten, will retry.`);
|
|
36630
|
+
return { textHash: undefined, written: 0, deleted: 0, failed: true };
|
|
36631
|
+
}
|
|
36632
|
+
}
|
|
36633
|
+
async function materialiseExtractedText(args) {
|
|
36634
|
+
const { workspaceSlug, baseUrl, pat, row, relPath, bytesInMirror, fetchImpl, output } = args;
|
|
36635
|
+
const signal = extractedTextSignal(row);
|
|
36636
|
+
if (signal === "absent") {
|
|
36637
|
+
const removed = deleteExtractedText(workspaceSlug, relPath);
|
|
36638
|
+
return { textHash: undefined, written: 0, deleted: removed ? 1 : 0 };
|
|
36639
|
+
}
|
|
36640
|
+
if (signal === "unknown") {
|
|
36641
|
+
return { textHash: undefined, written: 0, deleted: 0 };
|
|
36642
|
+
}
|
|
36643
|
+
let rendered;
|
|
36644
|
+
if (row.content_quality === "garbled" && bytesInMirror) {
|
|
36645
|
+
rendered = renderExtractedText({
|
|
36646
|
+
relPath,
|
|
36647
|
+
quality: "garbled",
|
|
36648
|
+
bytesInMirror,
|
|
36649
|
+
sourceUri: undefined,
|
|
36650
|
+
text: ""
|
|
36651
|
+
});
|
|
36652
|
+
} else {
|
|
36653
|
+
const got = await getFileText({ baseUrl, pat, id: row.id, fetchImpl });
|
|
36654
|
+
if (!got.ok)
|
|
36655
|
+
return { textHash: undefined, written: 0, deleted: 0 };
|
|
36656
|
+
if (got.row.text === null) {
|
|
36657
|
+
output?.warn(`files/${relPath}: feed reported extracted text, text route withheld it (${got.row.withheld ?? "no reason given"}) — no derived file written.`);
|
|
36658
|
+
const removed = deleteExtractedText(workspaceSlug, relPath);
|
|
36659
|
+
return { textHash: undefined, written: 0, deleted: removed ? 1 : 0 };
|
|
36660
|
+
}
|
|
36661
|
+
rendered = renderExtractedText({
|
|
36662
|
+
relPath,
|
|
36663
|
+
quality: got.row.content_quality,
|
|
36664
|
+
sourceUri: got.row.source_uri,
|
|
36665
|
+
text: got.row.text,
|
|
36666
|
+
bytesInMirror
|
|
36667
|
+
});
|
|
36668
|
+
}
|
|
36669
|
+
const hash2 = extractedTextHash(rendered);
|
|
36670
|
+
const onDisk = readExtractedText(workspaceSlug, relPath);
|
|
36671
|
+
if (onDisk !== null && extractedTextHash(onDisk) === hash2) {
|
|
36672
|
+
return { textHash: hash2, written: 0, deleted: 0 };
|
|
36673
|
+
}
|
|
36674
|
+
writeExtractedText(workspaceSlug, relPath, rendered);
|
|
36675
|
+
return { textHash: hash2, written: 1, deleted: 0 };
|
|
35307
36676
|
}
|
|
35308
36677
|
async function applyFileRow(args) {
|
|
35309
36678
|
const { workspaceSlug, baseUrl, pat, row, state, fetchImpl, output, suppressor } = args;
|
|
@@ -35321,28 +36690,88 @@ async function applyFileRow(args) {
|
|
|
35321
36690
|
if (row.deleted_at !== null) {
|
|
35322
36691
|
suppressor?.registerDelete(abs);
|
|
35323
36692
|
const didUnlink = deleteFilesFile(workspaceSlug, relPath);
|
|
36693
|
+
const removedText = deleteExtractedText(workspaceSlug, relPath);
|
|
36694
|
+
const textDeleted = removedText ? 1 : 0;
|
|
35324
36695
|
if (existing) {
|
|
35325
36696
|
const { state: next2 } = removeFileEntry(state, relPath);
|
|
35326
|
-
return { state: next2, action: "deleted" };
|
|
36697
|
+
return { state: next2, action: "deleted", textDeleted };
|
|
35327
36698
|
}
|
|
35328
|
-
return { state, action: didUnlink ? "deleted" : "skipped" };
|
|
36699
|
+
return { state, action: didUnlink ? "deleted" : "skipped", textDeleted };
|
|
36700
|
+
}
|
|
36701
|
+
const textSignal = extractedTextSignal(row);
|
|
36702
|
+
const textUnknown = textSignal === "unknown";
|
|
36703
|
+
if (storedBytesSignal(row) === "absent") {
|
|
36704
|
+
return applyPointerRow({
|
|
36705
|
+
workspaceSlug,
|
|
36706
|
+
baseUrl,
|
|
36707
|
+
pat,
|
|
36708
|
+
row,
|
|
36709
|
+
relPath,
|
|
36710
|
+
state,
|
|
36711
|
+
existing,
|
|
36712
|
+
textSignal,
|
|
36713
|
+
fetchImpl,
|
|
36714
|
+
output
|
|
36715
|
+
});
|
|
35329
36716
|
}
|
|
35330
36717
|
if (existing && existing.content_hash === row.content_hash && existsSync5(abs)) {
|
|
35331
|
-
|
|
36718
|
+
if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
|
|
36719
|
+
return { state, action: "noop", textUnknown };
|
|
36720
|
+
}
|
|
36721
|
+
const text2 = await applyExtractedText({
|
|
36722
|
+
workspaceSlug,
|
|
36723
|
+
baseUrl,
|
|
36724
|
+
pat,
|
|
36725
|
+
row,
|
|
36726
|
+
relPath,
|
|
36727
|
+
bytesInMirror: true,
|
|
36728
|
+
fetchImpl,
|
|
36729
|
+
output
|
|
36730
|
+
});
|
|
36731
|
+
const { state: next2 } = upsertFileEntry(state, {
|
|
36732
|
+
...existing,
|
|
36733
|
+
updated_at: row.updated_at,
|
|
36734
|
+
text_hash: textUnknown ? existing.text_hash : text2.textHash
|
|
36735
|
+
});
|
|
36736
|
+
return {
|
|
36737
|
+
state: next2,
|
|
36738
|
+
action: "noop",
|
|
36739
|
+
textWritten: text2.written,
|
|
36740
|
+
textDeleted: text2.deleted,
|
|
36741
|
+
textFailed: text2.failed,
|
|
36742
|
+
textUnknown
|
|
36743
|
+
};
|
|
36744
|
+
}
|
|
36745
|
+
const got = await getFile({ baseUrl, pat, id: row.id, fetchImpl });
|
|
36746
|
+
if (!got.ok) {
|
|
36747
|
+
return { state, action: "skipped" };
|
|
36748
|
+
}
|
|
36749
|
+
if (got.row.download_url === null) {
|
|
36750
|
+
if (storedBytesSignal(row) === "present") {
|
|
36751
|
+
output?.warn(`files/${relPath}: the file index says Plinth holds this file's bytes and the file route says it does not — treating it as having none, and leaving any local copy alone.`);
|
|
36752
|
+
}
|
|
36753
|
+
return applyPointerRow({
|
|
36754
|
+
workspaceSlug,
|
|
36755
|
+
baseUrl,
|
|
36756
|
+
pat,
|
|
36757
|
+
row,
|
|
36758
|
+
relPath,
|
|
36759
|
+
state,
|
|
36760
|
+
existing,
|
|
36761
|
+
textSignal,
|
|
36762
|
+
fetchImpl,
|
|
36763
|
+
output
|
|
36764
|
+
});
|
|
35332
36765
|
}
|
|
35333
36766
|
if (!suppressor && existing && existing.content_hash !== row.content_hash && existsSync5(abs)) {
|
|
35334
36767
|
try {
|
|
35335
|
-
const localBytes =
|
|
36768
|
+
const localBytes = readFileSync18(abs);
|
|
35336
36769
|
if (sha256Bytes(localBytes) !== existing.content_hash) {
|
|
35337
36770
|
const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes });
|
|
35338
36771
|
output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
|
|
35339
36772
|
}
|
|
35340
36773
|
} catch {}
|
|
35341
36774
|
}
|
|
35342
|
-
const got = await getFile({ baseUrl, pat, id: row.id, fetchImpl });
|
|
35343
|
-
if (!got.ok) {
|
|
35344
|
-
return { state, action: "skipped" };
|
|
35345
|
-
}
|
|
35346
36775
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
35347
36776
|
if (sha256Bytes(bytes) !== row.content_hash) {
|
|
35348
36777
|
output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
|
|
@@ -35350,9 +36779,28 @@ async function applyFileRow(args) {
|
|
|
35350
36779
|
}
|
|
35351
36780
|
suppressor?.registerWrite(abs, { bodyHash: row.content_hash, frontmatterHash: "" });
|
|
35352
36781
|
writeFilesBytes(workspaceSlug, relPath, bytes);
|
|
36782
|
+
let text;
|
|
36783
|
+
if (textUnknown) {
|
|
36784
|
+
const removed = deleteExtractedText(workspaceSlug, relPath);
|
|
36785
|
+
if (removed) {
|
|
36786
|
+
output?.warn(`files/${relPath}: bytes changed while the server said nothing about extracted text — removed the derived text, which described the previous version of this file.`);
|
|
36787
|
+
}
|
|
36788
|
+
text = { textHash: undefined, written: 0, deleted: removed ? 1 : 0 };
|
|
36789
|
+
} else {
|
|
36790
|
+
text = await applyExtractedText({
|
|
36791
|
+
workspaceSlug,
|
|
36792
|
+
baseUrl,
|
|
36793
|
+
pat,
|
|
36794
|
+
row,
|
|
36795
|
+
relPath,
|
|
36796
|
+
bytesInMirror: true,
|
|
36797
|
+
fetchImpl,
|
|
36798
|
+
output
|
|
36799
|
+
});
|
|
36800
|
+
}
|
|
35353
36801
|
let inode;
|
|
35354
36802
|
try {
|
|
35355
|
-
inode =
|
|
36803
|
+
inode = statSync10(abs).ino;
|
|
35356
36804
|
} catch {
|
|
35357
36805
|
inode = undefined;
|
|
35358
36806
|
}
|
|
@@ -35363,9 +36811,60 @@ async function applyFileRow(args) {
|
|
|
35363
36811
|
size_bytes: row.size_bytes,
|
|
35364
36812
|
mime_type: row.mime_type,
|
|
35365
36813
|
updated_at: row.updated_at,
|
|
35366
|
-
inode
|
|
36814
|
+
inode,
|
|
36815
|
+
text_hash: text.textHash
|
|
36816
|
+
});
|
|
36817
|
+
return {
|
|
36818
|
+
state: next,
|
|
36819
|
+
action: "written",
|
|
36820
|
+
textWritten: text.written,
|
|
36821
|
+
textDeleted: text.deleted,
|
|
36822
|
+
textFailed: text.failed,
|
|
36823
|
+
textUnknown
|
|
36824
|
+
};
|
|
36825
|
+
}
|
|
36826
|
+
async function applyPointerRow(args) {
|
|
36827
|
+
const { workspaceSlug, baseUrl, pat, row, relPath, state, textSignal } = args;
|
|
36828
|
+
if (textSignal === "absent") {
|
|
36829
|
+
args.output?.warn(`files/${relPath}: Plinth holds neither bytes nor text for this file — it has no local representation in the mirror.`);
|
|
36830
|
+
}
|
|
36831
|
+
const bytesInMirror = existsSync5(filesSubtreePath(workspaceSlug, relPath));
|
|
36832
|
+
if (bytesInMirror) {
|
|
36833
|
+
args.output?.warn(`files/${relPath}: Plinth holds no bytes for this file, but a local copy is present at that path — left exactly as it is. Plinth will not update it.`);
|
|
36834
|
+
}
|
|
36835
|
+
const text = await applyExtractedText({
|
|
36836
|
+
workspaceSlug,
|
|
36837
|
+
baseUrl,
|
|
36838
|
+
pat,
|
|
36839
|
+
row,
|
|
36840
|
+
relPath,
|
|
36841
|
+
bytesInMirror,
|
|
36842
|
+
fetchImpl: args.fetchImpl,
|
|
36843
|
+
output: args.output
|
|
35367
36844
|
});
|
|
35368
|
-
return {
|
|
36845
|
+
return {
|
|
36846
|
+
state,
|
|
36847
|
+
action: "noop",
|
|
36848
|
+
textWritten: text.written,
|
|
36849
|
+
textDeleted: text.deleted,
|
|
36850
|
+
textFailed: text.failed,
|
|
36851
|
+
textUnknown: textSignal === "unknown",
|
|
36852
|
+
pointer: true
|
|
36853
|
+
};
|
|
36854
|
+
}
|
|
36855
|
+
function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
36856
|
+
const signal = extractedTextSignal(row);
|
|
36857
|
+
if (signal === "unknown")
|
|
36858
|
+
return true;
|
|
36859
|
+
if (signal === "absent") {
|
|
36860
|
+
return existing.text_hash === undefined && !existsSync5(extractedTextPath(workspaceSlug, relPath));
|
|
36861
|
+
}
|
|
36862
|
+
if (existing.text_hash === undefined)
|
|
36863
|
+
return false;
|
|
36864
|
+
const onDisk = readExtractedText(workspaceSlug, relPath);
|
|
36865
|
+
if (onDisk === null)
|
|
36866
|
+
return false;
|
|
36867
|
+
return extractedTextHash(onDisk) === existing.text_hash;
|
|
35369
36868
|
}
|
|
35370
36869
|
|
|
35371
36870
|
// src/commands/sync.ts
|
|
@@ -35383,11 +36882,11 @@ function hasExistingContent(workspacePath) {
|
|
|
35383
36882
|
if (!existsSync6(workspacePath))
|
|
35384
36883
|
return false;
|
|
35385
36884
|
try {
|
|
35386
|
-
return
|
|
36885
|
+
return readdirSync5(workspacePath, { withFileTypes: true }).some((e2) => {
|
|
35387
36886
|
if (!e2.isDirectory())
|
|
35388
36887
|
return true;
|
|
35389
36888
|
try {
|
|
35390
|
-
return
|
|
36889
|
+
return readdirSync5(join14(workspacePath, e2.name)).length > 0;
|
|
35391
36890
|
} catch {
|
|
35392
36891
|
return true;
|
|
35393
36892
|
}
|
|
@@ -35407,7 +36906,7 @@ function unlinkIfInodeMatches(workspaceSlug, entityType, entrySlug, expectedInod
|
|
|
35407
36906
|
}
|
|
35408
36907
|
let liveInode;
|
|
35409
36908
|
try {
|
|
35410
|
-
liveInode =
|
|
36909
|
+
liveInode = statSync11(target).ino;
|
|
35411
36910
|
} catch {
|
|
35412
36911
|
return { didUnlink: false, skippedReason: "absent" };
|
|
35413
36912
|
}
|
|
@@ -35416,7 +36915,7 @@ function unlinkIfInodeMatches(workspaceSlug, entityType, entrySlug, expectedInod
|
|
|
35416
36915
|
}
|
|
35417
36916
|
try {
|
|
35418
36917
|
suppressor?.registerDelete(target);
|
|
35419
|
-
|
|
36918
|
+
unlinkSync7(target);
|
|
35420
36919
|
return { didUnlink: true };
|
|
35421
36920
|
} catch (err) {
|
|
35422
36921
|
if (err?.code === "ENOENT") {
|
|
@@ -35503,7 +37002,11 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
35503
37002
|
updateSyncCursor(slug, entityType, lastAdvancedCursor);
|
|
35504
37003
|
}
|
|
35505
37004
|
}
|
|
35506
|
-
const
|
|
37005
|
+
const needsDerivedTextRewalk = state.derived_text_version !== DERIVED_TEXT_VERSION;
|
|
37006
|
+
const filesStartCursor = needsDerivedTextRewalk ? null : entry2.sync_cursors?.["files"] ?? null;
|
|
37007
|
+
if (needsDerivedTextRewalk) {
|
|
37008
|
+
out.info("Re-reading the file index once to refresh extracted text (template changed).");
|
|
37009
|
+
}
|
|
35507
37010
|
const filesResult = await pullFiles({
|
|
35508
37011
|
workspaceSlug: slug,
|
|
35509
37012
|
baseUrl: entry2.api_base_url,
|
|
@@ -35514,16 +37017,21 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
35514
37017
|
now,
|
|
35515
37018
|
output: out,
|
|
35516
37019
|
suppressor: deps.suppressor,
|
|
35517
|
-
isFileFrozen: deps.isFileFrozen
|
|
37020
|
+
isFileFrozen: deps.isFileFrozen,
|
|
37021
|
+
forceTextRewalk: needsDerivedTextRewalk
|
|
35518
37022
|
});
|
|
35519
37023
|
if (filesResult.state !== state) {
|
|
35520
37024
|
state = filesResult.state;
|
|
35521
37025
|
stateDirty = true;
|
|
35522
37026
|
}
|
|
37027
|
+
if (needsDerivedTextRewalk && filesResult.textUnknown === 0) {
|
|
37028
|
+
state = { ...state, derived_text_version: DERIVED_TEXT_VERSION };
|
|
37029
|
+
stateDirty = true;
|
|
37030
|
+
}
|
|
35523
37031
|
counters["files"] = {
|
|
35524
37032
|
fetched: filesResult.fetched,
|
|
35525
|
-
written: filesResult.written,
|
|
35526
|
-
deleted: filesResult.deleted
|
|
37033
|
+
written: filesResult.written + filesResult.textWritten,
|
|
37034
|
+
deleted: filesResult.deleted + filesResult.textDeleted
|
|
35527
37035
|
};
|
|
35528
37036
|
if (filesResult.lastCursor !== filesStartCursor) {
|
|
35529
37037
|
updateSyncCursor(slug, "files", filesResult.lastCursor);
|
|
@@ -35705,7 +37213,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
35705
37213
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at) {
|
|
35706
37214
|
let localRaw = null;
|
|
35707
37215
|
try {
|
|
35708
|
-
localRaw =
|
|
37216
|
+
localRaw = readFileSync19(entityFilePath(workspaceSlug, strategy.type, live.slug), "utf8");
|
|
35709
37217
|
} catch {
|
|
35710
37218
|
localRaw = null;
|
|
35711
37219
|
}
|
|
@@ -35728,7 +37236,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
35728
37236
|
const hashes = computeMirrorHashes(strategy, content);
|
|
35729
37237
|
let inode;
|
|
35730
37238
|
try {
|
|
35731
|
-
inode =
|
|
37239
|
+
inode = statSync11(entityFilePath(workspaceSlug, strategy.type, live.slug)).ino;
|
|
35732
37240
|
} catch {
|
|
35733
37241
|
inode = undefined;
|
|
35734
37242
|
}
|
|
@@ -35785,7 +37293,7 @@ var syncCommand = defineCommand({
|
|
|
35785
37293
|
// src/commands/start.ts
|
|
35786
37294
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
35787
37295
|
var PULL_HEARTBEAT_MS = 900000;
|
|
35788
|
-
function backfillStateInodes(workspaceSlug, state, statFn = (p) =>
|
|
37296
|
+
function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync12(p)) {
|
|
35789
37297
|
let changed = false;
|
|
35790
37298
|
const nextEntries = {};
|
|
35791
37299
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -35818,7 +37326,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync10(p)
|
|
|
35818
37326
|
return { state, changed: false };
|
|
35819
37327
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
35820
37328
|
}
|
|
35821
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
37329
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync20(p, "utf8")) {
|
|
35822
37330
|
let changed = false;
|
|
35823
37331
|
const nextEntries = {};
|
|
35824
37332
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -35915,7 +37423,7 @@ function makeWatchHandler(deps) {
|
|
|
35915
37423
|
}
|
|
35916
37424
|
let st;
|
|
35917
37425
|
try {
|
|
35918
|
-
st =
|
|
37426
|
+
st = statSync12(absolutePath);
|
|
35919
37427
|
} catch (err) {
|
|
35920
37428
|
if (err?.code === "ENOENT")
|
|
35921
37429
|
return;
|
|
@@ -35946,7 +37454,7 @@ function makeWatchHandler(deps) {
|
|
|
35946
37454
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
35947
37455
|
return;
|
|
35948
37456
|
}
|
|
35949
|
-
const bytes =
|
|
37457
|
+
const bytes = readFileSync20(absolutePath);
|
|
35950
37458
|
const hash2 = sha256Bytes(bytes);
|
|
35951
37459
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
35952
37460
|
bodyHash: hash2,
|
|
@@ -36004,7 +37512,7 @@ function makeWatchHandler(deps) {
|
|
|
36004
37512
|
if (suppressor) {
|
|
36005
37513
|
let raw = null;
|
|
36006
37514
|
try {
|
|
36007
|
-
raw =
|
|
37515
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36008
37516
|
} catch {
|
|
36009
37517
|
raw = null;
|
|
36010
37518
|
}
|
|
@@ -36042,7 +37550,7 @@ function makeWatchHandler(deps) {
|
|
|
36042
37550
|
if (suppressor) {
|
|
36043
37551
|
let raw = null;
|
|
36044
37552
|
try {
|
|
36045
|
-
raw =
|
|
37553
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36046
37554
|
} catch {
|
|
36047
37555
|
raw = null;
|
|
36048
37556
|
}
|
|
@@ -36080,7 +37588,7 @@ function makeWatchHandler(deps) {
|
|
|
36080
37588
|
if (suppressor) {
|
|
36081
37589
|
let raw = null;
|
|
36082
37590
|
try {
|
|
36083
|
-
raw =
|
|
37591
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36084
37592
|
} catch {
|
|
36085
37593
|
raw = null;
|
|
36086
37594
|
}
|
|
@@ -36118,7 +37626,7 @@ function makeWatchHandler(deps) {
|
|
|
36118
37626
|
if (suppressor) {
|
|
36119
37627
|
let raw = null;
|
|
36120
37628
|
try {
|
|
36121
|
-
raw =
|
|
37629
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36122
37630
|
} catch {
|
|
36123
37631
|
raw = null;
|
|
36124
37632
|
}
|
|
@@ -36211,7 +37719,7 @@ function makeWatchHandler(deps) {
|
|
|
36211
37719
|
if (strategy) {
|
|
36212
37720
|
let raw = null;
|
|
36213
37721
|
try {
|
|
36214
|
-
raw =
|
|
37722
|
+
raw = readFileSync20(absolutePath, "utf8");
|
|
36215
37723
|
} catch {
|
|
36216
37724
|
raw = null;
|
|
36217
37725
|
}
|
|
@@ -37032,27 +38540,27 @@ var startCommand = defineCommand({
|
|
|
37032
38540
|
});
|
|
37033
38541
|
|
|
37034
38542
|
// src/commands/status.ts
|
|
37035
|
-
import { statSync as
|
|
38543
|
+
import { statSync as statSync13 } from "node:fs";
|
|
37036
38544
|
|
|
37037
38545
|
// src/lib/build-stamp.ts
|
|
37038
|
-
import { execFileSync as
|
|
37039
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
37040
|
-
import { dirname as
|
|
38546
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
38547
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as readFileSync21, writeFileSync as writeFileSync8 } from "node:fs";
|
|
38548
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
37041
38549
|
import { fileURLToPath } from "node:url";
|
|
37042
38550
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
37043
38551
|
function stampPath(distDir) {
|
|
37044
|
-
return
|
|
38552
|
+
return join15(distDir, BUILD_STAMP_FILENAME);
|
|
37045
38553
|
}
|
|
37046
38554
|
function readHead(repoDir) {
|
|
37047
38555
|
try {
|
|
37048
|
-
const sha =
|
|
38556
|
+
const sha = execFileSync4("git", ["rev-parse", "HEAD"], {
|
|
37049
38557
|
cwd: repoDir,
|
|
37050
38558
|
encoding: "utf8",
|
|
37051
38559
|
stdio: ["ignore", "pipe", "ignore"]
|
|
37052
38560
|
}).trim();
|
|
37053
38561
|
if (!sha)
|
|
37054
38562
|
return null;
|
|
37055
|
-
const porcelain =
|
|
38563
|
+
const porcelain = execFileSync4("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
37056
38564
|
return { sha, dirty: porcelain.trim().length > 0 };
|
|
37057
38565
|
} catch {
|
|
37058
38566
|
return null;
|
|
@@ -37060,7 +38568,7 @@ function readHead(repoDir) {
|
|
|
37060
38568
|
}
|
|
37061
38569
|
function readBuildStamp(distDir) {
|
|
37062
38570
|
try {
|
|
37063
|
-
const parsed = JSON.parse(
|
|
38571
|
+
const parsed = JSON.parse(readFileSync21(stampPath(distDir), "utf8"));
|
|
37064
38572
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
37065
38573
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
37066
38574
|
}
|
|
@@ -37072,9 +38580,9 @@ function readBuildStamp(distDir) {
|
|
|
37072
38580
|
function findRepoRoot(fromDir) {
|
|
37073
38581
|
let dir = fromDir;
|
|
37074
38582
|
for (;; ) {
|
|
37075
|
-
if (existsSync7(
|
|
38583
|
+
if (existsSync7(join15(dir, ".git")))
|
|
37076
38584
|
return dir;
|
|
37077
|
-
const parent =
|
|
38585
|
+
const parent = dirname10(dir);
|
|
37078
38586
|
if (parent === dir)
|
|
37079
38587
|
return null;
|
|
37080
38588
|
dir = parent;
|
|
@@ -37082,7 +38590,7 @@ function findRepoRoot(fromDir) {
|
|
|
37082
38590
|
}
|
|
37083
38591
|
function probeRepo(repoDir, stampSha) {
|
|
37084
38592
|
try {
|
|
37085
|
-
|
|
38593
|
+
execFileSync4("git", ["cat-file", "-e", `${stampSha}^{commit}`], {
|
|
37086
38594
|
cwd: repoDir,
|
|
37087
38595
|
stdio: "ignore"
|
|
37088
38596
|
});
|
|
@@ -37098,7 +38606,7 @@ function checkBuildStaleness(deps = {}) {
|
|
|
37098
38606
|
const metaUrl = deps.metaUrl ?? import.meta.url;
|
|
37099
38607
|
let distDir;
|
|
37100
38608
|
try {
|
|
37101
|
-
distDir =
|
|
38609
|
+
distDir = dirname10(fileURLToPath(metaUrl));
|
|
37102
38610
|
} catch {
|
|
37103
38611
|
return { kind: "unknown" };
|
|
37104
38612
|
}
|
|
@@ -37144,8 +38652,8 @@ function formatRelative(deltaMs) {
|
|
|
37144
38652
|
return `${Math.floor(h2 / 24)}d ago`;
|
|
37145
38653
|
}
|
|
37146
38654
|
function formatAbsolute(date5) {
|
|
37147
|
-
const
|
|
37148
|
-
return `${date5.getFullYear()}-${
|
|
38655
|
+
const pad3 = (n2) => String(n2).padStart(2, "0");
|
|
38656
|
+
return `${date5.getFullYear()}-${pad3(date5.getMonth() + 1)}-${pad3(date5.getDate())} ` + `${pad3(date5.getHours())}:${pad3(date5.getMinutes())}:${pad3(date5.getSeconds())}`;
|
|
37149
38657
|
}
|
|
37150
38658
|
function short(sha) {
|
|
37151
38659
|
return sha.slice(0, 7);
|
|
@@ -37168,7 +38676,7 @@ function renderBuild(out, staleness) {
|
|
|
37168
38676
|
}
|
|
37169
38677
|
function lastActivity(now) {
|
|
37170
38678
|
try {
|
|
37171
|
-
const mtimeMs =
|
|
38679
|
+
const mtimeMs = statSync13(daemonLogPath()).mtimeMs;
|
|
37172
38680
|
return formatRelative(now.getTime() - mtimeMs);
|
|
37173
38681
|
} catch {
|
|
37174
38682
|
return null;
|
|
@@ -37203,6 +38711,27 @@ async function runStatus(args, deps = {}) {
|
|
|
37203
38711
|
if (activity)
|
|
37204
38712
|
out.info(cont(`last activity ${activity}`));
|
|
37205
38713
|
out.info("");
|
|
38714
|
+
const sessions = listSessions(slug, {
|
|
38715
|
+
aliveFn: deps.aliveFn,
|
|
38716
|
+
startTimeFn: deps.sessionStartTimeFn,
|
|
38717
|
+
mtimeFn: deps.sessionMtimeFn
|
|
38718
|
+
});
|
|
38719
|
+
const selfPidRaw = process.env.CLAUDE_PID;
|
|
38720
|
+
const selfPid = selfPidRaw ? Number.parseInt(selfPidRaw, 10) : null;
|
|
38721
|
+
const selfSessionId = process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
38722
|
+
const sessionLines = renderSessionLines(sessions.live, now, Number.isInteger(selfPid) ? selfPid : null, selfSessionId, formatRelative);
|
|
38723
|
+
const count = sessions.live.length;
|
|
38724
|
+
const reapNote = sessions.reaped > 0 ? ` (cleared ${sessions.reaped} ended session${sessions.reaped === 1 ? "" : "s"})` : "";
|
|
38725
|
+
out.info(row("Sessions", count === 0 ? `None registered on this machine.${reapNote}` : `${count} live on this machine${reapNote}`));
|
|
38726
|
+
if (count > 0 || sessions.unknown > 0 || sessions.ended > 0) {
|
|
38727
|
+
if (count > 0)
|
|
38728
|
+
for (const line of sessionLines)
|
|
38729
|
+
out.info(cont(line));
|
|
38730
|
+
for (const line of sessionCaveats(count, sessions.unknown, sessions.ended)) {
|
|
38731
|
+
out.info(cont(line));
|
|
38732
|
+
}
|
|
38733
|
+
}
|
|
38734
|
+
out.info("");
|
|
37206
38735
|
let localState;
|
|
37207
38736
|
try {
|
|
37208
38737
|
localState = loadState(slug);
|
|
@@ -37291,7 +38820,7 @@ var versionCommand = defineCommand({
|
|
|
37291
38820
|
});
|
|
37292
38821
|
|
|
37293
38822
|
// src/commands/voice-gate.ts
|
|
37294
|
-
import { readFileSync as
|
|
38823
|
+
import { readFileSync as readFileSync24 } from "node:fs";
|
|
37295
38824
|
|
|
37296
38825
|
// src/lib/voice-gate/api.ts
|
|
37297
38826
|
var VoiceGateSurfaces = [
|
|
@@ -37393,15 +38922,15 @@ async function callVoiceGate(req) {
|
|
|
37393
38922
|
}
|
|
37394
38923
|
|
|
37395
38924
|
// src/lib/voice-gate/attempts.ts
|
|
37396
|
-
import { createHash as
|
|
38925
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
37397
38926
|
import {
|
|
37398
|
-
mkdirSync as
|
|
37399
|
-
readFileSync as
|
|
37400
|
-
renameSync as
|
|
37401
|
-
unlinkSync as
|
|
37402
|
-
writeFileSync as
|
|
38927
|
+
mkdirSync as mkdirSync11,
|
|
38928
|
+
readFileSync as readFileSync22,
|
|
38929
|
+
renameSync as renameSync8,
|
|
38930
|
+
unlinkSync as unlinkSync8,
|
|
38931
|
+
writeFileSync as writeFileSync9
|
|
37403
38932
|
} from "node:fs";
|
|
37404
|
-
import { join as
|
|
38933
|
+
import { join as join16 } from "node:path";
|
|
37405
38934
|
var MAX_REGENS = 2;
|
|
37406
38935
|
var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
37407
38936
|
var AttemptEntrySchema = exports_external.object({
|
|
@@ -37414,20 +38943,20 @@ var AttemptsFileSchema = exports_external.object({
|
|
|
37414
38943
|
entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
|
|
37415
38944
|
});
|
|
37416
38945
|
function attemptsDir(workspaceSlug) {
|
|
37417
|
-
return
|
|
38946
|
+
return join16(plinthHome(), "_voice_gate", workspaceSlug);
|
|
37418
38947
|
}
|
|
37419
38948
|
function attemptsPath(workspaceSlug) {
|
|
37420
|
-
return
|
|
38949
|
+
return join16(attemptsDir(workspaceSlug), "attempts.json");
|
|
37421
38950
|
}
|
|
37422
38951
|
function attemptKey(identity) {
|
|
37423
|
-
return
|
|
38952
|
+
return createHash4("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
|
|
37424
38953
|
}
|
|
37425
38954
|
function emptyFile() {
|
|
37426
38955
|
return { version: 1, entries: {} };
|
|
37427
38956
|
}
|
|
37428
38957
|
function load2(workspaceSlug) {
|
|
37429
38958
|
try {
|
|
37430
|
-
const raw =
|
|
38959
|
+
const raw = readFileSync22(attemptsPath(workspaceSlug), "utf8");
|
|
37431
38960
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
37432
38961
|
return parsed.success ? parsed.data : emptyFile();
|
|
37433
38962
|
} catch {
|
|
@@ -37442,19 +38971,19 @@ function prune(file2, now) {
|
|
|
37442
38971
|
}));
|
|
37443
38972
|
return { version: 1, entries };
|
|
37444
38973
|
}
|
|
37445
|
-
var
|
|
38974
|
+
var tmpCounter2 = 0;
|
|
37446
38975
|
function save(workspaceSlug, file2) {
|
|
37447
38976
|
const dir = attemptsDir(workspaceSlug);
|
|
37448
38977
|
const target = attemptsPath(workspaceSlug);
|
|
37449
|
-
const tmp = `${target}.${process.pid}.${
|
|
37450
|
-
|
|
37451
|
-
|
|
38978
|
+
const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
|
|
38979
|
+
mkdirSync11(dir, { recursive: true, mode: 448 });
|
|
38980
|
+
writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
|
|
37452
38981
|
`, { mode: 384 });
|
|
37453
38982
|
try {
|
|
37454
|
-
|
|
38983
|
+
renameSync8(tmp, target);
|
|
37455
38984
|
} catch (err) {
|
|
37456
38985
|
try {
|
|
37457
|
-
|
|
38986
|
+
unlinkSync8(tmp);
|
|
37458
38987
|
} catch {}
|
|
37459
38988
|
throw err;
|
|
37460
38989
|
}
|
|
@@ -37590,7 +39119,7 @@ function denyUngated(detail) {
|
|
|
37590
39119
|
}
|
|
37591
39120
|
|
|
37592
39121
|
// src/lib/voice-gate/payload.ts
|
|
37593
|
-
import { readFileSync as
|
|
39122
|
+
import { readFileSync as readFileSync23, statSync as statSync14 } from "node:fs";
|
|
37594
39123
|
import { homedir as homedir2 } from "node:os";
|
|
37595
39124
|
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
37596
39125
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -37600,7 +39129,7 @@ function isRawGwsDraftCreate(tokens) {
|
|
|
37600
39129
|
return tokens[0] === "gws" && tokens.includes("drafts") && tokens.includes("create");
|
|
37601
39130
|
}
|
|
37602
39131
|
var MCP_GMAIL_DRAFT_PATTERN = /^mcp__.*gmail.*__(create|update)_draft$/i;
|
|
37603
|
-
function
|
|
39132
|
+
function asString2(value) {
|
|
37604
39133
|
return typeof value === "string" ? value : null;
|
|
37605
39134
|
}
|
|
37606
39135
|
function asNonEmptyString(value) {
|
|
@@ -37747,11 +39276,11 @@ function isHelperInvocation(tokens) {
|
|
|
37747
39276
|
return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
|
|
37748
39277
|
}
|
|
37749
39278
|
function defaultReadBodyFile(path5) {
|
|
37750
|
-
const stat4 =
|
|
39279
|
+
const stat4 = statSync14(path5);
|
|
37751
39280
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
37752
39281
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
37753
39282
|
}
|
|
37754
|
-
return
|
|
39283
|
+
return readFileSync23(path5, "utf8");
|
|
37755
39284
|
}
|
|
37756
39285
|
function parseRecipients(...raw) {
|
|
37757
39286
|
const all = raw.filter((v2) => typeof v2 === "string").flatMap((v2) => v2.split(",")).map((v2) => v2.trim().toLowerCase()).filter((v2) => v2.length > 0);
|
|
@@ -37839,10 +39368,10 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
|
|
|
37839
39368
|
}
|
|
37840
39369
|
function classifyBash(payload, readBodyFile) {
|
|
37841
39370
|
const input = asRecord(payload.tool_input);
|
|
37842
|
-
const command =
|
|
39371
|
+
const command = asString2(input.command);
|
|
37843
39372
|
if (!command)
|
|
37844
39373
|
return { gated: false };
|
|
37845
|
-
const cwd =
|
|
39374
|
+
const cwd = asString2(payload.cwd);
|
|
37846
39375
|
let dirState = cwd ? { dir: cwd, known: true } : { dir: process.cwd(), known: true };
|
|
37847
39376
|
const items = [];
|
|
37848
39377
|
for (const segment of splitCommandSegments(command)) {
|
|
@@ -37883,7 +39412,7 @@ function classifyMcp(payload) {
|
|
|
37883
39412
|
]
|
|
37884
39413
|
};
|
|
37885
39414
|
}
|
|
37886
|
-
const subject =
|
|
39415
|
+
const subject = asString2(input.subject) ?? "";
|
|
37887
39416
|
return {
|
|
37888
39417
|
gated: true,
|
|
37889
39418
|
items: [
|
|
@@ -37891,7 +39420,7 @@ function classifyMcp(payload) {
|
|
|
37891
39420
|
draft: {
|
|
37892
39421
|
site: "mcp-gmail",
|
|
37893
39422
|
recipients: parseRecipients(asStringArray(input.to).join(","), asStringArray(input.cc).join(","), asStringArray(input.bcc).join(",")),
|
|
37894
|
-
threadId:
|
|
39423
|
+
threadId: asString2(input.replyToMessageId) || asString2(input.draftId) || null,
|
|
37895
39424
|
subject,
|
|
37896
39425
|
text: composeDraftText(subject, body)
|
|
37897
39426
|
}
|
|
@@ -37902,7 +39431,7 @@ function classifyMcp(payload) {
|
|
|
37902
39431
|
function classifyPayload(payload, deps = {}) {
|
|
37903
39432
|
const readBodyFile = deps.readBodyFile ?? defaultReadBodyFile;
|
|
37904
39433
|
try {
|
|
37905
|
-
const toolName =
|
|
39434
|
+
const toolName = asString2(payload.tool_name);
|
|
37906
39435
|
if (!toolName)
|
|
37907
39436
|
return { gated: false };
|
|
37908
39437
|
if (toolName === "Bash")
|
|
@@ -37928,7 +39457,7 @@ var diskAttempts = {
|
|
|
37928
39457
|
recordFailure: (slug, key, reason) => recordFailure(slug, key, reason),
|
|
37929
39458
|
clear: clearAttempt
|
|
37930
39459
|
};
|
|
37931
|
-
async function
|
|
39460
|
+
async function defaultReadStdin2() {
|
|
37932
39461
|
if (process.stdin.isTTY)
|
|
37933
39462
|
return "";
|
|
37934
39463
|
const chunks = [];
|
|
@@ -38037,7 +39566,7 @@ async function gateOneDraft(draft, sessionId, auth, deps) {
|
|
|
38037
39566
|
}
|
|
38038
39567
|
async function runVoiceGateHook(args, deps = {}) {
|
|
38039
39568
|
const sink = deps.sink ?? { stdout: process.stdout, stderr: process.stderr };
|
|
38040
|
-
const readStdin = deps.readStdin ??
|
|
39569
|
+
const readStdin = deps.readStdin ?? defaultReadStdin2;
|
|
38041
39570
|
const raw = await readStdin().catch(() => "");
|
|
38042
39571
|
let payload;
|
|
38043
39572
|
try {
|
|
@@ -38102,7 +39631,7 @@ function isSurface(value) {
|
|
|
38102
39631
|
async function runVoiceGateDirect(args, deps = {}) {
|
|
38103
39632
|
const { Output: Output2 } = await Promise.resolve().then(() => (init_output(), exports_output));
|
|
38104
39633
|
const out = new Output2({ sink: deps.sink, noColor: deps.noColor });
|
|
38105
|
-
const readStdin = deps.readStdin ??
|
|
39634
|
+
const readStdin = deps.readStdin ?? defaultReadStdin2;
|
|
38106
39635
|
const surfaceArg = args.surface ?? "email_draft";
|
|
38107
39636
|
if (!isSurface(surfaceArg)) {
|
|
38108
39637
|
out.error(`Unknown surface "${surfaceArg}". Expected one of: ${VoiceGateSurfaces.join(", ")}.`);
|
|
@@ -38111,7 +39640,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
38111
39640
|
let text;
|
|
38112
39641
|
if (args.file) {
|
|
38113
39642
|
try {
|
|
38114
|
-
text =
|
|
39643
|
+
text = readFileSync24(args.file, "utf8");
|
|
38115
39644
|
} catch (err) {
|
|
38116
39645
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
38117
39646
|
out.error(`Could not read ${args.file}: ${msg}`);
|
|
@@ -38256,7 +39785,9 @@ var main = defineCommand({
|
|
|
38256
39785
|
"voice-gate": voiceGateCommand,
|
|
38257
39786
|
backlinks: backlinksCommand,
|
|
38258
39787
|
declare: declareCommand,
|
|
38259
|
-
"review-tier": reviewTierCommand
|
|
39788
|
+
"review-tier": reviewTierCommand,
|
|
39789
|
+
session: sessionCommand,
|
|
39790
|
+
"memory-gate": memoryGateCommand
|
|
38260
39791
|
}
|
|
38261
39792
|
});
|
|
38262
39793
|
runMain(main);
|