@posthog/agent 2.3.388 → 2.3.401

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.
Files changed (34) hide show
  1. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
  2. package/dist/adapters/claude/mcp/tool-metadata.d.ts +24 -0
  3. package/dist/adapters/claude/mcp/tool-metadata.js +165 -0
  4. package/dist/adapters/claude/mcp/tool-metadata.js.map +1 -0
  5. package/dist/adapters/claude/tools.js.map +1 -1
  6. package/dist/agent.js +113 -3
  7. package/dist/agent.js.map +1 -1
  8. package/dist/handoff-checkpoint.d.ts +1 -0
  9. package/dist/handoff-checkpoint.js +17 -1
  10. package/dist/handoff-checkpoint.js.map +1 -1
  11. package/dist/index.d.ts +7 -9
  12. package/dist/index.js.map +1 -1
  13. package/dist/posthog-api.js +5 -1
  14. package/dist/posthog-api.js.map +1 -1
  15. package/dist/server/agent-server.js +258 -101
  16. package/dist/server/agent-server.js.map +1 -1
  17. package/dist/server/bin.cjs +248 -98
  18. package/dist/server/bin.cjs.map +1 -1
  19. package/dist/tree-tracker.js +128 -97
  20. package/dist/tree-tracker.js.map +1 -1
  21. package/package.json +7 -3
  22. package/src/adapters/claude/claude-agent.ts +5 -0
  23. package/src/adapters/claude/mcp/tool-metadata.test.ts +93 -0
  24. package/src/adapters/claude/mcp/tool-metadata.ts +33 -0
  25. package/src/adapters/claude/permissions/permission-handlers.test.ts +165 -0
  26. package/src/adapters/claude/permissions/permission-handlers.ts +105 -0
  27. package/src/adapters/claude/session/instructions.ts +9 -1
  28. package/src/adapters/claude/types.ts +2 -0
  29. package/src/handoff-checkpoint.test.ts +1 -0
  30. package/src/handoff-checkpoint.ts +17 -1
  31. package/src/sagas/apply-snapshot-saga.test.ts +1 -0
  32. package/src/sagas/apply-snapshot-saga.ts +68 -54
  33. package/src/sagas/capture-tree-saga.test.ts +18 -0
  34. package/src/sagas/capture-tree-saga.ts +64 -49
@@ -1,4 +1,4 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { ApplyTreeSaga as GitApplyTreeSaga } from "@posthog/git/sagas/tree";
4
4
  import { Saga } from "@posthog/shared";
@@ -37,64 +37,78 @@ export class ApplySnapshotSaga extends Saga<
37
37
 
38
38
  const archiveUrl = snapshot.archiveUrl;
39
39
 
