@usepowerplant/cli 0.4.23 → 0.4.25

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.
@@ -1,103 +0,0 @@
1
- #!/usr/bin/env node
2
- // Discovery and lifecycle events only. The collector owns scope, transcript
3
- // reads and uploads. Never persist prompts, messages, or full input.
4
- import { Buffer } from "node:buffer";
5
- import { createHash, randomUUID } from "node:crypto";
6
- import { appendFileSync, 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
- // Codex's events, the lifecycle event each records, and the input field
12
- // that holds its reason. The collector reads the journal in
13
- // apps/syncservice/src/lifecycleJournal.ts.
14
- const LIFECYCLE_EVENTS = new Map([
15
- ["SessionStart", { event: "session-start", reasonField: "source" }],
16
- ["Stop", { event: "turn-end" }],
17
- ["SessionEnd", { event: "session-end", reasonField: "reason" }],
18
- ]);
19
- let chunks = [];
20
- let bytes = 0;
21
- process.stdin.on("error", () => {});
22
- process.stdin.on("data", (chunk) => {
23
- bytes += chunk.length;
24
- if (bytes <= MAX_INPUT_BYTES) chunks.push(chunk);
25
- else chunks = [];
26
- });
27
- process.stdin.on("end", () => {
28
- const dir = path.join(os.homedir(), ".config", "powerplant");
29
- let input;
30
- try {
31
- if (bytes <= MAX_INPUT_BYTES) input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
32
- } catch {
33
- // Invalid input must not interrupt the agent.
34
- }
35
- const name = input?.hook_event_name;
36
- const lifecycle = typeof name === "string" ? LIFECYCLE_EVENTS.get(name) : undefined;
37
- const sessionId =
38
- lifecycle !== undefined &&
39
- typeof input.session_id === "string" &&
40
- /^[a-zA-Z0-9_-]{1,200}$/.test(input.session_id)
41
- ? input.session_id
42
- : null;
43
- if (sessionId !== null) {
44
- try {
45
- const record = {
46
- sessionId,
47
- transcriptPath:
48
- typeof input.transcript_path === "string" &&
49
- input.transcript_path.length <= 4096 &&
50
- path.isAbsolute(input.transcript_path)
51
- ? input.transcript_path
52
- : null,
53
- };
54
- const records = path.join(dir, "codex-transcripts");
55
- // eslint-disable-next-line security/detect-non-literal-fs-filename
56
- mkdirSync(records, { recursive: true, mode: 0o700 });
57
- const file = path.join(records, `${createHash("sha256").update(sessionId).digest("hex")}.json`);
58
- const temporary = `${file}.${randomUUID()}.tmp`;
59
- try {
60
- // eslint-disable-next-line security/detect-non-literal-fs-filename
61
- writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: "wx" });
62
- // eslint-disable-next-line security/detect-non-literal-fs-filename
63
- renameSync(temporary, file);
64
- } finally {
65
- rmSync(temporary, { force: true });
66
- }
67
- } catch {
68
- // An unavailable disk must not interrupt the agent.
69
- }
70
- try {
71
- const reason = lifecycle.reasonField === undefined ? undefined : input[lifecycle.reasonField];
72
- const at = new Date().toISOString();
73
- const entry = {
74
- harness: "codex-local",
75
- sessionId,
76
- event: lifecycle.event,
77
- nativeEvent: name,
78
- ...(typeof reason === "string" && /^[a-zA-Z_-]{1,40}$/.test(reason) && { reason }),
79
- at,
80
- };
81
- // Codex's own folder, one user-only file per UTC day; the collector
82
- // deletes old ones.
83
- const journal = path.join(dir, "lifecycle-journal", "codex-local");
84
- // eslint-disable-next-line security/detect-non-literal-fs-filename
85
- mkdirSync(journal, { recursive: true, mode: 0o700 });
86
- // eslint-disable-next-line security/detect-non-literal-fs-filename
87
- appendFileSync(path.join(journal, `${at.slice(0, 10)}.jsonl`), `${JSON.stringify(entry)}\n`, {
88
- mode: 0o600,
89
- });
90
- } catch {
91
- // The journal only adds evidence; transcript scans still find the session.
92
- }
93
- }
94
- try {
95
- // Publish after the pointer; existing file scans also recover missed hooks.
96
- // eslint-disable-next-line security/detect-non-literal-fs-filename
97
- mkdirSync(dir, { recursive: true });
98
- // eslint-disable-next-line security/detect-non-literal-fs-filename
99
- writeFileSync(path.join(dir, "session-change"), `${Date.now()}\n`, { mode: 0o600 });
100
- } catch {
101
- // Never report collector failures as agent output.
102
- }
103
- });
@@ -1,115 +0,0 @@
1
- #!/usr/bin/env node
2
- // Local discovery and lifecycle events only. The collector validates paths
3
- // and capture policy before opening transcripts. Never persist prompts,
4
- // email, or full input.
5
- import { createHash, randomUUID } from "node:crypto";
6
- import { Buffer } from "node:buffer";
7
- import { appendFileSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
- import os from "node:os";
9
- import path from "node:path";
10
-
11
- const MAX_INPUT_BYTES = 128 * 1024;
12
- // Cursor's events, the lifecycle event each records, and the input field
13
- // that holds its reason. The collector reads the journal in
14
- // apps/syncservice/src/lifecycleJournal.ts.
15
- const LIFECYCLE_EVENTS = new Map([
16
- ["sessionStart", { event: "session-start" }],
17
- ["stop", { event: "turn-end", reasonField: "status" }],
18
- ["sessionEnd", { event: "session-end", reasonField: "reason" }],
19
- ]);
20
- let chunks = [];
21
- let bytes = 0;
22
- process.stdin.on("error", () => {});
23
- process.stdin.on("data", (chunk) => {
24
- bytes += chunk.length;
25
- if (bytes <= MAX_INPUT_BYTES) chunks.push(chunk);
26
- else chunks = [];
27
- });
28
- process.stdin.on("end", () => {
29
- const dir = path.join(os.homedir(), ".config", "powerplant");
30
- let input;
31
- try {
32
- if (bytes <= MAX_INPUT_BYTES) input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
33
- } catch {
34
- // Invalid input must never stop the provider.
35
- }
36
- const name = input?.hook_event_name;
37
- const lifecycle = typeof name === "string" ? LIFECYCLE_EVENTS.get(name) : undefined;
38
- const sessionId =
39
- lifecycle !== undefined &&
40
- typeof input.conversation_id === "string" &&
41
- /^[a-zA-Z0-9_-]{1,200}$/.test(input.conversation_id)
42
- ? input.conversation_id
43
- : null;
44
- if (sessionId !== null) {
45
- try {
46
- const roots =
47
- Array.isArray(input.workspace_roots) &&
48
- input.workspace_roots.length <= 8 &&
49
- input.workspace_roots.every(
50
- (p) => typeof p === "string" && p.length <= 4096 && path.isAbsolute(p)
51
- )
52
- ? input.workspace_roots
53
- : [];
54
- const record = {
55
- sessionId,
56
- transcriptPath:
57
- typeof input.transcript_path === "string" &&
58
- input.transcript_path.length <= 4096 &&
59
- path.isAbsolute(input.transcript_path)
60
- ? input.transcript_path
61
- : null,
62
- workspaceRoots: roots,
63
- ...(typeof input.model === "string" &&
64
- input.model.length <= 256 && { model: input.model }),
65
- };
66
- const records = path.join(dir, "cursor-transcripts");
67
- // eslint-disable-next-line security/detect-non-literal-fs-filename
68
- mkdirSync(records, { recursive: true, mode: 0o700 });
69
- const file = path.join(records, `${createHash("sha256").update(sessionId).digest("hex")}.json`);
70
- const temporary = `${file}.${randomUUID()}.tmp`;
71
- try {
72
- // eslint-disable-next-line security/detect-non-literal-fs-filename
73
- writeFileSync(temporary, JSON.stringify(record), { mode: 0o600, flag: "wx" });
74
- // eslint-disable-next-line security/detect-non-literal-fs-filename
75
- renameSync(temporary, file);
76
- } finally {
77
- rmSync(temporary, { force: true });
78
- }
79
- } catch {
80
- // Discovery failure must never stop the provider. Existing capture remains.
81
- }
82
- try {
83
- const reason = lifecycle.reasonField === undefined ? undefined : input[lifecycle.reasonField];
84
- const at = new Date().toISOString();
85
- const entry = {
86
- harness: "cursor-local",
87
- sessionId,
88
- event: lifecycle.event,
89
- nativeEvent: name,
90
- ...(typeof reason === "string" && /^[a-zA-Z_-]{1,40}$/.test(reason) && { reason }),
91
- at,
92
- };
93
- // Cursor's own folder, one user-only file per UTC day; the collector
94
- // deletes old ones.
95
- const journal = path.join(dir, "lifecycle-journal", "cursor-local");
96
- // eslint-disable-next-line security/detect-non-literal-fs-filename
97
- mkdirSync(journal, { recursive: true, mode: 0o700 });
98
- // eslint-disable-next-line security/detect-non-literal-fs-filename
99
- appendFileSync(path.join(journal, `${at.slice(0, 10)}.jsonl`), `${JSON.stringify(entry)}\n`, {
100
- mode: 0o600,
101
- });
102
- } catch {
103
- // The journal only adds evidence; existing capture remains.
104
- }
105
- }
106
- try {
107
- // Publish after the pointer so the watcher sees a complete discovery record.
108
- // eslint-disable-next-line security/detect-non-literal-fs-filename
109
- mkdirSync(dir, { recursive: true });
110
- // eslint-disable-next-line security/detect-non-literal-fs-filename
111
- writeFileSync(path.join(dir, "session-change"), `${Date.now()}\n`, { mode: 0o600 });
112
- } catch {
113
- // No output to the agent, even if the disk is unavailable.
114
- }
115
- });
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Powerplant session lifecycle: the Claude Code SessionStart, Stop, and
4
- * SessionEnd hook.
5
- *
6
- * Claude Code fires these at real session boundaries (start, resume, clear,
7
- * quit) and when a turn ends. This hook appends one line per event to the
8
- * lifecycle journal, one file per UTC day in
9
- * ~/.config/powerplant/lifecycle-journal/claude-local/. Each line names the shared
10
- * lifecycle event and keeps Claude Code's own event name as detail. The
11
- * collector's scan marks a session "completed" the moment its SessionEnd
12
- * arrives, with the observed end time, instead of waiting out the
13
- * transcript-quiet grace window. The `at` stamp is when THIS process
14
- * observed the hook fire; it is never a claimed or reconstructed time.
15
- *
16
- * Only the session id, the event names, and Claude Code's source or reason
17
- * token are kept. Stop input carries the last assistant message; it is
18
- * never written.
19
- *
20
- * This file is the CANONICAL SOURCE; the collector's watch service copies it
21
- * to ~/.config/powerplant/hooks/ and wires it into the user's
22
- * ~/.claude/settings.json (hookInstall.ts), so every repo's Claude sessions
23
- * report, not just this checkout's. This hook NEVER blocks or breaks a
24
- * session: every failure path exits 0 with no output.
25
- *
26
- * Logic lives in the exported functions so the collector workspace can unit
27
- * test it (apps/syncservice/src/lifecycleHook.test.ts); only direct execution
28
- * runs main().
29
- */
30
-
31
- import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
32
- import os from "node:os";
33
- import path from "node:path";
34
- import { pathToFileURL } from "node:url";
35
-
36
- /** Claude Code's events, the lifecycle event each records, and the input
37
- * field that holds its reason. */
38
- const LIFECYCLE_EVENTS = new Map([
39
- ["SessionStart", { event: "session-start", reasonField: "source" }],
40
- ["Stop", { event: "turn-end" }],
41
- ["SessionEnd", { event: "session-end", reasonField: "reason" }],
42
- ]);
43
- const SESSION_ID = /^[a-zA-Z0-9_-]{1,200}$/;
44
- const REASON = /^[a-zA-Z_-]{1,40}$/;
45
-
46
- /** Claude Code's folder in the journal; the collector reads it
47
- * (lifecycleJournalHarnessDir in lifecycleJournal.ts). */
48
- export function journalDir(home) {
49
- return path.join(home, ".config", "powerplant", "lifecycle-journal", "claude-local");
50
- }
51
-
52
- /**
53
- * One journal entry from the hook's stdin JSON; null when the input isn't a
54
- * lifecycle event we can attribute to a session. SessionStart carries a
55
- * `source` (startup|resume|clear|compact|fork), SessionEnd a `reason`
56
- * (clear|resume|logout|prompt_input_exit|other); both land as `reason`.
57
- */
58
- export function journalEntryOf(inputText, at) {
59
- try {
60
- const input = JSON.parse(inputText);
61
- const nativeEvent = input?.hook_event_name;
62
- const mapping = typeof nativeEvent === "string" ? LIFECYCLE_EVENTS.get(nativeEvent) : undefined;
63
- const sessionId = input?.session_id;
64
- if (mapping === undefined || typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) return null;
65
- const reason = mapping.reasonField === undefined ? undefined : input[mapping.reasonField];
66
- return {
67
- harness: "claude-local",
68
- sessionId,
69
- event: mapping.event,
70
- nativeEvent,
71
- ...(typeof reason === "string" && REASON.test(reason) && { reason }),
72
- at,
73
- };
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- function main() {
80
- try {
81
- const entry = journalEntryOf(readFileSync(0, "utf8"), new Date().toISOString());
82
- if (entry === null) return;
83
- // The path is the user's homedir plus fixed segments and the date;
84
- // nothing from the hook input can steer where the journal lands.
85
- const dir = journalDir(os.homedir());
86
- // eslint-disable-next-line security/detect-non-literal-fs-filename
87
- mkdirSync(dir, { recursive: true, mode: 0o700 });
88
- // eslint-disable-next-line security/detect-non-literal-fs-filename
89
- appendFileSync(path.join(dir, `${entry.at.slice(0, 10)}.jsonl`), `${JSON.stringify(entry)}\n`, {
90
- mode: 0o600,
91
- });
92
- } catch {
93
- // never surface an error into someone's session
94
- }
95
- }
96
-
97
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
98
- main();
99
- }