@stackmemoryai/stackmemory 1.2.0 → 1.2.2
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/src/cli/claude-sm.js +65 -0
- package/dist/src/cli/commands/skills.js +123 -1
- package/dist/src/hooks/graphiti-hooks.js +149 -0
- package/dist/src/hooks/session-summary.js +30 -0
- package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
- package/dist/src/integrations/greptile/client.js +101 -0
- package/dist/src/integrations/greptile/config.js +14 -0
- package/dist/src/integrations/greptile/index.js +11 -0
- package/dist/src/integrations/greptile/types.js +4 -0
- package/dist/src/integrations/linear/webhook.js +16 -0
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
- package/dist/src/integrations/mcp/server.js +136 -0
- package/dist/src/integrations/mcp/tool-definitions.js +53 -1
- package/dist/src/skills/claude-skills.js +46 -1
- package/dist/src/skills/parallel-agent-skill.js +514 -0
- package/dist/src/utils/hook-installer.js +155 -0
- package/package.json +2 -2
- package/scripts/gepa/.before-optimize.md +140 -0
- package/scripts/gepa/config.json +7 -1
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
- package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
- package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
- package/scripts/gepa/generations/gen-000/baseline.md +48 -0
- package/scripts/gepa/generations/gen-001/baseline.md +172 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +176 -0
- package/scripts/gepa/generations/gen-001/variant-b.md +266 -0
- package/scripts/gepa/generations/gen-001/variant-c.md +142 -0
- package/scripts/gepa/generations/gen-001/variant-d.md +172 -0
- package/scripts/gepa/hooks/reflect.js +44 -5
- package/scripts/gepa/optimize.js +281 -39
- package/scripts/gepa/results/eval-1-baseline.json +218 -0
- package/scripts/gepa/results/eval-1-variant-a.json +218 -0
- package/scripts/gepa/results/eval-1-variant-b.json +218 -0
- package/scripts/gepa/results/eval-1-variant-c.json +198 -0
- package/scripts/gepa/results/eval-1-variant-d.json +198 -0
- package/scripts/gepa/state.json +44 -5
- package/scripts/install-claude-hooks-auto.js +176 -44
- package/templates/claude-hooks/auto-checkpoint.js +174 -0
- package/templates/claude-hooks/chime-on-stop.sh +22 -0
- package/templates/claude-hooks/session-rescue.sh +15 -0
- package/templates/claude-hooks/stop-checkpoint.js +120 -0
|
@@ -35,6 +35,14 @@ import {
|
|
|
35
35
|
sendCtrlC,
|
|
36
36
|
sessionExists
|
|
37
37
|
} from "../features/workers/tmux-manager.js";
|
|
38
|
+
import {
|
|
39
|
+
CANONICAL_HOOKS,
|
|
40
|
+
mergeSettings,
|
|
41
|
+
hasDeadHooks,
|
|
42
|
+
readSettings,
|
|
43
|
+
writeSettingsAtomic,
|
|
44
|
+
getSettingsPath
|
|
45
|
+
} from "../utils/hook-installer.js";
|
|
38
46
|
const DEFAULT_SM_CONFIG = {
|
|
39
47
|
defaultWorktree: false,
|
|
40
48
|
defaultSandbox: false,
|
|
@@ -332,6 +340,62 @@ class ClaudeSM {
|
|
|
332
340
|
}
|
|
333
341
|
return null;
|
|
334
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Ensure Claude Code hooks are installed. Copies missing scripts from
|
|
345
|
+
* templates and updates settings.json. Non-fatal on any error.
|
|
346
|
+
*/
|
|
347
|
+
ensureHooks() {
|
|
348
|
+
try {
|
|
349
|
+
const hooksDir = path.join(os.homedir(), ".claude", "hooks");
|
|
350
|
+
if (!fs.existsSync(hooksDir)) {
|
|
351
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
const candidateDirs = [
|
|
354
|
+
path.join(__dirname, "../../templates/claude-hooks"),
|
|
355
|
+
path.join(__dirname, "../../../templates/claude-hooks"),
|
|
356
|
+
path.join(
|
|
357
|
+
__dirname,
|
|
358
|
+
"..",
|
|
359
|
+
"..",
|
|
360
|
+
"..",
|
|
361
|
+
"..",
|
|
362
|
+
"templates",
|
|
363
|
+
"claude-hooks"
|
|
364
|
+
)
|
|
365
|
+
];
|
|
366
|
+
const templatesDir = candidateDirs.find(
|
|
367
|
+
(d) => fs.existsSync(d) && fs.readdirSync(d).length > 0
|
|
368
|
+
);
|
|
369
|
+
if (!templatesDir) return;
|
|
370
|
+
let copiedCount = 0;
|
|
371
|
+
for (const entry of CANONICAL_HOOKS) {
|
|
372
|
+
const dest = path.join(hooksDir, entry.scriptName);
|
|
373
|
+
if (!fs.existsSync(dest)) {
|
|
374
|
+
const src = path.join(templatesDir, entry.scriptName);
|
|
375
|
+
if (fs.existsSync(src)) {
|
|
376
|
+
fs.copyFileSync(src, dest);
|
|
377
|
+
try {
|
|
378
|
+
fs.chmodSync(dest, 493);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
copiedCount++;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const settingsPath = getSettingsPath();
|
|
386
|
+
const current = readSettings(settingsPath);
|
|
387
|
+
if (copiedCount > 0 || hasDeadHooks(current)) {
|
|
388
|
+
const merged = mergeSettings(current, hooksDir);
|
|
389
|
+
writeSettingsAtomic(merged, settingsPath);
|
|
390
|
+
}
|
|
391
|
+
if (copiedCount > 0) {
|
|
392
|
+
console.log(
|
|
393
|
+
chalk.gray(` Hooks: installed ${copiedCount} missing hook(s)`)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
335
399
|
loadContext() {
|
|
336
400
|
if (!this.config.contextEnabled) return;
|
|
337
401
|
try {
|
|
@@ -554,6 +618,7 @@ Session ended (exit ${exitCode ?? 0})`));
|
|
|
554
618
|
}
|
|
555
619
|
}
|
|
556
620
|
this.loadContext();
|
|
621
|
+
this.ensureHooks();
|
|
557
622
|
process.env["CLAUDE_INSTANCE_ID"] = this.config.instanceId;
|
|
558
623
|
if (this.config.worktreePath) {
|
|
559
624
|
process.env["CLAUDE_WORKTREE_PATH"] = this.config.worktreePath;
|
|
@@ -697,6 +697,104 @@ Searched ${result.data.timeRange.from} to ${result.data.timeRange.to}`
|
|
|
697
697
|
process.exit(1);
|
|
698
698
|
}
|
|
699
699
|
});
|
|
700
|
+
const agentCmd = skillsCmd.command("agent").description("Spawn parallel Claude agents in isolated workspaces");
|
|
701
|
+
agentCmd.command("research <question>").description("Explore codebase and save findings as a frame").option("--timeout <ms>", "Agent timeout in milliseconds", "300000").action(async (question, options) => {
|
|
702
|
+
const spinner = ora("Spawning research agent...").start();
|
|
703
|
+
try {
|
|
704
|
+
const { context } = await initializeSkillContext();
|
|
705
|
+
const { ParallelAgentSkill } = await import("../../skills/parallel-agent-skill.js");
|
|
706
|
+
const agentSkill = new ParallelAgentSkill(context);
|
|
707
|
+
const result = await agentSkill.research(question, {
|
|
708
|
+
timeout: parseInt(options.timeout)
|
|
709
|
+
});
|
|
710
|
+
spinner.stop();
|
|
711
|
+
if (result.success) {
|
|
712
|
+
console.log(chalk.green("\u2713"), result.message);
|
|
713
|
+
if (result.data?.findings) {
|
|
714
|
+
console.log(chalk.cyan("\nFindings:\n"));
|
|
715
|
+
console.log(result.data.findings);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
console.log(chalk.red("\u2717"), result.message);
|
|
719
|
+
}
|
|
720
|
+
await context.database.disconnect();
|
|
721
|
+
} catch (error) {
|
|
722
|
+
spinner.stop();
|
|
723
|
+
console.error(chalk.red("Error:"), error.message);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
agentCmd.command("maintain <task>").description("Low-stakes fix, produces a .patch file").option("--timeout <ms>", "Agent timeout in milliseconds", "300000").action(async (task, options) => {
|
|
728
|
+
const spinner = ora("Spawning maintenance agent...").start();
|
|
729
|
+
try {
|
|
730
|
+
const { context } = await initializeSkillContext();
|
|
731
|
+
const { ParallelAgentSkill } = await import("../../skills/parallel-agent-skill.js");
|
|
732
|
+
const agentSkill = new ParallelAgentSkill(context);
|
|
733
|
+
const result = await agentSkill.maintain(task, {
|
|
734
|
+
timeout: parseInt(options.timeout)
|
|
735
|
+
});
|
|
736
|
+
spinner.stop();
|
|
737
|
+
if (result.success) {
|
|
738
|
+
console.log(chalk.green("\u2713"), result.message);
|
|
739
|
+
if (result.data) {
|
|
740
|
+
console.log(chalk.cyan("\nPatch Details:"));
|
|
741
|
+
console.log(` Files changed: ${result.data.filesChanged}`);
|
|
742
|
+
console.log(` Additions: +${result.data.additions}`);
|
|
743
|
+
console.log(` Deletions: -${result.data.deletions}`);
|
|
744
|
+
console.log(chalk.yellow(`
|
|
745
|
+
${result.action}`));
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
console.log(chalk.red("\u2717"), result.message);
|
|
749
|
+
}
|
|
750
|
+
await context.database.disconnect();
|
|
751
|
+
} catch (error) {
|
|
752
|
+
spinner.stop();
|
|
753
|
+
console.error(chalk.red("Error:"), error.message);
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
agentCmd.command("spec-run <specPath>").description("Implement a spec on a branch, validate with lint+test+build").option("--timeout <ms>", "Agent timeout in milliseconds", "300000").action(async (specPath, options) => {
|
|
758
|
+
const spinner = ora("Spawning spec agent...").start();
|
|
759
|
+
try {
|
|
760
|
+
const { context } = await initializeSkillContext();
|
|
761
|
+
const { ParallelAgentSkill } = await import("../../skills/parallel-agent-skill.js");
|
|
762
|
+
const agentSkill = new ParallelAgentSkill(context);
|
|
763
|
+
const result = await agentSkill.specRun(specPath, {
|
|
764
|
+
timeout: parseInt(options.timeout)
|
|
765
|
+
});
|
|
766
|
+
spinner.stop();
|
|
767
|
+
if (result.success) {
|
|
768
|
+
console.log(chalk.green("\u2713"), result.message);
|
|
769
|
+
} else {
|
|
770
|
+
console.log(chalk.red("\u2717"), result.message);
|
|
771
|
+
}
|
|
772
|
+
if (result.data) {
|
|
773
|
+
console.log(chalk.cyan("\nResults:"));
|
|
774
|
+
console.log(` Branch: ${result.data.branch}`);
|
|
775
|
+
console.log(` Workspace: ${result.data.workDir}`);
|
|
776
|
+
const v = result.data.validation;
|
|
777
|
+
if (v) {
|
|
778
|
+
console.log(
|
|
779
|
+
` Lint: ${v.lint ? chalk.green("pass") : chalk.red("fail")} Test: ${v.test ? chalk.green("pass") : chalk.red("fail")} Build: ${v.build ? chalk.green("pass") : chalk.red("fail")}`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (result.data.diffStat) {
|
|
783
|
+
console.log(chalk.gray(`
|
|
784
|
+
${result.data.diffStat}`));
|
|
785
|
+
}
|
|
786
|
+
if (result.action) {
|
|
787
|
+
console.log(chalk.yellow(`
|
|
788
|
+
${result.action}`));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
await context.database.disconnect();
|
|
792
|
+
} catch (error) {
|
|
793
|
+
spinner.stop();
|
|
794
|
+
console.error(chalk.red("Error:"), error.message);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
700
798
|
skillsCmd.command("help [skill]").description("Show help for a specific skill").action(async (skill) => {
|
|
701
799
|
if (skill) {
|
|
702
800
|
switch (skill) {
|
|
@@ -725,6 +823,29 @@ Options:
|
|
|
725
823
|
--security Focus on security vulnerabilities
|
|
726
824
|
--performance Focus on performance issues
|
|
727
825
|
--verbose Show detailed output
|
|
826
|
+
`);
|
|
827
|
+
break;
|
|
828
|
+
case "agent":
|
|
829
|
+
console.log(`
|
|
830
|
+
agent \u2014 Parallel Agent Skill (Willison Patterns)
|
|
831
|
+
|
|
832
|
+
Spawn isolated Claude agents in disposable /tmp workspaces.
|
|
833
|
+
|
|
834
|
+
Subcommands:
|
|
835
|
+
research <question> Explore codebase, save findings as a frame
|
|
836
|
+
maintain <task> Low-stakes fix, produces a .patch file
|
|
837
|
+
spec-run <specPath> Implement spec on branch, validate
|
|
838
|
+
|
|
839
|
+
Options:
|
|
840
|
+
--timeout <ms> Agent timeout (default: 300000)
|
|
841
|
+
|
|
842
|
+
Examples:
|
|
843
|
+
stackmemory skills agent research "How does FTS5 search work?"
|
|
844
|
+
stackmemory skills agent maintain "Fix deprecation warning in webhook.ts"
|
|
845
|
+
stackmemory skills agent spec-run docs/specs/my-feature.md
|
|
846
|
+
|
|
847
|
+
Apply patches: git apply .stackmemory/patches/<file>.patch
|
|
848
|
+
Review specs: cd /tmp/sm-spec-* && git log --oneline
|
|
728
849
|
`);
|
|
729
850
|
break;
|
|
730
851
|
default:
|
|
@@ -752,8 +873,9 @@ Options:
|
|
|
752
873
|
console.log(
|
|
753
874
|
" spec - Generate iterative spec docs (one-pager, dev-spec, prompt-plan, agents)"
|
|
754
875
|
);
|
|
876
|
+
console.log(" linear-run - Execute Linear tasks via RLM orchestrator");
|
|
755
877
|
console.log(
|
|
756
|
-
"
|
|
878
|
+
" agent - Spawn parallel agents (research, maintain, spec-run)\n"
|
|
757
879
|
);
|
|
758
880
|
console.log(
|
|
759
881
|
chalk.yellow(
|
|
@@ -20,6 +20,17 @@ class GraphitiHooks {
|
|
|
20
20
|
emitter.registerHandler("session_start", this.onSessionStart.bind(this));
|
|
21
21
|
emitter.registerHandler("file_change", this.onFileChange.bind(this));
|
|
22
22
|
emitter.registerHandler("session_end", this.onSessionEnd.bind(this));
|
|
23
|
+
emitter.registerHandler("input_idle", this.onInputIdle.bind(this));
|
|
24
|
+
emitter.registerHandler("context_switch", this.onContextSwitch.bind(this));
|
|
25
|
+
emitter.registerHandler("prompt_submit", this.onPromptSubmit.bind(this));
|
|
26
|
+
emitter.registerHandler("tool_use", this.onToolUse.bind(this));
|
|
27
|
+
emitter.registerHandler(
|
|
28
|
+
"suggestion_ready",
|
|
29
|
+
this.onSuggestionReady.bind(this)
|
|
30
|
+
);
|
|
31
|
+
emitter.registerHandler("agent_start", this.onAgentStart.bind(this));
|
|
32
|
+
emitter.registerHandler("agent_complete", this.onAgentComplete.bind(this));
|
|
33
|
+
emitter.registerHandler("agent_error", this.onAgentError.bind(this));
|
|
23
34
|
logger.info("Graphiti hooks registered", {
|
|
24
35
|
endpoint: this.config.endpoint,
|
|
25
36
|
backend: this.config.backend,
|
|
@@ -82,6 +93,144 @@ class GraphitiHooks {
|
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
async onInputIdle(event) {
|
|
97
|
+
const idle = event;
|
|
98
|
+
try {
|
|
99
|
+
const episode = {
|
|
100
|
+
type: "input_idle",
|
|
101
|
+
content: {
|
|
102
|
+
idleDuration: idle.data.idleDuration,
|
|
103
|
+
lastInput: idle.data.lastInput
|
|
104
|
+
},
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
source: "stackmemory"
|
|
107
|
+
};
|
|
108
|
+
await this.client.upsertEpisode(episode);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.debug("Graphiti input_idle episode failed", {
|
|
111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async onContextSwitch(event) {
|
|
116
|
+
const ctx = event;
|
|
117
|
+
try {
|
|
118
|
+
const episode = {
|
|
119
|
+
type: "context_switch",
|
|
120
|
+
content: {
|
|
121
|
+
fromBranch: ctx.data.fromBranch,
|
|
122
|
+
toBranch: ctx.data.toBranch,
|
|
123
|
+
fromProject: ctx.data.fromProject,
|
|
124
|
+
toProject: ctx.data.toProject
|
|
125
|
+
},
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
source: "stackmemory"
|
|
128
|
+
};
|
|
129
|
+
await this.client.upsertEpisode(episode);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.debug("Graphiti context_switch episode failed", {
|
|
132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async onPromptSubmit(event) {
|
|
137
|
+
try {
|
|
138
|
+
const episode = {
|
|
139
|
+
type: "prompt_submit",
|
|
140
|
+
content: event.data || {},
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
source: "stackmemory"
|
|
143
|
+
};
|
|
144
|
+
await this.client.upsertEpisode(episode);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.debug("Graphiti prompt_submit episode failed", {
|
|
147
|
+
error: error instanceof Error ? error.message : String(error)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async onToolUse(event) {
|
|
152
|
+
try {
|
|
153
|
+
const episode = {
|
|
154
|
+
type: "tool_use",
|
|
155
|
+
content: event.data || {},
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
source: "stackmemory"
|
|
158
|
+
};
|
|
159
|
+
await this.client.upsertEpisode(episode);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.debug("Graphiti tool_use episode failed", {
|
|
162
|
+
error: error instanceof Error ? error.message : String(error)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async onSuggestionReady(event) {
|
|
167
|
+
const suggestion = event;
|
|
168
|
+
try {
|
|
169
|
+
const episode = {
|
|
170
|
+
type: "suggestion_ready",
|
|
171
|
+
content: {
|
|
172
|
+
source: suggestion.data.source,
|
|
173
|
+
confidence: suggestion.data.confidence,
|
|
174
|
+
preview: suggestion.data.preview
|
|
175
|
+
},
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
source: "stackmemory"
|
|
178
|
+
};
|
|
179
|
+
await this.client.upsertEpisode(episode);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.debug("Graphiti suggestion_ready episode failed", {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async onAgentStart(event) {
|
|
187
|
+
const e = event;
|
|
188
|
+
try {
|
|
189
|
+
const episode = {
|
|
190
|
+
type: "agent_start",
|
|
191
|
+
content: { agentType: e.data.agentType, task: e.data.task },
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
source: "stackmemory"
|
|
194
|
+
};
|
|
195
|
+
await this.client.upsertEpisode(episode);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.debug("Graphiti agent_start episode failed", {
|
|
198
|
+
error: error instanceof Error ? error.message : String(error)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async onAgentComplete(event) {
|
|
203
|
+
const e = event;
|
|
204
|
+
try {
|
|
205
|
+
const episode = {
|
|
206
|
+
type: "agent_complete",
|
|
207
|
+
content: { ...e.data },
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
source: "stackmemory"
|
|
210
|
+
};
|
|
211
|
+
await this.client.upsertEpisode(episode);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.debug("Graphiti agent_complete episode failed", {
|
|
214
|
+
error: error instanceof Error ? error.message : String(error)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async onAgentError(event) {
|
|
219
|
+
const e = event;
|
|
220
|
+
try {
|
|
221
|
+
const episode = {
|
|
222
|
+
type: "agent_error",
|
|
223
|
+
content: { agentType: e.data.agentType, error: e.data.error },
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
source: "stackmemory"
|
|
226
|
+
};
|
|
227
|
+
await this.client.upsertEpisode(episode);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.debug("Graphiti agent_error episode failed", {
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
85
234
|
// Expose a simple temporal query helper for future MCP tooling
|
|
86
235
|
async buildTemporalContext(query = {}) {
|
|
87
236
|
const now = Date.now();
|
|
@@ -3,6 +3,8 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
6
8
|
import { pickNextLinearTask } from "./linear-task-picker.js";
|
|
7
9
|
function formatDuration(ms) {
|
|
8
10
|
const seconds = Math.floor(ms / 1e3);
|
|
@@ -125,6 +127,34 @@ async function generateSuggestions(context) {
|
|
|
125
127
|
}
|
|
126
128
|
} catch {
|
|
127
129
|
}
|
|
130
|
+
try {
|
|
131
|
+
const patchDir = path.join(process.cwd(), ".stackmemory", "patches");
|
|
132
|
+
if (fs.existsSync(patchDir)) {
|
|
133
|
+
const patches = fs.readdirSync(patchDir).filter((f) => f.endsWith(".patch"));
|
|
134
|
+
if (patches.length > 0) {
|
|
135
|
+
suggestions.push({
|
|
136
|
+
key: String(keyIndex++),
|
|
137
|
+
label: `Review ${patches.length} pending patch${patches.length > 1 ? "es" : ""}`,
|
|
138
|
+
action: `ls -la .stackmemory/patches/`,
|
|
139
|
+
priority: 75
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const tmpEntries = fs.readdirSync("/tmp").filter((d) => d.startsWith("sm-spec-"));
|
|
147
|
+
if (tmpEntries.length > 0) {
|
|
148
|
+
const latest = path.join("/tmp", tmpEntries[tmpEntries.length - 1]);
|
|
149
|
+
suggestions.push({
|
|
150
|
+
key: String(keyIndex++),
|
|
151
|
+
label: `Inspect spec branch (${tmpEntries.length} workspace${tmpEntries.length > 1 ? "s" : ""})`,
|
|
152
|
+
action: `cd ${latest} && git log --oneline -5`,
|
|
153
|
+
priority: 65
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
128
158
|
const durationMs = Date.now() - context.sessionStartTime;
|
|
129
159
|
if (durationMs > 30 * 60 * 1e3) {
|
|
130
160
|
suggestions.push({
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { logger } from "../../core/monitoring/logger.js";
|
|
6
|
+
import { GraphitiClient } from "./client.js";
|
|
7
|
+
class LinearGraphitiBridge {
|
|
8
|
+
client;
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.client = new GraphitiClient(config);
|
|
11
|
+
}
|
|
12
|
+
async processWebhook(payload) {
|
|
13
|
+
const { action, data } = payload;
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
try {
|
|
16
|
+
const episode = {
|
|
17
|
+
type: `linear_issue_${action}`,
|
|
18
|
+
content: {
|
|
19
|
+
identifier: data.identifier,
|
|
20
|
+
title: data.title,
|
|
21
|
+
action,
|
|
22
|
+
state: data.state?.name,
|
|
23
|
+
priority: data.priority,
|
|
24
|
+
assignee: data.assignee?.name
|
|
25
|
+
},
|
|
26
|
+
timestamp: now,
|
|
27
|
+
source: "linear"
|
|
28
|
+
};
|
|
29
|
+
await this.client.upsertEpisode(episode);
|
|
30
|
+
if (action === "remove") return;
|
|
31
|
+
const entities = [
|
|
32
|
+
{
|
|
33
|
+
type: "Issue",
|
|
34
|
+
name: data.identifier,
|
|
35
|
+
summary: data.title,
|
|
36
|
+
properties: {
|
|
37
|
+
linearId: data.id,
|
|
38
|
+
state: data.state?.name,
|
|
39
|
+
priority: data.priority
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
if (data.assignee) {
|
|
44
|
+
entities.push({
|
|
45
|
+
type: "Person",
|
|
46
|
+
name: data.assignee.name,
|
|
47
|
+
properties: {
|
|
48
|
+
linearId: data.assignee.id,
|
|
49
|
+
email: data.assignee.email
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (data.team) {
|
|
54
|
+
entities.push({
|
|
55
|
+
type: "Team",
|
|
56
|
+
name: data.team.name,
|
|
57
|
+
properties: { linearId: data.team.id, key: data.team.key }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (data.labels?.length) {
|
|
61
|
+
for (const label of data.labels) {
|
|
62
|
+
entities.push({
|
|
63
|
+
type: "Label",
|
|
64
|
+
name: label.name,
|
|
65
|
+
properties: { linearId: label.id, color: label.color }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const entityResult = await this.client.upsertEntities(entities);
|
|
70
|
+
const issueId = entityResult.ids[0];
|
|
71
|
+
const relations = [];
|
|
72
|
+
let idx = 1;
|
|
73
|
+
if (data.assignee) {
|
|
74
|
+
relations.push({
|
|
75
|
+
fromId: issueId,
|
|
76
|
+
toId: entityResult.ids[idx],
|
|
77
|
+
type: "ASSIGNED_TO",
|
|
78
|
+
validFrom: now
|
|
79
|
+
});
|
|
80
|
+
idx++;
|
|
81
|
+
}
|
|
82
|
+
if (data.team) {
|
|
83
|
+
relations.push({
|
|
84
|
+
fromId: issueId,
|
|
85
|
+
toId: entityResult.ids[idx],
|
|
86
|
+
type: "BELONGS_TO",
|
|
87
|
+
validFrom: now
|
|
88
|
+
});
|
|
89
|
+
idx++;
|
|
90
|
+
}
|
|
91
|
+
if (data.labels?.length) {
|
|
92
|
+
for (let i = 0; i < data.labels.length; i++) {
|
|
93
|
+
relations.push({
|
|
94
|
+
fromId: issueId,
|
|
95
|
+
toId: entityResult.ids[idx + i],
|
|
96
|
+
type: "HAS_LABEL",
|
|
97
|
+
validFrom: now
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (relations.length > 0) {
|
|
102
|
+
await this.client.upsertRelations(relations);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.debug("Linear-Graphiti bridge error", {
|
|
106
|
+
action,
|
|
107
|
+
identifier: data.identifier,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
LinearGraphitiBridge
|
|
115
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { DEFAULT_GREPTILE_CONFIG } from "./config.js";
|
|
8
|
+
class GreptileClientError extends Error {
|
|
9
|
+
constructor(message, code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "GreptileClientError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
class GreptileClient {
|
|
16
|
+
config;
|
|
17
|
+
client = null;
|
|
18
|
+
transport = null;
|
|
19
|
+
connecting = null;
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.config = { ...DEFAULT_GREPTILE_CONFIG, ...config };
|
|
22
|
+
if (!this.config.enabled || !this.config.apiKey) {
|
|
23
|
+
throw new GreptileClientError(
|
|
24
|
+
"Greptile integration disabled (GREPTILE_API_KEY not set)",
|
|
25
|
+
"DISABLED"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async ensureConnected() {
|
|
30
|
+
if (this.client) return this.client;
|
|
31
|
+
if (this.connecting) {
|
|
32
|
+
await this.connecting;
|
|
33
|
+
return this.client;
|
|
34
|
+
}
|
|
35
|
+
this.connecting = this.connect();
|
|
36
|
+
try {
|
|
37
|
+
await this.connecting;
|
|
38
|
+
return this.client;
|
|
39
|
+
} finally {
|
|
40
|
+
this.connecting = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async connect() {
|
|
44
|
+
const transport = new StreamableHTTPClientTransport(
|
|
45
|
+
new URL(this.config.mcpEndpoint),
|
|
46
|
+
{
|
|
47
|
+
requestInit: {
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
reconnectionOptions: {
|
|
53
|
+
maxRetries: this.config.maxRetries,
|
|
54
|
+
initialReconnectionDelay: 1e3,
|
|
55
|
+
reconnectionDelayGrowFactor: 1.5,
|
|
56
|
+
maxReconnectionDelay: 1e4
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const client = new Client(
|
|
61
|
+
{ name: "stackmemory-greptile", version: "1.0.0" },
|
|
62
|
+
{ capabilities: {} }
|
|
63
|
+
);
|
|
64
|
+
transport.onclose = () => {
|
|
65
|
+
this.client = null;
|
|
66
|
+
this.transport = null;
|
|
67
|
+
};
|
|
68
|
+
await client.connect(transport);
|
|
69
|
+
this.client = client;
|
|
70
|
+
this.transport = transport;
|
|
71
|
+
}
|
|
72
|
+
async callTool(name, args = {}) {
|
|
73
|
+
const client = await this.ensureConnected();
|
|
74
|
+
const result = await client.callTool({ name, arguments: args });
|
|
75
|
+
if (result.content && Array.isArray(result.content)) {
|
|
76
|
+
const textParts = result.content.filter(
|
|
77
|
+
(c) => c.type === "text" && typeof c.text === "string"
|
|
78
|
+
).map((c) => c.text);
|
|
79
|
+
if (textParts.length === 0) return result;
|
|
80
|
+
const combined = textParts.join("\n");
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(combined);
|
|
83
|
+
} catch {
|
|
84
|
+
return combined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
async disconnect() {
|
|
90
|
+
if (this.transport) {
|
|
91
|
+
await this.transport.close();
|
|
92
|
+
}
|
|
93
|
+
this.client = null;
|
|
94
|
+
this.transport = null;
|
|
95
|
+
this.connecting = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
GreptileClient,
|
|
100
|
+
GreptileClientError
|
|
101
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
const DEFAULT_GREPTILE_CONFIG = {
|
|
6
|
+
enabled: !!process.env.GREPTILE_API_KEY,
|
|
7
|
+
mcpEndpoint: process.env.GREPTILE_MCP_ENDPOINT || "https://api.greptile.com/mcp",
|
|
8
|
+
apiKey: process.env.GREPTILE_API_KEY || "",
|
|
9
|
+
timeoutMs: 15e3,
|
|
10
|
+
maxRetries: 2
|
|
11
|
+
};
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_GREPTILE_CONFIG
|
|
14
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
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 { DEFAULT_GREPTILE_CONFIG } from "./config.js";
|
|
6
|
+
import { GreptileClient, GreptileClientError } from "./client.js";
|
|
7
|
+
export {
|
|
8
|
+
DEFAULT_GREPTILE_CONFIG,
|
|
9
|
+
GreptileClient,
|
|
10
|
+
GreptileClientError
|
|
11
|
+
};
|