Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master: fix(plugins): address Greptile feedback on testing.ts feat(plugins): add document CRUD methods to Plugin SDK
This commit is contained in:
@@ -165,6 +165,14 @@ export interface HostServices {
|
|||||||
createComment(params: WorkerToHostMethods["issues.createComment"][0]): Promise<WorkerToHostMethods["issues.createComment"][1]>;
|
createComment(params: WorkerToHostMethods["issues.createComment"][0]): Promise<WorkerToHostMethods["issues.createComment"][1]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Provides `issues.documents.list`, `issues.documents.get`, `issues.documents.upsert`, `issues.documents.delete`. */
|
||||||
|
issueDocuments: {
|
||||||
|
list(params: WorkerToHostMethods["issues.documents.list"][0]): Promise<WorkerToHostMethods["issues.documents.list"][1]>;
|
||||||
|
get(params: WorkerToHostMethods["issues.documents.get"][0]): Promise<WorkerToHostMethods["issues.documents.get"][1]>;
|
||||||
|
upsert(params: WorkerToHostMethods["issues.documents.upsert"][0]): Promise<WorkerToHostMethods["issues.documents.upsert"][1]>;
|
||||||
|
delete(params: WorkerToHostMethods["issues.documents.delete"][0]): Promise<WorkerToHostMethods["issues.documents.delete"][1]>;
|
||||||
|
};
|
||||||
|
|
||||||
/** Provides `agents.list`, `agents.get`, `agents.pause`, `agents.resume`, `agents.invoke`. */
|
/** Provides `agents.list`, `agents.get`, `agents.pause`, `agents.resume`, `agents.invoke`. */
|
||||||
agents: {
|
agents: {
|
||||||
list(params: WorkerToHostMethods["agents.list"][0]): Promise<WorkerToHostMethods["agents.list"][1]>;
|
list(params: WorkerToHostMethods["agents.list"][0]): Promise<WorkerToHostMethods["agents.list"][1]>;
|
||||||
@@ -298,6 +306,12 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
|||||||
"issues.listComments": "issue.comments.read",
|
"issues.listComments": "issue.comments.read",
|
||||||
"issues.createComment": "issue.comments.create",
|
"issues.createComment": "issue.comments.create",
|
||||||
|
|
||||||
|
// Issue Documents
|
||||||
|
"issues.documents.list": "issue.documents.read",
|
||||||
|
"issues.documents.get": "issue.documents.read",
|
||||||
|
"issues.documents.upsert": "issue.documents.write",
|
||||||
|
"issues.documents.delete": "issue.documents.write",
|
||||||
|
|
||||||
// Agents
|
// Agents
|
||||||
"agents.list": "agents.read",
|
"agents.list": "agents.read",
|
||||||
"agents.get": "agents.read",
|
"agents.get": "agents.read",
|
||||||
@@ -483,6 +497,20 @@ export function createHostClientHandlers(
|
|||||||
return services.issues.createComment(params);
|
return services.issues.createComment(params);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Issue Documents
|
||||||
|
"issues.documents.list": gated("issues.documents.list", async (params) => {
|
||||||
|
return services.issueDocuments.list(params);
|
||||||
|
}),
|
||||||
|
"issues.documents.get": gated("issues.documents.get", async (params) => {
|
||||||
|
return services.issueDocuments.get(params);
|
||||||
|
}),
|
||||||
|
"issues.documents.upsert": gated("issues.documents.upsert", async (params) => {
|
||||||
|
return services.issueDocuments.upsert(params);
|
||||||
|
}),
|
||||||
|
"issues.documents.delete": gated("issues.documents.delete", async (params) => {
|
||||||
|
return services.issueDocuments.delete(params);
|
||||||
|
}),
|
||||||
|
|
||||||
// Agents
|
// Agents
|
||||||
"agents.list": gated("agents.list", async (params) => {
|
"agents.list": gated("agents.list", async (params) => {
|
||||||
return services.agents.list(params);
|
return services.agents.list(params);
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import type {
|
|||||||
Project,
|
Project,
|
||||||
Issue,
|
Issue,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
|
IssueDocument,
|
||||||
|
IssueDocumentSummary,
|
||||||
Agent,
|
Agent,
|
||||||
Goal,
|
Goal,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
@@ -601,6 +603,32 @@ export interface WorkerToHostMethods {
|
|||||||
result: IssueComment,
|
result: IssueComment,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Issue Documents
|
||||||
|
"issues.documents.list": [
|
||||||
|
params: { issueId: string; companyId: string },
|
||||||
|
result: IssueDocumentSummary[],
|
||||||
|
];
|
||||||
|
"issues.documents.get": [
|
||||||
|
params: { issueId: string; key: string; companyId: string },
|
||||||
|
result: IssueDocument | null,
|
||||||
|
];
|
||||||
|
"issues.documents.upsert": [
|
||||||
|
params: {
|
||||||
|
issueId: string;
|
||||||
|
key: string;
|
||||||
|
body: string;
|
||||||
|
companyId: string;
|
||||||
|
title?: string;
|
||||||
|
format?: string;
|
||||||
|
changeSummary?: string;
|
||||||
|
},
|
||||||
|
result: IssueDocument,
|
||||||
|
];
|
||||||
|
"issues.documents.delete": [
|
||||||
|
params: { issueId: string; key: string; companyId: string },
|
||||||
|
result: void,
|
||||||
|
];
|
||||||
|
|
||||||
// Agents (read)
|
// Agents (read)
|
||||||
"agents.list": [
|
"agents.list": [
|
||||||
params: { companyId: string; status?: string; limit?: number; offset?: number },
|
params: { companyId: string; status?: string; limit?: number; offset?: number },
|
||||||
|
|||||||
@@ -425,6 +425,33 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
|||||||
issueComments.set(issueId, current);
|
issueComments.set(issueId, current);
|
||||||
return comment;
|
return comment;
|
||||||
},
|
},
|
||||||
|
documents: {
|
||||||
|
async list(issueId, companyId) {
|
||||||
|
requireCapability(manifest, capabilitySet, "issue.documents.read");
|
||||||
|
if (!isInCompany(issues.get(issueId), companyId)) return [];
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
async get(issueId, _key, companyId) {
|
||||||
|
requireCapability(manifest, capabilitySet, "issue.documents.read");
|
||||||
|
if (!isInCompany(issues.get(issueId), companyId)) return null;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
async upsert(input) {
|
||||||
|
requireCapability(manifest, capabilitySet, "issue.documents.write");
|
||||||
|
const parentIssue = issues.get(input.issueId);
|
||||||
|
if (!isInCompany(parentIssue, input.companyId)) {
|
||||||
|
throw new Error(`Issue not found: ${input.issueId}`);
|
||||||
|
}
|
||||||
|
throw new Error("documents.upsert is not implemented in test context");
|
||||||
|
},
|
||||||
|
async delete(issueId, _key, companyId) {
|
||||||
|
requireCapability(manifest, capabilitySet, "issue.documents.write");
|
||||||
|
const parentIssue = issues.get(issueId);
|
||||||
|
if (!isInCompany(parentIssue, companyId)) {
|
||||||
|
throw new Error(`Issue not found: ${issueId}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
async list(input) {
|
async list(input) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import type {
|
|||||||
Project,
|
Project,
|
||||||
Issue,
|
Issue,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
|
IssueDocument,
|
||||||
|
IssueDocumentSummary,
|
||||||
Agent,
|
Agent,
|
||||||
Goal,
|
Goal,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
@@ -61,6 +63,8 @@ export type {
|
|||||||
Project,
|
Project,
|
||||||
Issue,
|
Issue,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
|
IssueDocument,
|
||||||
|
IssueDocumentSummary,
|
||||||
Agent,
|
Agent,
|
||||||
Goal,
|
Goal,
|
||||||
} from "@paperclipai/shared";
|
} from "@paperclipai/shared";
|
||||||
@@ -774,6 +778,73 @@ export interface PluginCompaniesClient {
|
|||||||
get(companyId: string): Promise<Company | null>;
|
get(companyId: string): Promise<Company | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ctx.issues.documents` — read and write issue documents.
|
||||||
|
*
|
||||||
|
* Requires:
|
||||||
|
* - `issue.documents.read` for `list` and `get`
|
||||||
|
* - `issue.documents.write` for `upsert` and `delete`
|
||||||
|
*
|
||||||
|
* @see PLUGIN_SPEC.md §14 — SDK Surface
|
||||||
|
*/
|
||||||
|
export interface PluginIssueDocumentsClient {
|
||||||
|
/**
|
||||||
|
* List all documents attached to an issue.
|
||||||
|
*
|
||||||
|
* Returns summary metadata (id, key, title, format, timestamps) without
|
||||||
|
* the full document body. Use `get()` to fetch a specific document's body.
|
||||||
|
*
|
||||||
|
* Requires the `issue.documents.read` capability.
|
||||||
|
*/
|
||||||
|
list(issueId: string, companyId: string): Promise<IssueDocumentSummary[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single document by key, including its full body content.
|
||||||
|
*
|
||||||
|
* Returns `null` if no document exists with the given key.
|
||||||
|
*
|
||||||
|
* Requires the `issue.documents.read` capability.
|
||||||
|
*
|
||||||
|
* @param issueId - UUID of the issue
|
||||||
|
* @param key - Document key (e.g. `"plan"`, `"design-spec"`)
|
||||||
|
* @param companyId - UUID of the company
|
||||||
|
*/
|
||||||
|
get(issueId: string, key: string, companyId: string): Promise<IssueDocument | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a document on an issue.
|
||||||
|
*
|
||||||
|
* If a document with the given key already exists, it is updated and a new
|
||||||
|
* revision is created. If it does not exist, it is created.
|
||||||
|
*
|
||||||
|
* Requires the `issue.documents.write` capability.
|
||||||
|
*
|
||||||
|
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary
|
||||||
|
*/
|
||||||
|
upsert(input: {
|
||||||
|
issueId: string;
|
||||||
|
key: string;
|
||||||
|
body: string;
|
||||||
|
companyId: string;
|
||||||
|
title?: string;
|
||||||
|
format?: string;
|
||||||
|
changeSummary?: string;
|
||||||
|
}): Promise<IssueDocument>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document and all its revisions.
|
||||||
|
*
|
||||||
|
* No-ops silently if the document does not exist (idempotent).
|
||||||
|
*
|
||||||
|
* Requires the `issue.documents.write` capability.
|
||||||
|
*
|
||||||
|
* @param issueId - UUID of the issue
|
||||||
|
* @param key - Document key to delete
|
||||||
|
* @param companyId - UUID of the company
|
||||||
|
*/
|
||||||
|
delete(issueId: string, key: string, companyId: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `ctx.issues` — read and mutate issues plus comments.
|
* `ctx.issues` — read and mutate issues plus comments.
|
||||||
*
|
*
|
||||||
@@ -783,6 +854,8 @@ export interface PluginCompaniesClient {
|
|||||||
* - `issues.update` for update
|
* - `issues.update` for update
|
||||||
* - `issue.comments.read` for `listComments`
|
* - `issue.comments.read` for `listComments`
|
||||||
* - `issue.comments.create` for `createComment`
|
* - `issue.comments.create` for `createComment`
|
||||||
|
* - `issue.documents.read` for `documents.list` and `documents.get`
|
||||||
|
* - `issue.documents.write` for `documents.upsert` and `documents.delete`
|
||||||
*/
|
*/
|
||||||
export interface PluginIssuesClient {
|
export interface PluginIssuesClient {
|
||||||
list(input: {
|
list(input: {
|
||||||
@@ -814,6 +887,8 @@ export interface PluginIssuesClient {
|
|||||||
): Promise<Issue>;
|
): Promise<Issue>;
|
||||||
listComments(issueId: string, companyId: string): Promise<IssueComment[]>;
|
listComments(issueId: string, companyId: string): Promise<IssueComment[]>;
|
||||||
createComment(issueId: string, body: string, companyId: string): Promise<IssueComment>;
|
createComment(issueId: string, body: string, companyId: string): Promise<IssueComment>;
|
||||||
|
/** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */
|
||||||
|
documents: PluginIssueDocumentsClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1056,7 +1131,7 @@ export interface PluginContext {
|
|||||||
/** Read company metadata. Requires `companies.read`. */
|
/** Read company metadata. Requires `companies.read`. */
|
||||||
companies: PluginCompaniesClient;
|
companies: PluginCompaniesClient;
|
||||||
|
|
||||||
/** Read and write issues/comments. Requires issue capabilities. */
|
/** Read and write issues, comments, and documents. Requires issue capabilities. */
|
||||||
issues: PluginIssuesClient;
|
issues: PluginIssuesClient;
|
||||||
|
|
||||||
/** Read and manage agents. Requires `agents.read` for reads; `agents.pause` / `agents.resume` / `agents.invoke` for write ops. */
|
/** Read and manage agents. Requires `agents.read` for reads; `agents.pause` / `agents.resume` / `agents.invoke` for write ops. */
|
||||||
|
|||||||
@@ -612,6 +612,32 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
|||||||
async createComment(issueId: string, body: string, companyId: string) {
|
async createComment(issueId: string, body: string, companyId: string) {
|
||||||
return callHost("issues.createComment", { issueId, body, companyId });
|
return callHost("issues.createComment", { issueId, body, companyId });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
documents: {
|
||||||
|
async list(issueId: string, companyId: string) {
|
||||||
|
return callHost("issues.documents.list", { issueId, companyId });
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(issueId: string, key: string, companyId: string) {
|
||||||
|
return callHost("issues.documents.get", { issueId, key, companyId });
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsert(input) {
|
||||||
|
return callHost("issues.documents.upsert", {
|
||||||
|
issueId: input.issueId,
|
||||||
|
key: input.key,
|
||||||
|
body: input.body,
|
||||||
|
companyId: input.companyId,
|
||||||
|
title: input.title,
|
||||||
|
format: input.format,
|
||||||
|
changeSummary: input.changeSummary,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(issueId: string, key: string, companyId: string) {
|
||||||
|
return callHost("issues.documents.delete", { issueId, key, companyId });
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
agents: {
|
agents: {
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ export const PLUGIN_CAPABILITIES = [
|
|||||||
"project.workspaces.read",
|
"project.workspaces.read",
|
||||||
"issues.read",
|
"issues.read",
|
||||||
"issue.comments.read",
|
"issue.comments.read",
|
||||||
|
"issue.documents.read",
|
||||||
"agents.read",
|
"agents.read",
|
||||||
"goals.read",
|
"goals.read",
|
||||||
"goals.create",
|
"goals.create",
|
||||||
@@ -395,6 +396,7 @@ export const PLUGIN_CAPABILITIES = [
|
|||||||
"issues.create",
|
"issues.create",
|
||||||
"issues.update",
|
"issues.update",
|
||||||
"issue.comments.create",
|
"issue.comments.create",
|
||||||
|
"issue.documents.write",
|
||||||
"agents.pause",
|
"agents.pause",
|
||||||
"agents.resume",
|
"agents.resume",
|
||||||
"agents.invoke",
|
"agents.invoke",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { agentService } from "./agents.js";
|
|||||||
import { projectService } from "./projects.js";
|
import { projectService } from "./projects.js";
|
||||||
import { issueService } from "./issues.js";
|
import { issueService } from "./issues.js";
|
||||||
import { goalService } from "./goals.js";
|
import { goalService } from "./goals.js";
|
||||||
|
import { documentService } from "./documents.js";
|
||||||
import { heartbeatService } from "./heartbeat.js";
|
import { heartbeatService } from "./heartbeat.js";
|
||||||
import { subscribeCompanyLiveEvents } from "./live-events.js";
|
import { subscribeCompanyLiveEvents } from "./live-events.js";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
@@ -450,6 +451,7 @@ export function buildHostServices(
|
|||||||
const heartbeat = heartbeatService(db);
|
const heartbeat = heartbeatService(db);
|
||||||
const projects = projectService(db);
|
const projects = projectService(db);
|
||||||
const issues = issueService(db);
|
const issues = issueService(db);
|
||||||
|
const documents = documentService(db);
|
||||||
const goals = goalService(db);
|
const goals = goalService(db);
|
||||||
const activity = activityService(db);
|
const activity = activityService(db);
|
||||||
const costs = costService(db);
|
const costs = costService(db);
|
||||||
@@ -794,6 +796,43 @@ export function buildHostServices(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
issueDocuments: {
|
||||||
|
async list(params) {
|
||||||
|
const companyId = ensureCompanyId(params.companyId);
|
||||||
|
await ensurePluginAvailableForCompany(companyId);
|
||||||
|
requireInCompany("Issue", await issues.getById(params.issueId), companyId);
|
||||||
|
const rows = await documents.listIssueDocuments(params.issueId);
|
||||||
|
return rows as any;
|
||||||
|
},
|
||||||
|
async get(params) {
|
||||||
|
const companyId = ensureCompanyId(params.companyId);
|
||||||
|
await ensurePluginAvailableForCompany(companyId);
|
||||||
|
requireInCompany("Issue", await issues.getById(params.issueId), companyId);
|
||||||
|
const doc = await documents.getIssueDocumentByKey(params.issueId, params.key);
|
||||||
|
return (doc ?? null) as any;
|
||||||
|
},
|
||||||
|
async upsert(params) {
|
||||||
|
const companyId = ensureCompanyId(params.companyId);
|
||||||
|
await ensurePluginAvailableForCompany(companyId);
|
||||||
|
requireInCompany("Issue", await issues.getById(params.issueId), companyId);
|
||||||
|
const result = await documents.upsertIssueDocument({
|
||||||
|
issueId: params.issueId,
|
||||||
|
key: params.key,
|
||||||
|
body: params.body,
|
||||||
|
title: params.title ?? null,
|
||||||
|
format: params.format ?? "markdown",
|
||||||
|
changeSummary: params.changeSummary ?? null,
|
||||||
|
});
|
||||||
|
return result.document as any;
|
||||||
|
},
|
||||||
|
async delete(params) {
|
||||||
|
const companyId = ensureCompanyId(params.companyId);
|
||||||
|
await ensurePluginAvailableForCompany(companyId);
|
||||||
|
requireInCompany("Issue", await issues.getById(params.issueId), companyId);
|
||||||
|
await documents.deleteIssueDocument(params.issueId, params.key);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
agents: {
|
agents: {
|
||||||
async list(params) {
|
async list(params) {
|
||||||
const companyId = ensureCompanyId(params.companyId);
|
const companyId = ensureCompanyId(params.companyId);
|
||||||
|
|||||||
Reference in New Issue
Block a user