40
- await this.step({
41
- name: "create_tmp_dir",
42
- execute: () => mkdir(tmpDir, { recursive: true }),
43
- rollback: async () => {},
44
- });
40
+ try {
41
+ await this.step({
42
+ name: "create_tmp_dir",
43
+ execute: () => mkdir(tmpDir, { recursive: true }),
44
+ rollback: async () => {},
45
+ });
45
46
 
46
- const archivePath = join(tmpDir, `${snapshot.treeHash}.tar.gz`);
47
- this.archivePath = archivePath;
48
- await this.step({
49
- name: "download_archive",
50
- execute: async () => {
51
- const arrayBuffer = await apiClient.downloadArtifact(
52
- taskId,
53
- runId,
54
- archiveUrl,
55
- );
56
- if (!arrayBuffer) {
57
- throw new Error("Failed to download archive");
58
- }
59
- const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
60
- const binaryContent = Buffer.from(base64Content, "base64");
61
- await writeFile(archivePath, binaryContent);
62
- this.log.info("Tree archive downloaded", {
63
- treeHash: snapshot.treeHash,
64
- snapshotBytes: binaryContent.byteLength,
65
- snapshotWireBytes: arrayBuffer.byteLength,
66
- totalBytes: binaryContent.byteLength,
67
- totalWireBytes: arrayBuffer.byteLength,
68
- });
69
- },
70
- rollback: async () => {
71
- if (this.archivePath) {
72
- await rm(this.archivePath, { force: true }).catch(() => {});
73
- }
74
- },
75
- });
47
+ const archivePath = join(tmpDir, `${snapshot.treeHash}.tar.gz`);
48
+ this.archivePath = archivePath;
49
+ await this.step({
50
+ name: "download_archive",
51
+ execute: async () => {
52
+ const arrayBuffer = await apiClient.downloadArtifact(
53
+ taskId,
54
+ runId,
55
+ archiveUrl,
56
+ );
57
+ if (!arrayBuffer) {
58
+ throw new Error("Failed to download archive");
59
+ }
60
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
61
+ const binaryContent = Buffer.from(base64Content, "base64");
62
+ await writeFile(archivePath, binaryContent);
63
+ this.log.info("Tree archive downloaded", {
64
+ treeHash: snapshot.treeHash,
65
+ snapshotBytes: binaryContent.byteLength,
66
+ snapshotWireBytes: arrayBuffer.byteLength,
67
+ totalBytes: binaryContent.byteLength,
68
+ totalWireBytes: arrayBuffer.byteLength,
69
+ });
70
+ },
71
+ rollback: async () => {
72
+ if (this.archivePath) {
73
+ await rm(this.archivePath, { force: true }).catch(() => {});
74
+ }
75
+ },
76
+ });
76
77
 
77
- const gitApplySaga = new GitApplyTreeSaga(this.log);
78
- const applyResult = await gitApplySaga.run({
79
- baseDir: repositoryPath,
80
- treeHash: snapshot.treeHash,
81
- baseCommit: snapshot.baseCommit,
82
- changes: snapshot.changes,
83
- archivePath: this.archivePath,
84
- });
78
+ const gitApplySaga = new GitApplyTreeSaga(this.log);
79
+ const applyResult = await gitApplySaga.run({
80
+ baseDir: repositoryPath,
81
+ treeHash: snapshot.treeHash,
82
+ baseCommit: snapshot.baseCommit,
83
+ changes: snapshot.changes,
84
+ archivePath: this.archivePath,
85
+ });
85
86
 
86
- if (!applyResult.success) {
87
- throw new Error(`Failed to apply tree: ${applyResult.error}`);
88
- }
87
+ if (!applyResult.success) {
88
+ throw new Error(`Failed to apply tree: ${applyResult.error}`);
89
+ }
89
90
 
90
- await rm(this.archivePath, { force: true }).catch(() => {});
91
+ this.log.info("Tree snapshot applied", {
92
+ treeHash: snapshot.treeHash,
93
+ totalChanges: snapshot.changes.length,
94
+ deletedFiles: snapshot.changes.filter((c) => c.status === "D").length,
95
+ });
91
96
 
92
- this.log.info("Tree snapshot applied", {
93
- treeHash: snapshot.treeHash,
94
- totalChanges: snapshot.changes.length,
95
- deletedFiles: snapshot.changes.filter((c) => c.status === "D").length,
96
- });
97
+ return { treeHash: snapshot.treeHash };
98
+ } finally {
99
+ if (this.archivePath) {
100
+ await rm(this.archivePath, { force: true }).catch(() => {});
101
+ }
102
+ await this.removeTmpDirIfEmpty(tmpDir);
103
+ this.archivePath = null;
104
+ }
105
+ }
97
106
 
98
- return { treeHash: snapshot.treeHash };
107
+ private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
108
+ const entries = await readdir(tmpDir).catch(() => null);
109
+ if (!entries || entries.length > 0) {
110
+ return;
111
+ }
112
+ await rmdir(tmpDir).catch(() => {});
99
113
  }
100
114
  }
@@ -366,6 +366,24 @@ describe("CaptureTreeSaga", () => {
366
366
  const indexFiles = files.filter((f: string) => f.startsWith("index-"));
367
367
  expect(indexFiles).toHaveLength(0);
368
368
  });
369
+
370
+ it("cleans up uploaded tree archive and tmp dir on success", async () => {
371
+ const mockApiClient = createMockApiClient();
372
+
373
+ await repo.writeFile("new.ts", "content");
374
+
375
+ const saga = new CaptureTreeSaga(mockLogger);
376
+ const result = await saga.run({
377
+ repositoryPath: repo.path,
378
+ taskId: "task-1",
379
+ runId: "run-1",
380
+ lastTreeHash: null,
381
+ apiClient: mockApiClient,
382
+ });
383
+
384
+ expect(result.success).toBe(true);
385
+ expect(repo.exists(".posthog/tmp")).toBe(false);
386
+ });
369
387
  });
370
388
 
