@pstdio/sdk 0.2.1 → 0.3.0

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.
@@ -7,6 +7,7 @@ export { followupSession } from "./followup-session";
7
7
  export { getAttemptsForTicket } from "./get-attempts-for-ticket";
8
8
  export { removeAllWorktreesForTicket } from "./remove-all-worktrees-for-ticket";
9
9
  export { runCommand } from "./run-command";
10
+ export { type SaveTicketInput, type SaveTicketResult, saveTicket } from "./save-ticket";
10
11
  export { setTicketStatus } from "./set-ticket-status";
11
12
  export { setWorkspaceAttemptStatus } from "./set-workspace-attempt-status";
12
13
  export { type PullTicketsInput, type PullTicketsResult, pullTickets } from "./ticket-pull";
@@ -0,0 +1,14 @@
1
+ import type { PluginHelperContext } from "./context";
2
+ type SaveTicketInput = {
3
+ rootPath: string;
4
+ ticketId?: string;
5
+ status?: string;
6
+ tags?: string[];
7
+ log?: (message: string) => void;
8
+ };
9
+ type SaveTicketResult = {
10
+ ticketShorthand: string;
11
+ uploadedFileCount: number;
12
+ };
13
+ export declare const saveTicket: (ctx: PluginHelperContext, input: SaveTicketInput) => Promise<SaveTicketResult>;
14
+ export type { SaveTicketInput, SaveTicketResult };
@@ -1,5 +1,5 @@
1
1
  export { renderPrompt } from "../prompts";
2
2
  export { definePlugin } from "./define-plugin";
3
- export { bootstrapWorktree, createAttempt, createSession, createWorkspace, findTicketByRef, findWorkspaceByRef, followupSession, getAttemptsForTicket, type PullTicketsInput, type PullTicketsResult, pullTickets, removeAllWorktreesForTicket, runCommand, setTicketStatus, setWorkspaceAttemptStatus, updateTicketWhenAllAttemptsMatch, workspacesForTicket, } from "./helpers";
3
+ export { bootstrapWorktree, createAttempt, createSession, createWorkspace, findTicketByRef, findWorkspaceByRef, followupSession, getAttemptsForTicket, type PullTicketsInput, type PullTicketsResult, pullTickets, removeAllWorktreesForTicket, runCommand, type SaveTicketInput, type SaveTicketResult, saveTicket, setTicketStatus, setWorkspaceAttemptStatus, updateTicketWhenAllAttemptsMatch, workspacesForTicket, } from "./helpers";
4
4
  export type { HookResponse, PluginHooks, PostHookReturn, PostPluginHooks, PreHookReturn, PrePluginHooks, } from "./hooks";
5
- export type { ActionDefinition, ActionDescriptor, ActionInput, ActionParamDef, ActionParamValue, ActionPlacement, ActionTargetMap, ActionTriggerContext, ActionTriggerResult, AgentActionParam, AgentParamValue, LongTextActionParam, PluginDefinition, RepoActionParam, RepoParamValue, SelectActionParam, TargetType, TemplateSelectActionParam, TextActionParam, } from "./types";
5
+ export type { ActionDefinition, ActionDescriptor, ActionInput, ActionParamDef, ActionParamValue, ActionPlacement, ActionTargetMap, ActionTriggerContext, ActionTriggerResult, AgentActionParam, AgentParamValue, LongTextActionParam, PluginDefinition, RepoActionParam, RepoParamValue, ScheduleDefinition, ScheduledTriggerContext, SelectActionParam, TargetType, TemplateSelectActionParam, TextActionParam, } from "./types";
@@ -9,8 +9,16 @@ var assertActionTriggers = (plugin) => {
9
9
  }
10
10
  }
11
11
  };
12
+ var assertScheduleHandlers = (plugin) => {
13
+ for (const schedule of plugin.schedules ?? []) {
14
+ if (typeof schedule.handler !== "function") {
15
+ throw new Error(`Schedule "${schedule.name}" is missing handler(ctx)`);
16
+ }
17
+ }
18
+ };
12
19
  var definePlugin = (plugin) => {
13
20
  assertActionTriggers(plugin);
21
+ assertScheduleHandlers(plugin);
14
22
  return plugin;
15
23
  };
16
24
  // src/plugins/helpers/context.ts
