@agentlogs/pi 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.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @agentlogs/pi
2
+
3
+ AgentLogs extension for [pi](https://github.com/badlogic/pi-mono) - automatically captures and uploads AI coding session transcripts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @agentlogs/pi
9
+ # or
10
+ bun add -g @agentlogs/pi
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ Add the extension to your pi configuration:
16
+
17
+ **Option 1: Global settings** (`~/.pi/agent/settings.json`)
18
+
19
+ ```json
20
+ {
21
+ "extensions": ["@agentlogs/pi"]
22
+ }
23
+ ```
24
+
25
+ **Option 2: Project settings** (`.pi/settings.json`)
26
+
27
+ ```json
28
+ {
29
+ "extensions": ["@agentlogs/pi"]
30
+ }
31
+ ```
32
+
33
+ **Option 3: Package.json**
34
+
35
+ ```json
36
+ {
37
+ "pi": {
38
+ "extensions": ["@agentlogs/pi"]
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Features
44
+
45
+ ### Automatic Transcript Upload
46
+
47
+ When you end a pi session (Ctrl+D), your conversation transcript is automatically uploaded to AgentLogs.
48
+
49
+ ### Git Commit Tracking
50
+
51
+ When the AI makes git commits, the extension:
52
+
53
+ 1. Adds a link to the transcript in the commit message footer
54
+ 2. Tracks which commits are associated with which transcripts
55
+ 3. Makes it easy to find the AI conversation that led to any commit
56
+
57
+ ### Branch-Aware Transcripts
58
+
59
+ Pi supports conversation branching via `/tree`. The extension handles this by:
60
+
61
+ - Generating unique transcript IDs for each branch
62
+ - Only uploading the current branch (from leaf to root)
63
+ - Preserving links to older branches when you navigate away
64
+
65
+ ## Configuration
66
+
67
+ ### Environment Variables
68
+
69
+ - `AGENTLOGS_CLI_PATH` - Custom path to the agentlogs CLI (defaults to `npx -y agentlogs@latest`)
70
+
71
+ ### Repository Allowlist
72
+
73
+ By default, transcripts are only uploaded for repositories you've explicitly allowed:
74
+
75
+ ```bash
76
+ # Allow the current repo
77
+ agentlogs allow
78
+
79
+ # Set visibility
80
+ agentlogs allow --public
81
+ agentlogs allow --team
82
+ agentlogs allow --private
83
+
84
+ # Deny the current repo
85
+ agentlogs deny
86
+ ```
87
+
88
+ ## Development Setup
89
+
90
+ For local development, use the setup script:
91
+
92
+ ```bash
93
+ # From the repo root
94
+ ./packages/pi/scripts/dev-setup.sh
95
+
96
+ # This creates a symlink and shows the CLI path to export:
97
+ export AGENTLOGS_CLI_PATH="bun /path/to/agentlogs/packages/cli/src/index.ts"
98
+
99
+ # Now run pi - the extension loads automatically
100
+ pi
101
+ ```
102
+
103
+ To remove the dev setup:
104
+
105
+ ```bash
106
+ ./packages/pi/scripts/dev-teardown.sh
107
+ unset AGENTLOGS_CLI_PATH
108
+ ```
109
+
110
+ ## Debug Logging
111
+
112
+ Debug logs are written to `/tmp/agentlogs-pi.log` when not in production mode.
113
+
114
+ ```bash
115
+ # Watch logs in real-time
116
+ tail -f /tmp/agentlogs-pi.log
117
+ ```
118
+
119
+ ## CLI Commands
120
+
121
+ The extension uses the `agentlogs` CLI under the hood:
122
+
123
+ ```bash
124
+ # List recent sessions
125
+ agentlogs pi upload
126
+
127
+ # Upload a specific session
128
+ agentlogs pi upload <session-id>
129
+ agentlogs pi upload /path/to/session.jsonl
130
+
131
+ # Check login status
132
+ agentlogs status
133
+
134
+ # Login to AgentLogs
135
+ agentlogs login
136
+ ```
137
+
138
+ ## How It Works
139
+
140
+ 1. The extension registers handlers for pi's lifecycle events
141
+ 2. On `session_shutdown`, it shells out to `agentlogs pi hook` with session data
142
+ 3. The CLI converts the pi session format to AgentLogs' unified format
143
+ 4. The transcript is uploaded to the AgentLogs API
144
+
145
+ ## License
146
+
147
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{appendFileSync as V}from"node:fs";import{spawn as W}from"node:child_process";var X="/tmp/agentlogs-pi.log";function D(J,M){return}async function R(J,M){let Q=process.env.AGENTLOGS_CLI_PATH,b,j;if(Q){let q=Q.split(" ");b=q[0],j=[...q.slice(1),"pi","hook"]}else b="npx",j=["-y","agentlogs@latest","pi","hook"];return D("Running hook",{command:b,args:j,payload:J.hook_event_name}),new Promise((q)=>{let z=W(b,j,{cwd:M,stdio:["pipe","pipe","pipe"]}),B="",K="";z.stdout.on("data",(A)=>{B+=A.toString()}),z.stderr.on("data",(A)=>{K+=A.toString()}),z.stdin.write(JSON.stringify(J)),z.stdin.end(),z.on("close",(A)=>{if(D("Hook process exited",{code:A,stdout:B.slice(0,500),stderr:K.slice(0,500)}),A===0&&B.trim())try{let U=JSON.parse(B.trim());q(U)}catch{q({modified:!1})}else q({modified:!1})}),z.on("error",(A)=>{D("Hook spawn error",{error:String(A)}),q({modified:!1})})})}function Y(J){D("Extension initialized");let M=new Set;J.on("tool_call",async(b,j)=>{if(b.toolName!=="bash")return;let q=b.input?.command;if(typeof q!=="string"||!/\bgit\s+commit\b/.test(q))return;D("tool_call (git commit)",{toolName:b.toolName,toolCallId:b.toolCallId});let z=j.sessionManager.getSessionId(),B=j.sessionManager.getSessionFile(),K=await R({hook_event_name:"tool_call",session_id:z,tool_call_id:b.toolCallId,tool:b.toolName,tool_input:b.input,cwd:j.cwd,session_file:B},j.cwd);if(K.modified&&K.updatedInput)D("tool_call: input modified",{modified:!0}),M.add(b.toolCallId)}),J.on("tool_result",async(b,j)=>{if(b.toolName!=="bash")return;let q=M.has(b.toolCallId),z=b.content?.map((A)=>A.type==="text"?A.text:"").join("").trim()??"",B=z.includes("agentlogs.ai/s/");if(!q&&!B)return;M.delete(b.toolCallId),D("tool_result (git commit)",{toolName:b.toolName,toolCallId:b.toolCallId,hasLink:B,wasIntercepted:q});let K=j.sessionManager.getSessionId();R({hook_event_name:"tool_result",session_id:K,tool_call_id:b.toolCallId,tool:b.toolName,tool_output:{content:z},cwd:j.cwd},j.cwd).catch((A)=>D("tool_result hook error",{error:String(A)}))});let Q=(b,j)=>{let q=j.sessionManager.getSessionId(),z=j.sessionManager.getSessionFile(),B=j.sessionManager.getLeafId();if(!z){D(`${b}: no session file (ephemeral session)`);return}D(b,{sessionId:q,sessionFile:z,leafId:B}),R({hook_event_name:b,session_id:q,session_file:z,leaf_id:B??void 0,cwd:j.cwd},j.cwd).catch((K)=>D(`${b} hook error`,{error:String(K)}))};J.on("agent_end",async(b,j)=>{Q("agent_end",j)}),J.on("session_shutdown",async(b,j)=>{Q("session_shutdown",j)})}export{Y as default,Y as agentLogsExtension};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@agentlogs/pi",
3
+ "version": "0.0.1",
4
+ "description": "AgentLogs extension for Pi - automatically captures and uploads AI coding session transcripts",
5
+ "keywords": [
6
+ "agentlogs",
7
+ "ai-coding",
8
+ "extension",
9
+ "pi",
10
+ "transcript"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/agentlogs/agentlogs.git",
16
+ "directory": "packages/pi"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "type": "module",
23
+ "main": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "main": "dist/index.js",
28
+ "types": "dist/index.d.ts"
29
+ },
30
+ "scripts": {
31
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --define 'process.env.NODE_ENV=\"production\"' --minify",
32
+ "prepublishOnly": "bun run build",
33
+ "check": "tsgo --project tsconfig.json"
34
+ },
35
+ "peerDependencies": {
36
+ "bun": ">=1.0.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * AgentLogs Pi Extension
3
+ *
4
+ * Lightweight extension that shells out to the agentlogs CLI for all processing.
5
+ * The CLI handles transcript uploads, git commit interception, and commit tracking.
6
+ *
7
+ * @example
8
+ * // Install globally or in project
9
+ * npm install -g @agentlogs/pi
10
+ *
11
+ * // Add to pi settings.json
12
+ * { "extensions": ["@agentlogs/pi"] }
13
+ *
14
+ * // Or use the pi field in package.json
15
+ * { "pi": { "extensions": ["@agentlogs/pi"] } }
16
+ */
17
+
18
+ import { appendFileSync } from "node:fs";
19
+ import { spawn } from "node:child_process";
20
+
21
+ // ============================================================================
22
+ // Pi Types (inline to avoid external dependency)
23
+ // ============================================================================
24
+
25
+ interface ExtensionContext {
26
+ cwd: string;
27
+ sessionManager: {
28
+ getSessionId(): string;
29
+ getSessionFile(): string | undefined;
30
+ getLeafId(): string | null;
31
+ };
32
+ }
33
+
34
+ interface ToolCallEvent {
35
+ toolName: string;
36
+ toolCallId: string;
37
+ input: Record<string, unknown>;
38
+ }
39
+
40
+ interface ToolResultEvent {
41
+ toolName: string;
42
+ toolCallId: string;
43
+ content?: Array<{ type: string; text?: string }>;
44
+ }
45
+
46
+ interface ExtensionAPI {
47
+ on(event: "tool_call", handler: (event: ToolCallEvent, ctx: ExtensionContext) => void | Promise<void>): void;
48
+ on(event: "tool_result", handler: (event: ToolResultEvent, ctx: ExtensionContext) => void | Promise<void>): void;
49
+ on(event: "agent_end", handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
50
+ on(event: "session_shutdown", handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
51
+ }
52
+
53
+ // ============================================================================
54
+ // Debug Logging (compiled out in production builds)
55
+ // ============================================================================
56
+
57
+ const LOG_FILE = "/tmp/agentlogs-pi.log";
58
+
59
+ function log(message: string, data?: unknown): void {
60
+ if (process.env.NODE_ENV === "production") return;
61
+ const timestamp = new Date().toISOString();
62
+ const logLine = data
63
+ ? `[${timestamp}] ${message}\n${JSON.stringify(data, null, 2)}\n`
64
+ : `[${timestamp}] ${message}\n`;
65
+ try {
66
+ appendFileSync(LOG_FILE, logLine);
67
+ } catch {
68
+ // Ignore write errors
69
+ }
70
+ }
71
+
72
+ // ============================================================================
73
+ // Types
74
+ // ============================================================================
75
+
76
+ interface HookPayload {
77
+ hook_event_name: string;
78
+ session_id: string;
79
+ tool_call_id?: string;
80
+ tool?: string;
81
+ cwd?: string;
82
+ tool_input?: Record<string, unknown>;
83
+ tool_output?: Record<string, unknown>;
84
+ session_file?: string;
85
+ leaf_id?: string;
86
+ }
87
+
88
+ interface HookResponse {
89
+ modified: boolean;
90
+ updatedInput?: Record<string, unknown>;
91
+ }
92
+
93
+ // ============================================================================
94
+ // CLI Integration
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Shell out to the agentlogs CLI hook command.
99
+ * Passes hook data via stdin, receives response via stdout.
100
+ */
101
+ async function runHook(payload: HookPayload, cwd: string): Promise<HookResponse> {
102
+ const cliPath = process.env.AGENTLOGS_CLI_PATH;
103
+
104
+ let command: string;
105
+ let args: string[];
106
+
107
+ if (cliPath) {
108
+ const parts = cliPath.split(" ");
109
+ command = parts[0];
110
+ args = [...parts.slice(1), "pi", "hook"];
111
+ } else {
112
+ command = "npx";
113
+ args = ["-y", "agentlogs@latest", "pi", "hook"];
114
+ }
115
+
116
+ log("Running hook", { command, args, payload: payload.hook_event_name });
117
+
118
+ return new Promise((resolve) => {
119
+ const proc = spawn(command, args, {
120
+ cwd,
121
+ stdio: ["pipe", "pipe", "pipe"],
122
+ });
123
+
124
+ let stdout = "";
125
+ let stderr = "";
126
+
127
+ proc.stdout.on("data", (data) => {
128
+ stdout += data.toString();
129
+ });
130
+ proc.stderr.on("data", (data) => {
131
+ stderr += data.toString();
132
+ });
133
+
134
+ // Send payload via stdin
135
+ proc.stdin.write(JSON.stringify(payload));
136
+ proc.stdin.end();
137
+
138
+ proc.on("close", (code) => {
139
+ log("Hook process exited", { code, stdout: stdout.slice(0, 500), stderr: stderr.slice(0, 500) });
140
+
141
+ if (code === 0 && stdout.trim()) {
142
+ try {
143
+ const response = JSON.parse(stdout.trim()) as HookResponse;
144
+ resolve(response);
145
+ } catch {
146
+ resolve({ modified: false });
147
+ }
148
+ } else {
149
+ resolve({ modified: false });
150
+ }
151
+ });
152
+
153
+ proc.on("error", (err) => {
154
+ log("Hook spawn error", { error: String(err) });
155
+ resolve({ modified: false });
156
+ });
157
+ });
158
+ }
159
+
160
+ // ============================================================================
161
+ // Main Extension
162
+ // ============================================================================
163
+
164
+ export default function agentLogsExtension(pi: ExtensionAPI) {
165
+ log("Extension initialized");
166
+
167
+ // Track tool call IDs where we intercepted a git commit
168
+ const interceptedToolCallIds = new Set<string>();
169
+
170
+ // Handle tool calls (intercept git commits)
171
+ pi.on("tool_call", async (event, ctx) => {
172
+ // Only intercept bash tool
173
+ if (event.toolName !== "bash") {
174
+ return;
175
+ }
176
+
177
+ // Quick check: skip if not a git commit
178
+ const command = event.input?.command;
179
+ if (typeof command !== "string" || !/\bgit\s+commit\b/.test(command)) {
180
+ return;
181
+ }
182
+
183
+ log("tool_call (git commit)", {
184
+ toolName: event.toolName,
185
+ toolCallId: event.toolCallId,
186
+ });
187
+
188
+ const sessionId = ctx.sessionManager.getSessionId();
189
+ const sessionFile = ctx.sessionManager.getSessionFile();
190
+
191
+ const response = await runHook(
192
+ {
193
+ hook_event_name: "tool_call",
194
+ session_id: sessionId,
195
+ tool_call_id: event.toolCallId,
196
+ tool: event.toolName,
197
+ tool_input: event.input as Record<string, unknown>,
198
+ cwd: ctx.cwd,
199
+ session_file: sessionFile,
200
+ },
201
+ ctx.cwd,
202
+ );
203
+
204
+ if (response.modified && response.updatedInput) {
205
+ log("tool_call: input modified", { modified: true });
206
+ // Track this tool call ID so we know to process the result
207
+ interceptedToolCallIds.add(event.toolCallId);
208
+
209
+ // Note: Pi doesn't support modifying tool input from extensions
210
+ // The CLI will need to handle this differently (pre-upload the transcript)
211
+ // For now, we just track that we saw this commit
212
+ }
213
+ });
214
+
215
+ // Handle tool results (track commits)
216
+ pi.on("tool_result", async (event, ctx) => {
217
+ // Only handle bash tool
218
+ if (event.toolName !== "bash") {
219
+ return;
220
+ }
221
+
222
+ // Check if we intercepted this tool call
223
+ const wasIntercepted = interceptedToolCallIds.has(event.toolCallId);
224
+
225
+ // Also check if output contains our transcript link
226
+ const outputText =
227
+ event.content
228
+ ?.map((c) => (c.type === "text" ? c.text : ""))
229
+ .join("")
230
+ .trim() ?? "";
231
+ const hasLink = outputText.includes("agentlogs.ai/s/");
232
+
233
+ if (!wasIntercepted && !hasLink) {
234
+ return;
235
+ }
236
+
237
+ // Clean up tracked ID
238
+ interceptedToolCallIds.delete(event.toolCallId);
239
+
240
+ log("tool_result (git commit)", {
241
+ toolName: event.toolName,
242
+ toolCallId: event.toolCallId,
243
+ hasLink,
244
+ wasIntercepted,
245
+ });
246
+
247
+ const sessionId = ctx.sessionManager.getSessionId();
248
+
249
+ // Fire and forget - CLI handles commit tracking
250
+ runHook(
251
+ {
252
+ hook_event_name: "tool_result",
253
+ session_id: sessionId,
254
+ tool_call_id: event.toolCallId,
255
+ tool: event.toolName,
256
+ tool_output: { content: outputText },
257
+ cwd: ctx.cwd,
258
+ },
259
+ ctx.cwd,
260
+ ).catch((err) => log("tool_result hook error", { error: String(err) }));
261
+ });
262
+
263
+ // Helper to trigger transcript upload
264
+ const uploadTranscript = (eventName: string, ctx: ExtensionContext) => {
265
+ const sessionId = ctx.sessionManager.getSessionId();
266
+ const sessionFile = ctx.sessionManager.getSessionFile();
267
+ const leafId = ctx.sessionManager.getLeafId();
268
+
269
+ if (!sessionFile) {
270
+ log(`${eventName}: no session file (ephemeral session)`);
271
+ return;
272
+ }
273
+
274
+ log(eventName, { sessionId, sessionFile, leafId });
275
+
276
+ // Fire and forget - CLI handles the upload
277
+ runHook(
278
+ {
279
+ hook_event_name: eventName,
280
+ session_id: sessionId,
281
+ session_file: sessionFile,
282
+ leaf_id: leafId ?? undefined,
283
+ cwd: ctx.cwd,
284
+ },
285
+ ctx.cwd,
286
+ ).catch((err) => log(`${eventName} hook error`, { error: String(err) }));
287
+ };
288
+
289
+ // Handle agent end (upload after each turn)
290
+ pi.on("agent_end", async (_event, ctx) => {
291
+ uploadTranscript("agent_end", ctx);
292
+ });
293
+
294
+ // Handle session shutdown (final upload)
295
+ pi.on("session_shutdown", async (_event, ctx) => {
296
+ uploadTranscript("session_shutdown", ctx);
297
+ });
298
+ }
299
+
300
+ export { agentLogsExtension };