gsd-pi 2.3.5 → 2.3.7

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.
Files changed (26) hide show
  1. package/README.md +26 -12
  2. package/dist/cli.js +24 -1
  3. package/dist/wizard.js +16 -0
  4. package/package.json +1 -1
  5. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +60 -0
  6. package/scripts/postinstall.js +5 -3
  7. package/src/resources/extensions/ask-user-questions.ts +54 -5
  8. package/src/resources/extensions/gsd/auto.ts +17 -3
  9. package/src/resources/extensions/gsd/commands.ts +16 -3
  10. package/src/resources/extensions/gsd/index.ts +17 -1
  11. package/src/resources/extensions/gsd/preferences.ts +17 -1
  12. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  13. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  14. package/src/resources/extensions/gsd/worktree.ts +11 -0
  15. package/src/resources/extensions/remote-questions/config.ts +81 -0
  16. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  17. package/src/resources/extensions/remote-questions/format.ts +163 -0
  18. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  19. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  20. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  21. package/src/resources/extensions/remote-questions/status.ts +31 -0
  22. package/src/resources/extensions/remote-questions/store.ts +77 -0
  23. package/src/resources/extensions/remote-questions/types.ts +75 -0
  24. package/src/resources/extensions/github/formatters.ts +0 -207
  25. package/src/resources/extensions/github/gh-api.ts +0 -553
  26. package/src/resources/extensions/github/index.ts +0 -778
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Remote Questions — durable prompt store
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
9
+
10
+ function runtimeDir(): string {
11
+ return join(homedir(), ".gsd", "runtime", "remote-questions");
12
+ }
13
+
14
+ function recordPath(id: string): string {
15
+ return join(runtimeDir(), `${id}.json`);
16
+ }
17
+
18
+ export function createPromptRecord(prompt: RemotePrompt): RemotePromptRecord {
19
+ return {
20
+ version: 1,
21
+ id: prompt.id,
22
+ createdAt: prompt.createdAt,
23
+ updatedAt: Date.now(),
24
+ status: "pending",
25
+ channel: prompt.channel,
26
+ timeoutAt: prompt.timeoutAt,
27
+ pollIntervalMs: prompt.pollIntervalMs,
28
+ questions: prompt.questions,
29
+ context: prompt.context,
30
+ };
31
+ }
32
+
33
+ export function writePromptRecord(record: RemotePromptRecord): void {
34
+ mkdirSync(runtimeDir(), { recursive: true });
35
+ writeFileSync(recordPath(record.id), JSON.stringify(record, null, 2) + "\n", "utf-8");
36
+ }
37
+
38
+ export function readPromptRecord(id: string): RemotePromptRecord | null {
39
+ const path = recordPath(id);
40
+ if (!existsSync(path)) return null;
41
+ try {
42
+ return JSON.parse(readFileSync(path, "utf-8")) as RemotePromptRecord;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export function updatePromptRecord(
49
+ id: string,
50
+ updates: Partial<RemotePromptRecord>,
51
+ ): RemotePromptRecord | null {
52
+ const current = readPromptRecord(id);
53
+ if (!current) return null;
54
+ const next: RemotePromptRecord = {
55
+ ...current,
56
+ ...updates,
57
+ updatedAt: Date.now(),
58
+ };
59
+ writePromptRecord(next);
60
+ return next;
61
+ }
62
+
63
+ export function markPromptDispatched(id: string, ref: RemotePromptRef): RemotePromptRecord | null {
64
+ return updatePromptRecord(id, { ref, status: "pending" });
65
+ }
66
+
67
+ export function markPromptAnswered(id: string, response: RemoteAnswer): RemotePromptRecord | null {
68
+ return updatePromptRecord(id, { response, status: "answered", lastPollAt: Date.now() });
69
+ }
70
+
71
+ export function markPromptStatus(id: string, status: RemotePromptStatus, lastError?: string): RemotePromptRecord | null {
72
+ return updatePromptRecord(id, {
73
+ status,
74
+ lastPollAt: Date.now(),
75
+ ...(lastError ? { lastError } : {}),
76
+ });
77
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Remote Questions — shared types
3
+ */
4
+
5
+ export type RemoteChannel = "slack" | "discord";
6
+
7
+ export interface RemoteQuestionOption {
8
+ label: string;
9
+ description: string;
10
+ }
11
+
12
+ export interface RemoteQuestion {
13
+ id: string;
14
+ header: string;
15
+ question: string;
16
+ options: RemoteQuestionOption[];
17
+ allowMultiple: boolean;
18
+ }
19
+
20
+ export interface RemotePrompt {
21
+ id: string;
22
+ channel: RemoteChannel;
23
+ createdAt: number;
24
+ timeoutAt: number;
25
+ pollIntervalMs: number;
26
+ questions: RemoteQuestion[];
27
+ context?: {
28
+ source: string;
29
+ };
30
+ }
31
+
32
+ export interface RemotePromptRef {
33
+ id: string;
34
+ channel: RemoteChannel;
35
+ messageId: string;
36
+ channelId: string;
37
+ threadTs?: string;
38
+ threadUrl?: string;
39
+ }
40
+
41
+ export interface RemoteAnswer {
42
+ answers: Record<string, { answers: string[]; user_note?: string }>;
43
+ }
44
+
45
+ export type RemotePromptStatus = "pending" | "answered" | "timed_out" | "failed" | "cancelled";
46
+
47
+ export interface RemotePromptRecord {
48
+ version: 1;
49
+ id: string;
50
+ createdAt: number;
51
+ updatedAt: number;
52
+ status: RemotePromptStatus;
53
+ channel: RemoteChannel;
54
+ timeoutAt: number;
55
+ pollIntervalMs: number;
56
+ questions: RemoteQuestion[];
57
+ ref?: RemotePromptRef;
58
+ response?: RemoteAnswer;
59
+ lastPollAt?: number;
60
+ lastError?: string;
61
+ context?: {
62
+ source: string;
63
+ };
64
+ }
65
+
66
+ export interface RemoteDispatchResult {
67
+ ref: RemotePromptRef;
68
+ }
69
+
70
+ export interface ChannelAdapter {
71
+ readonly name: RemoteChannel;
72
+ validate(): Promise<void>;
73
+ sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult>;
74
+ pollAnswer(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null>;
75
+ }
@@ -1,207 +0,0 @@
1
- /**
2
- * Formatters — produce text summaries for issues, PRs, comments, etc.
3
- *
4
- * Used by both tools (LLM context) and renderers (TUI display).
5
- */
6
-
7
- import type { GhIssue, GhPullRequest, GhComment, GhReview, GhLabel, GhMilestone } from "./gh-api.js";
8
-
9
- // ─── Helpers ──────────────────────────────────────────────────────────────────
10
-
11
- function timeAgo(dateStr: string): string {
12
- const now = Date.now();
13
- const then = new Date(dateStr).getTime();
14
- const diff = now - then;
15
- const mins = Math.floor(diff / 60000);
16
- if (mins < 1) return "just now";
17
- if (mins < 60) return `${mins}m ago`;
18
- const hours = Math.floor(mins / 60);
19
- if (hours < 24) return `${hours}h ago`;
20
- const days = Math.floor(hours / 24);
21
- if (days < 30) return `${days}d ago`;
22
- const months = Math.floor(days / 30);
23
- if (months < 12) return `${months}mo ago`;
24
- return `${Math.floor(months / 12)}y ago`;
25
- }
26
-
27
- function stateIcon(state: string, draft?: boolean): string {
28
- if (draft) return "◇";
29
- switch (state) {
30
- case "open":
31
- return "●";
32
- case "closed":
33
- return "✓";
34
- case "merged":
35
- return "⊕";
36
- default:
37
- return "○";
38
- }
39
- }
40
-
41
- function truncateBody(body: string | null, maxLines = 10): string {
42
- if (!body) return "(no description)";
43
- const lines = body.split("\n");
44
- if (lines.length <= maxLines) return body;
45
- return lines.slice(0, maxLines).join("\n") + `\n... (${lines.length - maxLines} more lines)`;
46
- }
47
-
48
- // ─── Issue formatting ─────────────────────────────────────────────────────────
49
-
50
- export function formatIssueOneLiner(issue: GhIssue): string {
51
- const icon = stateIcon(issue.state);
52
- const labels = issue.labels.map((l) => l.name).join(", ");
53
- const labelStr = labels ? ` [${labels}]` : "";
54
- const assignee = issue.assignees.length ? ` → ${issue.assignees.map((a) => a.login).join(", ")}` : "";
55
- return `${icon} #${issue.number} ${issue.title}${labelStr}${assignee} (${timeAgo(issue.updated_at)})`;
56
- }
57
-
58
- export function formatIssueDetail(issue: GhIssue): string {
59
- const lines: string[] = [];
60
- lines.push(`# Issue #${issue.number}: ${issue.title}`);
61
- lines.push(`State: ${issue.state} | Author: @${issue.user.login} | Created: ${timeAgo(issue.created_at)} | Updated: ${timeAgo(issue.updated_at)}`);
62
-
63
- if (issue.assignees.length) {
64
- lines.push(`Assignees: ${issue.assignees.map((a) => `@${a.login}`).join(", ")}`);
65
- }
66
- if (issue.labels.length) {
67
- lines.push(`Labels: ${issue.labels.map((l) => l.name).join(", ")}`);
68
- }
69
- if (issue.milestone) {
70
- lines.push(`Milestone: ${issue.milestone.title}`);
71
- }
72
- lines.push(`Comments: ${issue.comments}`);
73
- lines.push(`URL: ${issue.html_url}`);
74
- lines.push("");
75
- lines.push(truncateBody(issue.body, 30));
76
- return lines.join("\n");
77
- }
78
-
79
- export function formatIssueList(issues: GhIssue[]): string {
80
- if (!issues.length) return "No issues found.";
81
- return issues.map(formatIssueOneLiner).join("\n");
82
- }
83
-
84
- // ─── PR formatting ────────────────────────────────────────────────────────────
85
-
86
- export function formatPROneLiner(pr: GhPullRequest): string {
87
- const icon = stateIcon(pr.merged_at ? "merged" : pr.state, pr.draft);
88
- const labels = pr.labels.map((l) => l.name).join(", ");
89
- const labelStr = labels ? ` [${labels}]` : "";
90
- const draftStr = pr.draft ? " (draft)" : "";
91
- const reviewers = pr.requested_reviewers.map((r) => r.login).join(", ");
92
- const reviewerStr = reviewers ? ` ⟵ ${reviewers}` : "";
93
- return `${icon} #${pr.number} ${pr.title}${draftStr}${labelStr}${reviewerStr} (${timeAgo(pr.updated_at)})`;
94
- }
95
-
96
- export function formatPRDetail(pr: GhPullRequest): string {
97
- const lines: string[] = [];
98
- const mergedState = pr.merged_at ? "merged" : pr.state;
99
- lines.push(`# PR #${pr.number}: ${pr.title}`);
100
- lines.push(`State: ${mergedState}${pr.draft ? " (draft)" : ""} | Author: @${pr.user.login} | Created: ${timeAgo(pr.created_at)} | Updated: ${timeAgo(pr.updated_at)}`);
101
- lines.push(`Branch: ${pr.head.ref} → ${pr.base.ref}`);
102
-
103
- if (pr.assignees.length) {
104
- lines.push(`Assignees: ${pr.assignees.map((a) => `@${a.login}`).join(", ")}`);
105
- }
106
- if (pr.labels.length) {
107
- lines.push(`Labels: ${pr.labels.map((l) => l.name).join(", ")}`);
108
- }
109
- if (pr.milestone) {
110
- lines.push(`Milestone: ${pr.milestone.title}`);
111
- }
112
- if (pr.requested_reviewers.length) {
113
- lines.push(`Reviewers: ${pr.requested_reviewers.map((r) => `@${r.login}`).join(", ")}`);
114
- }
115
-
116
- lines.push(`Mergeable: ${pr.mergeable === null ? "checking..." : pr.mergeable ? "yes" : "no"} (${pr.mergeable_state})`);
117
- lines.push(`Comments: ${pr.comments} | Review comments: ${pr.review_comments}`);
118
- lines.push(`URL: ${pr.html_url}`);
119
- lines.push("");
120
- lines.push(truncateBody(pr.body, 30));
121
- return lines.join("\n");
122
- }
123
-
124
- export function formatPRList(prs: GhPullRequest[]): string {
125
- if (!prs.length) return "No pull requests found.";
126
- return prs.map(formatPROneLiner).join("\n");
127
- }
128
-
129
- // ─── Comment formatting ──────────────────────────────────────────────────────
130
-
131
- export function formatComment(comment: GhComment): string {
132
- return `@${comment.user.login} (${timeAgo(comment.created_at)}):\n${truncateBody(comment.body, 8)}`;
133
- }
134
-
135
- export function formatCommentList(comments: GhComment[]): string {
136
- if (!comments.length) return "No comments.";
137
- return comments.map(formatComment).join("\n\n---\n\n");
138
- }
139
-
140
- // ─── Review formatting ───────────────────────────────────────────────────────
141
-
142
- function reviewStateIcon(state: string): string {
143
- switch (state) {
144
- case "APPROVED":
145
- return "✓";
146
- case "CHANGES_REQUESTED":
147
- return "✗";
148
- case "COMMENTED":
149
- return "💬";
150
- case "DISMISSED":
151
- return "—";
152
- case "PENDING":
153
- return "…";
154
- default:
155
- return "?";
156
- }
157
- }
158
-
159
- export function formatReview(review: GhReview): string {
160
- const icon = reviewStateIcon(review.state);
161
- const body = review.body ? `\n${truncateBody(review.body, 5)}` : "";
162
- return `${icon} @${review.user.login}: ${review.state} (${timeAgo(review.submitted_at)})${body}`;
163
- }
164
-
165
- export function formatReviewList(reviews: GhReview[]): string {
166
- if (!reviews.length) return "No reviews.";
167
- return reviews.map(formatReview).join("\n\n");
168
- }
169
-
170
- // ─── Label / Milestone formatting ─────────────────────────────────────────────
171
-
172
- export function formatLabel(label: GhLabel): string {
173
- const desc = label.description ? ` — ${label.description}` : "";
174
- return `• ${label.name} (#${label.color})${desc}`;
175
- }
176
-
177
- export function formatLabelList(labels: GhLabel[]): string {
178
- if (!labels.length) return "No labels.";
179
- return labels.map(formatLabel).join("\n");
180
- }
181
-
182
- export function formatMilestone(ms: GhMilestone): string {
183
- const progress = ms.open_issues + ms.closed_issues > 0 ? Math.round((ms.closed_issues / (ms.open_issues + ms.closed_issues)) * 100) : 0;
184
- const due = ms.due_on ? ` | Due: ${new Date(ms.due_on).toISOString().split("T")[0]}` : "";
185
- return `• ${ms.title} (${ms.state}) — ${progress}% complete (${ms.closed_issues}/${ms.open_issues + ms.closed_issues})${due}`;
186
- }
187
-
188
- export function formatMilestoneList(milestones: GhMilestone[]): string {
189
- if (!milestones.length) return "No milestones.";
190
- return milestones.map(formatMilestone).join("\n");
191
- }
192
-
193
- // ─── File change formatting ───────────────────────────────────────────────────
194
-
195
- export function formatFileChanges(
196
- files: { filename: string; status: string; additions: number; deletions: number; changes: number }[],
197
- ): string {
198
- if (!files.length) return "No files changed.";
199
- const lines = files.map((f) => {
200
- const statusIcon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
201
- return `${statusIcon} ${f.filename} (+${f.additions} -${f.deletions})`;
202
- });
203
- const totalAdd = files.reduce((s, f) => s + f.additions, 0);
204
- const totalDel = files.reduce((s, f) => s + f.deletions, 0);
205
- lines.push(`\n${files.length} files changed, +${totalAdd} -${totalDel}`);
206
- return lines.join("\n");
207
- }