@cryptiklemur/lattice 5.8.3 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{angular-html-BpO2Tp5w.js → angular-html-BDIcxkJq.js} +1 -1
- package/dist/client/assets/{angular-ts-DgHuzSgH.js → angular-ts-Bt22ouNH.js} +1 -1
- package/dist/client/assets/{apl-DN-3g-kA.js → apl-p8qkxzEK.js} +1 -1
- package/dist/client/assets/{astro-DC7TkZXH.js → astro-CIaMc49M.js} +1 -1
- package/dist/client/assets/{blade-DJeBezn7.js → blade-BR56EAMD.js} +1 -1
- package/dist/client/assets/{c-CWPHtG_V.js → c-Dli0HzAh.js} +1 -1
- package/dist/client/assets/{cobol-TtN3pbhH.js → cobol-Cad15ECy.js} +1 -1
- package/dist/client/assets/{coffee-DR36C-Bj.js → coffee-DpyATEbF.js} +1 -1
- package/dist/client/assets/{cpp-C9XaUQ0i.js → cpp-KN8_NFsf.js} +1 -1
- package/dist/client/assets/{crystal-T-sTBJEW.js → crystal-CuyGv0kh.js} +1 -1
- package/dist/client/assets/{css-BX5yO7SA.js → css-Cm3q4bxn.js} +1 -1
- package/dist/client/assets/{dist-VJaucYqW.js → dist-BjxsMc4u.js} +2 -2
- package/dist/client/assets/{edge-B6UK8iH4.js → edge-B6S7CSbx.js} +1 -1
- package/dist/client/assets/{elixir-CgftVsub.js → elixir-CNUy9H8T.js} +1 -1
- package/dist/client/assets/{elm-Bm5ehGFJ.js → elm-CNfcWmb9.js} +1 -1
- package/dist/client/assets/{erb-BGdFwtqx.js → erb-DWebzDaI.js} +1 -1
- package/dist/client/assets/{git-rebase-Dgch70i-.js → git-rebase-B_Pt2ZBK.js} +1 -1
- package/dist/client/assets/{glimmer-js-CO2m7K3o.js → glimmer-js-CVwoOd72.js} +1 -1
- package/dist/client/assets/{glimmer-ts-Rh9OBnOT.js → glimmer-ts-CjtFSxjz.js} +1 -1
- package/dist/client/assets/{glsl-DKLX4cjW.js → glsl-CP4rggAA.js} +1 -1
- package/dist/client/assets/{graphql-CirzzkU1.js → graphql-Dbm6sAtp.js} +1 -1
- package/dist/client/assets/{hack-CQC9znUp.js → hack-Bj9y3SGf.js} +1 -1
- package/dist/client/assets/{haml-ze151Tzg.js → haml-DRGrdf3f.js} +1 -1
- package/dist/client/assets/{handlebars-CxjP8Lo0.js → handlebars-CFKjcBMg.js} +1 -1
- package/dist/client/assets/{html-CqXIaUHF.js → html-Vcd4eHHg.js} +1 -1
- package/dist/client/assets/{html-derivative-BzQtEeTI.js → html-derivative-BF0YbD4L.js} +1 -1
- package/dist/client/assets/{http-DuZ92gpQ.js → http-CGVTa2NT.js} +1 -1
- package/dist/client/assets/{hurl-CAbh6Y6a.js → hurl-B0GrsGqd.js} +1 -1
- package/dist/client/assets/{index-E8YNABWy.js → index-CX1tudsF.js} +132 -132
- package/dist/client/assets/index-DlfI20Gn.css +2 -0
- package/dist/client/assets/{java-DzcsTbJs.js → java-BJHQqHsm.js} +1 -1
- package/dist/client/assets/{javascript-DClRq2ts.js → javascript-CmuMsKrc.js} +1 -1
- package/dist/client/assets/{jinja-B5sT9_-9.js → jinja-JxCLeq1j.js} +1 -1
- package/dist/client/assets/{jison-CU3zhnCb.js → jison-BdgAUhei.js} +1 -1
- package/dist/client/assets/{json-CQp0L0ej.js → json-DtPissHL.js} +1 -1
- package/dist/client/assets/{jsx-Bf-5FvbF.js → jsx-DUAxxDkP.js} +1 -1
- package/dist/client/assets/{julia-CydtGy78.js → julia-DxDlbL6e.js} +1 -1
- package/dist/client/assets/{just-DdklfRff.js → just-CVmAAx2R.js} +1 -1
- package/dist/client/assets/{latex-Br5dIruj.js → latex-uwxggTWA.js} +1 -1
- package/dist/client/assets/{liquid-BzrfNGvH.js → liquid-xsETAJJy.js} +1 -1
- package/dist/client/assets/{lua-Cj4dlLGr.js → lua-B2Hh8PgD.js} +1 -1
- package/dist/client/assets/{marko-BSCcyzMZ.js → marko-yDeGxD87.js} +1 -1
- package/dist/client/assets/{mdc-BPOjCacH.js → mdc-QMp4ieYR.js} +1 -1
- package/dist/client/assets/{nginx-DbzWTwI6.js → nginx-7gmRmcqz.js} +1 -1
- package/dist/client/assets/{nim-CRdChtbV.js → nim-CA8SNY_7.js} +1 -1
- package/dist/client/assets/{perl-C3QVEeKS.js → perl-lx5nW4VC.js} +1 -1
- package/dist/client/assets/{php-C1EdFiJm.js → php-DgHiW953.js} +1 -1
- package/dist/client/assets/{pug-CO6P9E1X.js → pug-CbbB1vwb.js} +1 -1
- package/dist/client/assets/{qml-iW4zlehx.js → qml-COrzwCIh.js} +1 -1
- package/dist/client/assets/{r-C6XBkrUL.js → r-Dv7pZJDH.js} +1 -1
- package/dist/client/assets/{razor-CeQnxDgb.js → razor-D2m8EDP5.js} +1 -1
- package/dist/client/assets/{regexp-Dy8aAKap.js → regexp-BXLT-jPc.js} +1 -1
- package/dist/client/assets/{rst-OwTtwL0i.js → rst-_S6rrUYh.js} +1 -1
- package/dist/client/assets/{ruby-D7JWM2N5.js → ruby-C3XO7tYY.js} +1 -1
- package/dist/client/assets/{sas-DiK66SDU.js → sas-DP2k4iuN.js} +1 -1
- package/dist/client/assets/{scss-qrlTvxMb.js → scss-lhLFMXGn.js} +1 -1
- package/dist/client/assets/{shellscript-D7AOrbZb.js → shellscript-BYlBPHen.js} +1 -1
- package/dist/client/assets/{shellsession-DjQiM7TM.js → shellsession-CbVyQKWZ.js} +1 -1
- package/dist/client/assets/{soy-BL7E9JSD.js → soy-Be8a0lHq.js} +1 -1
- package/dist/client/assets/{sql-CteFkLc2.js → sql-2KxvU9YS.js} +1 -1
- package/dist/client/assets/{stata-B3MgNvuI.js → stata-BxlWftTS.js} +1 -1
- package/dist/client/assets/{surrealql-COKgmBsN.js → surrealql-CJ-q86nR.js} +1 -1
- package/dist/client/assets/{svelte-Gjt4fCGF.js → svelte-Q1ml0OiY.js} +1 -1
- package/dist/client/assets/{templ-Bj518YFy.js → templ-BbfPZhtu.js} +1 -1
- package/dist/client/assets/{tex-DRQn3t1e.js → tex-Dcth4Gi6.js} +1 -1
- package/dist/client/assets/{ts-tags-Ca2ut20u.js → ts-tags-BKhSOXI3.js} +1 -1
- package/dist/client/assets/{tsx-q8zyOWFk.js → tsx-CS6iQ0XH.js} +1 -1
- package/dist/client/assets/{twig-TjCAErzr.js → twig-BHp31ZxS.js} +1 -1
- package/dist/client/assets/{typescript-p7KFNEGW.js → typescript-16YJBTaO.js} +1 -1
- package/dist/client/assets/{vue-BPqqN4qD.js → vue-CMKwTi4r.js} +1 -1
- package/dist/client/assets/{vue-html-DWzvGKv5.js → vue-html-Dr8VUA2G.js} +1 -1
- package/dist/client/assets/{vue-vine-ooxqXIjP.js → vue-vine-DZUqDerl.js} +1 -1
- package/dist/client/assets/{xml-B8jNsjt9.js → xml-CBbBKKDC.js} +1 -1
- package/dist/client/assets/{xsl-B6pyn4Tp.js → xsl-DWEX6PKX.js} +1 -1
- package/dist/client/assets/{yaml-BN6nvuC4.js → yaml-DvKvvh3X.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/daemon.js +42 -2
- package/dist/server/features/context-analyzer.js +239 -0
- package/dist/server/features/session-history.js +127 -0
- package/dist/server/features/specs.js +87 -1
- package/dist/server/features/superpowers.js +173 -0
- package/dist/server/handlers/chat.js +4 -0
- package/dist/server/handlers/context-hooks.js +171 -0
- package/dist/server/handlers/hooks.js +233 -0
- package/dist/server/handlers/session.js +1 -1
- package/dist/server/handlers/specs.js +57 -0
- package/dist/server/handlers/superpowers.js +13 -0
- package/dist/server/logger.js +1 -0
- package/dist/server/project/sdk-bridge.js +67 -2
- package/dist/server/project/session.js +2 -1
- package/package.json +1 -1
- package/dist/client/assets/index-etW8QK5W.css +0 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getLatticeHome } from "../config.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
function getSessionDir() {
|
|
6
|
+
const dir = join(getLatticeHome(), "session-history");
|
|
7
|
+
if (!existsSync(dir)) {
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
function sessionPath(hookSessionId) {
|
|
13
|
+
const safe = hookSessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
14
|
+
return join(getSessionDir(), safe + ".json");
|
|
15
|
+
}
|
|
16
|
+
const sessionCache = new Map();
|
|
17
|
+
let indexLoaded = false;
|
|
18
|
+
export function loadSessionHistory() {
|
|
19
|
+
if (indexLoaded)
|
|
20
|
+
return Array.from(sessionCache.values());
|
|
21
|
+
const dir = getSessionDir();
|
|
22
|
+
try {
|
|
23
|
+
const files = readdirSync(dir).filter(function (f) { return f.endsWith(".json"); });
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(join(dir, file), "utf-8");
|
|
27
|
+
const session = JSON.parse(raw);
|
|
28
|
+
if (session.hookSessionId) {
|
|
29
|
+
sessionCache.set(session.hookSessionId, session);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
log.server("Failed to read session history file: %s", file);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
indexLoaded = true;
|
|
37
|
+
log.server("Loaded %d historical sessions", sessionCache.size);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
indexLoaded = true;
|
|
41
|
+
}
|
|
42
|
+
return Array.from(sessionCache.values());
|
|
43
|
+
}
|
|
44
|
+
export function saveSession(session) {
|
|
45
|
+
sessionCache.set(session.hookSessionId, session);
|
|
46
|
+
try {
|
|
47
|
+
const path = sessionPath(session.hookSessionId);
|
|
48
|
+
writeFileSync(path, JSON.stringify(session), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
log.server("Failed to save session history: %s", String(err));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getSession(hookSessionId) {
|
|
55
|
+
return sessionCache.get(hookSessionId) || null;
|
|
56
|
+
}
|
|
57
|
+
export function upsertFromSnapshot(hookSessionId, snapshot) {
|
|
58
|
+
let existing = sessionCache.get(hookSessionId);
|
|
59
|
+
if (!existing) {
|
|
60
|
+
existing = {
|
|
61
|
+
hookSessionId,
|
|
62
|
+
...snapshot,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
endedAt: null,
|
|
65
|
+
active: true,
|
|
66
|
+
toolEvents: [],
|
|
67
|
+
toolDeltas: [],
|
|
68
|
+
anomalyCount: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
existing.inputTokens = snapshot.inputTokens;
|
|
73
|
+
existing.outputTokens = snapshot.outputTokens;
|
|
74
|
+
existing.cacheReadTokens = snapshot.cacheReadTokens;
|
|
75
|
+
existing.cacheCreationTokens = snapshot.cacheCreationTokens;
|
|
76
|
+
existing.contextWindow = snapshot.contextWindow;
|
|
77
|
+
existing.usedPercent = snapshot.usedPercent;
|
|
78
|
+
existing.costUsd = snapshot.costUsd;
|
|
79
|
+
existing.durationMs = snapshot.durationMs;
|
|
80
|
+
existing.modelId = snapshot.modelId || existing.modelId;
|
|
81
|
+
existing.modelName = snapshot.modelName || existing.modelName;
|
|
82
|
+
existing.projectName = snapshot.projectName || existing.projectName;
|
|
83
|
+
existing.projectSlug = snapshot.projectSlug || existing.projectSlug;
|
|
84
|
+
existing.timestamp = Date.now();
|
|
85
|
+
}
|
|
86
|
+
saveSession(existing);
|
|
87
|
+
}
|
|
88
|
+
export function addToolEventToHistory(hookSessionId, event) {
|
|
89
|
+
const existing = sessionCache.get(hookSessionId);
|
|
90
|
+
if (!existing)
|
|
91
|
+
return;
|
|
92
|
+
if (existing.toolEvents.length >= 500) {
|
|
93
|
+
existing.toolEvents = existing.toolEvents.slice(-499);
|
|
94
|
+
}
|
|
95
|
+
existing.toolEvents.push(event);
|
|
96
|
+
}
|
|
97
|
+
export function addToolDeltaToHistory(hookSessionId, delta) {
|
|
98
|
+
const existing = sessionCache.get(hookSessionId);
|
|
99
|
+
if (!existing)
|
|
100
|
+
return;
|
|
101
|
+
if (existing.toolDeltas.length >= 500) {
|
|
102
|
+
existing.toolDeltas = existing.toolDeltas.slice(-499);
|
|
103
|
+
}
|
|
104
|
+
existing.toolDeltas.push(delta);
|
|
105
|
+
}
|
|
106
|
+
export function markSessionEnded(hookSessionId) {
|
|
107
|
+
const existing = sessionCache.get(hookSessionId);
|
|
108
|
+
if (!existing)
|
|
109
|
+
return;
|
|
110
|
+
existing.active = false;
|
|
111
|
+
existing.endedAt = Date.now();
|
|
112
|
+
saveSession(existing);
|
|
113
|
+
}
|
|
114
|
+
export function listSessions(options) {
|
|
115
|
+
let sessions = Array.from(sessionCache.values());
|
|
116
|
+
if (options?.projectSlug) {
|
|
117
|
+
sessions = sessions.filter(function (s) { return s.projectSlug === options.projectSlug; });
|
|
118
|
+
}
|
|
119
|
+
if (options?.active !== undefined) {
|
|
120
|
+
sessions = sessions.filter(function (s) { return s.active === options.active; });
|
|
121
|
+
}
|
|
122
|
+
sessions.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
123
|
+
if (options?.limit) {
|
|
124
|
+
sessions = sessions.slice(0, options.limit);
|
|
125
|
+
}
|
|
126
|
+
return sessions;
|
|
127
|
+
}
|
|
@@ -197,7 +197,92 @@ export function deleteSpec(id) {
|
|
|
197
197
|
}
|
|
198
198
|
return false;
|
|
199
199
|
}
|
|
200
|
-
export function
|
|
200
|
+
export function populateSpec(id, fields, sessionId) {
|
|
201
|
+
for (var i = 0; i < specs.length; i++) {
|
|
202
|
+
if (specs[i].id !== id)
|
|
203
|
+
continue;
|
|
204
|
+
var spec = specs[i];
|
|
205
|
+
if (fields.title && typeof fields.title === "string")
|
|
206
|
+
spec.title = fields.title;
|
|
207
|
+
if (fields.tagline && typeof fields.tagline === "string")
|
|
208
|
+
spec.tagline = fields.tagline;
|
|
209
|
+
if (fields.priority && typeof fields.priority === "string")
|
|
210
|
+
spec.priority = fields.priority;
|
|
211
|
+
if (fields.estimatedEffort && typeof fields.estimatedEffort === "string")
|
|
212
|
+
spec.estimatedEffort = fields.estimatedEffort;
|
|
213
|
+
if (fields.tags && Array.isArray(fields.tags))
|
|
214
|
+
spec.tags = fields.tags;
|
|
215
|
+
if (fields.summary && typeof fields.summary === "string")
|
|
216
|
+
spec.sections.summary = fields.summary;
|
|
217
|
+
if (fields.currentState && typeof fields.currentState === "string")
|
|
218
|
+
spec.sections.currentState = fields.currentState;
|
|
219
|
+
if (fields.requirements && typeof fields.requirements === "string")
|
|
220
|
+
spec.sections.requirements = fields.requirements;
|
|
221
|
+
if (fields.implementationPlan && typeof fields.implementationPlan === "string")
|
|
222
|
+
spec.sections.implementationPlan = fields.implementationPlan;
|
|
223
|
+
if (fields.testing && typeof fields.testing === "string")
|
|
224
|
+
spec.sections.testing = fields.testing;
|
|
225
|
+
spec.updatedAt = Date.now();
|
|
226
|
+
spec.activity.push({
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
type: "ai-note",
|
|
229
|
+
detail: "Spec populated from brainstorm session",
|
|
230
|
+
sessionId,
|
|
231
|
+
});
|
|
232
|
+
saveSpecs();
|
|
233
|
+
return spec;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
export function parseSpecPopulate(text) {
|
|
238
|
+
var startTag = "<spec-populate>";
|
|
239
|
+
var endTag = "</spec-populate>";
|
|
240
|
+
var startIdx = text.indexOf(startTag);
|
|
241
|
+
var endIdx = text.indexOf(endTag);
|
|
242
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
|
|
243
|
+
return null;
|
|
244
|
+
var jsonStr = text.slice(startIdx + startTag.length, endIdx).trim();
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(jsonStr);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function parsePlanContent(text) {
|
|
253
|
+
var startTag = "<plan-content>";
|
|
254
|
+
var endTag = "</plan-content>";
|
|
255
|
+
var startIdx = text.indexOf(startTag);
|
|
256
|
+
var endIdx = text.indexOf(endTag);
|
|
257
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
|
|
258
|
+
return null;
|
|
259
|
+
return text.slice(startIdx + startTag.length, endIdx).trim();
|
|
260
|
+
}
|
|
261
|
+
export function parseSpecActivity(text) {
|
|
262
|
+
var startTag = "<spec-activity>";
|
|
263
|
+
var endTag = "</spec-activity>";
|
|
264
|
+
var startIdx = text.indexOf(startTag);
|
|
265
|
+
var endIdx = text.indexOf(endTag);
|
|
266
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
|
|
267
|
+
return null;
|
|
268
|
+
var jsonStr = text.slice(startIdx + startTag.length, endIdx).trim();
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(jsonStr);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export function findSpecBySession(sessionId) {
|
|
277
|
+
for (var i = 0; i < specs.length; i++) {
|
|
278
|
+
for (var j = 0; j < specs[i].linkedSessions.length; j++) {
|
|
279
|
+
if (specs[i].linkedSessions[j].sessionId === sessionId)
|
|
280
|
+
return specs[i];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
export function linkSession(specId, sessionId, note, sessionType) {
|
|
201
286
|
for (var i = 0; i < specs.length; i++) {
|
|
202
287
|
if (specs[i].id !== specId)
|
|
203
288
|
continue;
|
|
@@ -207,6 +292,7 @@ export function linkSession(specId, sessionId, note) {
|
|
|
207
292
|
sessionId,
|
|
208
293
|
linkedAt: now,
|
|
209
294
|
note,
|
|
295
|
+
sessionType,
|
|
210
296
|
});
|
|
211
297
|
spec.activity.push({
|
|
212
298
|
timestamp: now,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
var TRACKED_SKILLS = [
|
|
6
|
+
"brainstorming",
|
|
7
|
+
"writing-plans",
|
|
8
|
+
"subagent-driven-development",
|
|
9
|
+
"executing-plans",
|
|
10
|
+
];
|
|
11
|
+
var installed = false;
|
|
12
|
+
var version = null;
|
|
13
|
+
var installPath = null;
|
|
14
|
+
var skillContent = new Map();
|
|
15
|
+
var watcher = null;
|
|
16
|
+
function getPluginsFilePath() {
|
|
17
|
+
return join(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
18
|
+
}
|
|
19
|
+
function detectSuperpowers() {
|
|
20
|
+
var pluginsFile = getPluginsFilePath();
|
|
21
|
+
if (!existsSync(pluginsFile)) {
|
|
22
|
+
installed = false;
|
|
23
|
+
version = null;
|
|
24
|
+
installPath = null;
|
|
25
|
+
skillContent.clear();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
var raw = readFileSync(pluginsFile, "utf-8");
|
|
30
|
+
var data = JSON.parse(raw);
|
|
31
|
+
var plugins = data.plugins || {};
|
|
32
|
+
var found = false;
|
|
33
|
+
for (var key of Object.keys(plugins)) {
|
|
34
|
+
if (key.startsWith("superpowers@")) {
|
|
35
|
+
var entries = plugins[key];
|
|
36
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
37
|
+
var entry = entries[0];
|
|
38
|
+
installed = true;
|
|
39
|
+
version = entry.version || null;
|
|
40
|
+
installPath = entry.installPath || null;
|
|
41
|
+
found = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!found) {
|
|
47
|
+
installed = false;
|
|
48
|
+
version = null;
|
|
49
|
+
installPath = null;
|
|
50
|
+
skillContent.clear();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
loadSkills();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
log.superpowers("Failed to read plugins file: %O", err);
|
|
57
|
+
installed = false;
|
|
58
|
+
version = null;
|
|
59
|
+
installPath = null;
|
|
60
|
+
skillContent.clear();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function loadSkills() {
|
|
64
|
+
skillContent.clear();
|
|
65
|
+
if (!installPath)
|
|
66
|
+
return;
|
|
67
|
+
for (var name of TRACKED_SKILLS) {
|
|
68
|
+
var skillPath = join(installPath, "skills", name, "SKILL.md");
|
|
69
|
+
if (existsSync(skillPath)) {
|
|
70
|
+
try {
|
|
71
|
+
var content = readFileSync(skillPath, "utf-8");
|
|
72
|
+
skillContent.set(name, content);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
log.superpowers("Failed to read skill %s: %O", name, err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
log.superpowers("Superpowers v%s detected with %d skills", version, skillContent.size);
|
|
80
|
+
}
|
|
81
|
+
export function initSuperpowers() {
|
|
82
|
+
detectSuperpowers();
|
|
83
|
+
var pluginsDir = join(homedir(), ".claude", "plugins");
|
|
84
|
+
if (existsSync(pluginsDir)) {
|
|
85
|
+
try {
|
|
86
|
+
watcher = watch(pluginsDir, function (_eventType, filename) {
|
|
87
|
+
if (filename === "installed_plugins.json") {
|
|
88
|
+
detectSuperpowers();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
log.superpowers("Failed to watch plugins directory: %O", err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function isSuperpowersInstalled() {
|
|
98
|
+
return installed;
|
|
99
|
+
}
|
|
100
|
+
export function getSuperpowersVersion() {
|
|
101
|
+
return version;
|
|
102
|
+
}
|
|
103
|
+
export function getSkillContent(skillName) {
|
|
104
|
+
return skillContent.get(skillName) ?? null;
|
|
105
|
+
}
|
|
106
|
+
export function getAvailableSkills() {
|
|
107
|
+
return Array.from(skillContent.keys());
|
|
108
|
+
}
|
|
109
|
+
export function buildBrainstormPrompt(spec, projectSlug) {
|
|
110
|
+
var content = getSkillContent("brainstorming");
|
|
111
|
+
if (!content)
|
|
112
|
+
return "";
|
|
113
|
+
var append = content + "\n\n---\n\n";
|
|
114
|
+
append += "## Lattice Integration Instructions\n\n";
|
|
115
|
+
append += "You are helping design a spec in the Lattice project management tool.\n\n";
|
|
116
|
+
append += "**Project:** " + projectSlug + "\n";
|
|
117
|
+
append += "**Spec ID:** " + spec.id + "\n\n";
|
|
118
|
+
append += "When you reach the 'write design doc' phase, instead of writing a file to disk, ";
|
|
119
|
+
append += "output the design as a JSON block inside `<spec-populate>` tags. The JSON should contain ";
|
|
120
|
+
append += "any of these fields that you have determined from the conversation:\n\n";
|
|
121
|
+
append += "```json\n";
|
|
122
|
+
append += '{\n "title": "Spec title",\n "tagline": "One-line summary",\n';
|
|
123
|
+
append += ' "summary": "Full summary section content",\n';
|
|
124
|
+
append += ' "currentState": "What exists today",\n';
|
|
125
|
+
append += ' "requirements": "What needs to change",\n';
|
|
126
|
+
append += ' "implementationPlan": "High-level approach",\n';
|
|
127
|
+
append += ' "testing": "How to verify",\n';
|
|
128
|
+
append += ' "priority": "high|medium|low",\n';
|
|
129
|
+
append += ' "estimatedEffort": "small|medium|large|xl",\n';
|
|
130
|
+
append += ' "tags": ["tag1", "tag2"]\n}\n```\n\n';
|
|
131
|
+
append += "Lattice will automatically populate the spec fields from this output.\n";
|
|
132
|
+
append += "After outputting the spec-populate block, suggest that the user can now write an implementation plan from the spec editor.\n";
|
|
133
|
+
return append;
|
|
134
|
+
}
|
|
135
|
+
export function buildWritePlanPrompt(spec, projectSlug) {
|
|
136
|
+
var content = getSkillContent("writing-plans");
|
|
137
|
+
if (!content)
|
|
138
|
+
return "";
|
|
139
|
+
var append = content + "\n\n---\n\n";
|
|
140
|
+
append += "## Lattice Integration Instructions\n\n";
|
|
141
|
+
append += "You are writing an implementation plan for a spec in Lattice.\n\n";
|
|
142
|
+
append += "**Project:** " + projectSlug + "\n";
|
|
143
|
+
append += "**Spec:** " + spec.title + "\n\n";
|
|
144
|
+
append += "### Spec Content\n\n";
|
|
145
|
+
if (spec.sections.summary)
|
|
146
|
+
append += "**Summary:** " + spec.sections.summary + "\n\n";
|
|
147
|
+
if (spec.sections.currentState)
|
|
148
|
+
append += "**Current State:** " + spec.sections.currentState + "\n\n";
|
|
149
|
+
if (spec.sections.requirements)
|
|
150
|
+
append += "**Requirements:** " + spec.sections.requirements + "\n\n";
|
|
151
|
+
if (spec.sections.testing)
|
|
152
|
+
append += "**Testing:** " + spec.sections.testing + "\n\n";
|
|
153
|
+
append += "When you have finished writing the plan, output it inside `<plan-content>` tags.\n";
|
|
154
|
+
append += "Lattice will store it in the spec's implementation plan section.\n";
|
|
155
|
+
return append;
|
|
156
|
+
}
|
|
157
|
+
export function buildExecutePrompt(spec, projectSlug) {
|
|
158
|
+
var content = getSkillContent("subagent-driven-development") || getSkillContent("executing-plans");
|
|
159
|
+
if (!content)
|
|
160
|
+
return "";
|
|
161
|
+
var append = content + "\n\n---\n\n";
|
|
162
|
+
append += "## Lattice Integration Instructions\n\n";
|
|
163
|
+
append += "You are executing an implementation plan from a Lattice spec.\n\n";
|
|
164
|
+
append += "**Project:** " + projectSlug + "\n";
|
|
165
|
+
append += "**Spec:** " + spec.title + "\n\n";
|
|
166
|
+
append += "### Implementation Plan\n\n";
|
|
167
|
+
if (spec.sections.implementationPlan) {
|
|
168
|
+
append += spec.sections.implementationPlan + "\n\n";
|
|
169
|
+
}
|
|
170
|
+
append += "As you complete milestones, report progress by outputting `<spec-activity>` blocks:\n";
|
|
171
|
+
append += '```\n<spec-activity>{"type": "ai-note", "detail": "Completed task N: description"}</spec-activity>\n```\n';
|
|
172
|
+
return append;
|
|
173
|
+
}
|
|
@@ -8,6 +8,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { getDailySpend } from "../analytics/engine.js";
|
|
10
10
|
import { log } from "../logger.js";
|
|
11
|
+
import { findSpecBySession } from "../features/specs.js";
|
|
11
12
|
function formatSdkRule(rule) {
|
|
12
13
|
if (!rule.ruleContent)
|
|
13
14
|
return rule.toolName;
|
|
@@ -176,6 +177,7 @@ registerHandler("chat", function (clientId, message) {
|
|
|
176
177
|
var attachments = sendMsg.attachmentIds
|
|
177
178
|
? getAttachments(clientId, sendMsg.attachmentIds)
|
|
178
179
|
: [];
|
|
180
|
+
var linkedSpec = findSpecBySession(active.sessionId);
|
|
179
181
|
startChatStream({
|
|
180
182
|
projectSlug: active.projectSlug,
|
|
181
183
|
sessionId: active.sessionId,
|
|
@@ -186,6 +188,8 @@ registerHandler("chat", function (clientId, message) {
|
|
|
186
188
|
env: Object.keys(env).length > 0 ? env : undefined,
|
|
187
189
|
model: sendMsg.model,
|
|
188
190
|
effort: sendMsg.effort,
|
|
191
|
+
systemPrompt: sendMsg.systemPrompt,
|
|
192
|
+
specId: linkedSpec ? linkedSpec.id : undefined,
|
|
189
193
|
});
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { registerHandler } from "../ws/router.js";
|
|
2
|
+
import { sendTo } from "../ws/broadcast.js";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from "node:fs";
|
|
4
|
+
import { join, dirname, resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { getLatticeHome } from "../config.js";
|
|
8
|
+
import { log } from "../logger.js";
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const HOOKS_SRC_DIR = join(__dirname, "..", "hooks");
|
|
11
|
+
function getClaudeSettingsPath() {
|
|
12
|
+
return process.env.CLAUDE_SETTINGS_PATH || join(homedir(), ".claude", "settings.json");
|
|
13
|
+
}
|
|
14
|
+
function getHooksInstallDir() {
|
|
15
|
+
return resolve(join(getLatticeHome(), "hooks"));
|
|
16
|
+
}
|
|
17
|
+
function readClaudeSettings() {
|
|
18
|
+
const path = getClaudeSettingsPath();
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeClaudeSettings(settings) {
|
|
29
|
+
const path = getClaudeSettingsPath();
|
|
30
|
+
const dir = dirname(path);
|
|
31
|
+
if (!existsSync(dir))
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
function buildHookEntry(command) {
|
|
36
|
+
return {
|
|
37
|
+
matcher: "",
|
|
38
|
+
hooks: [{
|
|
39
|
+
type: "command",
|
|
40
|
+
command,
|
|
41
|
+
timeout: 5,
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function installHookScripts() {
|
|
46
|
+
const installDir = getHooksInstallDir();
|
|
47
|
+
if (!existsSync(installDir))
|
|
48
|
+
mkdirSync(installDir, { recursive: true });
|
|
49
|
+
const scripts = ["post_tool_use.sh", "event_forward.sh", "statusline.sh"];
|
|
50
|
+
for (const script of scripts) {
|
|
51
|
+
const src = join(HOOKS_SRC_DIR, script);
|
|
52
|
+
const dest = join(installDir, script);
|
|
53
|
+
if (existsSync(src)) {
|
|
54
|
+
copyFileSync(src, dest);
|
|
55
|
+
chmodSync(dest, 0o755);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return installDir;
|
|
59
|
+
}
|
|
60
|
+
export function installHooks() {
|
|
61
|
+
try {
|
|
62
|
+
const hooksDir = installHookScripts();
|
|
63
|
+
const settings = readClaudeSettings();
|
|
64
|
+
const postToolUse = join(hooksDir, "post_tool_use.sh");
|
|
65
|
+
const eventForward = join(hooksDir, "event_forward.sh");
|
|
66
|
+
const statusline = join(hooksDir, "statusline.sh");
|
|
67
|
+
// Merge hooks without overwriting existing non-Lattice hooks
|
|
68
|
+
const existingHooks = (settings.hooks || {});
|
|
69
|
+
function addLatticeHook(eventType, command) {
|
|
70
|
+
const entries = existingHooks[eventType] || [];
|
|
71
|
+
// Remove any existing Lattice hooks
|
|
72
|
+
const filtered = entries.filter(function (e) {
|
|
73
|
+
return !e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
|
|
74
|
+
});
|
|
75
|
+
filtered.push(buildHookEntry(command));
|
|
76
|
+
existingHooks[eventType] = filtered;
|
|
77
|
+
}
|
|
78
|
+
addLatticeHook("PostToolUse", '"' + postToolUse + '"');
|
|
79
|
+
addLatticeHook("SessionStart", '"' + eventForward + '" SessionStart');
|
|
80
|
+
addLatticeHook("Stop", '"' + eventForward + '" Stop');
|
|
81
|
+
addLatticeHook("PreCompact", '"' + eventForward + '" PreCompact');
|
|
82
|
+
addLatticeHook("PostCompact", '"' + eventForward + '" PostCompact');
|
|
83
|
+
settings.hooks = existingHooks;
|
|
84
|
+
// Add statusLine entry
|
|
85
|
+
settings.statusLine = {
|
|
86
|
+
type: "command",
|
|
87
|
+
command: '"' + statusline + '"',
|
|
88
|
+
};
|
|
89
|
+
writeClaudeSettings(settings);
|
|
90
|
+
log.server("Context analyzer hooks installed to %s", hooksDir);
|
|
91
|
+
return { success: true, message: "Hooks installed. Claude Code will now report context data to Lattice." };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
log.server("Failed to install hooks: %O", err);
|
|
95
|
+
return { success: false, message: "Failed to install hooks: " + (err.message || String(err)) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function uninstallHooks() {
|
|
99
|
+
try {
|
|
100
|
+
const hooksDir = getHooksInstallDir();
|
|
101
|
+
const settings = readClaudeSettings();
|
|
102
|
+
const existingHooks = (settings.hooks || {});
|
|
103
|
+
// Remove Lattice hooks from each event type
|
|
104
|
+
for (const eventType of Object.keys(existingHooks)) {
|
|
105
|
+
existingHooks[eventType] = existingHooks[eventType].filter(function (e) {
|
|
106
|
+
return !e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
|
|
107
|
+
});
|
|
108
|
+
if (existingHooks[eventType].length === 0) {
|
|
109
|
+
delete existingHooks[eventType];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
settings.hooks = existingHooks;
|
|
113
|
+
if (Object.keys(existingHooks).length === 0) {
|
|
114
|
+
delete settings.hooks;
|
|
115
|
+
}
|
|
116
|
+
// Remove statusLine if it points to our script
|
|
117
|
+
const sl = settings.statusLine;
|
|
118
|
+
if (sl && sl.command && (sl.command.includes("lattice") || sl.command.includes(hooksDir))) {
|
|
119
|
+
delete settings.statusLine;
|
|
120
|
+
}
|
|
121
|
+
writeClaudeSettings(settings);
|
|
122
|
+
log.server("Context analyzer hooks uninstalled");
|
|
123
|
+
return { success: true, message: "Hooks removed from Claude Code settings." };
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return { success: false, message: "Failed to uninstall hooks: " + (err.message || String(err)) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function checkHooksInstalled() {
|
|
130
|
+
try {
|
|
131
|
+
const hooksDir = getHooksInstallDir();
|
|
132
|
+
const settings = readClaudeSettings();
|
|
133
|
+
const existingHooks = (settings.hooks || {});
|
|
134
|
+
const postToolUse = existingHooks["PostToolUse"] || [];
|
|
135
|
+
return postToolUse.some(function (e) {
|
|
136
|
+
return e.hooks.some(function (h) { return h.command.includes("lattice") || h.command.includes(hooksDir); });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
registerHandler("context", function (clientId, message) {
|
|
144
|
+
const msg = message;
|
|
145
|
+
const action = msg.action;
|
|
146
|
+
if (action === "install_hooks") {
|
|
147
|
+
const result = installHooks();
|
|
148
|
+
sendTo(clientId, {
|
|
149
|
+
type: "context:hooks_status",
|
|
150
|
+
installed: result.success ? true : checkHooksInstalled(),
|
|
151
|
+
message: result.message,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (action === "uninstall_hooks") {
|
|
156
|
+
const result = uninstallHooks();
|
|
157
|
+
sendTo(clientId, {
|
|
158
|
+
type: "context:hooks_status",
|
|
159
|
+
installed: checkHooksInstalled(),
|
|
160
|
+
message: result.message,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (action === "check_hooks") {
|
|
165
|
+
sendTo(clientId, {
|
|
166
|
+
type: "context:hooks_status",
|
|
167
|
+
installed: checkHooksInstalled(),
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
});
|