@@ -177,7 +185,7 @@ var runCommand = async (cwd, command, options = {}) => {
177
185
  try {
178
186
  proc = Bun.spawn([cmd, ...args], {
179
187
  cwd,
180
- env: options.env,
188
+ env: options.env ?? { ...process.env },
181
189
  stdin: "ignore",
182
190
  stdout: stdio,
183
191
  stderr: stdio
@@ -194,6 +202,251 @@ var runCommand = async (cwd, command, options = {}) => {
194
202
  stderr: stderr.trim()
195
203
  };
196
204
  };
205
+ // src/plugins/helpers/save-ticket.ts
206
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
207
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
208
+ var TICKETS_DIR = join(".pstdio", "tickets");
209
+ var TICKET_FILES_DIR = "files";
210
+ var TICKET_ARTIFACTS_DIR = "artifacts";
211
+ var MAX_DISPLAY_TITLE_LENGTH = 50;
212
+ var ACTIONABLE_FRONTMATTER_KEYS = ["blocked_reason", "parent_id", "status"];
213
+ var resolveTicketDir = (rootPath, shorthand) => {
214
+ const exactDir = join(rootPath, TICKETS_DIR, shorthand);
215
+ if (!existsSync(exactDir))
216
+ return null;
217
+ if (!statSync(exactDir).isDirectory()) {
218
+ throw new Error(`Invalid ticket path for ${shorthand}: .pstdio/tickets/${shorthand} is not a directory.`);
219
+ }
220
+ return exactDir;
221
+ };
222
+ var readTicketFile = (rootPath, shorthand) => {
223
+ const dir = resolveTicketDir(rootPath, shorthand);
224
+ if (!dir)
225
+ return null;
226
+ const filePath = join(dir, "ticket.md");
227
+ if (!existsSync(filePath))
228
+ return null;
229
+ return readFileSync(filePath, "utf8");
230
+ };
231
+ var writeTicketFile = (rootPath, shorthand, content) => {
232
+ const dir = resolveTicketDir(rootPath, shorthand) ?? join(rootPath, TICKETS_DIR, shorthand);
233
+ mkdirSync(dir, { recursive: true });
234
+ writeFileSync(join(dir, "ticket.md"), content);
235
+ };
236
+ var walkFiles = (baseDir, currentDir, files) => {
237
+ const entries = readdirSync(currentDir, { withFileTypes: true });
238
+ for (const entry of entries) {
239
+ const fullPath = join(currentDir, entry.name);
240
+ if (entry.isDirectory()) {
241
+ walkFiles(baseDir, fullPath, files);
242
+ continue;
243
+ }
244
+ if (!entry.isFile())
245
+ continue;
246
+ files.push(relative(baseDir, fullPath).split("\\").join("/"));
247
+ }
248
+ };
249
+ var listDirFiles = (rootPath, shorthand, subDir) => {
250
+ const dir = resolveTicketDir(rootPath, shorthand);
251
+ if (!dir)
252
+ return [];
253
+ const baseDir = join(dir, subDir);
254
+ if (!existsSync(baseDir))
255
+ return [];
256
+ const files = [];
257
+ walkFiles(baseDir, baseDir, files);
258
+ files.sort();
259
+ return files;
260
+ };
261
+ var readSafe = (rootPath, shorthand, subDir, requestedPath) => {
262
+ const ticketDir = resolveTicketDir(rootPath, shorthand);
263
+ if (!ticketDir)
264
+ throw new Error(`Ticket directory not found: ${shorthand}`);
265
+ const baseDir = join(ticketDir, subDir);
266
+ const target = resolve(baseDir, requestedPath);
267
+ const rel = relative(baseDir, target);
268
+ if (isAbsolute(rel) || rel.startsWith("..")) {
269
+ throw new Error(`Path resolves outside ticket ${subDir} directory: ${requestedPath}`);
270
+ }
271
+ return readFileSync(target);
272
+ };
273
+ var findFrontmatterClosingIndex = (content) => {
274
+ if (!content.startsWith("---"))
275
+ return -1;
276
+ return content.indexOf("---", 3);
277
+ };
278
+ var stripFrontmatter = (content) => {
279
+ const closing = findFrontmatterClosingIndex(content);
280
+ if (closing === -1)
281
+ return content;
282
+ return content.slice(closing + 3);
283
+ };
284
+ var parseFrontmatter = (content) => {
285
+ const closing = findFrontmatterClosingIndex(content);
286
+ if (closing === -1)
287
+ return {};
288
+ const block = content.slice(3, closing).trim();
289
+ const result = {};
290
+ for (const line of block.split(`
291
+ `)) {
292
+ const colonIndex = line.indexOf(":");
293
+ if (colonIndex === -1)
294
+ continue;
295
+ const key = line.slice(0, colonIndex).trim();
296
+ const raw = line.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, "");
297
+ if (!raw)
298
+ continue;
299
+ if (ACTIONABLE_FRONTMATTER_KEYS.includes(key)) {
300
+ result[key] = raw;
301
+ }
302
+ }
303
+ return result;
304
+ };
305
+ var frontmatterLines = (content) => {
306
+ const closing = findFrontmatterClosingIndex(content);
307
+ if (closing === -1)
308
+ return [];
309
+ const block = content.slice(3, closing).trim();
310
+ if (!block)
311
+ return [];
312
+ return block.split(`
313
+ `);
314
+ };
315
+ var frontmatterKey = (line) => {
316
+ const colon = line.indexOf(":");
317
+ if (colon === -1)
318
+ return null;
319
+ return line.slice(0, colon).trim();
320
+ };
321
+ var applyFrontmatter = (frontmatter, content) => {
322
+ const body = stripFrontmatter(content).replace(/^\n+/, "");
323
+ if (!body)
324
+ return frontmatter;
325
+ return `${frontmatter}
326
+
327
+ ${body}`;
328
+ };
329
+ var applyFrontmatterValues = (frontmatter, content) => {
330
+ if (findFrontmatterClosingIndex(content) === -1)
331
+ return applyFrontmatter(frontmatter, content);
332
+ const overrides = new Map;
333
+ const overrideOrder = [];
334
+ for (const line of frontmatterLines(frontmatter)) {
335
+ const key = frontmatterKey(line);
336
+ if (!key)
337
+ continue;
338
+ overrides.set(key, line);
339
+ overrideOrder.push(key);
340
+ }
341
+ const existing = frontmatterLines(content);
342
+ const merged = existing.map((line) => {
343
+ const key = frontmatterKey(line);
344
+ if (!key || !overrides.has(key))
345
+ return line;
346
+ return overrides.get(key);
347
+ });
348
+ for (const key of overrideOrder) {
349
+ if (existing.some((line2) => frontmatterKey(line2) === key))
350
+ continue;
351
+ const line = overrides.get(key);
352
+ if (line)
353
+ merged.push(line);
354
+ }
355
+ return applyFrontmatter(["---", ...merged, "---"].join(`
356
+ `), content);
357
+ };
358
+ var markLocalTicketAsSaved = (content) => applyFrontmatterValues(["---", "draft: false", "---"].join(`
359
+ `), content);
360
+ var stripMarkdownFormatting = (text) => text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*([^*]*)\*\*/g, "$1").replace(/\*([^*]*)\*/g, "$1").replace(/`([^`]*)`/g, "$1");
361
+ var slugify = (text, maxLength) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
362
+ var firstHeadingOrLine = (content) => {
363
+ const lines = content.split(`
364
+ `);
365
+ for (const line of lines) {
366
+ const trimmed = line.trim();
367
+ if (trimmed.startsWith("# "))
368
+ return trimmed.slice(2).trim();
369
+ }
370
+ for (const line of lines) {
371
+ const trimmed = line.trim();
372
+ if (trimmed.length > 0)
373
+ return trimmed;
374
+ }
375
+ return null;
376
+ };
377
+ var extractDisplayTitle = (content) => {
378
+ const raw = firstHeadingOrLine(stripFrontmatter(content)) ?? "untitled";
379
+ return slugify(stripMarkdownFormatting(raw), MAX_DISPLAY_TITLE_LENGTH);
380
+ };
381
+ var resolveStatusId = async (ctx, statusName) => {
382
+ const statuses = await ctx.client.statuses.list(ctx.projectId);
383
+ const found = statuses.find((status) => status.name === statusName);
384
+ if (!found)
385
+ throw new Error(`Status not found: ${statusName}`);
386
+ return found.id;
387
+ };
388
+ var resolveTagIds = async (ctx, tagNames) => {
389
+ const tags = await ctx.client.tags.list(ctx.projectId);
390
+ const options = tags.flatMap((tag) => tag.options);
391
+ return tagNames.map((name) => {
392
+ const found = options.find((option) => option.name === name);
393
+ if (!found)
394
+ throw new Error(`Tag option not found: ${name}`);
395
+ return found.id;
396
+ });
397
+ };
398
+ var saveTicket = async (ctx, input) => {
399
+ const log = input.log ?? (() => {});
400
+ const ticket = await findTicketByRef(ctx, { ticketId: input.ticketId });
401
+ if (!ticket)
402
+ throw new Error(`Ticket not found: ${input.ticketId ?? "<none>"}`);
403
+ const shorthand = ticket.shorthand;
404
+ const content = readTicketFile(input.rootPath, shorthand);
405
+ if (content === null)
406
+ throw new Error(`Local ticket not found: .pstdio/tickets/${shorthand}/ticket.md`);
407
+ const frontmatter = parseFrontmatter(content);
408
+ const statusName = input.status ?? frontmatter.status;
409
+ const statusId = statusName ? await resolveStatusId(ctx, statusName) : undefined;
410
+ const tagIds = input.tags?.length ? await resolveTagIds(ctx, input.tags) : undefined;
411
+ const body = stripFrontmatter(content).replace(/^\n+/, "");
412
+ const uploaded = await ctx.client.tickets.uploadFile(ticket.id, {
413
+ file_name: "ticket.md",
414
+ content_base64: Buffer.from(body).toString("base64"),
415
+ mime_type: "text/markdown"
416
+ });
417
+ await ctx.client.tickets.update(ticket.id, {
418
+ blocked_reason: frontmatter.blocked_reason,
419
+ file_id: uploaded.id,
420
+ display_title: extractDisplayTitle(body),
421
+ draft: false,
422
+ parent_id: frontmatter.parent_id,
423
+ tag_ids: tagIds,
424
+ status_id: statusId
425
+ });
426
+ let uploadedFileCount = 0;
427
+ for (const fileName of listDirFiles(input.rootPath, shorthand, TICKET_FILES_DIR)) {
428
+ const data = readSafe(input.rootPath, shorthand, TICKET_FILES_DIR, fileName);
429
+ await ctx.client.tickets.uploadFile(ticket.id, {
430
+ file_name: fileName,
431
+ content_base64: data.toString("base64")
432
+ });
433
+ uploadedFileCount++;
434
+ }
435
+ for (const relativePath of listDirFiles(input.rootPath, shorthand, TICKET_ARTIFACTS_DIR)) {
436
+ const data = readSafe(input.rootPath, shorthand, TICKET_ARTIFACTS_DIR, relativePath);
437
+ await ctx.client.tickets.uploadFile(ticket.id, {
438
+ file_name: basename(relativePath),
439
+ relative_path: relativePath,
440
+ content_base64: data.toString("base64")
441
+ });
442
+ uploadedFileCount++;
443
+ }
444
+ writeTicketFile(input.rootPath, shorthand, markLocalTicketAsSaved(content));
445
+ log(`Saved ticket ${shorthand}`);
446
+ if (uploadedFileCount > 0)
447
+ log(`Uploaded ${uploadedFileCount} ticket files`);
448
+ return { ticketShorthand: shorthand, uploadedFileCount };
449
+ };
197
450
  // src/plugins/helpers/set-ticket-status.ts
198
451
  var setTicketStatus = async (ctx, input) => {
199
452
  const [ticket, statuses] = await Promise.all([
@@ -221,10 +474,10 @@ var setWorkspaceAttemptStatus = async (ctx, input) => {
221
474
  return true;
222
475
  };
223
476
  // src/plugins/helpers/ticket-pull.ts
224
- import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
225
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
226
- var TICKETS_DIR = join(".pstdio", "tickets");
227
- var TICKET_FILES_DIR = "files";
477
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
478
+ import { dirname, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
479
+ var TICKETS_DIR2 = join2(".pstdio", "tickets");
480
+ var TICKET_FILES_DIR2 = "files";
228
481
  var escapeYamlScalar = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
229
482
  var buildTicketFrontmatter = (fields) => {
230
483
  const lines = ["---"];
@@ -251,7 +504,7 @@ var buildTicketFrontmatter = (fields) => {
251
504
  return lines.join(`
