@stackmemoryai/stackmemory 0.5.20 → 0.5.22
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/cli/claude-sm.js +215 -20
- package/dist/cli/claude-sm.js.map +2 -2
- package/dist/core/trace/index.js +0 -68
- package/dist/core/trace/index.js.map +2 -2
- package/dist/hooks/linear-task-picker.js +180 -0
- package/dist/hooks/linear-task-picker.js.map +7 -0
- package/dist/hooks/session-summary.js +197 -0
- package/dist/hooks/session-summary.js.map +7 -0
- package/dist/hooks/sms-action-runner.js +87 -20
- package/dist/hooks/sms-action-runner.js.map +2 -2
- package/dist/hooks/sms-webhook.js +6 -6
- package/dist/hooks/sms-webhook.js.map +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { LinearClient } from "../integrations/linear/client.js";
|
|
6
|
+
import { LinearAuthManager } from "../integrations/linear/auth.js";
|
|
7
|
+
const TEST_KEYWORDS = [
|
|
8
|
+
"test",
|
|
9
|
+
"spec",
|
|
10
|
+
"unit test",
|
|
11
|
+
"integration test",
|
|
12
|
+
"e2e",
|
|
13
|
+
"end-to-end",
|
|
14
|
+
"jest",
|
|
15
|
+
"vitest",
|
|
16
|
+
"mocha"
|
|
17
|
+
];
|
|
18
|
+
const VALIDATION_KEYWORDS = [
|
|
19
|
+
"validate",
|
|
20
|
+
"verify",
|
|
21
|
+
"verification",
|
|
22
|
+
"acceptance criteria",
|
|
23
|
+
"ac:",
|
|
24
|
+
"acceptance:",
|
|
25
|
+
"given when then",
|
|
26
|
+
"criteria:"
|
|
27
|
+
];
|
|
28
|
+
const QA_KEYWORDS = ["qa", "quality", "regression", "coverage", "assertion"];
|
|
29
|
+
const TEST_LABELS = [
|
|
30
|
+
"needs-tests",
|
|
31
|
+
"test-required",
|
|
32
|
+
"qa-review",
|
|
33
|
+
"has-ac",
|
|
34
|
+
"acceptance-criteria",
|
|
35
|
+
"tdd",
|
|
36
|
+
"testing"
|
|
37
|
+
];
|
|
38
|
+
function containsKeywords(text, keywords) {
|
|
39
|
+
const lowerText = text.toLowerCase();
|
|
40
|
+
return keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
|
|
41
|
+
}
|
|
42
|
+
function scoreTask(issue, preferTestTasks) {
|
|
43
|
+
let score = 0;
|
|
44
|
+
const description = issue.description || "";
|
|
45
|
+
const title = issue.title || "";
|
|
46
|
+
const fullText = `${title} ${description}`;
|
|
47
|
+
if (containsKeywords(fullText, TEST_KEYWORDS)) {
|
|
48
|
+
score += preferTestTasks ? 10 : 5;
|
|
49
|
+
}
|
|
50
|
+
if (containsKeywords(fullText, VALIDATION_KEYWORDS)) {
|
|
51
|
+
score += preferTestTasks ? 8 : 4;
|
|
52
|
+
}
|
|
53
|
+
if (containsKeywords(fullText, QA_KEYWORDS)) {
|
|
54
|
+
score += preferTestTasks ? 5 : 2;
|
|
55
|
+
}
|
|
56
|
+
const labelNames = issue.labels?.nodes?.map((l) => l.name.toLowerCase()) || [];
|
|
57
|
+
const hasTestLabel = TEST_LABELS.some(
|
|
58
|
+
(tl) => labelNames.some((ln) => ln.includes(tl))
|
|
59
|
+
);
|
|
60
|
+
if (hasTestLabel) {
|
|
61
|
+
score += preferTestTasks ? 5 : 3;
|
|
62
|
+
}
|
|
63
|
+
if (issue.priority === 1) {
|
|
64
|
+
score += 5;
|
|
65
|
+
} else if (issue.priority === 2) {
|
|
66
|
+
score += 3;
|
|
67
|
+
} else if (issue.priority === 3) {
|
|
68
|
+
score += 1;
|
|
69
|
+
}
|
|
70
|
+
if (description.includes("## Acceptance") || description.includes("### AC") || description.includes("- [ ]")) {
|
|
71
|
+
score += 2;
|
|
72
|
+
}
|
|
73
|
+
if (issue.estimate) {
|
|
74
|
+
score += 1;
|
|
75
|
+
}
|
|
76
|
+
return score;
|
|
77
|
+
}
|
|
78
|
+
function getLinearClient() {
|
|
79
|
+
const apiKey = process.env["LINEAR_API_KEY"];
|
|
80
|
+
if (apiKey) {
|
|
81
|
+
return new LinearClient({ apiKey });
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const authManager = new LinearAuthManager();
|
|
85
|
+
const tokens = authManager.loadTokens();
|
|
86
|
+
if (tokens?.accessToken) {
|
|
87
|
+
return new LinearClient({ accessToken: tokens.accessToken });
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
async function pickNextLinearTask(options = {}) {
|
|
94
|
+
const client = getLinearClient();
|
|
95
|
+
if (!client) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const { teamId, preferTestTasks = true, limit = 20 } = options;
|
|
99
|
+
try {
|
|
100
|
+
const [backlogIssues, unstartedIssues] = await Promise.all([
|
|
101
|
+
client.getIssues({ teamId, stateType: "backlog", limit }),
|
|
102
|
+
client.getIssues({ teamId, stateType: "unstarted", limit })
|
|
103
|
+
]);
|
|
104
|
+
const allIssues = [...backlogIssues, ...unstartedIssues];
|
|
105
|
+
const unassignedIssues = allIssues.filter((issue) => !issue.assignee);
|
|
106
|
+
if (unassignedIssues.length === 0) {
|
|
107
|
+
if (allIssues.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues;
|
|
112
|
+
const scoredIssues = issuesToScore.map((issue) => ({
|
|
113
|
+
issue,
|
|
114
|
+
score: scoreTask(issue, preferTestTasks)
|
|
115
|
+
}));
|
|
116
|
+
scoredIssues.sort((a, b) => b.score - a.score);
|
|
117
|
+
const best = scoredIssues[0];
|
|
118
|
+
if (!best) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const description = best.issue.description || "";
|
|
122
|
+
const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS);
|
|
123
|
+
return {
|
|
124
|
+
id: best.issue.id,
|
|
125
|
+
identifier: best.issue.identifier,
|
|
126
|
+
title: best.issue.title,
|
|
127
|
+
priority: best.issue.priority,
|
|
128
|
+
hasTestRequirements,
|
|
129
|
+
estimatedPoints: best.issue.estimate,
|
|
130
|
+
url: best.issue.url,
|
|
131
|
+
score: best.score
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("[linear-task-picker] Error fetching tasks:", error);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function getTopTaskSuggestions(options = {}, count = 3) {
|
|
139
|
+
const client = getLinearClient();
|
|
140
|
+
if (!client) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const { teamId, preferTestTasks = true, limit = 30 } = options;
|
|
144
|
+
try {
|
|
145
|
+
const [backlogIssues, unstartedIssues] = await Promise.all([
|
|
146
|
+
client.getIssues({ teamId, stateType: "backlog", limit }),
|
|
147
|
+
client.getIssues({ teamId, stateType: "unstarted", limit })
|
|
148
|
+
]);
|
|
149
|
+
const allIssues = [...backlogIssues, ...unstartedIssues];
|
|
150
|
+
const unassignedIssues = allIssues.filter((issue) => !issue.assignee);
|
|
151
|
+
const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues;
|
|
152
|
+
const scoredIssues = issuesToScore.map((issue) => ({
|
|
153
|
+
issue,
|
|
154
|
+
score: scoreTask(issue, preferTestTasks)
|
|
155
|
+
}));
|
|
156
|
+
scoredIssues.sort((a, b) => b.score - a.score);
|
|
157
|
+
return scoredIssues.slice(0, count).map(({ issue, score }) => {
|
|
158
|
+
const description = issue.description || "";
|
|
159
|
+
const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS);
|
|
160
|
+
return {
|
|
161
|
+
id: issue.id,
|
|
162
|
+
identifier: issue.identifier,
|
|
163
|
+
title: issue.title,
|
|
164
|
+
priority: issue.priority,
|
|
165
|
+
hasTestRequirements,
|
|
166
|
+
estimatedPoints: issue.estimate,
|
|
167
|
+
url: issue.url,
|
|
168
|
+
score
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error("[linear-task-picker] Error fetching tasks:", error);
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
getTopTaskSuggestions,
|
|
178
|
+
pickNextLinearTask
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=linear-task-picker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/hooks/linear-task-picker.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Linear Task Picker\n * Picks the next best task from Linear queue, prioritizing tasks with test/validation requirements\n */\n\nimport { LinearClient, LinearIssue } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\nexport interface TaskSuggestion {\n id: string;\n identifier: string; // e.g., \"STA-123\"\n title: string;\n priority: number;\n hasTestRequirements: boolean;\n estimatedPoints?: number;\n url: string;\n score: number;\n}\n\nexport interface PickerOptions {\n teamId?: string;\n preferTestTasks?: boolean;\n limit?: number;\n}\n\n// Keywords indicating test/validation requirements\nconst TEST_KEYWORDS = [\n 'test',\n 'spec',\n 'unit test',\n 'integration test',\n 'e2e',\n 'end-to-end',\n 'jest',\n 'vitest',\n 'mocha',\n];\n\nconst VALIDATION_KEYWORDS = [\n 'validate',\n 'verify',\n 'verification',\n 'acceptance criteria',\n 'ac:',\n 'acceptance:',\n 'given when then',\n 'criteria:',\n];\n\nconst QA_KEYWORDS = ['qa', 'quality', 'regression', 'coverage', 'assertion'];\n\n// Labels that indicate test requirements\nconst TEST_LABELS = [\n 'needs-tests',\n 'test-required',\n 'qa-review',\n 'has-ac',\n 'acceptance-criteria',\n 'tdd',\n 'testing',\n];\n\n/**\n * Check if text contains any of the keywords (case-insensitive)\n */\nfunction containsKeywords(text: string, keywords: string[]): boolean {\n const lowerText = text.toLowerCase();\n return keywords.some((kw) => lowerText.includes(kw.toLowerCase()));\n}\n\n/**\n * Score a task based on test/validation requirements\n */\nfunction scoreTask(issue: LinearIssue, preferTestTasks: boolean): number {\n let score = 0;\n const description = issue.description || '';\n const title = issue.title || '';\n const fullText = `${title} ${description}`;\n\n // +10 if has test/validation keywords in description\n if (containsKeywords(fullText, TEST_KEYWORDS)) {\n score += preferTestTasks ? 10 : 5;\n }\n\n if (containsKeywords(fullText, VALIDATION_KEYWORDS)) {\n score += preferTestTasks ? 8 : 4;\n }\n\n if (containsKeywords(fullText, QA_KEYWORDS)) {\n score += preferTestTasks ? 5 : 2;\n }\n\n // +5 if has test-related labels\n const labelNames =\n issue.labels?.nodes?.map((l: { name: string }) => l.name.toLowerCase()) ||\n [];\n const hasTestLabel = TEST_LABELS.some((tl) =>\n labelNames.some((ln: string) => ln.includes(tl))\n );\n if (hasTestLabel) {\n score += preferTestTasks ? 5 : 3;\n }\n\n // +3 for higher priority (urgent=1, high=2)\n if (issue.priority === 1) {\n score += 5; // Urgent\n } else if (issue.priority === 2) {\n score += 3; // High\n } else if (issue.priority === 3) {\n score += 1; // Medium\n }\n\n // +2 if has acceptance criteria pattern\n if (\n description.includes('## Acceptance') ||\n description.includes('### AC') ||\n description.includes('- [ ]')\n ) {\n score += 2;\n }\n\n // +1 if has estimate (indicates well-scoped)\n if (issue.estimate) {\n score += 1;\n }\n\n return score;\n}\n\n/**\n * Get Linear client instance\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey) {\n return new LinearClient({ apiKey });\n }\n\n // Fall back to OAuth\n try {\n const authManager = new LinearAuthManager();\n const tokens = authManager.loadTokens();\n if (tokens?.accessToken) {\n return new LinearClient({ accessToken: tokens.accessToken });\n }\n } catch {\n // Auth not available\n }\n\n return null;\n}\n\n/**\n * Pick the next best task from Linear\n */\nexport async function pickNextLinearTask(\n options: PickerOptions = {}\n): Promise<TaskSuggestion | null> {\n const client = getLinearClient();\n if (!client) {\n return null;\n }\n\n const { teamId, preferTestTasks = true, limit = 20 } = options;\n\n try {\n // Fetch backlog and unstarted issues\n const [backlogIssues, unstartedIssues] = await Promise.all([\n client.getIssues({ teamId, stateType: 'backlog', limit }),\n client.getIssues({ teamId, stateType: 'unstarted', limit }),\n ]);\n\n const allIssues = [...backlogIssues, ...unstartedIssues];\n\n // Filter out assigned issues (we want unassigned ones)\n const unassignedIssues = allIssues.filter((issue) => !issue.assignee);\n\n if (unassignedIssues.length === 0) {\n // If no unassigned, consider all\n if (allIssues.length === 0) {\n return null;\n }\n }\n\n const issuesToScore =\n unassignedIssues.length > 0 ? unassignedIssues : allIssues;\n\n // Score and sort\n const scoredIssues = issuesToScore.map((issue) => ({\n issue,\n score: scoreTask(issue, preferTestTasks),\n }));\n\n scoredIssues.sort((a, b) => b.score - a.score);\n\n const best = scoredIssues[0];\n if (!best) {\n return null;\n }\n\n const description = best.issue.description || '';\n const hasTestRequirements =\n containsKeywords(description, TEST_KEYWORDS) ||\n containsKeywords(description, VALIDATION_KEYWORDS);\n\n return {\n id: best.issue.id,\n identifier: best.issue.identifier,\n title: best.issue.title,\n priority: best.issue.priority,\n hasTestRequirements,\n estimatedPoints: best.issue.estimate,\n url: best.issue.url,\n score: best.score,\n };\n } catch (error) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n return null;\n }\n}\n\n/**\n * Get multiple task suggestions (for showing options)\n */\nexport async function getTopTaskSuggestions(\n options: PickerOptions = {},\n count: number = 3\n): Promise<TaskSuggestion[]> {\n const client = getLinearClient();\n if (!client) {\n return [];\n }\n\n const { teamId, preferTestTasks = true, limit = 30 } = options;\n\n try {\n const [backlogIssues, unstartedIssues] = await Promise.all([\n client.getIssues({ teamId, stateType: 'backlog', limit }),\n client.getIssues({ teamId, stateType: 'unstarted', limit }),\n ]);\n\n const allIssues = [...backlogIssues, ...unstartedIssues];\n const unassignedIssues = allIssues.filter((issue) => !issue.assignee);\n const issuesToScore =\n unassignedIssues.length > 0 ? unassignedIssues : allIssues;\n\n const scoredIssues = issuesToScore.map((issue) => ({\n issue,\n score: scoreTask(issue, preferTestTasks),\n }));\n\n scoredIssues.sort((a, b) => b.score - a.score);\n\n return scoredIssues.slice(0, count).map(({ issue, score }) => {\n const description = issue.description || '';\n const hasTestRequirements =\n containsKeywords(description, TEST_KEYWORDS) ||\n containsKeywords(description, VALIDATION_KEYWORDS);\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n priority: issue.priority,\n hasTestRequirements,\n estimatedPoints: issue.estimate,\n url: issue.url,\n score,\n };\n });\n } catch (error) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n return [];\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { pickNextLinearTask } from "./linear-task-picker.js";
|
|
7
|
+
function formatDuration(ms) {
|
|
8
|
+
const seconds = Math.floor(ms / 1e3);
|
|
9
|
+
const minutes = Math.floor(seconds / 60);
|
|
10
|
+
const hours = Math.floor(minutes / 60);
|
|
11
|
+
if (hours > 0) {
|
|
12
|
+
return `${hours}h ${minutes % 60}min`;
|
|
13
|
+
}
|
|
14
|
+
if (minutes > 0) {
|
|
15
|
+
return `${minutes}min`;
|
|
16
|
+
}
|
|
17
|
+
return `${seconds}s`;
|
|
18
|
+
}
|
|
19
|
+
function getCurrentBranch() {
|
|
20
|
+
try {
|
|
21
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch {
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function hasUncommittedChanges() {
|
|
30
|
+
try {
|
|
31
|
+
const status = execSync("git status --porcelain", {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
34
|
+
});
|
|
35
|
+
const lines = status.trim().split("\n").filter(Boolean);
|
|
36
|
+
return { changed: lines.length > 0, count: lines.length };
|
|
37
|
+
} catch {
|
|
38
|
+
return { changed: false, count: 0 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isInWorktree() {
|
|
42
|
+
try {
|
|
43
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
46
|
+
});
|
|
47
|
+
const gitDir = execSync("git rev-parse --git-dir", {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
50
|
+
}).trim();
|
|
51
|
+
return gitDir.includes(".git/worktrees/");
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function hasTestScript() {
|
|
57
|
+
try {
|
|
58
|
+
const packageJson = execSync("cat package.json", {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
61
|
+
});
|
|
62
|
+
const pkg = JSON.parse(packageJson);
|
|
63
|
+
return !!(pkg.scripts?.test || pkg.scripts?.["test:run"]);
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function generateSuggestions(context) {
|
|
69
|
+
const suggestions = [];
|
|
70
|
+
let keyIndex = 1;
|
|
71
|
+
const changes = hasUncommittedChanges();
|
|
72
|
+
const inWorktree = isInWorktree();
|
|
73
|
+
const hasTests = hasTestScript();
|
|
74
|
+
if (context.exitCode !== 0 && context.exitCode !== null) {
|
|
75
|
+
suggestions.push({
|
|
76
|
+
key: String(keyIndex++),
|
|
77
|
+
label: "Review error logs",
|
|
78
|
+
action: "cat ~/.claude/logs/claude-*.log | tail -50",
|
|
79
|
+
priority: 100
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (changes.changed) {
|
|
83
|
+
suggestions.push({
|
|
84
|
+
key: String(keyIndex++),
|
|
85
|
+
label: `Commit changes (${changes.count} files)`,
|
|
86
|
+
action: "git add -A && git commit",
|
|
87
|
+
priority: 90
|
|
88
|
+
});
|
|
89
|
+
const branch = getCurrentBranch();
|
|
90
|
+
if (branch !== "main" && branch !== "master" && branch !== "unknown") {
|
|
91
|
+
suggestions.push({
|
|
92
|
+
key: String(keyIndex++),
|
|
93
|
+
label: "Create PR",
|
|
94
|
+
action: "gh pr create --fill",
|
|
95
|
+
priority: 80
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (hasTests && changes.changed) {
|
|
100
|
+
suggestions.push({
|
|
101
|
+
key: String(keyIndex++),
|
|
102
|
+
label: "Run tests",
|
|
103
|
+
action: "npm run test:run",
|
|
104
|
+
priority: 85
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (inWorktree) {
|
|
108
|
+
suggestions.push({
|
|
109
|
+
key: String(keyIndex++),
|
|
110
|
+
label: "Merge to main",
|
|
111
|
+
action: "cwm",
|
|
112
|
+
// custom alias
|
|
113
|
+
priority: 70
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const linearTask = await pickNextLinearTask({ preferTestTasks: true });
|
|
118
|
+
if (linearTask) {
|
|
119
|
+
suggestions.push({
|
|
120
|
+
key: String(keyIndex++),
|
|
121
|
+
label: `Start: ${linearTask.identifier} - ${linearTask.title.substring(0, 40)}${linearTask.title.length > 40 ? "..." : ""}${linearTask.hasTestRequirements ? " (has tests)" : ""}`,
|
|
122
|
+
action: `stackmemory task start ${linearTask.id} --assign-me`,
|
|
123
|
+
priority: 60
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
const durationMs = Date.now() - context.sessionStartTime;
|
|
129
|
+
if (durationMs > 30 * 60 * 1e3) {
|
|
130
|
+
suggestions.push({
|
|
131
|
+
key: String(keyIndex++),
|
|
132
|
+
label: "Take a break",
|
|
133
|
+
action: 'echo "Great work! Time for a coffee break."',
|
|
134
|
+
priority: 10
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
suggestions.sort((a, b) => b.priority - a.priority);
|
|
138
|
+
suggestions.forEach((s, i) => {
|
|
139
|
+
s.key = String(i + 1);
|
|
140
|
+
});
|
|
141
|
+
return suggestions;
|
|
142
|
+
}
|
|
143
|
+
async function generateSessionSummary(context) {
|
|
144
|
+
const durationMs = Date.now() - context.sessionStartTime;
|
|
145
|
+
const duration = formatDuration(durationMs);
|
|
146
|
+
const branch = context.branch || getCurrentBranch();
|
|
147
|
+
let status = "success";
|
|
148
|
+
if (context.exitCode !== 0 && context.exitCode !== null) {
|
|
149
|
+
status = "error";
|
|
150
|
+
}
|
|
151
|
+
const suggestions = await generateSuggestions(context);
|
|
152
|
+
let linearTask;
|
|
153
|
+
try {
|
|
154
|
+
linearTask = await pickNextLinearTask({ preferTestTasks: true });
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
duration,
|
|
159
|
+
exitCode: context.exitCode,
|
|
160
|
+
branch,
|
|
161
|
+
status,
|
|
162
|
+
suggestions,
|
|
163
|
+
linearTask
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function formatSummaryMessage(summary) {
|
|
167
|
+
const statusEmoji = summary.status === "success" ? "" : "";
|
|
168
|
+
const exitInfo = summary.exitCode !== null ? ` | Exit: ${summary.exitCode}` : "";
|
|
169
|
+
let message = `Claude session complete ${statusEmoji}
|
|
170
|
+
`;
|
|
171
|
+
message += `Duration: ${summary.duration}${exitInfo} | Branch: ${summary.branch}
|
|
172
|
+
|
|
173
|
+
`;
|
|
174
|
+
if (summary.suggestions.length > 0) {
|
|
175
|
+
message += `What to do next:
|
|
176
|
+
`;
|
|
177
|
+
for (const s of summary.suggestions.slice(0, 4)) {
|
|
178
|
+
message += `${s.key}. ${s.label}
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
message += `
|
|
182
|
+
Reply with number or custom action`;
|
|
183
|
+
} else {
|
|
184
|
+
message += `No pending actions. Nice work!`;
|
|
185
|
+
}
|
|
186
|
+
return message;
|
|
187
|
+
}
|
|
188
|
+
function getActionForKey(suggestions, key) {
|
|
189
|
+
const suggestion = suggestions.find((s) => s.key === key);
|
|
190
|
+
return suggestion?.action || null;
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
formatSummaryMessage,
|
|
194
|
+
generateSessionSummary,
|
|
195
|
+
getActionForKey
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=session-summary.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/hooks/session-summary.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Session Summary Generator\n * Generates intelligent suggestions for what to do next after a Claude session\n */\n\nimport { execSync } from 'child_process';\nimport { pickNextLinearTask, TaskSuggestion } from './linear-task-picker.js';\n\nexport interface SessionContext {\n instanceId: string;\n exitCode: number | null;\n sessionStartTime: number;\n worktreePath?: string;\n branch?: string;\n task?: string;\n}\n\nexport interface Suggestion {\n key: string;\n label: string;\n action: string;\n priority: number;\n}\n\nexport interface SessionSummary {\n duration: string;\n exitCode: number | null;\n branch: string;\n status: 'success' | 'error' | 'interrupted';\n suggestions: Suggestion[];\n linearTask?: TaskSuggestion;\n}\n\n/**\n * Format duration in human-readable form\n */\nfunction formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n\n if (hours > 0) {\n return `${hours}h ${minutes % 60}min`;\n }\n if (minutes > 0) {\n return `${minutes}min`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Get current git branch\n */\nfunction getCurrentBranch(): string {\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Check for uncommitted changes\n */\nfunction hasUncommittedChanges(): { changed: boolean; count: number } {\n try {\n const status = execSync('git status --porcelain', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const lines = status.trim().split('\\n').filter(Boolean);\n return { changed: lines.length > 0, count: lines.length };\n } catch {\n return { changed: false, count: 0 };\n }\n}\n\n/**\n * Check if we're in a worktree\n */\nfunction isInWorktree(): boolean {\n try {\n execSync('git rev-parse --is-inside-work-tree', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n // Check if it's a worktree (not the main repo)\n const gitDir = execSync('git rev-parse --git-dir', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return gitDir.includes('.git/worktrees/');\n } catch {\n return false;\n }\n}\n\n/**\n * Check if tests exist and might need running\n */\nfunction hasTestScript(): boolean {\n try {\n const packageJson = execSync('cat package.json', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const pkg = JSON.parse(packageJson);\n return !!(pkg.scripts?.test || pkg.scripts?.['test:run']);\n } catch {\n return false;\n }\n}\n\n/**\n * Generate suggestions based on session context\n */\nasync function generateSuggestions(\n context: SessionContext\n): Promise<Suggestion[]> {\n const suggestions: Suggestion[] = [];\n let keyIndex = 1;\n\n const changes = hasUncommittedChanges();\n const inWorktree = isInWorktree();\n const hasTests = hasTestScript();\n\n // Error case - suggest reviewing logs\n if (context.exitCode !== 0 && context.exitCode !== null) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Review error logs',\n action: 'cat ~/.claude/logs/claude-*.log | tail -50',\n priority: 100,\n });\n }\n\n // Uncommitted changes - suggest commit or PR\n if (changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Commit changes (${changes.count} files)`,\n action: 'git add -A && git commit',\n priority: 90,\n });\n\n // If on feature branch, suggest PR\n const branch = getCurrentBranch();\n if (branch !== 'main' && branch !== 'master' && branch !== 'unknown') {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Create PR',\n action: 'gh pr create --fill',\n priority: 80,\n });\n }\n }\n\n // If tests exist and changes were made, suggest running tests\n if (hasTests && changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Run tests',\n action: 'npm run test:run',\n priority: 85,\n });\n }\n\n // Worktree-specific suggestions\n if (inWorktree) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Merge to main',\n action: 'cwm', // custom alias\n priority: 70,\n });\n }\n\n // Try to get next Linear task\n try {\n const linearTask = await pickNextLinearTask({ preferTestTasks: true });\n if (linearTask) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Start: ${linearTask.identifier} - ${linearTask.title.substring(0, 40)}${linearTask.title.length > 40 ? '...' : ''}${linearTask.hasTestRequirements ? ' (has tests)' : ''}`,\n action: `stackmemory task start ${linearTask.id} --assign-me`,\n priority: 60,\n });\n }\n } catch {\n // Linear not available, skip\n }\n\n // Long session suggestion\n const durationMs = Date.now() - context.sessionStartTime;\n if (durationMs > 30 * 60 * 1000) {\n // > 30 minutes\n suggestions.push({\n key: String(keyIndex++),\n label: 'Take a break',\n action: 'echo \"Great work! Time for a coffee break.\"',\n priority: 10,\n });\n }\n\n // Sort by priority (highest first) and re-key\n suggestions.sort((a, b) => b.priority - a.priority);\n suggestions.forEach((s, i) => {\n s.key = String(i + 1);\n });\n\n return suggestions;\n}\n\n/**\n * Generate full session summary\n */\nexport async function generateSessionSummary(\n context: SessionContext\n): Promise<SessionSummary> {\n const durationMs = Date.now() - context.sessionStartTime;\n const duration = formatDuration(durationMs);\n const branch = context.branch || getCurrentBranch();\n\n let status: 'success' | 'error' | 'interrupted' = 'success';\n if (context.exitCode !== 0 && context.exitCode !== null) {\n status = 'error';\n }\n\n const suggestions = await generateSuggestions(context);\n\n // Extract linear task if present\n let linearTask: TaskSuggestion | undefined;\n try {\n linearTask = await pickNextLinearTask({ preferTestTasks: true });\n } catch {\n // Linear not available\n }\n\n return {\n duration,\n exitCode: context.exitCode,\n branch,\n status,\n suggestions,\n linearTask,\n };\n}\n\n/**\n * Format session summary as WhatsApp message\n */\nexport function formatSummaryMessage(summary: SessionSummary): string {\n const statusEmoji = summary.status === 'success' ? '' : '';\n const exitInfo =\n summary.exitCode !== null ? ` | Exit: ${summary.exitCode}` : '';\n\n let message = `Claude session complete ${statusEmoji}\\n`;\n message += `Duration: ${summary.duration}${exitInfo} | Branch: ${summary.branch}\\n\\n`;\n\n if (summary.suggestions.length > 0) {\n message += `What to do next:\\n`;\n for (const s of summary.suggestions.slice(0, 4)) {\n message += `${s.key}. ${s.label}\\n`;\n }\n message += `\\nReply with number or custom action`;\n } else {\n message += `No pending actions. Nice work!`;\n }\n\n return message;\n}\n\n/**\n * Get action for a suggestion key\n */\nexport function getActionForKey(\n suggestions: Suggestion[],\n key: string\n): string | null {\n const suggestion = suggestions.find((s) => s.key === key);\n return suggestion?.action || null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,10 +5,12 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { existsSync, readFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
-
import {
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
9
|
import { randomBytes } from "crypto";
|
|
10
10
|
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
|
|
11
11
|
import { ActionQueueSchema, parseConfigSafe } from "./schemas.js";
|
|
12
|
+
import { LinearClient } from "../integrations/linear/client.js";
|
|
13
|
+
import { LinearAuthManager } from "../integrations/linear/auth.js";
|
|
12
14
|
const SAFE_ACTION_PATTERNS = [
|
|
13
15
|
// Git/GitHub CLI commands (limited to safe operations)
|
|
14
16
|
{ pattern: /^gh pr (view|list|status|checks) (\d+)$/ },
|
|
@@ -20,8 +22,20 @@ const SAFE_ACTION_PATTERNS = [
|
|
|
20
22
|
{ pattern: /^npm (test|run build)$/ },
|
|
21
23
|
// StackMemory commands
|
|
22
24
|
{ pattern: /^stackmemory (status|notify check|context list)$/ },
|
|
25
|
+
// Task start with optional --assign-me flag (Linear task ID is UUID format)
|
|
26
|
+
{
|
|
27
|
+
pattern: /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/
|
|
28
|
+
},
|
|
29
|
+
// Git commands
|
|
30
|
+
{ pattern: /^git (status|diff|log|branch)( --[a-z-]+)*$/ },
|
|
31
|
+
{ pattern: /^git add -A && git commit$/ },
|
|
32
|
+
{ pattern: /^gh pr create --fill$/ },
|
|
33
|
+
// Custom aliases (cwm = claude worktree merge)
|
|
34
|
+
{ pattern: /^cwm$/ },
|
|
23
35
|
// Simple echo/confirmation (no variables)
|
|
24
|
-
{
|
|
36
|
+
{
|
|
37
|
+
pattern: /^echo "?(Done|OK|Confirmed|Acknowledged|Great work! Time for a coffee break\.)"?$/
|
|
38
|
+
}
|
|
25
39
|
];
|
|
26
40
|
function isActionAllowed(action) {
|
|
27
41
|
const trimmed = action.trim();
|
|
@@ -73,7 +87,60 @@ function queueAction(promptId, response, action) {
|
|
|
73
87
|
saveActionQueue(queue);
|
|
74
88
|
return id;
|
|
75
89
|
}
|
|
76
|
-
function
|
|
90
|
+
function getLinearClient() {
|
|
91
|
+
const apiKey = process.env["LINEAR_API_KEY"];
|
|
92
|
+
if (apiKey) {
|
|
93
|
+
return new LinearClient({ apiKey });
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const authManager = new LinearAuthManager();
|
|
97
|
+
const tokens = authManager.loadTokens();
|
|
98
|
+
if (tokens?.accessToken) {
|
|
99
|
+
return new LinearClient({ accessToken: tokens.accessToken });
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
async function handleSpecialAction(action) {
|
|
106
|
+
const taskStartMatch = action.match(
|
|
107
|
+
/^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/
|
|
108
|
+
);
|
|
109
|
+
if (taskStartMatch) {
|
|
110
|
+
const issueId = taskStartMatch[1];
|
|
111
|
+
const client = getLinearClient();
|
|
112
|
+
if (!client) {
|
|
113
|
+
return {
|
|
114
|
+
handled: true,
|
|
115
|
+
success: false,
|
|
116
|
+
error: "Linear not configured. Set LINEAR_API_KEY or run stackmemory linear setup."
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const result = await client.startIssue(issueId);
|
|
121
|
+
if (result.success && result.issue) {
|
|
122
|
+
return {
|
|
123
|
+
handled: true,
|
|
124
|
+
success: true,
|
|
125
|
+
output: `Started: ${result.issue.identifier} - ${result.issue.title}`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
handled: true,
|
|
130
|
+
success: false,
|
|
131
|
+
error: result.error || "Failed to start issue"
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
handled: true,
|
|
136
|
+
success: false,
|
|
137
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { handled: false };
|
|
142
|
+
}
|
|
143
|
+
async function executeActionSafe(action, _response) {
|
|
77
144
|
if (!isActionAllowed(action)) {
|
|
78
145
|
console.error(`[sms-action] Action not in allowlist: ${action}`);
|
|
79
146
|
return {
|
|
@@ -81,6 +148,14 @@ function executeActionSafe(action, _response) {
|
|
|
81
148
|
error: `Action not allowed. Only pre-approved commands can be executed via SMS.`
|
|
82
149
|
};
|
|
83
150
|
}
|
|
151
|
+
const specialResult = await handleSpecialAction(action);
|
|
152
|
+
if (specialResult.handled) {
|
|
153
|
+
return {
|
|
154
|
+
success: specialResult.success || false,
|
|
155
|
+
output: specialResult.output,
|
|
156
|
+
error: specialResult.error
|
|
157
|
+
};
|
|
158
|
+
}
|
|
84
159
|
try {
|
|
85
160
|
console.log(`[sms-action] Executing safe action: ${action}`);
|
|
86
161
|
const parts = action.split(" ");
|
|
@@ -121,30 +196,22 @@ function markActionCompleted(id, result, error) {
|
|
|
121
196
|
saveActionQueue(queue);
|
|
122
197
|
}
|
|
123
198
|
}
|
|
124
|
-
function executeAction(action) {
|
|
199
|
+
async function executeAction(action) {
|
|
125
200
|
markActionRunning(action.id);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// 1 minute timeout
|
|
132
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
133
|
-
});
|
|
134
|
-
markActionCompleted(action.id, output);
|
|
135
|
-
return { success: true, output };
|
|
136
|
-
} catch (err) {
|
|
137
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
138
|
-
markActionCompleted(action.id, void 0, error);
|
|
139
|
-
return { success: false, error };
|
|
201
|
+
const result = await executeActionSafe(action.action, action.response);
|
|
202
|
+
if (result.success) {
|
|
203
|
+
markActionCompleted(action.id, result.output);
|
|
204
|
+
} else {
|
|
205
|
+
markActionCompleted(action.id, void 0, result.error);
|
|
140
206
|
}
|
|
207
|
+
return result;
|
|
141
208
|
}
|
|
142
|
-
function processAllPendingActions() {
|
|
209
|
+
async function processAllPendingActions() {
|
|
143
210
|
const pending = getPendingActions();
|
|
144
211
|
let succeeded = 0;
|
|
145
212
|
let failed = 0;
|
|
146
213
|
for (const action of pending) {
|
|
147
|
-
const result = executeAction(action);
|
|
214
|
+
const result = await executeAction(action);
|
|
148
215
|
if (result.success) {
|
|
149
216
|
succeeded++;
|
|
150
217
|
} else {
|