@pstdio/sdk 0.2.1 → 0.4.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.
- package/dist/api/sessions.d.ts +1 -1
- package/dist/api/tickets.d.ts +1 -1
- package/dist/api/workspaces.d.ts +1 -1
- package/dist/client/index.js +71 -0
- package/dist/client/projects.d.ts +2 -1
- package/dist/client/sessions.d.ts +2 -1
- package/dist/client/tickets.d.ts +3 -1
- package/dist/client/workspaces.d.ts +2 -1
- package/dist/plugins/helpers/index.d.ts +1 -0
- package/dist/plugins/helpers/save-ticket.d.ts +14 -0
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +312 -42
- package/dist/plugins/types.d.ts +18 -0
- package/package.json +1 -1
package/dist/api/sessions.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { ApprovalInput, CreateSessionInput, FollowUpInput, ResolveSessionIdInput, ResolveSessionIdResponse, SessionConversationResponse, } from "pstdio-api-contracts";
|
|
1
|
+
export type { ApprovalInput, CreateSessionInput, FollowUpInput, ListSessionActivityInput, ListSessionActivityResponse, ResolveSessionIdInput, ResolveSessionIdResponse, SessionConversationResponse, } from "pstdio-api-contracts";
|
package/dist/api/tickets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { CreateTicketAttemptInput, CreateTicketInput, TicketAttemptMode, UpdateTicketInput, UpdateWhenAttemptStatusInput, UpdateWhenAttemptStatusResponse, UploadTicketFileInput, } from "pstdio-api-contracts";
|
|
1
|
+
export type { CreateTicketAttemptInput, CreateTicketInput, ListProjectActivityForTicketsInput, ListTicketActivityInput, ListTicketActivityResponse, TicketAttemptMode, UpdateTicketInput, UpdateWhenAttemptStatusInput, UpdateWhenAttemptStatusResponse, UploadTicketFileInput, } from "pstdio-api-contracts";
|
|
2
2
|
import type { Ticket, Workspace } from "../resources";
|
|
3
3
|
export type ListTicketsInput = {
|
|
4
4
|
status?: string;
|
package/dist/api/workspaces.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { CreateWorkspaceInput, RemoveWorktreeResponse, UpdateAttemptStatusInput, UpdateAttemptStatusResponse, } from "pstdio-api-contracts";
|
|
1
|
+
export type { CreateWorkspaceInput, ListWorkspaceActivityInput, ListWorkspaceActivityResponse, RemoveWorktreeResponse, UpdateAttemptStatusInput, UpdateAttemptStatusResponse, } from "pstdio-api-contracts";
|
package/dist/client/index.js
CHANGED
|
@@ -24,6 +24,23 @@ var createProjectClient = (request) => ({
|
|
|
24
24
|
get: (projectId) => request(`/v1/projects/${projectId}`),
|
|
25
25
|
create: (input) => request("/v1/projects", { method: "POST", body: input }),
|
|
26
26
|
delete: (projectId) => request(`/v1/projects/${projectId}`, { method: "DELETE" }),
|
|
27
|
+
listActivity: (projectId, input = {}) => {
|
|
28
|
+
const params = new URLSearchParams;
|
|
29
|
+
if (input.resource_type)
|
|
30
|
+
params.append("resource_type", input.resource_type);
|
|
31
|
+
if (input.event_type)
|
|
32
|
+
params.append("event_type", input.event_type);
|
|
33
|
+
if (input.from)
|
|
34
|
+
params.append("from", input.from);
|
|
35
|
+
if (input.to)
|
|
36
|
+
params.append("to", input.to);
|
|
37
|
+
if (input.cursor)
|
|
38
|
+
params.append("cursor", input.cursor);
|
|
39
|
+
if (input.limit !== undefined)
|
|
40
|
+
params.append("limit", String(input.limit));
|
|
41
|
+
const query = params.toString();
|
|
42
|
+
return request(`/v1/projects/${projectId}/activity${query ? `?${query}` : ""}`);
|
|
43
|
+
},
|
|
27
44
|
listPlugins: (projectId) => request(`/v1/projects/${projectId}/plugins`),
|
|
28
45
|
registerPlugins: (projectId) => request(`/v1/projects/${projectId}/plugins/register`, { method: "POST" }),
|
|
29
46
|
listRepos: (projectId) => request(`/v1/projects/${projectId}/repos`),
|
|
@@ -97,6 +114,21 @@ var createRequest = (options) => {
|
|
|
97
114
|
|
|
98
115
|
// src/client/sessions.ts
|
|
99
116
|
var createSessionClient = (request) => ({
|
|
117
|
+
listActivity: (sessionId, input = {}) => {
|
|
118
|
+
const params = new URLSearchParams;
|
|
119
|
+
if (input.event_type)
|
|
120
|
+
params.append("event_type", input.event_type);
|
|
121
|
+
if (input.from)
|
|
122
|
+
params.append("from", input.from);
|
|
123
|
+
if (input.to)
|
|
124
|
+
params.append("to", input.to);
|
|
125
|
+
if (input.cursor)
|
|
126
|
+
params.append("cursor", input.cursor);
|
|
127
|
+
if (input.limit !== undefined)
|
|
128
|
+
params.append("limit", String(input.limit));
|
|
129
|
+
const query = params.toString();
|
|
130
|
+
return request(`/v1/sessions/${sessionId}/activity${query ? `?${query}` : ""}`);
|
|
131
|
+
},
|
|
100
132
|
list: (projectId) => request(`/v1/sessions?project_id=${projectId}`),
|
|
101
133
|
get: (sessionId) => request(`/v1/sessions/${sessionId}`),
|
|
102
134
|
create: (input) => request("/v1/sessions", { method: "POST", body: input }),
|
|
@@ -179,6 +211,22 @@ var buildTicketsQuery = (projectId, input = {}) => {
|
|
|
179
211
|
params.append("search", input.search);
|
|
180
212
|
return params.toString();
|
|
181
213
|
};
|
|
214
|
+
var buildActivityQuery = (input = {}) => {
|
|
215
|
+
const params = new URLSearchParams;
|
|
216
|
+
if (input.resource_type)
|
|
217
|
+
params.append("resource_type", input.resource_type);
|
|
218
|
+
if (input.event_type)
|
|
219
|
+
params.append("event_type", input.event_type);
|
|
220
|
+
if (input.from)
|
|
221
|
+
params.append("from", input.from);
|
|
222
|
+
if (input.to)
|
|
223
|
+
params.append("to", input.to);
|
|
224
|
+
if (input.cursor)
|
|
225
|
+
params.append("cursor", input.cursor);
|
|
226
|
+
if (input.limit !== undefined)
|
|
227
|
+
params.append("limit", String(input.limit));
|
|
228
|
+
return params.toString();
|
|
229
|
+
};
|
|
182
230
|
var createTicketClient = (request, options = {}) => ({
|
|
183
231
|
list: (projectId, input) => request(`/v1/tickets?${buildTicketsQuery(projectId, input)}`),
|
|
184
232
|
get: (ticketId) => request(`/v1/tickets/${ticketId}`),
|
|
@@ -186,6 +234,14 @@ var createTicketClient = (request, options = {}) => ({
|
|
|
186
234
|
update: (ticketId, input) => request(`/v1/tickets/${ticketId}`, { method: "PATCH", body: input }),
|
|
187
235
|
delete: (ticketId) => request(`/v1/tickets/${ticketId}`, { method: "DELETE" }),
|
|
188
236
|
createAttempt: (ticketId, input) => request(`/v1/tickets/${ticketId}/attempts`, { method: "POST", body: input }),
|
|
237
|
+
listActivity: (ticketId, input) => {
|
|
238
|
+
const query = buildActivityQuery(input);
|
|
239
|
+
return request(`/v1/tickets/${ticketId}/activity${query ? `?${query}` : ""}`);
|
|
240
|
+
},
|
|
241
|
+
listProjectActivity: (projectId, input) => {
|
|
242
|
+
const query = buildActivityQuery(input);
|
|
243
|
+
return request(`/v1/projects/${projectId}/activity${query ? `?${query}` : ""}`);
|
|
244
|
+
},
|
|
189
245
|
updateWhenAttemptStatus: (ticketId, input) => request(`/v1/tickets/${ticketId}/update-when-attempt-status`, { method: "POST", body: input }),
|
|
190
246
|
listFiles: (ticketId) => request(`/v1/tickets/${ticketId}/files`),
|
|
191
247
|
getFileContent: async (ticketId, fileId) => {
|
|
@@ -206,6 +262,21 @@ var createTicketClient = (request, options = {}) => ({
|
|
|
206
262
|
|
|
207
263
|
// src/client/workspaces.ts
|
|
208
264
|
var createWorkspaceClient = (request) => ({
|
|
265
|
+
listActivity: (workspaceId, input = {}) => {
|
|
266
|
+
const params = new URLSearchParams;
|
|
267
|
+
if (input.event_type)
|
|
268
|
+
params.append("event_type", input.event_type);
|
|
269
|
+
if (input.from)
|
|
270
|
+
params.append("from", input.from);
|
|
271
|
+
if (input.to)
|
|
272
|
+
params.append("to", input.to);
|
|
273
|
+
if (input.cursor)
|
|
274
|
+
params.append("cursor", input.cursor);
|
|
275
|
+
if (input.limit !== undefined)
|
|
276
|
+
params.append("limit", String(input.limit));
|
|
277
|
+
const query = params.toString();
|
|
278
|
+
return request(`/v1/workspaces/${workspaceId}/activity${query ? `?${query}` : ""}`);
|
|
279
|
+
},
|
|
209
280
|
list: (projectId) => request(`/v1/workspaces?project_id=${projectId}`),
|
|
210
281
|
getByShorthand: (projectId, shorthand) => request(`/v1/workspaces/by-shorthand?project_id=${encodeURIComponent(projectId)}&shorthand=${encodeURIComponent(shorthand)}`),
|
|
211
282
|
create: (input) => request("/v1/workspaces", { method: "POST", body: input }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateProjectInput, RegisterRepoInput, Repo } from "pstdio-api-contracts";
|
|
1
|
+
import type { CreateProjectInput, ListProjectActivityForTicketsInput, ListTicketActivityResponse, RegisterRepoInput, Repo } from "pstdio-api-contracts";
|
|
2
2
|
import type { Project } from "../resources";
|
|
3
3
|
import type { RequestFn } from "./request";
|
|
4
4
|
type RegisteredPlugin = {
|
|
@@ -16,6 +16,7 @@ export type ProjectClient = {
|
|
|
16
16
|
delete(projectId: string): Promise<void>;
|
|
17
17
|
listPlugins(projectId: string): Promise<RegisteredPluginsResponse>;
|
|
18
18
|
registerPlugins(projectId: string): Promise<RegisteredPluginsResponse>;
|
|
19
|
+
listActivity(projectId: string, input?: ListProjectActivityForTicketsInput): Promise<ListTicketActivityResponse>;
|
|
19
20
|
listRepos(projectId: string): Promise<Repo[]>;
|
|
20
21
|
registerRepo(projectId: string, input: RegisterRepoInput): Promise<Repo>;
|
|
21
22
|
removeRepo(projectId: string, repoId: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalInput, CreateSessionInput, FollowUpInput, ResolveSessionIdInput, ResolveSessionIdResponse, SessionConversationResponse } from "pstdio-api-contracts";
|
|
1
|
+
import type { ApprovalInput, CreateSessionInput, FollowUpInput, ListSessionActivityInput, ListSessionActivityResponse, ResolveSessionIdInput, ResolveSessionIdResponse, SessionConversationResponse } from "pstdio-api-contracts";
|
|
2
2
|
import type { Session } from "../resources";
|
|
3
3
|
import type { RequestFn } from "./request";
|
|
4
4
|
export type SessionClient = {
|
|
@@ -11,5 +11,6 @@ export type SessionClient = {
|
|
|
11
11
|
getConversation(sessionId: string): Promise<SessionConversationResponse>;
|
|
12
12
|
resolveSessionId(input: ResolveSessionIdInput): Promise<ResolveSessionIdResponse>;
|
|
13
13
|
updateStatus(sessionId: string, status: string): Promise<Session>;
|
|
14
|
+
listActivity(sessionId: string, input?: ListSessionActivityInput): Promise<ListSessionActivityResponse>;
|
|
14
15
|
};
|
|
15
16
|
export declare const createSessionClient: (request: RequestFn) => SessionClient;
|
package/dist/client/tickets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateTicketAttemptInput, CreateTicketInput, ListTicketsInput, TicketAttemptResponse, UpdateTicketInput, UpdateWhenAttemptStatusInput, UpdateWhenAttemptStatusResponse, UploadTicketFileInput } from "../api/tickets";
|
|
1
|
+
import type { CreateTicketAttemptInput, CreateTicketInput, ListProjectActivityForTicketsInput, ListTicketActivityInput, ListTicketActivityResponse, ListTicketsInput, TicketAttemptResponse, UpdateTicketInput, UpdateWhenAttemptStatusInput, UpdateWhenAttemptStatusResponse, UploadTicketFileInput } from "../api/tickets";
|
|
2
2
|
import type { Ticket, TicketDetail, TicketFile, TicketListItem } from "../resources";
|
|
3
3
|
import { type ClientOptions, type RequestFn } from "./request";
|
|
4
4
|
export type TicketClient = {
|
|
@@ -8,6 +8,8 @@ export type TicketClient = {
|
|
|
8
8
|
update(ticketId: string, input: UpdateTicketInput): Promise<Ticket>;
|
|
9
9
|
delete(ticketId: string): Promise<void>;
|
|
10
10
|
createAttempt(ticketId: string, input: CreateTicketAttemptInput): Promise<TicketAttemptResponse>;
|
|
11
|
+
listActivity(ticketId: string, input?: ListTicketActivityInput): Promise<ListTicketActivityResponse>;
|
|
12
|
+
listProjectActivity(projectId: string, input?: ListProjectActivityForTicketsInput): Promise<ListTicketActivityResponse>;
|
|
11
13
|
updateWhenAttemptStatus(ticketId: string, input: UpdateWhenAttemptStatusInput): Promise<UpdateWhenAttemptStatusResponse>;
|
|
12
14
|
listFiles(ticketId: string): Promise<TicketFile[]>;
|
|
13
15
|
getFileContent(ticketId: string, fileId: string): Promise<Uint8Array>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateWorkspaceInput, RemoveWorktreeResponse, UpdateAttemptStatusInput, UpdateAttemptStatusResponse } from "pstdio-api-contracts";
|
|
1
|
+
import type { CreateWorkspaceInput, ListWorkspaceActivityInput, ListWorkspaceActivityResponse, RemoveWorktreeResponse, UpdateAttemptStatusInput, UpdateAttemptStatusResponse } from "pstdio-api-contracts";
|
|
2
2
|
import type { Workspace, WorkspaceListItem } from "../resources";
|
|
3
3
|
import type { RequestFn } from "./request";
|
|
4
4
|
export type WorkspaceClient = {
|
|
@@ -6,6 +6,7 @@ export type WorkspaceClient = {
|
|
|
6
6
|
getByShorthand(projectId: string, shorthand: string): Promise<Workspace>;
|
|
7
7
|
create(input: CreateWorkspaceInput): Promise<Workspace>;
|
|
8
8
|
updateAttemptStatus(workspaceId: string, input: UpdateAttemptStatusInput): Promise<UpdateAttemptStatusResponse>;
|
|
9
|
+
listActivity(workspaceId: string, input?: ListWorkspaceActivityInput): Promise<ListWorkspaceActivityResponse>;
|
|
9
10
|
removeWorktree(workspaceId: string): Promise<RemoveWorktreeResponse>;
|
|
10
11
|
delete(workspaceId: string): Promise<void>;
|
|
11
12
|
};
|
|
@@ -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 };
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/plugins/index.js
CHANGED
|
@@ -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
|
|
@@ -170,14 +178,30 @@ var removeAllWorktreesForTicket = async (ctx, input) => {
|
|
|
170
178
|
return removed;
|
|
171
179
|
};
|
|
172
180
|
// src/plugins/helpers/run-command.ts
|
|
181
|
+
import { accessSync, constants } from "node:fs";
|
|
182
|
+
import { delimiter, join } from "node:path";
|
|
183
|
+
var resolveCommandPath = (command, env) => {
|
|
184
|
+
if (command.includes("/") || command.includes("\\"))
|
|
185
|
+
return command;
|
|
186
|
+
for (const dir of env.PATH?.split(delimiter) ?? []) {
|
|
187
|
+
const candidate = join(dir || ".", command);
|
|
188
|
+
try {
|
|
189
|
+
accessSync(candidate, constants.X_OK);
|
|
190
|
+
return candidate;
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
return command;
|
|
194
|
+
};
|
|
173
195
|
var runCommand = async (cwd, command, options = {}) => {
|
|
174
196
|
const [cmd, ...args] = command;
|
|
175
197
|
const stdio = options.quiet ? "ignore" : "pipe";
|
|
198
|
+
const env = { ...process.env, ...options.env };
|
|
199
|
+
const resolvedCmd = resolveCommandPath(cmd, env);
|
|
176
200
|
let proc;
|
|
177
201
|
try {
|
|
178
|
-
proc = Bun.spawn([
|
|
202
|
+
proc = Bun.spawn([resolvedCmd, ...args], {
|
|
179
203
|
cwd,
|
|
180
|
-
env
|
|
204
|
+
env,
|
|
181
205
|
stdin: "ignore",
|
|
182
206
|
stdout: stdio,
|
|
183
207
|
stderr: stdio
|
|
@@ -194,6 +218,251 @@ var runCommand = async (cwd, command, options = {}) => {
|
|
|
194
218
|
stderr: stderr.trim()
|
|
195
219
|
};
|
|
196
220
|
};
|
|
221
|
+
// src/plugins/helpers/save-ticket.ts
|
|
222
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
223
|
+
import { basename, isAbsolute, join as join2, relative, resolve } from "node:path";
|
|
224
|
+
var TICKETS_DIR = join2(".pstdio", "tickets");
|
|
225
|
+
var TICKET_FILES_DIR = "files";
|
|
226
|
+
var TICKET_ARTIFACTS_DIR = "artifacts";
|
|
227
|
+
var MAX_DISPLAY_TITLE_LENGTH = 50;
|
|
228
|
+
var ACTIONABLE_FRONTMATTER_KEYS = ["blocked_reason", "parent_id", "status"];
|
|
229
|
+
var resolveTicketDir = (rootPath, shorthand) => {
|
|
230
|
+
const exactDir = join2(rootPath, TICKETS_DIR, shorthand);
|
|
231
|
+
if (!existsSync(exactDir))
|
|
232
|
+
return null;
|
|
233
|
+
if (!statSync(exactDir).isDirectory()) {
|
|
234
|
+
throw new Error(`Invalid ticket path for ${shorthand}: .pstdio/tickets/${shorthand} is not a directory.`);
|
|
235
|
+
}
|
|
236
|
+
return exactDir;
|
|
237
|
+
};
|
|
238
|
+
var readTicketFile = (rootPath, shorthand) => {
|
|
239
|
+
const dir = resolveTicketDir(rootPath, shorthand);
|
|
240
|
+
if (!dir)
|
|
241
|
+
return null;
|
|
242
|
+
const filePath = join2(dir, "ticket.md");
|
|
243
|
+
if (!existsSync(filePath))
|
|
244
|
+
return null;
|
|
245
|
+
return readFileSync(filePath, "utf8");
|
|
246
|
+
};
|
|
247
|
+
var writeTicketFile = (rootPath, shorthand, content) => {
|
|
248
|
+
const dir = resolveTicketDir(rootPath, shorthand) ?? join2(rootPath, TICKETS_DIR, shorthand);
|
|
249
|
+
mkdirSync(dir, { recursive: true });
|
|
250
|
+
writeFileSync(join2(dir, "ticket.md"), content);
|
|
251
|
+
};
|
|
252
|
+
var walkFiles = (baseDir, currentDir, files) => {
|
|
253
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const fullPath = join2(currentDir, entry.name);
|
|
256
|
+
if (entry.isDirectory()) {
|
|
257
|
+
walkFiles(baseDir, fullPath, files);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!entry.isFile())
|
|
261
|
+
continue;
|
|
262
|
+
files.push(relative(baseDir, fullPath).split("\\").join("/"));
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
var listDirFiles = (rootPath, shorthand, subDir) => {
|
|
266
|
+
const dir = resolveTicketDir(rootPath, shorthand);
|
|
267
|
+
if (!dir)
|
|
268
|
+
return [];
|
|
269
|
+
const baseDir = join2(dir, subDir);
|
|
270
|
+
if (!existsSync(baseDir))
|
|
271
|
+
return [];
|
|
272
|
+
const files = [];
|
|
273
|
+
walkFiles(baseDir, baseDir, files);
|
|
274
|
+
files.sort();
|
|
275
|
+
return files;
|
|
276
|
+
};
|
|
277
|
+
var readSafe = (rootPath, shorthand, subDir, requestedPath) => {
|
|
278
|
+
const ticketDir = resolveTicketDir(rootPath, shorthand);
|
|
279
|
+
if (!ticketDir)
|
|
280
|
+
throw new Error(`Ticket directory not found: ${shorthand}`);
|
|
281
|
+
const baseDir = join2(ticketDir, subDir);
|
|
282
|
+
const target = resolve(baseDir, requestedPath);
|
|
283
|
+
const rel = relative(baseDir, target);
|
|
284
|
+
if (isAbsolute(rel) || rel.startsWith("..")) {
|
|
285
|
+
throw new Error(`Path resolves outside ticket ${subDir} directory: ${requestedPath}`);
|
|
286
|
+
}
|
|
287
|
+
return readFileSync(target);
|
|
288
|
+
};
|
|
289
|
+
var findFrontmatterClosingIndex = (content) => {
|
|
290
|
+
if (!content.startsWith("---"))
|
|
291
|
+
return -1;
|
|
292
|
+
return content.indexOf("---", 3);
|
|
293
|
+
};
|
|
294
|
+
var stripFrontmatter = (content) => {
|
|
295
|
+
const closing = findFrontmatterClosingIndex(content);
|
|
296
|
+
if (closing === -1)
|
|
297
|
+
return content;
|
|
298
|
+
return content.slice(closing + 3);
|
|
299
|
+
};
|
|
300
|
+
var parseFrontmatter = (content) => {
|
|
301
|
+
const closing = findFrontmatterClosingIndex(content);
|
|
302
|
+
if (closing === -1)
|
|
303
|
+
return {};
|
|
304
|
+
const block = content.slice(3, closing).trim();
|
|
305
|
+
const result = {};
|
|
306
|
+
for (const line of block.split(`
|
|
307
|
+
`)) {
|
|
308
|
+
const colonIndex = line.indexOf(":");
|
|
309
|
+
if (colonIndex === -1)
|
|
310
|
+
continue;
|
|
311
|
+
const key = line.slice(0, colonIndex).trim();
|
|
312
|
+
const raw = line.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, "");
|
|
313
|
+
if (!raw)
|
|
314
|
+
continue;
|
|
315
|
+
if (ACTIONABLE_FRONTMATTER_KEYS.includes(key)) {
|
|
316
|
+
result[key] = raw;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
};
|
|
321
|
+
var frontmatterLines = (content) => {
|
|
322
|
+
const closing = findFrontmatterClosingIndex(content);
|
|
323
|
+
if (closing === -1)
|
|
324
|
+
return [];
|
|
325
|
+
const block = content.slice(3, closing).trim();
|
|
326
|
+
if (!block)
|
|
327
|
+
return [];
|
|
328
|
+
return block.split(`
|
|
329
|
+
`);
|
|
330
|
+
};
|
|
331
|
+
var frontmatterKey = (line) => {
|
|
332
|
+
const colon = line.indexOf(":");
|
|
333
|
+
if (colon === -1)
|
|
334
|
+
return null;
|
|
335
|
+
return line.slice(0, colon).trim();
|
|
336
|
+
};
|
|
337
|
+
var applyFrontmatter = (frontmatter, content) => {
|
|
338
|
+
const body = stripFrontmatter(content).replace(/^\n+/, "");
|
|
339
|
+
if (!body)
|
|
340
|
+
return frontmatter;
|
|
341
|
+
return `${frontmatter}
|
|
342
|
+
|
|
343
|
+
${body}`;
|
|
344
|
+
};
|
|
345
|
+
var applyFrontmatterValues = (frontmatter, content) => {
|
|
346
|
+
if (findFrontmatterClosingIndex(content) === -1)
|
|
347
|
+
return applyFrontmatter(frontmatter, content);
|
|
348
|
+
const overrides = new Map;
|
|
349
|
+
const overrideOrder = [];
|
|
350
|
+
for (const line of frontmatterLines(frontmatter)) {
|
|
351
|
+
const key = frontmatterKey(line);
|
|
352
|
+
if (!key)
|
|
353
|
+
continue;
|
|
354
|
+
overrides.set(key, line);
|
|
355
|
+
overrideOrder.push(key);
|
|
356
|
+
}
|
|
357
|
+
const existing = frontmatterLines(content);
|
|
358
|
+
const merged = existing.map((line) => {
|
|
359
|
+
const key = frontmatterKey(line);
|
|
360
|
+
if (!key || !overrides.has(key))
|
|
361
|
+
return line;
|
|
362
|
+
return overrides.get(key);
|
|
363
|
+
});
|
|
364
|
+
for (const key of overrideOrder) {
|
|
365
|
+
if (existing.some((line2) => frontmatterKey(line2) === key))
|
|
366
|
+
continue;
|
|
367
|
+
const line = overrides.get(key);
|
|
368
|
+
if (line)
|
|
369
|
+
merged.push(line);
|
|
370
|
+
}
|
|
371
|
+
return applyFrontmatter(["---", ...merged, "---"].join(`
|
|
372
|
+
`), content);
|
|
373
|
+
};
|
|
374
|
+
var markLocalTicketAsSaved = (content) => applyFrontmatterValues(["---", "draft: false", "---"].join(`
|
|
375
|
+
`), content);
|
|
376
|
+
var stripMarkdownFormatting = (text) => text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*([^*]*)\*\*/g, "$1").replace(/\*([^*]*)\*/g, "$1").replace(/`([^`]*)`/g, "$1");
|
|
377
|
+
var slugify = (text, maxLength) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
|
|
378
|
+
var firstHeadingOrLine = (content) => {
|
|
379
|
+
const lines = content.split(`
|
|
380
|
+
`);
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
const trimmed = line.trim();
|
|
383
|
+
if (trimmed.startsWith("# "))
|
|
384
|
+
return trimmed.slice(2).trim();
|
|
385
|
+
}
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
const trimmed = line.trim();
|
|
388
|
+
if (trimmed.length > 0)
|
|
389
|
+
return trimmed;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
};
|
|
393
|
+
var extractDisplayTitle = (content) => {
|
|
394
|
+
const raw = firstHeadingOrLine(stripFrontmatter(content)) ?? "untitled";
|
|
395
|
+
return slugify(stripMarkdownFormatting(raw), MAX_DISPLAY_TITLE_LENGTH);
|
|
396
|
+
};
|
|
397
|
+
var resolveStatusId = async (ctx, statusName) => {
|
|
398
|
+
const statuses = await ctx.client.statuses.list(ctx.projectId);
|
|
399
|
+
const found = statuses.find((status) => status.name === statusName);
|
|
400
|
+
if (!found)
|
|
401
|
+
throw new Error(`Status not found: ${statusName}`);
|
|
402
|
+
return found.id;
|
|
403
|
+
};
|
|
404
|
+
var resolveTagIds = async (ctx, tagNames) => {
|
|
405
|
+
const tags = await ctx.client.tags.list(ctx.projectId);
|
|
406
|
+
const options = tags.flatMap((tag) => tag.options);
|
|
407
|
+
return tagNames.map((name) => {
|
|
408
|
+
const found = options.find((option) => option.name === name);
|
|
409
|
+
if (!found)
|
|
410
|
+
throw new Error(`Tag option not found: ${name}`);
|
|
411
|
+
return found.id;
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
var saveTicket = async (ctx, input) => {
|
|
415
|
+
const log = input.log ?? (() => {});
|
|
416
|
+
const ticket = await findTicketByRef(ctx, { ticketId: input.ticketId });
|
|
417
|
+
if (!ticket)
|
|
418
|
+
throw new Error(`Ticket not found: ${input.ticketId ?? "<none>"}`);
|
|
419
|
+
const shorthand = ticket.shorthand;
|
|
420
|
+
const content = readTicketFile(input.rootPath, shorthand);
|
|
421
|
+
if (content === null)
|
|
422
|
+
throw new Error(`Local ticket not found: .pstdio/tickets/${shorthand}/ticket.md`);
|
|
423
|
+
const frontmatter = parseFrontmatter(content);
|
|
424
|
+
const statusName = input.status ?? frontmatter.status;
|
|
425
|
+
const statusId = statusName ? await resolveStatusId(ctx, statusName) : undefined;
|
|
426
|
+
const tagIds = input.tags?.length ? await resolveTagIds(ctx, input.tags) : undefined;
|
|
427
|
+
const body = stripFrontmatter(content).replace(/^\n+/, "");
|
|
428
|
+
const uploaded = await ctx.client.tickets.uploadFile(ticket.id, {
|
|
429
|
+
file_name: "ticket.md",
|
|
430
|
+
content_base64: Buffer.from(body).toString("base64"),
|
|
431
|
+
mime_type: "text/markdown"
|
|
432
|
+
});
|
|
433
|
+
await ctx.client.tickets.update(ticket.id, {
|
|
434
|
+
blocked_reason: frontmatter.blocked_reason,
|
|
435
|
+
file_id: uploaded.id,
|
|
436
|
+
display_title: extractDisplayTitle(body),
|
|
437
|
+
draft: false,
|
|
438
|
+
parent_id: frontmatter.parent_id,
|
|
439
|
+
tag_ids: tagIds,
|
|
440
|
+
status_id: statusId
|
|
441
|
+
});
|
|
442
|
+
let uploadedFileCount = 0;
|
|
443
|
+
for (const fileName of listDirFiles(input.rootPath, shorthand, TICKET_FILES_DIR)) {
|
|
444
|
+
const data = readSafe(input.rootPath, shorthand, TICKET_FILES_DIR, fileName);
|
|
445
|
+
await ctx.client.tickets.uploadFile(ticket.id, {
|
|
446
|
+
file_name: fileName,
|
|
447
|
+
content_base64: data.toString("base64")
|
|
448
|
+
});
|
|
449
|
+
uploadedFileCount++;
|
|
450
|
+
}
|
|
451
|
+
for (const relativePath of listDirFiles(input.rootPath, shorthand, TICKET_ARTIFACTS_DIR)) {
|
|
452
|
+
const data = readSafe(input.rootPath, shorthand, TICKET_ARTIFACTS_DIR, relativePath);
|
|
453
|
+
await ctx.client.tickets.uploadFile(ticket.id, {
|
|
454
|
+
file_name: basename(relativePath),
|
|
455
|
+
relative_path: relativePath,
|
|
456
|
+
content_base64: data.toString("base64")
|
|
457
|
+
});
|
|
458
|
+
uploadedFileCount++;
|
|
459
|
+
}
|
|
460
|
+
writeTicketFile(input.rootPath, shorthand, markLocalTicketAsSaved(content));
|
|
461
|
+
log(`Saved ticket ${shorthand}`);
|
|
462
|
+
if (uploadedFileCount > 0)
|
|
463
|
+
log(`Uploaded ${uploadedFileCount} ticket files`);
|
|
464
|
+
return { ticketShorthand: shorthand, uploadedFileCount };
|
|
465
|
+
};
|
|
197
466
|
// src/plugins/helpers/set-ticket-status.ts
|
|
198
467
|
var setTicketStatus = async (ctx, input) => {
|
|
199
468
|
const [ticket, statuses] = await Promise.all([
|
|
@@ -221,10 +490,10 @@ var setWorkspaceAttemptStatus = async (ctx, input) => {
|
|
|
221
490
|
return true;
|
|
222
491
|
};
|
|
223
492
|
// 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
|
|
227
|
-
var
|
|
493
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
494
|
+
import { dirname, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve2 } from "node:path";
|
|
495
|
+
var TICKETS_DIR2 = join3(".pstdio", "tickets");
|
|
496
|
+
var TICKET_FILES_DIR2 = "files";
|
|
228
497
|
var escapeYamlScalar = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
229
498
|
var buildTicketFrontmatter = (fields) => {
|
|
230
499
|
const lines = ["---"];
|
|
@@ -251,7 +520,7 @@ var buildTicketFrontmatter = (fields) => {
|
|
|
251
520
|
return lines.join(`
|
|
252
521
|
`);
|
|
253
522
|
};
|
|
254
|
-
var
|
|
523
|
+
var stripFrontmatter2 = (content) => {
|
|
255
524
|
if (!content.startsWith("---"))
|
|
256
525
|
return content;
|
|
257
526
|
const closingIndex = content.indexOf("---", 3);
|
|
@@ -259,54 +528,54 @@ var stripFrontmatter = (content) => {
|
|
|
259
528
|
return content;
|
|
260
529
|
return content.slice(closingIndex + 3);
|
|
261
530
|
};
|
|
262
|
-
var
|
|
263
|
-
const body =
|
|
531
|
+
var applyFrontmatter2 = (frontmatter, content) => {
|
|
532
|
+
const body = stripFrontmatter2(content).replace(/^\n+/, "");
|
|
264
533
|
if (!body)
|
|
265
534
|
return frontmatter;
|
|
266
535
|
return `${frontmatter}
|
|
267
536
|
|
|
268
537
|
${body}`;
|
|
269
538
|
};
|
|
270
|
-
var toRelativeFilePath = (baseDir, absolutePath) =>
|
|
271
|
-
var
|
|
272
|
-
const exactDir =
|
|
273
|
-
if (!
|
|
539
|
+
var toRelativeFilePath = (baseDir, absolutePath) => relative2(baseDir, absolutePath).split("\\").join("/");
|
|
540
|
+
var resolveTicketDir2 = (rootPath, shorthand) => {
|
|
541
|
+
const exactDir = join3(rootPath, TICKETS_DIR2, shorthand);
|
|
542
|
+
if (!existsSync2(exactDir))
|
|
274
543
|
return null;
|
|
275
|
-
if (!
|
|
544
|
+
if (!statSync2(exactDir).isDirectory()) {
|
|
276
545
|
throw new Error(`Invalid ticket path for ${shorthand}: .pstdio/tickets/${shorthand} is not a directory.`);
|
|
277
546
|
}
|
|
278
547
|
return exactDir;
|
|
279
548
|
};
|
|
280
|
-
var
|
|
281
|
-
const existingDir =
|
|
282
|
-
const dir = existingDir ??
|
|
283
|
-
const filePath =
|
|
284
|
-
if (!overwrite &&
|
|
549
|
+
var writeTicketFile2 = (rootPath, shorthand, content, overwrite = true) => {
|
|
550
|
+
const existingDir = resolveTicketDir2(rootPath, shorthand);
|
|
551
|
+
const dir = existingDir ?? join3(rootPath, TICKETS_DIR2, shorthand);
|
|
552
|
+
const filePath = join3(dir, "ticket.md");
|
|
553
|
+
if (!overwrite && existsSync2(filePath)) {
|
|
285
554
|
throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
|
|
286
555
|
}
|
|
287
|
-
|
|
288
|
-
|
|
556
|
+
mkdirSync2(dir, { recursive: true });
|
|
557
|
+
writeFileSync2(filePath, content);
|
|
289
558
|
return filePath;
|
|
290
559
|
};
|
|
291
560
|
var resolveTicketAttachmentPath = (rootPath, shorthand, fileName) => {
|
|
292
|
-
const ticketDir =
|
|
561
|
+
const ticketDir = resolveTicketDir2(rootPath, shorthand);
|
|
293
562
|
if (!ticketDir)
|
|
294
563
|
throw new Error(`Ticket directory not found for ${shorthand}`);
|
|
295
|
-
const filesDir =
|
|
296
|
-
const targetPath =
|
|
297
|
-
const rel =
|
|
298
|
-
if (
|
|
564
|
+
const filesDir = join3(ticketDir, TICKET_FILES_DIR2);
|
|
565
|
+
const targetPath = resolve2(filesDir, fileName);
|
|
566
|
+
const rel = relative2(filesDir, targetPath);
|
|
567
|
+
if (isAbsolute2(rel) || rel.startsWith("..")) {
|
|
299
568
|
throw new Error(`Ticket file path resolves outside ticket files directory: ${fileName}`);
|
|
300
569
|
}
|
|
301
570
|
return targetPath;
|
|
302
571
|
};
|
|
303
572
|
var writeTicketAttachment = (rootPath, shorthand, fileName, content, overwrite = false) => {
|
|
304
573
|
const filePath = resolveTicketAttachmentPath(rootPath, shorthand, fileName);
|
|
305
|
-
if (!overwrite &&
|
|
574
|
+
if (!overwrite && existsSync2(filePath)) {
|
|
306
575
|
throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
|
|
307
576
|
}
|
|
308
|
-
|
|
309
|
-
|
|
577
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
578
|
+
writeFileSync2(filePath, content);
|
|
310
579
|
return filePath;
|
|
311
580
|
};
|
|
312
581
|
var isNotFoundError = (error) => typeof error === "object" && error !== null && ("status" in error) && error.status === 404;
|
|
@@ -371,8 +640,8 @@ var pullSingleTicket = async (ctx, rootPath, ticketListItem, force, log) => {
|
|
|
371
640
|
blocked_reason: ticket.blocked_reason,
|
|
372
641
|
tag_names: ticketListItem.tag_names ?? []
|
|
373
642
|
});
|
|
374
|
-
const content =
|
|
375
|
-
const filePath =
|
|
643
|
+
const content = applyFrontmatter2(frontmatter, ticket.content ?? "");
|
|
644
|
+
const filePath = writeTicketFile2(rootPath, ticketListItem.shorthand, content, force);
|
|
376
645
|
const ticketDir = filePath.replace(/\/ticket\.md$/, "").replace(`${rootPath}/`, "");
|
|
377
646
|
const files = await ctx.client.tickets.listFiles(ticket.id);
|
|
378
647
|
const attachments = files.filter((file) => file.id !== ticket.file_id);
|
|
@@ -424,24 +693,24 @@ var updateTicketWhenAllAttemptsMatch = async (ctx, input) => {
|
|
|
424
693
|
return result.updated;
|
|
425
694
|
};
|
|
426
695
|
// src/plugins/helpers/worktree-bootstrap.ts
|
|
427
|
-
import { cpSync, existsSync as
|
|
428
|
-
import { join as
|
|
696
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
697
|
+
import { join as join4 } from "node:path";
|
|
429
698
|
var AGENT_DIRS = [".claude", ".opencode", ".agents"];
|
|
430
699
|
var bootstrapWorktree = async (ctx, input) => {
|
|
431
700
|
const { repoPath, worktreePath, ticketId } = input;
|
|
432
|
-
const repoConfig =
|
|
433
|
-
const worktreeConfigDir =
|
|
434
|
-
const worktreeConfig =
|
|
435
|
-
if (
|
|
436
|
-
|
|
701
|
+
const repoConfig = join4(repoPath, ".pstdio", "config.json");
|
|
702
|
+
const worktreeConfigDir = join4(worktreePath, ".pstdio");
|
|
703
|
+
const worktreeConfig = join4(worktreeConfigDir, "config.json");
|
|
704
|
+
if (existsSync3(repoConfig)) {
|
|
705
|
+
mkdirSync3(worktreeConfigDir, { recursive: true });
|
|
437
706
|
cpSync(repoConfig, worktreeConfig);
|
|
438
707
|
}
|
|
439
708
|
for (const agentDir of AGENT_DIRS) {
|
|
440
|
-
const fromDir =
|
|
441
|
-
const toDir =
|
|
442
|
-
if (!
|
|
709
|
+
const fromDir = join4(repoPath, agentDir);
|
|
710
|
+
const toDir = join4(worktreePath, agentDir);
|
|
711
|
+
if (!existsSync3(fromDir))
|
|
443
712
|
continue;
|
|
444
|
-
|
|
713
|
+
mkdirSync3(toDir, { recursive: true });
|
|
445
714
|
cpSync(fromDir, toDir, { recursive: true });
|
|
446
715
|
}
|
|
447
716
|
if (!ticketId)
|
|
@@ -453,6 +722,7 @@ export {
|
|
|
453
722
|
updateTicketWhenAllAttemptsMatch,
|
|
454
723
|
setWorkspaceAttemptStatus,
|
|
455
724
|
setTicketStatus,
|
|
725
|
+
saveTicket,
|
|
456
726
|
runCommand,
|
|
457
727
|
renderPrompt,
|
|
458
728
|
removeAllWorktreesForTicket,
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -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 {};
|