371
389
  describe("git state isolation", () => {
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile, rm } from "node:fs/promises";
2
+ import { readdir, readFile, rm, rmdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { CaptureTreeSaga as GitCaptureTreeSaga } from "@posthog/git/sagas/tree";
5
5
  import { Saga } from "@posthog/shared";
@@ -45,60 +45,67 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
45
45
  ? join(tmpDir, `tree-${Date.now()}.tar.gz`)
46
46
  : undefined;
47
47
 
48
- const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
49
- const captureResult = await gitCaptureSaga.run({
50
- baseDir: repositoryPath,
51
- lastTreeHash,
52
- archivePath,
53
- });
48
+ try {
49
+ const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
50
+ const captureResult = await gitCaptureSaga.run({
51
+ baseDir: repositoryPath,
52
+ lastTreeHash,
53
+ archivePath,
54
+ });
54
55
 
55
- if (!captureResult.success) {
56
- throw new Error(`Failed to capture tree: ${captureResult.error}`);
57
- }
56
+ if (!captureResult.success) {
57
+ throw new Error(`Failed to capture tree: ${captureResult.error}`);
58
+ }
58
59
 
59
- const {
60
- snapshot: gitSnapshot,
61
- archivePath: createdArchivePath,
62
- changed,
63
- } = captureResult.data;
64
-
65
- if (!changed || !gitSnapshot) {
66
- this.log.debug("No changes since last capture", { lastTreeHash });
67
- return { snapshot: null, newTreeHash: lastTreeHash };
68
- }
60
+ const {
61
+ snapshot: gitSnapshot,
62
+ archivePath: createdArchivePath,
63
+ changed,
64
+ } = captureResult.data;
69
65
 
70
- let archiveUrl: string | undefined;
71
- if (apiClient && createdArchivePath) {
72
- try {
73
- archiveUrl = await this.uploadArchive(
74
- createdArchivePath,
75
- gitSnapshot.treeHash,
76
- apiClient,
77
- taskId,
78
- runId,
79
- );
80
- } finally {
81
- await rm(createdArchivePath, { force: true }).catch(() => {});
66
+ if (!changed || !gitSnapshot) {
67
+ this.log.debug("No changes since last capture", { lastTreeHash });
68
+ return { snapshot: null, newTreeHash: lastTreeHash };
82
69
  }
83
- }
84
70
 
85
- const snapshot: TreeSnapshot = {
86
- treeHash: gitSnapshot.treeHash,
87
- baseCommit: gitSnapshot.baseCommit,
88
- changes: gitSnapshot.changes,
89
- timestamp: gitSnapshot.timestamp,
90
- interrupted,
91
- archiveUrl,
92
- };
93
-
94
- this.log.info("Tree captured", {
95
- treeHash: snapshot.treeHash,
96
- changes: snapshot.changes.length,
97
- interrupted,
98
- archiveUrl,
99
- });
71
+ let archiveUrl: string | undefined;
72
+ if (apiClient && createdArchivePath) {
73
+ try {
74
+ archiveUrl = await this.uploadArchive(
75
+ createdArchivePath,
76
+ gitSnapshot.treeHash,
77
+ apiClient,
78
+ taskId,
79
+ runId,
80
+ );
81
+ } finally {
82
+ await rm(createdArchivePath, { force: true }).catch(() => {});
83
+ }
84
+ }
100
85
 
101
- return { snapshot, newTreeHash: snapshot.treeHash };
86
+ const snapshot: TreeSnapshot = {
87
+ treeHash: gitSnapshot.treeHash,
88
+ baseCommit: gitSnapshot.baseCommit,
89
+ changes: gitSnapshot.changes,
90
+ timestamp: gitSnapshot.timestamp,
91
+ interrupted,
92
+ archiveUrl,
93
+ };
94
+
95
+ this.log.info("Tree captured", {
96
+ treeHash: snapshot.treeHash,
97
+ changes: snapshot.changes.length,
98
+ interrupted,
99
+ archiveUrl,
100
+ });
101
+
102
+ return { snapshot, newTreeHash: snapshot.treeHash };
103
+ } finally {
104
+ if (archivePath) {
105
+ await rm(archivePath, { force: true }).catch(() => {});
106
+ }
107
+ await this.removeTmpDirIfEmpty(tmpDir);
108
+ }
102
109
  }
103
110
 
104
111
  private async uploadArchive(
@@ -147,4 +154,12 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
147
154
 
148
155
  return archiveUrl;
149
156
  }
157
+
158
+ private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
159
+ const entries = await readdir(tmpDir).catch(() => null);
160
+ if (!entries || entries.length > 0) {
161
+ return;
162
+ }
163
+ await rmdir(tmpDir).catch(() => {});
164
+ }
150
165
  }