@usepowerplant/cli 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Local discovery only. The collector validates paths and capture policy
|
|
3
|
+
// before opening transcripts. Never persist prompts, email, or full input.
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const MAX_INPUT_BYTES = 128 * 1024;
|
|
11
|
+
let chunks = [];
|
|
12
|
+
let bytes = 0;
|
|
13
|
+
process.stdin.on("error", () => {});
|
|
14
|
+
process.stdin.on("data", (chunk) => {
|
|
15
|
+
bytes += chunk.length;
|
|
16
|
+
if (bytes <= MAX_INPUT_BYTES) chunks.push(chunk);
|
|
17
|
+
else chunks = [];
|
|
18
|
+
});
|
|
19
|
+
process.stdin.on("end", () => {
|
|
20
|
+
const dir = path.join(os.homedir(), ".config", "powerplant");
|
|
21
|
+
try {
|
|
22
|
+
if (bytes <= MAX_INPUT_BYTES) {
|
|
23
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
24
|
+
if (
|
|
25
|
+
["sessionStart", "sessionEnd", "stop"].includes(input?.hook_event_name) &&
|
|
26
|
+
typeof input.conversation_id === "string" &&
|
|
27
|
+
/^[a-zA-Z0-9_-]{1,200}$/.test(input.conversation_id)
|
|
28
|
+
) {
|
|
29
|
+
const roots =
|
|
30
|
+
Array.isArray(input.workspace_roots) &&
|
|
31
|
+
input.workspace_roots.length <= 8 &&
|
|
32
|
+
input.workspace_roots.every(
|
|
33
|
+
(p) => typeof p === "string" && p.length <= 4096 && path.isAbsolute(p)
|
|
34
|
+
)
|
|
35
|
+
? input.workspace_roots
|
|
36
|
+
: [];
|
|
37
|
+
const record = {
|
|
38
|
+
sessionId: input.conversation_id,
|
|
39
|
+
transcriptPath:
|
|
40
|
+
typeof input.transcript_path === "string" &&
|
|
41
|
+
input.transcript_path.length <= 4096 &&
|
|
42
|
+
path.isAbsolute(input.transcript_path)
|
|
43
|
+
? input.transcript_path
|
|
44
|
+
: null,
|
|
45
|
+
workspaceRoots: roots,
|
|
46
|
+
...(typeof input.model === "string" &&
|
|
47
|
+
input.model.length <= 256 && { model: input.model }),
|
|
48
|
+
};
|
|
49
|
+
const records = path.join(dir, "cursor-transcripts");
|
|
50
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
51
|
+
mkdirSync(records, { recursive: true, mode: 0o700 });
|
|
52
|
+
const file = path.join(
|
|
53
|
+
records,
|
|
54
|
+
`${createHash("sha256").update(record.sessionId).digest("hex")}.json`
|
|
55
|
+
);
|
|
56
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
57
|
+
try {
|
|
58
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
59
|
+
writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: "wx" });
|
|
60
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
61
|
+
renameSync(temporary, file);
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(temporary, { force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Discovery failure must never stop the provider. Existing capture remains.
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
// Publish after the pointer so the watcher sees a complete discovery record.
|
|
72
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
75
|
+
writeFileSync(path.join(dir, "session-change"), `${Date.now()}\n`, { mode: 0o600 });
|
|
76
|
+
} catch {
|
|
77
|
+
// No output to the agent, even if the disk is unavailable.
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Canonical standalone hook, bundled by packages/cli/build.mjs.
|
|
3
|
+
// Wake the local collector without inspecting or saving the hook payload.
|
|
4
|
+
// Existing readers still enforce capture scope and redact before uploading.
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
// Drain provider input without retaining it or emitting an agent response.
|
|
10
|
+
process.stdin.on("error", () => {});
|
|
11
|
+
process.stdin.resume();
|
|
12
|
+
try {
|
|
13
|
+
const dir = path.join(os.homedir(), ".config", "powerplant");
|
|
14
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
// A fixed-size signal coalesces events. No paths or content come from stdin.
|
|
17
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
18
|
+
writeFileSync(path.join(dir, "session-change"), `${Date.now()}\n`, { mode: 0o600 });
|
|
19
|
+
} catch {
|
|
20
|
+
// A disk failure must not disrupt the agent. Recovery scans still run.
|
|
21
|
+
}
|