@posthog/agent 2.0.0 → 2.0.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.
Files changed (131) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +170 -1157
  35. package/dist/index.js +9373 -5135
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +10503 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +10558 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +65 -13
  51. package/src/acp-extensions.ts +98 -16
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -688
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +96 -587
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +54 -137
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/src/adapters/claude/claude.ts +0 -1947
  119. package/src/adapters/claude/mcp-server.ts +0 -810
  120. package/src/adapters/claude/utils.ts +0 -267
  121. package/src/adapters/connection.ts +0 -95
  122. package/src/file-manager.ts +0 -273
  123. package/src/git-manager.ts +0 -577
  124. package/src/schemas.ts +0 -241
  125. package/src/session-store.ts +0 -259
  126. package/src/task-manager.ts +0 -163
  127. package/src/todo-manager.ts +0 -180
  128. package/src/tools/registry.ts +0 -134
  129. package/src/tools/types.ts +0 -133
  130. package/src/utils/tapped-stream.ts +0 -60
  131. package/src/worktree-manager.ts +0 -974
@@ -1,267 +0,0 @@
1
- // A pushable async iterable: allows you to push items and consume them with for-await.
2
-
3
- import { readFileSync } from "node:fs";
4
- import { platform } from "node:os";
5
- import type { Readable, Writable } from "node:stream";
6
- import { ReadableStream, WritableStream } from "node:stream/web";
7
- import type { Logger } from "@/utils/logger.js";
8
-
9
- // Useful for bridging push-based and async-iterator-based code.
10
- export class Pushable<T> implements AsyncIterable<T> {
11
- private queue: T[] = [];
12
- private resolvers: ((value: IteratorResult<T>) => void)[] = [];
13
- private done = false;
14
-
15
- push(item: T) {
16
- const resolve = this.resolvers.shift();
17
- if (resolve) {
18
- resolve({ value: item, done: false });
19
- } else {
20
- this.queue.push(item);
21
- }
22
- }
23
-
24
- end() {
25
- this.done = true;
26
- for (const resolve of this.resolvers) {
27
- resolve({ value: undefined as unknown as T, done: true });
28
- }
29
- this.resolvers = [];
30
- }
31
-
32
- [Symbol.asyncIterator](): AsyncIterator<T> {
33
- return {
34
- next: (): Promise<IteratorResult<T>> => {
35
- if (this.queue.length > 0) {
36
- const value = this.queue.shift() as T;
37
- return Promise.resolve({ value, done: false });
38
- }
39
- if (this.done) {
40
- return Promise.resolve({
41
- value: undefined as unknown as T,
42
- done: true,
43
- });
44
- }
45
- return new Promise<IteratorResult<T>>((resolve) => {
46
- this.resolvers.push(resolve);
47
- });
48
- },
49
- };
50
- }
51
- }
52
-
53
- // Helper to convert Node.js streams to Web Streams
54
- export function nodeToWebWritable(
55
- nodeStream: Writable,
56
- ): WritableStream<Uint8Array> {
57
- return new WritableStream<Uint8Array>({
58
- write(chunk) {
59
- return new Promise<void>((resolve, reject) => {
60
- nodeStream.write(Buffer.from(chunk), (err) => {
61
- if (err) {
62
- reject(err);
63
- } else {
64
- resolve();
65
- }
66
- });
67
- });
68
- },
69
- });
70
- }
71
-
72
- export function nodeToWebReadable(
73
- nodeStream: Readable,
74
- ): ReadableStream<Uint8Array> {
75
- return new ReadableStream<Uint8Array>({
76
- start(controller) {
77
- nodeStream.on("data", (chunk: Buffer) => {
78
- controller.enqueue(new Uint8Array(chunk));
79
- });
80
- nodeStream.on("end", () => controller.close());
81
- nodeStream.on("error", (err) => controller.error(err));
82
- },
83
- });
84
- }
85
-
86
- export function unreachable(value: never, logger: Logger) {
87
- let valueAsString: string;
88
- try {
89
- valueAsString = JSON.stringify(value);
90
- } catch {
91
- valueAsString = value;
92
- }
93
- logger.error(`Unexpected case: ${valueAsString}`);
94
- }
95
-
96
- export function sleep(time: number): Promise<void> {
97
- return new Promise((resolve) => setTimeout(resolve, time));
98
- }
99
-
100
- interface ManagedSettings {
101
- permissions?: {
102
- allow?: string[];
103
- deny?: string[];
104
- };
105
- env?: Record<string, string>;
106
- }
107
-
108
- // Following the rules in https://docs.anthropic.com/en/docs/claude-code/settings#settings-files
109
- // This can be removed once the SDK supports it natively.
110
- function getManagedSettingsPath(): string {
111
- const os = platform();
112
- switch (os) {
113
- case "darwin":
114
- return "/Library/Application Support/ClaudeCode/managed-settings.json";
115
- case "linux": // including WSL
116
- return "/etc/claude-code/managed-settings.json";
117
- case "win32":
118
- return "C:\\ProgramData\\ClaudeCode\\managed-settings.json";
119
- default:
120
- return "/etc/claude-code/managed-settings.json";
121
- }
122
- }
123
-
124
- export function loadManagedSettings(): ManagedSettings | null {
125
- try {
126
- return JSON.parse(
127
- readFileSync(getManagedSettingsPath(), "utf8"),
128
- ) as ManagedSettings;
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- export function applyEnvironmentSettings(settings: ManagedSettings): void {
135
- if (settings.env) {
136
- for (const [key, value] of Object.entries(settings.env)) {
137
- process.env[key] = value;
138
- }
139
- }
140
- }
141
-
142
- export type StreamPair = {
143
- readable: globalThis.ReadableStream<Uint8Array>;
144
- writable: globalThis.WritableStream<Uint8Array>;
145
- };
146
-
147
- export type BidirectionalStreamPair = {
148
- client: StreamPair;
149
- agent: StreamPair;
150
- };
151
-
152
- function pushableToReadableStream(
153
- pushable: Pushable<Uint8Array>,
154
- ): globalThis.ReadableStream<Uint8Array> {
155
- const iterator = pushable[Symbol.asyncIterator]();
156
- return new ReadableStream<Uint8Array>({
157
- async pull(controller) {
158
- const { value, done } = await iterator.next();
159
- if (done) {
160
- controller.close();
161
- } else {
162
- controller.enqueue(value);
163
- }
164
- },
165
- }) as unknown as globalThis.ReadableStream<Uint8Array>;
166
- }
167
-
168
- export function createBidirectionalStreams(): BidirectionalStreamPair {
169
- const clientToAgentPushable = new Pushable<Uint8Array>();
170
- const agentToClientPushable = new Pushable<Uint8Array>();
171
-
172
- const clientToAgentReadable = pushableToReadableStream(clientToAgentPushable);
173
- const agentToClientReadable = pushableToReadableStream(agentToClientPushable);
174
-
175
- const clientToAgentWritable = new WritableStream<Uint8Array>({
176
- write(chunk) {
177
- clientToAgentPushable.push(chunk);
178
- },
179
- close() {
180
- clientToAgentPushable.end();
181
- },
182
- }) as globalThis.WritableStream<Uint8Array>;
183
-
184
- const agentToClientWritable = new WritableStream<Uint8Array>({
185
- write(chunk) {
186
- agentToClientPushable.push(chunk);
187
- },
188
- close() {
189
- agentToClientPushable.end();
190
- },
191
- }) as globalThis.WritableStream<Uint8Array>;
192
-
193
- return {
194
- client: {
195
- readable: agentToClientReadable,
196
- writable: clientToAgentWritable,
197
- },
198
- agent: {
199
- readable: clientToAgentReadable,
200
- writable: agentToClientWritable,
201
- },
202
- };
203
- }
204
-
205
- export interface ExtractLinesResult {
206
- content: string;
207
- wasLimited: boolean;
208
- linesRead: number;
209
- }
210
-
211
- /**
212
- * Extracts lines from file content with byte limit enforcement.
213
- *
214
- * @param fullContent - The complete file content
215
- * @param maxContentLength - Maximum number of UTF-16 Code Units to return
216
- * @returns Object containing extracted content and metadata
217
- */
218
- export function extractLinesWithByteLimit(
219
- fullContent: string,
220
- maxContentLength: number,
221
- ): ExtractLinesResult {
222
- if (fullContent === "") {
223
- return {
224
- content: "",
225
- wasLimited: false,
226
- linesRead: 1,
227
- };
228
- }
229
-
230
- let linesSeen = 0;
231
- let index = 0;
232
- linesSeen = 0;
233
-
234
- let contentLength = 0;
235
- let wasLimited = false;
236
-
237
- while (true) {
238
- const nextIndex = fullContent.indexOf("\n", index);
239
-
240
- if (nextIndex < 0) {
241
- // Last line in file (no trailing newline)
242
- if (linesSeen > 0 && fullContent.length > maxContentLength) {
243
- wasLimited = true;
244
- break;
245
- }
246
- linesSeen += 1;
247
- contentLength = fullContent.length;
248
- break;
249
- } else {
250
- // Line with newline - include up to the newline
251
- const newContentLength = nextIndex + 1;
252
- if (linesSeen > 0 && newContentLength > maxContentLength) {
253
- wasLimited = true;
254
- break;
255
- }
256
- linesSeen += 1;
257
- contentLength = newContentLength;
258
- index = newContentLength;
259
- }
260
- }
261
-
262
- return {
263
- content: fullContent.slice(0, contentLength),
264
- wasLimited,
265
- linesRead: linesSeen,
266
- };
267
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * Shared ACP connection factory.
3
- *
4
- * Creates ACP connections for the Claude Code agent.
5
- */
6
-
7
- import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
8
- import type { SessionStore } from "@/session-store.js";
9
- import { Logger } from "@/utils/logger.js";
10
- import { createTappedWritableStream } from "@/utils/tapped-stream.js";
11
- import { ClaudeAcpAgent } from "./claude/claude.js";
12
- import { createBidirectionalStreams, type StreamPair } from "./claude/utils.js";
13
-
14
- export type AgentFramework = "claude";
15
-
16
- export type AcpConnectionConfig = {
17
- framework?: AgentFramework;
18
- sessionStore?: SessionStore;
19
- sessionId?: string;
20
- taskId?: string;
21
- };
22
-
23
- export type InProcessAcpConnection = {
24
- agentConnection: AgentSideConnection;
25
- clientStreams: StreamPair;
26
- };
27
-
28
- /**
29
- * Creates an ACP connection with the specified agent framework.
30
- *
31
- * @param config - Configuration including framework selection
32
- * @returns Connection with agent and client streams
33
- */
34
- export function createAcpConnection(
35
- config: AcpConnectionConfig = {},
36
- ): InProcessAcpConnection {
37
- const logger = new Logger({ debug: true, prefix: "[AcpConnection]" });
38
- const streams = createBidirectionalStreams();
39
-
40
- const { sessionStore, framework = "claude" } = config;
41
-
42
- // Tap both streams for automatic persistence
43
- // All messages (bidirectional) will be persisted as they flow through
44
- let agentWritable = streams.agent.writable;
45
- let clientWritable = streams.client.writable;
46
-
47
- if (config.sessionId && sessionStore) {
48
- // Register session for persistence BEFORE tapping streams
49
- // This ensures all messages from the start get persisted
50
- if (!sessionStore.isRegistered(config.sessionId)) {
51
- sessionStore.register(config.sessionId, {
52
- taskId: config.taskId ?? config.sessionId,
53
- runId: config.sessionId,
54
- logUrl: "", // Will be updated when we get the real logUrl
55
- });
56
- }
57
-
58
- // Tap agent→client stream
59
- agentWritable = createTappedWritableStream(streams.agent.writable, {
60
- onMessage: (line) => {
61
- sessionStore.appendRawLine(config.sessionId!, line);
62
- },
63
- logger,
64
- });
65
-
66
- // Tap client→agent stream
67
- clientWritable = createTappedWritableStream(streams.client.writable, {
68
- onMessage: (line) => {
69
- sessionStore.appendRawLine(config.sessionId!, line);
70
- },
71
- logger,
72
- });
73
- } else {
74
- logger.info("Tapped streams NOT enabled", {
75
- hasSessionId: !!config.sessionId,
76
- hasSessionStore: !!sessionStore,
77
- });
78
- }
79
-
80
- const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
81
-
82
- // Create the Claude agent
83
- const agentConnection = new AgentSideConnection((client) => {
84
- logger.info("Creating Claude agent");
85
- return new ClaudeAcpAgent(client, sessionStore);
86
- }, agentStream);
87
-
88
- return {
89
- agentConnection,
90
- clientStreams: {
91
- readable: streams.client.readable,
92
- writable: clientWritable,
93
- },
94
- };
95
- }
@@ -1,273 +0,0 @@
1
- import { promises as fs } from "node:fs";
2
- import { extname, join } from "node:path";
3
- import z from "zod";
4
- import type { SupportingFile } from "./types.js";
5
- import { Logger } from "./utils/logger.js";
6
-
7
- export interface TaskFile {
8
- name: string;
9
- content: string;
10
- type: "plan" | "context" | "reference" | "output" | "artifact";
11
- }
12
-
13
- export interface LocalArtifact {
14
- name: string;
15
- content: string;
16
- type: TaskFile["type"];
17
- contentType: string;
18
- size: number;
19
- }
20
-
21
- export class PostHogFileManager {
22
- private repositoryPath: string;
23
- private logger: Logger;
24
-
25
- constructor(repositoryPath: string, logger?: Logger) {
26
- this.repositoryPath = repositoryPath;
27
- this.logger =
28
- logger || new Logger({ debug: false, prefix: "[FileManager]" });
29
- }
30
-
31
- private getTaskDirectory(taskId: string): string {
32
- return join(this.repositoryPath, ".posthog", taskId);
33
- }
34
-
35
- private getTaskFilePath(taskId: string, fileName: string): string {
36
- return join(this.getTaskDirectory(taskId), fileName);
37
- }
38
-
39
- async ensureTaskDirectory(taskId: string): Promise<void> {
40
- const taskDir = this.getTaskDirectory(taskId);
41
- try {
42
- await fs.access(taskDir);
43
- } catch {
44
- await fs.mkdir(taskDir, { recursive: true });
45
- }
46
- }
47
-
48
- async writeTaskFile(taskId: string, file: TaskFile): Promise<void> {
49
- await this.ensureTaskDirectory(taskId);
50
- const filePath = this.getTaskFilePath(taskId, file.name);
51
-
52
- this.logger.debug("Writing task file", {
53
- filePath,
54
- contentLength: file.content.length,
55
- contentType: typeof file.content,
56
- });
57
-
58
- await fs.writeFile(filePath, file.content, "utf8");
59
-
60
- this.logger.debug("File written successfully", { filePath });
61
- }
62
-
63
- async readTaskFile(taskId: string, fileName: string): Promise<string | null> {
64
- try {
65
- const filePath = this.getTaskFilePath(taskId, fileName);
66
- return await fs.readFile(filePath, "utf8");
67
- } catch (error) {
68
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
69
- return null;
70
- }
71
- throw error;
72
- }
73
- }
74
-
75
- async listTaskFiles(taskId: string): Promise<string[]> {
76
- try {
77
- const taskDir = this.getTaskDirectory(taskId);
78
- const files = await fs.readdir(taskDir);
79
- return files.filter((file) => !file.startsWith("."));
80
- } catch (error) {
81
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
82
- return [];
83
- }
84
- throw error;
85
- }
86
- }
87
-
88
- async deleteTaskFile(taskId: string, fileName: string): Promise<void> {
89
- try {
90
- const filePath = this.getTaskFilePath(taskId, fileName);
91
- await fs.unlink(filePath);
92
- } catch (error) {
93
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
94
- throw error;
95
- }
96
- }
97
- }
98
-
99
- async taskDirectoryExists(taskId: string): Promise<boolean> {
100
- try {
101
- const taskDir = this.getTaskDirectory(taskId);
102
- await fs.access(taskDir);
103
- return true;
104
- } catch {
105
- return false;
106
- }
107
- }
108
-
109
- async cleanupTaskDirectory(taskId: string): Promise<void> {
110
- try {
111
- const taskDir = this.getTaskDirectory(taskId);
112
- await fs.rm(taskDir, { recursive: true, force: true });
113
- } catch (error) {
114
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
115
- throw error;
116
- }
117
- }
118
- }
119
-
120
- // Convenience methods for common file types
121
- async writePlan(taskId: string, plan: string): Promise<void> {
122
- this.logger.debug("Writing plan", {
123
- taskId,
124
- planLength: plan.length,
125
- contentPreview: plan.substring(0, 200),
126
- });
127
-
128
- await this.writeTaskFile(taskId, {
129
- name: "plan.md",
130
- content: plan,
131
- type: "plan",
132
- });
133
-
134
- this.logger.info("Plan file written", { taskId });
135
- }
136
-
137
- async readPlan(taskId: string): Promise<string | null> {
138
- return await this.readTaskFile(taskId, "plan.md");
139
- }
140
-
141
- async writeContext(taskId: string, context: string): Promise<void> {
142
- await this.writeTaskFile(taskId, {
143
- name: "context.md",
144
- content: context,
145
- type: "context",
146
- });
147
- }
148
-
149
- async readContext(taskId: string): Promise<string | null> {
150
- return await this.readTaskFile(taskId, "context.md");
151
- }
152
-
153
- async writeRequirements(taskId: string, requirements: string): Promise<void> {
154
- await this.writeTaskFile(taskId, {
155
- name: "requirements.md",
156
- content: requirements,
157
- type: "reference",
158
- });
159
- }
160
-
161
- async readRequirements(taskId: string): Promise<string | null> {
162
- return await this.readTaskFile(taskId, "requirements.md");
163
- }
164
-
165
- async writeTodos(taskId: string, data: unknown): Promise<void> {
166
- const todos = z.object({
167
- metadata: z.object({
168
- total: z.number(),
169
- completed: z.number(),
170
- }),
171
- });
172
-
173
- const validatedData = todos.parse(data);
174
- this.logger.debug("Writing todos", {
175
- taskId,
176
- total: validatedData.metadata?.total ?? 0,
177
- completed: validatedData.metadata?.completed ?? 0,
178
- });
179
-
180
- await this.writeTaskFile(taskId, {
181
- name: "todos.json",
182
- content: JSON.stringify(validatedData, null, 2),
183
- type: "artifact",
184
- });
185
-
186
- this.logger.info("Todos file written", {
187
- taskId,
188
- total: validatedData.metadata?.total ?? 0,
189
- completed: validatedData.metadata?.completed ?? 0,
190
- });
191
- }
192
-
193
- async readTodos(taskId: string): Promise<unknown | null> {
194
- try {
195
- const content = await this.readTaskFile(taskId, "todos.json");
196
- return content ? JSON.parse(content) : null;
197
- } catch (error) {
198
- this.logger.debug("Failed to parse todos.json", { error });
199
- return null;
200
- }
201
- }
202
-
203
- async getTaskFiles(taskId: string): Promise<SupportingFile[]> {
204
- const fileNames = await this.listTaskFiles(taskId);
205
- const files: SupportingFile[] = [];
206
-
207
- for (const fileName of fileNames) {
208
- const content = await this.readTaskFile(taskId, fileName);
209
- if (content !== null) {
210
- // Determine type based on file name
211
- const type = this.resolveFileType(fileName);
212
-
213
- files.push({
214
- name: fileName,
215
- content,
216
- type,
217
- created_at: new Date().toISOString(), // Could be enhanced with file stats
218
- });
219
- }
220
- }
221
-
222
- return files;
223
- }
224
-
225
- async collectTaskArtifacts(taskId: string): Promise<LocalArtifact[]> {
226
- const fileNames = await this.listTaskFiles(taskId);
227
- const artifacts: LocalArtifact[] = [];
228
-
229
- for (const fileName of fileNames) {
230
- const content = await this.readTaskFile(taskId, fileName);
231
- if (content === null) {
232
- continue;
233
- }
234
-
235
- const type = this.resolveFileType(fileName);
236
- const contentType = this.inferContentType(fileName);
237
- const size = Buffer.byteLength(content, "utf8");
238
-
239
- artifacts.push({
240
- name: fileName,
241
- content,
242
- type,
243
- contentType,
244
- size,
245
- });
246
- }
247
-
248
- return artifacts;
249
- }
250
-
251
- private resolveFileType(fileName: string): TaskFile["type"] {
252
- if (fileName === "plan.md") return "plan";
253
- if (fileName === "context.md") return "context";
254
- if (fileName === "requirements.md") return "reference";
255
- if (fileName.startsWith("output_")) return "output";
256
- if (fileName.endsWith(".md")) return "reference";
257
- return "artifact";
258
- }
259
-
260
- private inferContentType(fileName: string): string {
261
- const extension = extname(fileName).toLowerCase();
262
- switch (extension) {
263
- case ".md":
264
- return "text/markdown";
265
- case ".json":
266
- return "application/json";
267
- case ".txt":
268
- return "text/plain";
269
- default:
270
- return "text/plain";
271
- }
272
- }
273
- }