@posthog/agent 2.1.47 → 2.1.53

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/dist/index.js CHANGED
@@ -809,10 +809,10 @@ var require_src2 = __commonJS({
809
809
  var fs_1 = __require("fs");
810
810
  var debug_1 = __importDefault(require_src());
811
811
  var log = debug_1.default("@kwsites/file-exists");
812
- function check(path7, isFile, isDirectory) {
813
- log(`checking %s`, path7);
812
+ function check(path8, isFile, isDirectory) {
813
+ log(`checking %s`, path8);
814
814
  try {
815
- const stat = fs_1.statSync(path7);
815
+ const stat = fs_1.statSync(path8);
816
816
  if (stat.isFile() && isFile) {
817
817
  log(`[OK] path represents a file`);
818
818
  return true;
@@ -832,8 +832,8 @@ var require_src2 = __commonJS({
832
832
  throw e;
833
833
  }
834
834
  }
835
- function exists2(path7, type = exports.READABLE) {
836
- return check(path7, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
835
+ function exists2(path8, type = exports.READABLE) {
836
+ return check(path8, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
837
837
  }
838
838
  exports.exists = exists2;
839
839
  exports.FILE = 1;
@@ -1169,174 +1169,12 @@ import {
1169
1169
  import {
1170
1170
  query
1171
1171
  } from "@anthropic-ai/claude-agent-sdk";
1172
-
1173
- // ../shared/dist/index.js
1174
- var consoleLogger = {
1175
- info: (_message, _data) => {
1176
- },
1177
- debug: (_message, _data) => {
1178
- },
1179
- error: (_message, _data) => {
1180
- },
1181
- warn: (_message, _data) => {
1182
- }
1183
- };
1184
- var Saga = class {
1185
- completedSteps = [];
1186
- currentStepName = "unknown";
1187
- stepTimings = [];
1188
- log;
1189
- constructor(logger) {
1190
- this.log = logger ?? consoleLogger;
1191
- }
1192
- /**
1193
- * Run the saga with the given input.
1194
- * Returns a discriminated union result - either success with data or failure with error details.
1195
- */
1196
- async run(input) {
1197
- this.completedSteps = [];
1198
- this.currentStepName = "unknown";
1199
- this.stepTimings = [];
1200
- const sagaStart = performance.now();
1201
- this.log.info("Starting saga", { sagaName: this.constructor.name });
1202
- try {
1203
- const result = await this.execute(input);
1204
- const totalDuration = performance.now() - sagaStart;
1205
- this.log.debug("Saga completed successfully", {
1206
- sagaName: this.constructor.name,
1207
- stepsCompleted: this.completedSteps.length,
1208
- totalDurationMs: Math.round(totalDuration),
1209
- stepTimings: this.stepTimings
1210
- });
1211
- return { success: true, data: result };
1212
- } catch (error) {
1213
- this.log.error("Saga failed, initiating rollback", {
1214
- sagaName: this.constructor.name,
1215
- failedStep: this.currentStepName,
1216
- error: error instanceof Error ? error.message : String(error)
1217
- });
1218
- await this.rollback();
1219
- return {
1220
- success: false,
1221
- error: error instanceof Error ? error.message : String(error),
1222
- failedStep: this.currentStepName
1223
- };
1224
- }
1225
- }
1226
- /**
1227
- * Execute a step with its rollback action.
1228
- * If the step succeeds, its rollback action is stored for potential rollback.
1229
- * The step name is automatically tracked for error reporting.
1230
- *
1231
- * @param config - Step configuration with name, execute, and rollback functions
1232
- * @returns The result of the execute function
1233
- * @throws Re-throws any error from the execute function (triggers rollback)
1234
- */
1235
- async step(config) {
1236
- this.currentStepName = config.name;
1237
- this.log.debug(`Executing step: ${config.name}`);
1238
- const stepStart = performance.now();
1239
- const result = await config.execute();
1240
- const durationMs = Math.round(performance.now() - stepStart);
1241
- this.stepTimings.push({ name: config.name, durationMs });
1242
- this.log.debug(`Step completed: ${config.name}`, { durationMs });
1243
- this.completedSteps.push({
1244
- name: config.name,
1245
- rollback: () => config.rollback(result)
1246
- });
1247
- return result;
1248
- }
1249
- /**
1250
- * Execute a step that doesn't need rollback.
1251
- * Useful for read-only operations or operations that are idempotent.
1252
- * The step name is automatically tracked for error reporting.
1253
- *
1254
- * @param name - Step name for logging and error tracking
1255
- * @param execute - The action to execute
1256
- * @returns The result of the execute function
1257
- */
1258
- async readOnlyStep(name, execute) {
1259
- this.currentStepName = name;
1260
- this.log.debug(`Executing read-only step: ${name}`);
1261
- const stepStart = performance.now();
1262
- const result = await execute();
1263
- const durationMs = Math.round(performance.now() - stepStart);
1264
- this.stepTimings.push({ name, durationMs });
1265
- this.log.debug(`Read-only step completed: ${name}`, { durationMs });
1266
- return result;
1267
- }
1268
- /**
1269
- * Roll back all completed steps in reverse order.
1270
- * Rollback errors are logged but don't stop the rollback of other steps.
1271
- */
1272
- async rollback() {
1273
- this.log.info("Rolling back saga", {
1274
- stepsToRollback: this.completedSteps.length
1275
- });
1276
- const stepsReversed = [...this.completedSteps].reverse();
1277
- for (const step of stepsReversed) {
1278
- try {
1279
- this.log.debug(`Rolling back step: ${step.name}`);
1280
- await step.rollback();
1281
- this.log.debug(`Step rolled back: ${step.name}`);
1282
- } catch (error) {
1283
- this.log.error(`Failed to rollback step: ${step.name}`, {
1284
- error: error instanceof Error ? error.message : String(error)
1285
- });
1286
- }
1287
- }
1288
- this.log.info("Rollback completed", {
1289
- stepsAttempted: this.completedSteps.length
1290
- });
1291
- }
1292
- /**
1293
- * Get the number of completed steps (useful for testing)
1294
- */
1295
- getCompletedStepCount() {
1296
- return this.completedSteps.length;
1297
- }
1298
- };
1299
- var NOOP_COLLECTOR = {
1300
- time: (_label, fn) => fn(),
1301
- timeSync: (_label, fn) => fn(),
1302
- record: () => {
1303
- },
1304
- summarize: () => {
1305
- }
1306
- };
1307
- function createTimingCollector(enabled, log) {
1308
- if (!enabled) return NOOP_COLLECTOR;
1309
- const steps = {};
1310
- return {
1311
- async time(label, fn) {
1312
- const start = Date.now();
1313
- const result = await fn();
1314
- steps[label] = Date.now() - start;
1315
- return result;
1316
- },
1317
- timeSync(label, fn) {
1318
- const start = Date.now();
1319
- const result = fn();
1320
- steps[label] = Date.now() - start;
1321
- return result;
1322
- },
1323
- record(label, ms) {
1324
- steps[label] = ms;
1325
- },
1326
- summarize(label) {
1327
- const total = Object.values(steps).reduce((a, b) => a + b, 0);
1328
- log(`[timing] ${label}: ${total}ms`, steps);
1329
- }
1330
- };
1331
- }
1332
-
1333
- // src/adapters/claude/claude-agent.ts
1334
1172
  import { v7 as uuidv7 } from "uuid";
1335
1173
 
1336
1174
  // package.json
1337
1175
  var package_default = {
1338
1176
  name: "@posthog/agent",
1339
- version: "2.1.47",
1177
+ version: "2.1.53",
1340
1178
  repository: "https://github.com/PostHog/twig",
1341
1179
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
1342
1180
  exports: {
@@ -1758,8 +1596,8 @@ var ToolContentBuilder = class {
1758
1596
  this.items.push({ type: "content", content: image(data, mimeType, uri) });
1759
1597
  return this;
1760
1598
  }
1761
- diff(path7, oldText, newText) {
1762
- this.items.push({ type: "diff", path: path7, oldText, newText });
1599
+ diff(path8, oldText, newText) {
1600
+ this.items.push({ type: "diff", path: path8, oldText, newText });
1763
1601
  return this;
1764
1602
  }
1765
1603
  build() {
@@ -1949,13 +1787,13 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ d
1949
1787
  locations: []
1950
1788
  };
1951
1789
  case "Edit": {
1952
- const path7 = input?.file_path ? String(input.file_path) : void 0;
1790
+ const path8 = input?.file_path ? String(input.file_path) : void 0;
1953
1791
  let oldText = input?.old_string ? String(input.old_string) : null;
1954
1792
  let newText = input?.new_string ? String(input.new_string) : "";
1955
1793
  let affectedLines = [];
1956
- if (path7 && oldText) {
1794
+ if (path8 && oldText) {
1957
1795
  try {
1958
- const oldContent = cachedFileContent[path7] || "";
1796
+ const oldContent = cachedFileContent[path8] || "";
1959
1797
  const newContent = replaceAndCalculateLocation(oldContent, [
1960
1798
  {
1961
1799
  oldText,
@@ -1971,17 +1809,17 @@ function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ d
1971
1809
  }
1972
1810
  }
1973
1811
  return {
1974
- title: path7 ? `Edit \`${path7}\`` : "Edit",
1812
+ title: path8 ? `Edit \`${path8}\`` : "Edit",
1975
1813
  kind: "edit",
1976
- content: input && path7 ? [
1814
+ content: input && path8 ? [
1977
1815
  {
1978
1816
  type: "diff",
1979
- path: path7,
1817
+ path: path8,
1980
1818
  oldText,
1981
1819
  newText
1982
1820
  }
1983
1821
  ] : [],
1984
- locations: path7 ? affectedLines.length > 0 ? affectedLines.map((line) => ({ line, path: path7 })) : [{ path: path7 }] : []
1822
+ locations: path8 ? affectedLines.length > 0 ? affectedLines.map((line) => ({ line, path: path8 })) : [{ path: path8 }] : []
1985
1823
  };
1986
1824
  }
1987
1825
  case "Write": {
@@ -3455,12 +3293,10 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3455
3293
  logWriter;
3456
3294
  options;
3457
3295
  lastSentConfigOptions;
3458
- debug;
3459
3296
  constructor(client, logWriter, options) {
3460
3297
  super(client);
3461
3298
  this.logWriter = logWriter;
3462
3299
  this.options = options;
3463
- this.debug = options?.debug ?? false;
3464
3300
  this.toolUseCache = {};
3465
3301
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
3466
3302
  }
@@ -3503,36 +3339,26 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3503
3339
  }
3504
3340
  async newSession(params) {
3505
3341
  this.checkAuthStatus();
3506
- const tc = createTimingCollector(
3507
- this.debug,
3508
- (msg, data) => this.logger.info(msg, data)
3509
- );
3510
3342
  const meta = params._meta;
3511
3343
  const sessionId = uuidv7();
3512
3344
  const permissionMode = meta?.permissionMode && TWIG_EXECUTION_MODES.includes(meta.permissionMode) ? meta.permissionMode : "default";
3513
- const mcpServers = tc.timeSync(
3514
- "parseMcpServers",
3515
- () => parseMcpServers(params)
3516
- );
3517
- const options = tc.timeSync(
3518
- "buildSessionOptions",
3519
- () => buildSessionOptions({
3520
- cwd: params.cwd,
3521
- mcpServers,
3522
- permissionMode,
3523
- canUseTool: this.createCanUseTool(sessionId),
3524
- logger: this.logger,
3525
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3526
- userProvidedOptions: meta?.claudeCode?.options,
3527
- sessionId,
3528
- isResume: false,
3529
- onModeChange: this.createOnModeChange(sessionId),
3530
- onProcessSpawned: this.options?.onProcessSpawned,
3531
- onProcessExited: this.options?.onProcessExited
3532
- })
3533
- );
3345
+ const mcpServers = parseMcpServers(params);
3346
+ const options = buildSessionOptions({
3347
+ cwd: params.cwd,
3348
+ mcpServers,
3349
+ permissionMode,
3350
+ canUseTool: this.createCanUseTool(sessionId),
3351
+ logger: this.logger,
3352
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3353
+ userProvidedOptions: meta?.claudeCode?.options,
3354
+ sessionId,
3355
+ isResume: false,
3356
+ onModeChange: this.createOnModeChange(sessionId),
3357
+ onProcessSpawned: this.options?.onProcessSpawned,
3358
+ onProcessExited: this.options?.onProcessExited
3359
+ });
3534
3360
  const input = new Pushable();
3535
- const q = tc.timeSync("sdkQuery", () => query({ prompt: input, options }));
3361
+ const q = query({ prompt: input, options });
3536
3362
  const session = this.createSession(
3537
3363
  sessionId,
3538
3364
  q,
@@ -3544,27 +3370,17 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3544
3370
  session.taskRunId = meta?.taskRunId;
3545
3371
  this.registerPersistence(sessionId, meta);
3546
3372
  if (meta?.taskRunId) {
3547
- await tc.time(
3548
- "extNotification",
3549
- () => this.client.extNotification("_posthog/sdk_session", {
3550
- taskRunId: meta.taskRunId,
3551
- sessionId,
3552
- adapter: "claude"
3553
- })
3554
- );
3373
+ await this.client.extNotification("_posthog/sdk_session", {
3374
+ taskRunId: meta.taskRunId,
3375
+ sessionId,
3376
+ adapter: "claude"
3377
+ });
3555
3378
  }
3556
- const modelOptions = await tc.time(
3557
- "fetchModels",
3558
- () => this.getModelConfigOptions()
3559
- );
3560
- this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
3379
+ const modelOptions = await this.getModelConfigOptions();
3380
+ this.deferBackgroundFetches(q, sessionId, mcpServers);
3561
3381
  session.modelId = modelOptions.currentModelId;
3562
3382
  await this.trySetModel(q, modelOptions.currentModelId);
3563
- const configOptions = await tc.time(
3564
- "buildConfigOptions",
3565
- () => this.buildConfigOptions(modelOptions)
3566
- );
3567
- tc.summarize("newSession");
3383
+ const configOptions = await this.buildConfigOptions(modelOptions);
3568
3384
  return {
3569
3385
  sessionId,
3570
3386
  configOptions
@@ -3574,10 +3390,6 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3574
3390
  return this.resumeSession(params);
3575
3391
  }
3576
3392
  async resumeSession(params) {
3577
- const tc = createTimingCollector(
3578
- this.debug,
3579
- (msg, data) => this.logger.info(msg, data)
3580
- );
3581
3393
  const meta = params._meta;
3582
3394
  const sessionId = meta?.sessionId;
3583
3395
  if (!sessionId) {
@@ -3586,32 +3398,22 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3586
3398
  if (this.sessionId === sessionId) {
3587
3399
  return {};
3588
3400
  }
3589
- const mcpServers = tc.timeSync(
3590
- "parseMcpServers",
3591
- () => parseMcpServers(params)
3592
- );
3401
+ const mcpServers = parseMcpServers(params);
3593
3402
  const permissionMode = meta?.permissionMode && TWIG_EXECUTION_MODES.includes(meta.permissionMode) ? meta.permissionMode : "default";
3594
- const { query: q, session } = await tc.time(
3595
- "initializeQuery",
3596
- () => this.initializeQuery({
3597
- cwd: params.cwd,
3598
- permissionMode,
3599
- mcpServers,
3600
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3601
- userProvidedOptions: meta?.claudeCode?.options,
3602
- sessionId,
3603
- isResume: true,
3604
- additionalDirectories: meta?.claudeCode?.options?.additionalDirectories
3605
- })
3606
- );
3403
+ const { query: q, session } = await this.initializeQuery({
3404
+ cwd: params.cwd,
3405
+ permissionMode,
3406
+ mcpServers,
3407
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3408
+ userProvidedOptions: meta?.claudeCode?.options,
3409
+ sessionId,
3410
+ isResume: true,
3411
+ additionalDirectories: meta?.claudeCode?.options?.additionalDirectories
3412
+ });
3607
3413
  session.taskRunId = meta?.taskRunId;
3608
3414
  this.registerPersistence(sessionId, meta);
3609
- this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
3610
- const configOptions = await tc.time(
3611
- "buildConfigOptions",
3612
- () => this.buildConfigOptions()
3613
- );
3614
- tc.summarize("resumeSession");
3415
+ this.deferBackgroundFetches(q, sessionId, mcpServers);
3416
+ const configOptions = await this.buildConfigOptions();
3615
3417
  return { configOptions };
3616
3418
  }
3617
3419
  async prompt(params) {
@@ -3799,13 +3601,10 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3799
3601
  * Fire-and-forget: fetch slash commands and MCP tool metadata in parallel.
3800
3602
  * Both populate caches used later — neither is needed to return configOptions.
3801
3603
  */
3802
- deferBackgroundFetches(tc, q, sessionId, mcpServers) {
3604
+ deferBackgroundFetches(q, sessionId, mcpServers) {
3803
3605
  Promise.all([
3804
- tc.time("slashCommands", () => getAvailableSlashCommands(q)),
3805
- tc.time(
3806
- "mcpMetadata",
3807
- () => fetchMcpToolMetadata(mcpServers, this.logger)
3808
- )
3606
+ getAvailableSlashCommands(q),
3607
+ fetchMcpToolMetadata(mcpServers, this.logger)
3809
3608
  ]).then(([slashCommands]) => {
3810
3609
  this.sendAvailableCommandsUpdate(sessionId, slashCommands);
3811
3610
  }).catch((err) => {
@@ -4099,10 +3898,7 @@ function createClaudeConnection(config) {
4099
3898
  const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
4100
3899
  let agent = null;
4101
3900
  const agentConnection = new AgentSideConnection((client) => {
4102
- agent = new ClaudeAcpAgent(client, logWriter, {
4103
- ...config.processCallbacks,
4104
- debug: config.debug
4105
- });
3901
+ agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
4106
3902
  logger.info(`Created ${agent.adapterName} agent`);
4107
3903
  return agent;
4108
3904
  }, agentStream);
@@ -4517,6 +4313,8 @@ var PostHogAPIClient = class {
4517
4313
  };
4518
4314
 
4519
4315
  // src/session-log-writer.ts
4316
+ import fs3 from "fs";
4317
+ import path4 from "path";
4520
4318
  var SessionLogWriter = class _SessionLogWriter {
4521
4319
  static FLUSH_DEBOUNCE_MS = 500;
4522
4320
  static FLUSH_MAX_INTERVAL_MS = 5e3;
@@ -4530,8 +4328,10 @@ var SessionLogWriter = class _SessionLogWriter {
4530
4328
  sessions = /* @__PURE__ */ new Map();
4531
4329
  messageCounts = /* @__PURE__ */ new Map();
4532
4330
  logger;
4331
+ localCachePath;
4533
4332
  constructor(options = {}) {
4534
4333
  this.posthogAPI = options.posthogAPI;
4334
+ this.localCachePath = options.localCachePath;
4535
4335
  this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
4536
4336
  }
4537
4337
  async flushAll() {
@@ -4561,6 +4361,21 @@ var SessionLogWriter = class _SessionLogWriter {
4561
4361
  });
4562
4362
  this.sessions.set(sessionId, { context });
4563
4363
  this.lastFlushAttemptTime.set(sessionId, Date.now());
4364
+ if (this.localCachePath) {
4365
+ const sessionDir = path4.join(
4366
+ this.localCachePath,
4367
+ "sessions",
4368
+ context.runId
4369
+ );
4370
+ try {
4371
+ fs3.mkdirSync(sessionDir, { recursive: true });
4372
+ } catch (error) {
4373
+ this.logger.warn("Failed to create local cache directory", {
4374
+ sessionDir,
4375
+ error
4376
+ });
4377
+ }
4378
+ }
4564
4379
  }
4565
4380
  isRegistered(sessionId) {
4566
4381
  return this.sessions.has(sessionId);
@@ -4598,6 +4413,7 @@ var SessionLogWriter = class _SessionLogWriter {
4598
4413
  timestamp,
4599
4414
  notification: message
4600
4415
  };
4416
+ this.writeToLocalCache(sessionId, entry);
4601
4417
  if (this.posthogAPI) {
4602
4418
  const pending = this.pendingEntries.get(sessionId) ?? [];
4603
4419
  pending.push(entry);
@@ -4698,6 +4514,7 @@ var SessionLogWriter = class _SessionLogWriter {
4698
4514
  }
4699
4515
  }
4700
4516
  };
4517
+ this.writeToLocalCache(sessionId, entry);
4701
4518
  if (this.posthogAPI) {
4702
4519
  const pending = this.pendingEntries.get(sessionId) ?? [];
4703
4520
  pending.push(entry);
@@ -4725,6 +4542,23 @@ var SessionLogWriter = class _SessionLogWriter {
4725
4542
  const timeout = setTimeout(() => this.flush(sessionId), delay2);
4726
4543
  this.flushTimeouts.set(sessionId, timeout);
4727
4544
  }
4545
+ writeToLocalCache(sessionId, entry) {
4546
+ if (!this.localCachePath) return;
4547
+ const session = this.sessions.get(sessionId);
4548
+ if (!session) return;
4549
+ const logPath = path4.join(
4550
+ this.localCachePath,
4551
+ "sessions",
4552
+ session.context.runId,
4553
+ "logs.ndjson"
4554
+ );
4555
+ try {
4556
+ fs3.appendFileSync(logPath, `${JSON.stringify(entry)}
4557
+ `);
4558
+ } catch (error) {
4559
+ this.logger.warn("Failed to write to local cache", { logPath, error });
4560
+ }
4561
+ }
4728
4562
  };
4729
4563
 
4730
4564
  // src/agent.ts
@@ -4748,7 +4582,8 @@ var Agent = class {
4748
4582
  if (config.posthog && !config.skipLogPersistence) {
4749
4583
  this.sessionLogWriter = new SessionLogWriter({
4750
4584
  posthogAPI: this.posthogAPI,
4751
- logger: this.logger.child("SessionLogWriter")
4585
+ logger: this.logger.child("SessionLogWriter"),
4586
+ localCachePath: config.localCachePath
4752
4587
  });
4753
4588
  }
4754
4589
  }
@@ -4802,7 +4637,6 @@ var Agent = class {
4802
4637
  taskId,
4803
4638
  deviceType: "local",
4804
4639
  logger: this.logger,
4805
- debug: this.debug,
4806
4640
  processCallbacks: options.processCallbacks,
4807
4641
  allowedModelIds,
4808
4642
  codexOptions: options.adapter === "codex" && gatewayConfig ? {
@@ -4903,9 +4737,136 @@ var OtelLogWriter = class {
4903
4737
  }
4904
4738
  };
4905
4739
 
4740
+ // ../shared/dist/index.js
4741
+ var consoleLogger = {
4742
+ info: (_message, _data) => {
4743
+ },
4744
+ debug: (_message, _data) => {
4745
+ },
4746
+ error: (_message, _data) => {
4747
+ },
4748
+ warn: (_message, _data) => {
4749
+ }
4750
+ };
4751
+ var Saga = class {
4752
+ completedSteps = [];
4753
+ currentStepName = "unknown";
4754
+ stepTimings = [];
4755
+ log;
4756
+ constructor(logger) {
4757
+ this.log = logger ?? consoleLogger;
4758
+ }
4759
+ /**
4760
+ * Run the saga with the given input.
4761
+ * Returns a discriminated union result - either success with data or failure with error details.
4762
+ */
4763
+ async run(input) {
4764
+ this.completedSteps = [];
4765
+ this.currentStepName = "unknown";
4766
+ this.stepTimings = [];
4767
+ const sagaStart = performance.now();
4768
+ this.log.info("Starting saga", { sagaName: this.constructor.name });
4769
+ try {
4770
+ const result = await this.execute(input);
4771
+ const totalDuration = performance.now() - sagaStart;
4772
+ this.log.debug("Saga completed successfully", {
4773
+ sagaName: this.constructor.name,
4774
+ stepsCompleted: this.completedSteps.length,
4775
+ totalDurationMs: Math.round(totalDuration),
4776
+ stepTimings: this.stepTimings
4777
+ });
4778
+ return { success: true, data: result };
4779
+ } catch (error) {
4780
+ this.log.error("Saga failed, initiating rollback", {
4781
+ sagaName: this.constructor.name,
4782
+ failedStep: this.currentStepName,
4783
+ error: error instanceof Error ? error.message : String(error)
4784
+ });
4785
+ await this.rollback();
4786
+ return {
4787
+ success: false,
4788
+ error: error instanceof Error ? error.message : String(error),
4789
+ failedStep: this.currentStepName
4790
+ };
4791
+ }
4792
+ }
4793
+ /**
4794
+ * Execute a step with its rollback action.
4795
+ * If the step succeeds, its rollback action is stored for potential rollback.
4796
+ * The step name is automatically tracked for error reporting.
4797
+ *
4798
+ * @param config - Step configuration with name, execute, and rollback functions
4799
+ * @returns The result of the execute function
4800
+ * @throws Re-throws any error from the execute function (triggers rollback)
4801
+ */
4802
+ async step(config) {
4803
+ this.currentStepName = config.name;
4804
+ this.log.debug(`Executing step: ${config.name}`);
4805
+ const stepStart = performance.now();
4806
+ const result = await config.execute();
4807
+ const durationMs = Math.round(performance.now() - stepStart);
4808
+ this.stepTimings.push({ name: config.name, durationMs });
4809
+ this.log.debug(`Step completed: ${config.name}`, { durationMs });
4810
+ this.completedSteps.push({
4811
+ name: config.name,
4812
+ rollback: () => config.rollback(result)
4813
+ });
4814
+ return result;
4815
+ }
4816
+ /**
4817
+ * Execute a step that doesn't need rollback.
4818
+ * Useful for read-only operations or operations that are idempotent.
4819
+ * The step name is automatically tracked for error reporting.
4820
+ *
4821
+ * @param name - Step name for logging and error tracking
4822
+ * @param execute - The action to execute
4823
+ * @returns The result of the execute function
4824
+ */
4825
+ async readOnlyStep(name, execute) {
4826
+ this.currentStepName = name;
4827
+ this.log.debug(`Executing read-only step: ${name}`);
4828
+ const stepStart = performance.now();
4829
+ const result = await execute();
4830
+ const durationMs = Math.round(performance.now() - stepStart);
4831
+ this.stepTimings.push({ name, durationMs });
4832
+ this.log.debug(`Read-only step completed: ${name}`, { durationMs });
4833
+ return result;
4834
+ }
4835
+ /**
4836
+ * Roll back all completed steps in reverse order.
4837
+ * Rollback errors are logged but don't stop the rollback of other steps.
4838
+ */
4839
+ async rollback() {
4840
+ this.log.info("Rolling back saga", {
4841
+ stepsToRollback: this.completedSteps.length
4842
+ });
4843
+ const stepsReversed = [...this.completedSteps].reverse();
4844
+ for (const step of stepsReversed) {
4845
+ try {
4846
+ this.log.debug(`Rolling back step: ${step.name}`);
4847
+ await step.rollback();
4848
+ this.log.debug(`Step rolled back: ${step.name}`);
4849
+ } catch (error) {
4850
+ this.log.error(`Failed to rollback step: ${step.name}`, {
4851
+ error: error instanceof Error ? error.message : String(error)
4852
+ });
4853
+ }
4854
+ }
4855
+ this.log.info("Rollback completed", {
4856
+ stepsAttempted: this.completedSteps.length
4857
+ });
4858
+ }
4859
+ /**
4860
+ * Get the number of completed steps (useful for testing)
4861
+ */
4862
+ getCompletedStepCount() {
4863
+ return this.completedSteps.length;
4864
+ }
4865
+ };
4866
+
4906
4867
  // ../git/dist/queries.js
4907
- import * as fs4 from "fs/promises";
4908
- import * as path5 from "path";
4868
+ import * as fs5 from "fs/promises";
4869
+ import * as path6 from "path";
4909
4870
 
4910
4871
  // ../../node_modules/simple-git/dist/esm/index.js
4911
4872
  var import_file_exists = __toESM(require_dist(), 1);
@@ -4944,8 +4905,8 @@ function pathspec(...paths) {
4944
4905
  cache.set(key, paths);
4945
4906
  return key;
4946
4907
  }
4947
- function isPathSpec(path7) {
4948
- return path7 instanceof String && cache.has(path7);
4908
+ function isPathSpec(path8) {
4909
+ return path8 instanceof String && cache.has(path8);
4949
4910
  }
4950
4911
  function toPaths(pathSpec) {
4951
4912
  return cache.get(pathSpec) || [];
@@ -5034,8 +4995,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
5034
4995
  function forEachLineWithContent(input, callback) {
5035
4996
  return toLinesWithContent(input, true).map((line) => callback(line));
5036
4997
  }
5037
- function folderExists(path7) {
5038
- return (0, import_file_exists.exists)(path7, import_file_exists.FOLDER);
4998
+ function folderExists(path8) {
4999
+ return (0, import_file_exists.exists)(path8, import_file_exists.FOLDER);
5039
5000
  }
5040
5001
  function append(target, item) {
5041
5002
  if (Array.isArray(target)) {
@@ -5439,8 +5400,8 @@ function checkIsRepoRootTask() {
5439
5400
  commands,
5440
5401
  format: "utf-8",
5441
5402
  onError,
5442
- parser(path7) {
5443
- return /^\.(git)?$/.test(path7.trim());
5403
+ parser(path8) {
5404
+ return /^\.(git)?$/.test(path8.trim());
5444
5405
  }
5445
5406
  };
5446
5407
  }
@@ -5874,11 +5835,11 @@ function parseGrep(grep) {
5874
5835
  const paths = /* @__PURE__ */ new Set();
5875
5836
  const results = {};
5876
5837
  forEachLineWithContent(grep, (input) => {
5877
- const [path7, line, preview] = input.split(NULL);
5878
- paths.add(path7);
5879
- (results[path7] = results[path7] || []).push({
5838
+ const [path8, line, preview] = input.split(NULL);
5839
+ paths.add(path8);
5840
+ (results[path8] = results[path8] || []).push({
5880
5841
  line: asNumber(line),
5881
- path: path7,
5842
+ path: path8,
5882
5843
  preview
5883
5844
  });
5884
5845
  });
@@ -6643,14 +6604,14 @@ var init_hash_object = __esm({
6643
6604
  init_task();
6644
6605
  }
6645
6606
  });
6646
- function parseInit(bare, path7, text2) {
6607
+ function parseInit(bare, path8, text2) {
6647
6608
  const response = String(text2).trim();
6648
6609
  let result;
6649
6610
  if (result = initResponseRegex.exec(response)) {
6650
- return new InitSummary(bare, path7, false, result[1]);
6611
+ return new InitSummary(bare, path8, false, result[1]);
6651
6612
  }
6652
6613
  if (result = reInitResponseRegex.exec(response)) {
6653
- return new InitSummary(bare, path7, true, result[1]);
6614
+ return new InitSummary(bare, path8, true, result[1]);
6654
6615
  }
6655
6616
  let gitDir = "";
6656
6617
  const tokens = response.split(" ");
@@ -6661,7 +6622,7 @@ function parseInit(bare, path7, text2) {
6661
6622
  break;
6662
6623
  }
6663
6624
  }
6664
- return new InitSummary(bare, path7, /^re/i.test(response), gitDir);
6625
+ return new InitSummary(bare, path8, /^re/i.test(response), gitDir);
6665
6626
  }
6666
6627
  var InitSummary;
6667
6628
  var initResponseRegex;
@@ -6670,9 +6631,9 @@ var init_InitSummary = __esm({
6670
6631
  "src/lib/responses/InitSummary.ts"() {
6671
6632
  "use strict";
6672
6633
  InitSummary = class {
6673
- constructor(bare, path7, existing, gitDir) {
6634
+ constructor(bare, path8, existing, gitDir) {
6674
6635
  this.bare = bare;
6675
- this.path = path7;
6636
+ this.path = path8;
6676
6637
  this.existing = existing;
6677
6638
  this.gitDir = gitDir;
6678
6639
  }
@@ -6684,7 +6645,7 @@ var init_InitSummary = __esm({
6684
6645
  function hasBareCommand(command) {
6685
6646
  return command.includes(bareCommand);
6686
6647
  }
6687
- function initTask(bare = false, path7, customArgs) {
6648
+ function initTask(bare = false, path8, customArgs) {
6688
6649
  const commands = ["init", ...customArgs];
6689
6650
  if (bare && !hasBareCommand(commands)) {
6690
6651
  commands.splice(1, 0, bareCommand);
@@ -6693,7 +6654,7 @@ function initTask(bare = false, path7, customArgs) {
6693
6654
  commands,
6694
6655
  format: "utf-8",
6695
6656
  parser(text2) {
6696
- return parseInit(commands.includes("--bare"), path7, text2);
6657
+ return parseInit(commands.includes("--bare"), path8, text2);
6697
6658
  }
6698
6659
  };
6699
6660
  }
@@ -7509,12 +7470,12 @@ var init_FileStatusSummary = __esm({
7509
7470
  "use strict";
7510
7471
  fromPathRegex = /^(.+)\0(.+)$/;
7511
7472
  FileStatusSummary = class {
7512
- constructor(path7, index, working_dir) {
7513
- this.path = path7;
7473
+ constructor(path8, index, working_dir) {
7474
+ this.path = path8;
7514
7475
  this.index = index;
7515
7476
  this.working_dir = working_dir;
7516
7477
  if (index === "R" || working_dir === "R") {
7517
- const detail = fromPathRegex.exec(path7) || [null, path7, path7];
7478
+ const detail = fromPathRegex.exec(path8) || [null, path8, path8];
7518
7479
  this.from = detail[2] || "";
7519
7480
  this.path = detail[1] || "";
7520
7481
  }
@@ -7545,14 +7506,14 @@ function splitLine(result, lineStr) {
7545
7506
  default:
7546
7507
  return;
7547
7508
  }
7548
- function data(index, workingDir, path7) {
7509
+ function data(index, workingDir, path8) {
7549
7510
  const raw = `${index}${workingDir}`;
7550
7511
  const handler = parsers6.get(raw);
7551
7512
  if (handler) {
7552
- handler(result, path7);
7513
+ handler(result, path8);
7553
7514
  }
7554
7515
  if (raw !== "##" && raw !== "!!") {
7555
- result.files.push(new FileStatusSummary(path7, index, workingDir));
7516
+ result.files.push(new FileStatusSummary(path8, index, workingDir));
7556
7517
  }
7557
7518
  }
7558
7519
  }
@@ -7865,9 +7826,9 @@ var init_simple_git_api = __esm({
7865
7826
  next
7866
7827
  );
7867
7828
  }
7868
- hashObject(path7, write) {
7829
+ hashObject(path8, write) {
7869
7830
  return this._runTask(
7870
- hashObjectTask(path7, write === true),
7831
+ hashObjectTask(path8, write === true),
7871
7832
  trailingFunctionArgument(arguments)
7872
7833
  );
7873
7834
  }
@@ -8220,8 +8181,8 @@ var init_branch = __esm({
8220
8181
  }
8221
8182
  });
8222
8183
  function toPath(input) {
8223
- const path7 = input.trim().replace(/^["']|["']$/g, "");
8224
- return path7 && normalize(path7);
8184
+ const path8 = input.trim().replace(/^["']|["']$/g, "");
8185
+ return path8 && normalize(path8);
8225
8186
  }
8226
8187
  var parseCheckIgnore;
8227
8188
  var init_CheckIgnore = __esm({
@@ -8535,8 +8496,8 @@ __export(sub_module_exports, {
8535
8496
  subModuleTask: () => subModuleTask,
8536
8497
  updateSubModuleTask: () => updateSubModuleTask
8537
8498
  });
8538
- function addSubModuleTask(repo, path7) {
8539
- return subModuleTask(["add", repo, path7]);
8499
+ function addSubModuleTask(repo, path8) {
8500
+ return subModuleTask(["add", repo, path8]);
8540
8501
  }
8541
8502
  function initSubModuleTask(customArgs) {
8542
8503
  return subModuleTask(["init", ...customArgs]);
@@ -8866,8 +8827,8 @@ var require_git = __commonJS2({
8866
8827
  }
8867
8828
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
8868
8829
  };
8869
- Git2.prototype.submoduleAdd = function(repo, path7, then) {
8870
- return this._runTask(addSubModuleTask2(repo, path7), trailingFunctionArgument2(arguments));
8830
+ Git2.prototype.submoduleAdd = function(repo, path8, then) {
8831
+ return this._runTask(addSubModuleTask2(repo, path8), trailingFunctionArgument2(arguments));
8871
8832
  };
8872
8833
  Git2.prototype.submoduleUpdate = function(args, then) {
8873
8834
  return this._runTask(
@@ -9468,22 +9429,22 @@ function createGitClient(baseDir, options) {
9468
9429
 
9469
9430
  // ../git/dist/lock-detector.js
9470
9431
  import { execFile } from "child_process";
9471
- import fs3 from "fs/promises";
9472
- import path4 from "path";
9432
+ import fs4 from "fs/promises";
9433
+ import path5 from "path";
9473
9434
  import { promisify } from "util";
9474
9435
  var execFileAsync = promisify(execFile);
9475
9436
  async function getIndexLockPath(repoPath) {
9476
9437
  try {
9477
9438
  const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "index.lock"], { cwd: repoPath });
9478
- return path4.resolve(repoPath, stdout.trim());
9439
+ return path5.resolve(repoPath, stdout.trim());
9479
9440
  } catch {
9480
- return path4.join(repoPath, ".git", "index.lock");
9441
+ return path5.join(repoPath, ".git", "index.lock");
9481
9442
  }
9482
9443
  }
9483
9444
  async function getLockInfo(repoPath) {
9484
9445
  const lockPath = await getIndexLockPath(repoPath);
9485
9446
  try {
9486
- const stat = await fs3.stat(lockPath);
9447
+ const stat = await fs4.stat(lockPath);
9487
9448
  return {
9488
9449
  path: lockPath,
9489
9450
  ageMs: Date.now() - stat.mtimeMs
@@ -9494,7 +9455,7 @@ async function getLockInfo(repoPath) {
9494
9455
  }
9495
9456
  async function removeLock(repoPath) {
9496
9457
  const lockPath = await getIndexLockPath(repoPath);
9497
- await fs3.rm(lockPath, { force: true });
9458
+ await fs4.rm(lockPath, { force: true });
9498
9459
  }
9499
9460
  async function isLocked(repoPath) {
9500
9461
  return await getLockInfo(repoPath) !== null;
@@ -9674,8 +9635,8 @@ import { join as join5 } from "path";
9674
9635
 
9675
9636
  // ../git/dist/sagas/tree.js
9676
9637
  import { existsSync as existsSync4 } from "fs";
9677
- import * as fs5 from "fs/promises";
9678
- import * as path6 from "path";
9638
+ import * as fs6 from "fs/promises";
9639
+ import * as path7 from "path";
9679
9640
  import * as tar from "tar";
9680
9641
 
9681
9642
  // ../git/dist/git-saga.js
@@ -9701,14 +9662,14 @@ var CaptureTreeSaga = class extends GitSaga {
9701
9662
  tempIndexPath = null;
9702
9663
  async executeGitOperations(input) {
9703
9664
  const { baseDir, lastTreeHash, archivePath, signal } = input;
9704
- const tmpDir = path6.join(baseDir, ".git", "twig-tmp");
9665
+ const tmpDir = path7.join(baseDir, ".git", "twig-tmp");
9705
9666
  await this.step({
9706
9667
  name: "create_tmp_dir",
9707
- execute: () => fs5.mkdir(tmpDir, { recursive: true }),
9668
+ execute: () => fs6.mkdir(tmpDir, { recursive: true }),
9708
9669
  rollback: async () => {
9709
9670
  }
9710
9671
  });
9711
- this.tempIndexPath = path6.join(tmpDir, `index-${Date.now()}`);
9672
+ this.tempIndexPath = path7.join(tmpDir, `index-${Date.now()}`);
9712
9673
  const tempIndexGit = this.git.env({
9713
9674
  ...process.env,
9714
9675
  GIT_INDEX_FILE: this.tempIndexPath
@@ -9718,7 +9679,7 @@ var CaptureTreeSaga = class extends GitSaga {
9718
9679
  execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
9719
9680
  rollback: async () => {
9720
9681
  if (this.tempIndexPath) {
9721
- await fs5.rm(this.tempIndexPath, { force: true }).catch(() => {
9682
+ await fs6.rm(this.tempIndexPath, { force: true }).catch(() => {
9722
9683
  });
9723
9684
  }
9724
9685
  }
@@ -9727,7 +9688,7 @@ var CaptureTreeSaga = class extends GitSaga {
9727
9688
  const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
9728
9689
  if (lastTreeHash && treeHash === lastTreeHash) {
9729
9690
  this.log.debug("No changes since last capture", { treeHash });
9730
- await fs5.rm(this.tempIndexPath, { force: true }).catch(() => {
9691
+ await fs6.rm(this.tempIndexPath, { force: true }).catch(() => {
9731
9692
  });
9732
9693
  return { snapshot: null, changed: false };
9733
9694
  }
@@ -9739,7 +9700,7 @@ var CaptureTreeSaga = class extends GitSaga {
9739
9700
  }
9740
9701
  });
9741
9702
  const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
9742
- await fs5.rm(this.tempIndexPath, { force: true }).catch(() => {
9703
+ await fs6.rm(this.tempIndexPath, { force: true }).catch(() => {
9743
9704
  });
9744
9705
  const snapshot = {
9745
9706
  treeHash,
@@ -9763,15 +9724,15 @@ var CaptureTreeSaga = class extends GitSaga {
9763
9724
  if (filesToArchive.length === 0) {
9764
9725
  return void 0;
9765
9726
  }
9766
- const existingFiles = filesToArchive.filter((f) => existsSync4(path6.join(baseDir, f)));
9727
+ const existingFiles = filesToArchive.filter((f) => existsSync4(path7.join(baseDir, f)));
9767
9728
  if (existingFiles.length === 0) {
9768
9729
  return void 0;
9769
9730
  }
9770
9731
  await this.step({
9771
9732
  name: "create_archive",
9772
9733
  execute: async () => {
9773
- const archiveDir = path6.dirname(archivePath);
9774
- await fs5.mkdir(archiveDir, { recursive: true });
9734
+ const archiveDir = path7.dirname(archivePath);
9735
+ await fs6.mkdir(archiveDir, { recursive: true });
9775
9736
  await tar.create({
9776
9737
  gzip: true,
9777
9738
  file: archivePath,
@@ -9779,7 +9740,7 @@ var CaptureTreeSaga = class extends GitSaga {
9779
9740
  }, existingFiles);
9780
9741
  },
9781
9742
  rollback: async () => {
9782
- await fs5.rm(archivePath, { force: true }).catch(() => {
9743
+ await fs6.rm(archivePath, { force: true }).catch(() => {
9783
9744
  });
9784
9745
  }
9785
9746
  });
@@ -9878,9 +9839,9 @@ var ApplyTreeSaga = class extends GitSaga {
9878
9839
  const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
9879
9840
  await this.readOnlyStep("backup_existing_files", async () => {
9880
9841
  for (const filePath of filesToExtract) {
9881
- const fullPath = path6.join(baseDir, filePath);
9842
+ const fullPath = path7.join(baseDir, filePath);
9882
9843
  try {
9883
- const content = await fs5.readFile(fullPath);
9844
+ const content = await fs6.readFile(fullPath);
9884
9845
  this.fileBackups.set(filePath, content);
9885
9846
  } catch {
9886
9847
  }
@@ -9897,16 +9858,16 @@ var ApplyTreeSaga = class extends GitSaga {
9897
9858
  },
9898
9859
  rollback: async () => {
9899
9860
  for (const filePath of this.extractedFiles) {
9900
- const fullPath = path6.join(baseDir, filePath);
9861
+ const fullPath = path7.join(baseDir, filePath);
9901
9862
  const backup = this.fileBackups.get(filePath);
9902
9863
  if (backup) {
9903
- const dir = path6.dirname(fullPath);
9904
- await fs5.mkdir(dir, { recursive: true }).catch(() => {
9864
+ const dir = path7.dirname(fullPath);
9865
+ await fs6.mkdir(dir, { recursive: true }).catch(() => {
9905
9866
  });
9906
- await fs5.writeFile(fullPath, backup).catch(() => {
9867
+ await fs6.writeFile(fullPath, backup).catch(() => {
9907
9868
  });
9908
9869
  } else {
9909
- await fs5.rm(fullPath, { force: true }).catch(() => {
9870
+ await fs6.rm(fullPath, { force: true }).catch(() => {
9910
9871
  });
9911
9872
  }
9912
9873
  }
@@ -9914,10 +9875,10 @@ var ApplyTreeSaga = class extends GitSaga {
9914
9875
  });
9915
9876
  }
9916
9877
  for (const change of changes.filter((c) => c.status === "D")) {
9917
- const fullPath = path6.join(baseDir, change.path);
9878
+ const fullPath = path7.join(baseDir, change.path);
9918
9879
  const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
9919
9880
  try {
9920
- return await fs5.readFile(fullPath);
9881
+ return await fs6.readFile(fullPath);
9921
9882
  } catch {
9922
9883
  return null;
9923
9884
  }
@@ -9925,15 +9886,15 @@ var ApplyTreeSaga = class extends GitSaga {
9925
9886
  await this.step({
9926
9887
  name: `delete_${change.path}`,
9927
9888
  execute: async () => {
9928
- await fs5.rm(fullPath, { force: true });
9889
+ await fs6.rm(fullPath, { force: true });
9929
9890
  this.log.debug(`Deleted file: ${change.path}`);
9930
9891
  },
9931
9892
  rollback: async () => {
9932
9893
  if (backupContent) {
9933
- const dir = path6.dirname(fullPath);
9934
- await fs5.mkdir(dir, { recursive: true }).catch(() => {
9894
+ const dir = path7.dirname(fullPath);
9895
+ await fs6.mkdir(dir, { recursive: true }).catch(() => {
9935
9896
  });
9936
- await fs5.writeFile(fullPath, backupContent).catch(() => {
9897
+ await fs6.writeFile(fullPath, backupContent).catch(() => {
9937
9898
  });
9938
9899
  }
9939
9900
  }