@residue/cli 0.0.1

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,137 @@
1
+ /**
2
+ * Residue adapter for pi coding agent.
3
+ *
4
+ * Hooks into pi's session lifecycle to call the residue CLI,
5
+ * linking AI conversations to git commits.
6
+ *
7
+ * Uses a persistent state file (.residue/hooks/pi.state) to survive
8
+ * process crashes. On startup, any leftover state from a previous
9
+ * run is cleaned up automatically.
10
+ *
11
+ * Lifecycle:
12
+ * session_start -> end stale session (if any), then residue session start
13
+ * session_switch -> residue session end + session start (swap tracked session)
14
+ * session_shutdown -> residue session end (marks session as ended)
15
+ */
16
+
17
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
+
19
+ const STATE_FILE = ".residue/hooks/pi.state";
20
+
21
+ export default function (pi: ExtensionAPI) {
22
+ let piVersion = "unknown";
23
+ let isResidueAvailable = true;
24
+
25
+ async function detectPiVersion(): Promise<string> {
26
+ const result = await pi.exec("pi", ["--version"]);
27
+ if (result.code === 0 && result.stdout.trim()) {
28
+ return result.stdout.trim();
29
+ }
30
+ return "unknown";
31
+ }
32
+
33
+ async function checkResidueAvailable(): Promise<boolean> {
34
+ const result = await pi.exec("which", ["residue"]);
35
+ return result.code === 0;
36
+ }
37
+
38
+ async function readStateFile(): Promise<string | undefined> {
39
+ const result = await pi.exec("cat", [STATE_FILE]);
40
+ if (result.code === 0 && result.stdout.trim()) {
41
+ return result.stdout.trim();
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ async function writeStateFile(params: {
47
+ sessionId: string;
48
+ }): Promise<void> {
49
+ await pi.exec("mkdir", ["-p", ".residue/hooks"]);
50
+ // Use sh -c to write via redirect since pi.exec may not support shell features
51
+ await pi.exec("sh", ["-c", `printf '%s' '${params.sessionId}' > ${STATE_FILE}`]);
52
+ }
53
+
54
+ async function removeStateFile(): Promise<void> {
55
+ await pi.exec("rm", ["-f", STATE_FILE]);
56
+ }
57
+
58
+ /**
59
+ * End a session by ID. Does not touch the state file.
60
+ */
61
+ async function endSessionById(params: {
62
+ sessionId: string;
63
+ }): Promise<void> {
64
+ if (!isResidueAvailable) return;
65
+ await pi.exec("residue", ["session", "end", "--id", params.sessionId]);
66
+ }
67
+
68
+ /**
69
+ * End whatever session is recorded in the state file, then remove
70
+ * the state file. This cleans up after crashes / missed shutdowns.
71
+ */
72
+ async function endStaleSession(): Promise<void> {
73
+ const staleId = await readStateFile();
74
+ if (!staleId) return;
75
+ await endSessionById({ sessionId: staleId });
76
+ await removeStateFile();
77
+ }
78
+
79
+ async function startResidueSession(params: {
80
+ sessionFile: string | undefined;
81
+ }): Promise<void> {
82
+ if (!params.sessionFile) return;
83
+ if (!isResidueAvailable) return;
84
+
85
+ const result = await pi.exec("residue", [
86
+ "session",
87
+ "start",
88
+ "--agent",
89
+ "pi",
90
+ "--data",
91
+ params.sessionFile,
92
+ "--agent-version",
93
+ piVersion,
94
+ ]);
95
+
96
+ if (result.code === 0 && result.stdout.trim()) {
97
+ const sessionId = result.stdout.trim();
98
+ await writeStateFile({ sessionId });
99
+ }
100
+ }
101
+
102
+ async function endResidueSession(): Promise<void> {
103
+ const sessionId = await readStateFile();
104
+ if (!sessionId) return;
105
+ if (!isResidueAvailable) return;
106
+
107
+ await endSessionById({ sessionId });
108
+ await removeStateFile();
109
+ }
110
+
111
+ pi.on("session_start", async (_event, ctx) => {
112
+ isResidueAvailable = await checkResidueAvailable();
113
+ if (!isResidueAvailable) return;
114
+
115
+ piVersion = await detectPiVersion();
116
+
117
+ // Clean up any leftover session from a previous run that
118
+ // did not shut down cleanly.
119
+ await endStaleSession();
120
+
121
+ const sessionFile = ctx.sessionManager.getSessionFile();
122
+ await startResidueSession({ sessionFile });
123
+ });
124
+
125
+ pi.on("session_switch", async (_event, ctx) => {
126
+ if (!isResidueAvailable) return;
127
+
128
+ await endResidueSession();
129
+
130
+ const sessionFile = ctx.sessionManager.getSessionFile();
131
+ await startResidueSession({ sessionFile });
132
+ });
133
+
134
+ pi.on("session_shutdown", async () => {
135
+ await endResidueSession();
136
+ });
137
+ }