252
505
  `);
253
506
  };
254
- var stripFrontmatter = (content) => {
507
+ var stripFrontmatter2 = (content) => {
255
508
  if (!content.startsWith("---"))
256
509
  return content;
257
510
  const closingIndex = content.indexOf("---", 3);
@@ -259,54 +512,54 @@ var stripFrontmatter = (content) => {
259
512
  return content;
260
513
  return content.slice(closingIndex + 3);
261
514
  };
262
- var applyFrontmatter = (frontmatter, content) => {
263
- const body = stripFrontmatter(content).replace(/^\n+/, "");
515
+ var applyFrontmatter2 = (frontmatter, content) => {
516
+ const body = stripFrontmatter2(content).replace(/^\n+/, "");
264
517
  if (!body)
265
518
  return frontmatter;
266
519
  return `${frontmatter}
267
520
 
268
521
  ${body}`;
269
522
  };
270
- var toRelativeFilePath = (baseDir, absolutePath) => relative(baseDir, absolutePath).split("\\").join("/");
271
- var resolveTicketDir = (rootPath, shorthand) => {
272
- const exactDir = join(rootPath, TICKETS_DIR, shorthand);
273
- if (!existsSync(exactDir))
523
+ var toRelativeFilePath = (baseDir, absolutePath) => relative2(baseDir, absolutePath).split("\\").join("/");
524
+ var resolveTicketDir2 = (rootPath, shorthand) => {
525
+ const exactDir = join2(rootPath, TICKETS_DIR2, shorthand);
526
+ if (!existsSync2(exactDir))
274
527
  return null;
275
- if (!statSync(exactDir).isDirectory()) {
528
+ if (!statSync2(exactDir).isDirectory()) {
276
529
  throw new Error(`Invalid ticket path for ${shorthand}: .pstdio/tickets/${shorthand} is not a directory.`);
277
530
  }
278
531
  return exactDir;
279
532
  };
280
- var writeTicketFile = (rootPath, shorthand, content, overwrite = true) => {
281
- const existingDir = resolveTicketDir(rootPath, shorthand);
282
- const dir = existingDir ?? join(rootPath, TICKETS_DIR, shorthand);
283
- const filePath = join(dir, "ticket.md");
284
- if (!overwrite && existsSync(filePath)) {
533
+ var writeTicketFile2 = (rootPath, shorthand, content, overwrite = true) => {
534
+ const existingDir = resolveTicketDir2(rootPath, shorthand);
535
+ const dir = existingDir ?? join2(rootPath, TICKETS_DIR2, shorthand);
536
+ const filePath = join2(dir, "ticket.md");
537
+ if (!overwrite && existsSync2(filePath)) {
285
538
  throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
286
539
  }
287
- mkdirSync(dir, { recursive: true });
288
- writeFileSync(filePath, content);
540
+ mkdirSync2(dir, { recursive: true });
541
+ writeFileSync2(filePath, content);
289
542
  return filePath;
290
543
  };
291
544
  var resolveTicketAttachmentPath = (rootPath, shorthand, fileName) => {
292
- const ticketDir = resolveTicketDir(rootPath, shorthand);
545
+ const ticketDir = resolveTicketDir2(rootPath, shorthand);
293
546
  if (!ticketDir)
294
547
  throw new Error(`Ticket directory not found for ${shorthand}`);
295
- const filesDir = join(ticketDir, TICKET_FILES_DIR);
296
- const targetPath = resolve(filesDir, fileName);
297
- const rel = relative(filesDir, targetPath);
298
- if (isAbsolute(rel) || rel.startsWith("..")) {
548
+ const filesDir = join2(ticketDir, TICKET_FILES_DIR2);
549
+ const targetPath = resolve2(filesDir, fileName);
550
+ const rel = relative2(filesDir, targetPath);
551
+ if (isAbsolute2(rel) || rel.startsWith("..")) {
299
552
  throw new Error(`Ticket file path resolves outside ticket files directory: ${fileName}`);
300
553
  }
301
554
  return targetPath;
302
555
  };
303
556
  var writeTicketAttachment = (rootPath, shorthand, fileName, content, overwrite = false) => {
304
557
  const filePath = resolveTicketAttachmentPath(rootPath, shorthand, fileName);
305
- if (!overwrite && existsSync(filePath)) {
558
+ if (!overwrite && existsSync2(filePath)) {
306
559
  throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
307
560
  }
308
- mkdirSync(dirname(filePath), { recursive: true });
309
- writeFileSync(filePath, content);
561
+ mkdirSync2(dirname(filePath), { recursive: true });
562
+ writeFileSync2(filePath, content);
310
563
  return filePath;
311
564
  };
312
565
  var isNotFoundError = (error) => typeof error === "object" && error !== null && ("status" in error) && error.status === 404;
@@ -371,8 +624,8 @@ var pullSingleTicket = async (ctx, rootPath, ticketListItem, force, log) => {
371
624
  blocked_reason: ticket.blocked_reason,
372
625
  tag_names: ticketListItem.tag_names ?? []
373
626
  });
374
- const content = applyFrontmatter(frontmatter, ticket.content ?? "");
375
- const filePath = writeTicketFile(rootPath, ticketListItem.shorthand, content, force);
627
+ const content = applyFrontmatter2(frontmatter, ticket.content ?? "");
628
+ const filePath = writeTicketFile2(rootPath, ticketListItem.shorthand, content, force);
376
629
  const ticketDir = filePath.replace(/\/ticket\.md$/, "").replace(`${rootPath}/`, "");
377
630
  const files = await ctx.client.tickets.listFiles(ticket.id);
378
631
  const attachments = files.filter((file) => file.id !== ticket.file_id);
@@ -424,24 +677,24 @@ var updateTicketWhenAllAttemptsMatch = async (ctx, input) => {
424
677
  return result.updated;
425
678
  };
426
679
  // src/plugins/helpers/worktree-bootstrap.ts
427
- import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
428
- import { join as join2 } from "node:path";
680
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
681
+ import { join as join3 } from "node:path";
429
682
  var AGENT_DIRS = [".claude", ".opencode", ".agents"];
430
683
  var bootstrapWorktree = async (ctx, input) => {
431
684
  const { repoPath, worktreePath, ticketId } = input;
432
- const repoConfig = join2(repoPath, ".pstdio", "config.json");
433
- const worktreeConfigDir = join2(worktreePath, ".pstdio");
434
- const worktreeConfig = join2(worktreeConfigDir, "config.json");
435
- if (existsSync2(repoConfig)) {
436
- mkdirSync2(worktreeConfigDir, { recursive: true });
685
+ const repoConfig = join3(repoPath, ".pstdio", "config.json");
686
+ const worktreeConfigDir = join3(worktreePath, ".pstdio");
687
+ const worktreeConfig = join3(worktreeConfigDir, "config.json");
688
+ if (existsSync3(repoConfig)) {
689
+ mkdirSync3(worktreeConfigDir, { recursive: true });
437
690
  cpSync(repoConfig, worktreeConfig);
438
691
  }
439
692
  for (const agentDir of AGENT_DIRS) {
440
- const fromDir = join2(repoPath, agentDir);
441
- const toDir = join2(worktreePath, agentDir);
442
- if (!existsSync2(fromDir))
693
+ const fromDir = join3(repoPath, agentDir);
694
+ const toDir = join3(worktreePath, agentDir);
695
+ if (!existsSync3(fromDir))
443
696
  continue;
444
- mkdirSync2(toDir, { recursive: true });
697
+ mkdirSync3(toDir, { recursive: true });
445
698
  cpSync(fromDir, toDir, { recursive: true });
446
699
  }
447
700
  if (!ticketId)
@@ -453,6 +706,7 @@ export {
453
706
  updateTicketWhenAllAttemptsMatch,
454
707
  setWorkspaceAttemptStatus,
455
708
  setTicketStatus,
709
+ saveTicket,
456
710
  runCommand,
457
711
  renderPrompt,
458
712
  removeAllWorktreesForTicket,
@@ -86,9 +86,27 @@ export type ActionDescriptor = {
86
86
  export type ActionDefinition = ActionDescriptor & {
87
87
  trigger: ActionTrigger;
88
88
  };
89
+ export type ScheduledTriggerContext = {
90
+ client: PstdioClient;
91
+ projectId: string;
92
+ trigger: {
93
+ type: "schedule";
94
+ };
95
+ scheduleName: string;
96
+ scheduledFor: string;
97
+ runId: string;
98
+ };
99
+ type ScheduleHandler = (ctx: ScheduledTriggerContext) => void | Promise<void>;
100
+ export type ScheduleDefinition = {
101
+ name: string;
102
+ cron: string;
103
+ timeoutMs?: number;
104
+ handler: ScheduleHandler;
105
+ };
89
106
  export type PluginDefinition = {
90
107
  key?: string;
91
108
  actions?: ActionInput[];
92
109
  hooks?: PluginHooks;
110
+ schedules?: ScheduleDefinition[];
93
111
  };
94
112
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pstdio/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/pufflyai/prompt-studio"