@pstdio/sdk 0.2.0 → 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.
- package/dist/api/actions.d.ts +1 -0
- package/dist/client/index.js +23 -1
- package/dist/client/projects.d.ts +11 -0
- package/dist/hooks/session.d.ts +1 -1
- package/dist/plugins/helpers/create-session.d.ts +1 -1
- package/dist/plugins/helpers/followup-session.d.ts +1 -1
- package/dist/plugins/helpers/index.d.ts +1 -0
- package/dist/plugins/helpers/run-command.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 -53
- package/dist/plugins/types.d.ts +25 -2
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/skill.d.ts +1 -1
- package/package.json +2 -2
package/dist/api/actions.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -24,6 +24,8 @@ 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
|
+
listPlugins: (projectId) => request(`/v1/projects/${projectId}/plugins`),
|
|
28
|
+
registerPlugins: (projectId) => request(`/v1/projects/${projectId}/plugins/register`, { method: "POST" }),
|
|
27
29
|
listRepos: (projectId) => request(`/v1/projects/${projectId}/repos`),
|
|
28
30
|
registerRepo: (projectId, input) => request(`/v1/projects/${projectId}/repos`, { method: "POST", body: input }),
|
|
29
31
|
removeRepo: (projectId, repoId) => request(`/v1/projects/${projectId}/repos/${repoId}`, { method: "DELETE" })
|
|
@@ -38,9 +40,29 @@ class PstdioApiError extends Error {
|
|
|
38
40
|
this.status = status;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
var formatZodIssues = (issues) => issues.map((issue) => {
|
|
44
|
+
const path = Array.isArray(issue.path) ? issue.path.filter((part) => part !== "").join(".") : "";
|
|
45
|
+
const message = typeof issue.message === "string" ? issue.message : JSON.stringify(issue);
|
|
46
|
+
return path ? `${path}: ${message}` : message;
|
|
47
|
+
}).join("; ");
|
|
48
|
+
var stringifyErrorField = (error) => {
|
|
49
|
+
if (typeof error === "string")
|
|
50
|
+
return error;
|
|
51
|
+
if (error && typeof error === "object") {
|
|
52
|
+
const issues = error.issues;
|
|
53
|
+
if (Array.isArray(issues))
|
|
54
|
+
return formatZodIssues(issues);
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(error);
|
|
57
|
+
} catch {
|
|
58
|
+
return String(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return String(error);
|
|
62
|
+
};
|
|
41
63
|
var readErrorMessage = (errorBody, status) => {
|
|
42
64
|
if (errorBody && typeof errorBody === "object" && errorBody !== null && "error" in errorBody) {
|
|
43
|
-
const message =
|
|
65
|
+
const message = stringifyErrorField(errorBody.error);
|
|
44
66
|
const hookOutput = "hook_output" in errorBody && typeof errorBody.hook_output === "string" ? errorBody.hook_output.trim() : "";
|
|
45
67
|
return hookOutput ? `${message}
|
|
46
68
|
${hookOutput}` : message;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type { CreateProjectInput, RegisterRepoInput, Repo } from "pstdio-api-contracts";
|
|
2
2
|
import type { Project } from "../resources";
|
|
3
3
|
import type { RequestFn } from "./request";
|
|
4
|
+
type RegisteredPlugin = {
|
|
5
|
+
identity: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
};
|
|
8
|
+
type RegisteredPluginsResponse = {
|
|
9
|
+
plugins: RegisteredPlugin[];
|
|
10
|
+
pluginsDir: string | null;
|
|
11
|
+
};
|
|
4
12
|
export type ProjectClient = {
|
|
5
13
|
list(): Promise<Project[]>;
|
|
6
14
|
get(projectId: string): Promise<Project>;
|
|
7
15
|
create(input: CreateProjectInput): Promise<Project>;
|
|
8
16
|
delete(projectId: string): Promise<void>;
|
|
17
|
+
listPlugins(projectId: string): Promise<RegisteredPluginsResponse>;
|
|
18
|
+
registerPlugins(projectId: string): Promise<RegisteredPluginsResponse>;
|
|
9
19
|
listRepos(projectId: string): Promise<Repo[]>;
|
|
10
20
|
registerRepo(projectId: string, input: RegisterRepoInput): Promise<Repo>;
|
|
11
21
|
removeRepo(projectId: string, repoId: string): Promise<void>;
|
|
12
22
|
};
|
|
13
23
|
export declare const createProjectClient: (request: RequestFn) => ProjectClient;
|
|
24
|
+
export {};
|
package/dist/hooks/session.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { BaseHookContext } from "./base";
|
|
|
2
2
|
import type { HookTicket, HookWorkspace } from "./entities";
|
|
3
3
|
export type SessionHookContext = BaseHookContext & {
|
|
4
4
|
sessionId: string;
|
|
5
|
-
sessionStatus: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled";
|
|
5
|
+
sessionStatus: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled" | "disconnected";
|
|
6
6
|
originalSessionId?: string;
|
|
7
7
|
workspace?: HookWorkspace;
|
|
8
8
|
workspaceId?: string;
|
|
@@ -5,7 +5,7 @@ export declare const createSession: (ctx: PluginHelperContext, input: CreateSess
|
|
|
5
5
|
id: string;
|
|
6
6
|
project_id: string | null;
|
|
7
7
|
title: string;
|
|
8
|
-
status: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled";
|
|
8
|
+
status: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled" | "disconnected";
|
|
9
9
|
archived: boolean;
|
|
10
10
|
last_request_started: string | null;
|
|
11
11
|
last_request_ended: string | null;
|
|
@@ -7,7 +7,7 @@ export declare const followupSession: (ctx: PluginHelperContext, input: Followup
|
|
|
7
7
|
id: string;
|
|
8
8
|
project_id: string | null;
|
|
9
9
|
title: string;
|
|
10
|
-
status: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled";
|
|
10
|
+
status: "in_progress" | "awaiting_input" | "completed" | "failed" | "cancelled" | "disconnected";
|
|
11
11
|
archived: boolean;
|
|
12
12
|
last_request_started: string | null;
|
|
13
13
|
last_request_ended: string | null;
|
|
@@ -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, 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,24 +178,274 @@ var removeAllWorktreesForTicket = async (ctx, input) => {
|
|
|
170
178
|
return removed;
|
|
171
179
|
};
|
|
172
180
|
// src/plugins/helpers/run-command.ts
|
|
173
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
174
181
|
var runCommand = async (cwd, command, options = {}) => {
|
|
175
182
|
const [cmd, ...args] = command;
|
|
176
183
|
const stdio = options.quiet ? "ignore" : "pipe";
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
exitCode: code ?? 1,
|
|
186
|
-
stdout: stdout.join("").trim(),
|
|
187
|
-
stderr: stderr.join("").trim()
|
|
188
|
-
});
|
|
184
|
+
let proc;
|
|
185
|
+
try {
|
|
186
|
+
proc = Bun.spawn([cmd, ...args], {
|
|
187
|
+
cwd,
|
|
188
|
+
env: options.env ?? { ...process.env },
|
|
189
|
+
stdin: "ignore",
|
|
190
|
+
stdout: stdio,
|
|
191
|
+
stderr: stdio
|
|
189
192
|
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { exitCode: 1, stdout: "", stderr: error.message };
|
|
195
|
+
}
|
|
196
|
+
const readStream = (stream) => stream && typeof stream !== "number" ? new Response(stream).text() : Promise.resolve("");
|
|
197
|
+
const [stdout, stderr] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr)]);
|
|
198
|
+
const exitCode = await proc.exited;
|
|
199
|
+
return {
|
|
200
|
+
exitCode,
|
|
201
|
+
stdout: stdout.trim(),
|
|
202
|
+
stderr: stderr.trim()
|
|
203
|
+
};
|
|
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);
|
|
190
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 };
|
|
191
449
|
};
|
|
192
450
|
// src/plugins/helpers/set-ticket-status.ts
|
|
193
451
|
var setTicketStatus = async (ctx, input) => {
|
|
@@ -216,10 +474,10 @@ var setWorkspaceAttemptStatus = async (ctx, input) => {
|
|
|
216
474
|
return true;
|
|
217
475
|
};
|
|
218
476
|
// src/plugins/helpers/ticket-pull.ts
|
|
219
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
220
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
221
|
-
var
|
|
222
|
-
var
|
|
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";
|
|
223
481
|
var escapeYamlScalar = (value) => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
224
482
|
var buildTicketFrontmatter = (fields) => {
|
|
225
483
|
const lines = ["---"];
|
|
@@ -246,7 +504,7 @@ var buildTicketFrontmatter = (fields) => {
|
|
|
246
504
|
return lines.join(`
|
|
247
505
|
`);
|
|
248
506
|
};
|
|
249
|
-
var
|
|
507
|
+
var stripFrontmatter2 = (content) => {
|
|
250
508
|
if (!content.startsWith("---"))
|
|
251
509
|
return content;
|
|
252
510
|
const closingIndex = content.indexOf("---", 3);
|
|
@@ -254,54 +512,54 @@ var stripFrontmatter = (content) => {
|
|
|
254
512
|
return content;
|
|
255
513
|
return content.slice(closingIndex + 3);
|
|
256
514
|
};
|
|
257
|
-
var
|
|
258
|
-
const body =
|
|
515
|
+
var applyFrontmatter2 = (frontmatter, content) => {
|
|
516
|
+
const body = stripFrontmatter2(content).replace(/^\n+/, "");
|
|
259
517
|
if (!body)
|
|
260
518
|
return frontmatter;
|
|
261
519
|
return `${frontmatter}
|
|
262
520
|
|
|
263
521
|
${body}`;
|
|
264
522
|
};
|
|
265
|
-
var toRelativeFilePath = (baseDir, absolutePath) =>
|
|
266
|
-
var
|
|
267
|
-
const exactDir =
|
|
268
|
-
if (!
|
|
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))
|
|
269
527
|
return null;
|
|
270
|
-
if (!
|
|
528
|
+
if (!statSync2(exactDir).isDirectory()) {
|
|
271
529
|
throw new Error(`Invalid ticket path for ${shorthand}: .pstdio/tickets/${shorthand} is not a directory.`);
|
|
272
530
|
}
|
|
273
531
|
return exactDir;
|
|
274
532
|
};
|
|
275
|
-
var
|
|
276
|
-
const existingDir =
|
|
277
|
-
const dir = existingDir ??
|
|
278
|
-
const filePath =
|
|
279
|
-
if (!overwrite &&
|
|
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)) {
|
|
280
538
|
throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
|
|
281
539
|
}
|
|
282
|
-
|
|
283
|
-
|
|
540
|
+
mkdirSync2(dir, { recursive: true });
|
|
541
|
+
writeFileSync2(filePath, content);
|
|
284
542
|
return filePath;
|
|
285
543
|
};
|
|
286
544
|
var resolveTicketAttachmentPath = (rootPath, shorthand, fileName) => {
|
|
287
|
-
const ticketDir =
|
|
545
|
+
const ticketDir = resolveTicketDir2(rootPath, shorthand);
|
|
288
546
|
if (!ticketDir)
|
|
289
547
|
throw new Error(`Ticket directory not found for ${shorthand}`);
|
|
290
|
-
const filesDir =
|
|
291
|
-
const targetPath =
|
|
292
|
-
const rel =
|
|
293
|
-
if (
|
|
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("..")) {
|
|
294
552
|
throw new Error(`Ticket file path resolves outside ticket files directory: ${fileName}`);
|
|
295
553
|
}
|
|
296
554
|
return targetPath;
|
|
297
555
|
};
|
|
298
556
|
var writeTicketAttachment = (rootPath, shorthand, fileName, content, overwrite = false) => {
|
|
299
557
|
const filePath = resolveTicketAttachmentPath(rootPath, shorthand, fileName);
|
|
300
|
-
if (!overwrite &&
|
|
558
|
+
if (!overwrite && existsSync2(filePath)) {
|
|
301
559
|
throw new Error(`Local file already exists: ${toRelativeFilePath(rootPath, filePath)}. Use force to overwrite.`);
|
|
302
560
|
}
|
|
303
|
-
|
|
304
|
-
|
|
561
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
562
|
+
writeFileSync2(filePath, content);
|
|
305
563
|
return filePath;
|
|
306
564
|
};
|
|
307
565
|
var isNotFoundError = (error) => typeof error === "object" && error !== null && ("status" in error) && error.status === 404;
|
|
@@ -366,8 +624,8 @@ var pullSingleTicket = async (ctx, rootPath, ticketListItem, force, log) => {
|
|
|
366
624
|
blocked_reason: ticket.blocked_reason,
|
|
367
625
|
tag_names: ticketListItem.tag_names ?? []
|
|
368
626
|
});
|
|
369
|
-
const content =
|
|
370
|
-
const filePath =
|
|
627
|
+
const content = applyFrontmatter2(frontmatter, ticket.content ?? "");
|
|
628
|
+
const filePath = writeTicketFile2(rootPath, ticketListItem.shorthand, content, force);
|
|
371
629
|
const ticketDir = filePath.replace(/\/ticket\.md$/, "").replace(`${rootPath}/`, "");
|
|
372
630
|
const files = await ctx.client.tickets.listFiles(ticket.id);
|
|
373
631
|
const attachments = files.filter((file) => file.id !== ticket.file_id);
|
|
@@ -419,24 +677,24 @@ var updateTicketWhenAllAttemptsMatch = async (ctx, input) => {
|
|
|
419
677
|
return result.updated;
|
|
420
678
|
};
|
|
421
679
|
// src/plugins/helpers/worktree-bootstrap.ts
|
|
422
|
-
import { cpSync, existsSync as
|
|
423
|
-
import { join as
|
|
680
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
681
|
+
import { join as join3 } from "node:path";
|
|
424
682
|
var AGENT_DIRS = [".claude", ".opencode", ".agents"];
|
|
425
683
|
var bootstrapWorktree = async (ctx, input) => {
|
|
426
684
|
const { repoPath, worktreePath, ticketId } = input;
|
|
427
|
-
const repoConfig =
|
|
428
|
-
const worktreeConfigDir =
|
|
429
|
-
const worktreeConfig =
|
|
430
|
-
if (
|
|
431
|
-
|
|
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 });
|
|
432
690
|
cpSync(repoConfig, worktreeConfig);
|
|
433
691
|
}
|
|
434
692
|
for (const agentDir of AGENT_DIRS) {
|
|
435
|
-
const fromDir =
|
|
436
|
-
const toDir =
|
|
437
|
-
if (!
|
|
693
|
+
const fromDir = join3(repoPath, agentDir);
|
|
694
|
+
const toDir = join3(worktreePath, agentDir);
|
|
695
|
+
if (!existsSync3(fromDir))
|
|
438
696
|
continue;
|
|
439
|
-
|
|
697
|
+
mkdirSync3(toDir, { recursive: true });
|
|
440
698
|
cpSync(fromDir, toDir, { recursive: true });
|
|
441
699
|
}
|
|
442
700
|
if (!ticketId)
|
|
@@ -448,6 +706,7 @@ export {
|
|
|
448
706
|
updateTicketWhenAllAttemptsMatch,
|
|
449
707
|
setWorkspaceAttemptStatus,
|
|
450
708
|
setTicketStatus,
|
|
709
|
+
saveTicket,
|
|
451
710
|
runCommand,
|
|
452
711
|
renderPrompt,
|
|
453
712
|
removeAllWorktreesForTicket,
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export type ActionTriggerContext<TTargetType extends TargetType = TargetType> =
|
|
|
61
61
|
targetId: string;
|
|
62
62
|
target: ActionTargetMap[TTargetType];
|
|
63
63
|
} : never);
|
|
64
|
+
export type ActionTriggerResult = {
|
|
65
|
+
session_id?: string;
|
|
66
|
+
message?: string;
|
|
67
|
+
};
|
|
68
|
+
type ActionTrigger<TTargetType extends TargetType = TargetType> = ((ctx: ActionTriggerContext<TTargetType>) => void) | ((ctx: ActionTriggerContext<TTargetType>) => ActionTriggerResult) | ((ctx: ActionTriggerContext<TTargetType>) => Promise<ActionTriggerResult | undefined>);
|
|
64
69
|
export type ActionInput = {
|
|
65
70
|
[K in TargetType]: {
|
|
66
71
|
key: string;
|
|
@@ -68,7 +73,7 @@ export type ActionInput = {
|
|
|
68
73
|
targetType: K;
|
|
69
74
|
placement: ActionPlacement;
|
|
70
75
|
params?: ActionParamDef[];
|
|
71
|
-
trigger:
|
|
76
|
+
trigger: ActionTrigger<K>;
|
|
72
77
|
};
|
|
73
78
|
}[TargetType];
|
|
74
79
|
export type ActionDescriptor = {
|
|
@@ -79,11 +84,29 @@ export type ActionDescriptor = {
|
|
|
79
84
|
params?: ActionParamDef[];
|
|
80
85
|
};
|
|
81
86
|
export type ActionDefinition = ActionDescriptor & {
|
|
82
|
-
trigger:
|
|
87
|
+
trigger: ActionTrigger;
|
|
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;
|
|
83
105
|
};
|
|
84
106
|
export type PluginDefinition = {
|
|
85
107
|
key?: string;
|
|
86
108
|
actions?: ActionInput[];
|
|
87
109
|
hooks?: PluginHooks;
|
|
110
|
+
schedules?: ScheduleDefinition[];
|
|
88
111
|
};
|
|
89
112
|
export {};
|
|
@@ -3,7 +3,7 @@ export type { AgentAvailabilityType, AgentConfig, AgentInfo, AgentModel } from "
|
|
|
3
3
|
export type { FileRecord } from "./file";
|
|
4
4
|
export type { Project } from "./project";
|
|
5
5
|
export type { Session, SessionStatus } from "./session";
|
|
6
|
-
export type { Skill, SkillWithContent } from "./skill";
|
|
6
|
+
export type { Skill, SkillFile, SkillWithContent } from "./skill";
|
|
7
7
|
export type { AttemptStatus, Status } from "./status";
|
|
8
8
|
export type { Tag, TagOption } from "./tag";
|
|
9
9
|
export type { Template, TemplateType, TemplateWithContent } from "./template";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { Skill, SkillWithContent } from "pstdio-api-contracts";
|
|
1
|
+
export type { Skill, SkillFile, SkillWithContent } from "pstdio-api-contracts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pstdio/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/pufflyai/prompt-studio"
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@types/bun": "latest",
|
|
57
57
|
"@types/mustache": "^4.2.6",
|
|
58
58
|
"pstdio-api-contracts": "workspace:*",
|
|
59
|
-
"typescript": "
|
|
59
|
+
"typescript": "6.0.2"
|
|
60
60
|
}
|
|
61
61
|
}
|