claude-alfred 0.1.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/README.ja.md +283 -0
- package/README.md +283 -0
- package/content/hooks/hooks.json +41 -0
- package/content/mcp/.mcp.json +12 -0
- package/dist/audit-DujZ6YAy.mjs +18 -0
- package/dist/cli.mjs +509 -0
- package/dist/dispatcher-BzOdcjaa.mjs +93 -0
- package/dist/embedder-BshPIMrW.mjs +215 -0
- package/dist/epic-CdRKNGvP.mjs +227 -0
- package/dist/fts-BDdUbNfM.mjs +195 -0
- package/dist/helpers-BsdW4kgn.mjs +94 -0
- package/dist/knowledge-CCCixwb8.mjs +156 -0
- package/dist/post-tool-qemgso2b.mjs +88 -0
- package/dist/postinstall.mjs +49 -0
- package/dist/pre-compact-Cmg9kprV.mjs +181 -0
- package/dist/project-CpgK3fwQ.mjs +79 -0
- package/dist/schema-CcIFwr_0.mjs +289 -0
- package/dist/server-DF7CXxKi.mjs +2635 -0
- package/dist/server-Dsf47Pd4.mjs +19220 -0
- package/dist/session-start-DUYF6E0V.mjs +209 -0
- package/dist/store-Clcihees.mjs +338 -0
- package/dist/types-C3butmI8.mjs +6823 -0
- package/dist/user-prompt-BDeST0mR.mjs +144 -0
- package/dist/vectors-DvuAqDeO.mjs +83 -0
- package/package.json +46 -0
- package/web/dist/assets/activity-UyW12k7Z.js +1 -0
- package/web/dist/assets/api-BI8AW-mC.js +1 -0
- package/web/dist/assets/dist-BHj_gZG8.js +1 -0
- package/web/dist/assets/dist-DDZSXOC-.js +1 -0
- package/web/dist/assets/index-B9C85vN2.js +10 -0
- package/web/dist/assets/index-bIyYMf1a.css +1 -0
- package/web/dist/assets/knowledge-DmvXTX67.js +5 -0
- package/web/dist/assets/link-BSgD_zxQ.js +1 -0
- package/web/dist/assets/matchContext-CO01nzZ3.js +1 -0
- package/web/dist/assets/progress-DBmt_Ww6.js +6 -0
- package/web/dist/assets/routes-zEN1XNFl.js +1 -0
- package/web/dist/assets/scroll-area-DPCDB42s.js +45 -0
- package/web/dist/assets/separator-5sy8HYz5.js +1 -0
- package/web/dist/assets/skeleton-D7GRd6oJ.js +1 -0
- package/web/dist/assets/tabs-VSkG1f0-.js +1 -0
- package/web/dist/assets/tasks-CKNc1U7M.js +1 -0
- package/web/dist/assets/tasks._slug-DPzi78wf.js +8 -0
- package/web/dist/assets/utils-Dw49HYRP.js +1 -0
- package/web/dist/index.html +17 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as incrementHitCount, r as getKnowledgeByIDs, u as searchKnowledgeKeyword } from "./knowledge-CCCixwb8.mjs";
|
|
3
|
+
import { r as vectorSearchKnowledge } from "./vectors-DvuAqDeO.mjs";
|
|
4
|
+
import { i as subTypeHalfLife, n as searchKnowledgeFTS, r as subTypeBoost } from "./fts-BDdUbNfM.mjs";
|
|
5
|
+
//#region src/mcp/helpers.ts
|
|
6
|
+
const RECENCY_FLOOR = .5;
|
|
7
|
+
function truncate(s, maxLen) {
|
|
8
|
+
const runes = [...s];
|
|
9
|
+
if (runes.length <= maxLen) return s;
|
|
10
|
+
return runes.slice(0, maxLen).join("") + "...";
|
|
11
|
+
}
|
|
12
|
+
function recencyFactor(createdAt, subType, now) {
|
|
13
|
+
const halfLife = subTypeHalfLife(subType);
|
|
14
|
+
if (halfLife <= 0) return 1;
|
|
15
|
+
const parsed = Date.parse(createdAt);
|
|
16
|
+
if (isNaN(parsed)) return 1;
|
|
17
|
+
const ageDays = (now.getTime() - parsed) / (1e3 * 60 * 60 * 24);
|
|
18
|
+
if (ageDays <= 0) return 1;
|
|
19
|
+
const factor = Math.exp(-Math.LN2 * ageDays / halfLife);
|
|
20
|
+
return factor < RECENCY_FLOOR ? RECENCY_FLOOR : factor;
|
|
21
|
+
}
|
|
22
|
+
function applyRecencySignal(docs, now) {
|
|
23
|
+
if (docs.length === 0) return docs;
|
|
24
|
+
const scored = docs.map((doc, i) => {
|
|
25
|
+
const posScore = 1 / (i + 1);
|
|
26
|
+
const rf = recencyFactor(doc.createdAt, doc.subType, now);
|
|
27
|
+
const stb = subTypeBoost(doc.subType);
|
|
28
|
+
return {
|
|
29
|
+
doc,
|
|
30
|
+
score: posScore * rf * stb
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
if (scored.length > 1) scored.sort((a, b) => b.score - a.score);
|
|
34
|
+
return scored.map((s) => s.doc);
|
|
35
|
+
}
|
|
36
|
+
async function searchPipeline(store, emb, query, limit, overRetrieve) {
|
|
37
|
+
const res = {
|
|
38
|
+
docs: [],
|
|
39
|
+
searchMethod: "",
|
|
40
|
+
warnings: []
|
|
41
|
+
};
|
|
42
|
+
if (emb) try {
|
|
43
|
+
const matches = vectorSearchKnowledge(store, await emb.embedForSearch(query), overRetrieve);
|
|
44
|
+
if (matches.length > 0) {
|
|
45
|
+
const ids = matches.map((m) => m.sourceId);
|
|
46
|
+
const docs = getKnowledgeByIDs(store, ids);
|
|
47
|
+
const docMap = new Map(docs.map((d) => [d.id, d]));
|
|
48
|
+
const ordered = [];
|
|
49
|
+
for (const id of ids) {
|
|
50
|
+
const d = docMap.get(id);
|
|
51
|
+
if (d) ordered.push(d);
|
|
52
|
+
}
|
|
53
|
+
res.docs = ordered;
|
|
54
|
+
res.searchMethod = "vector";
|
|
55
|
+
if (res.docs.length > limit) try {
|
|
56
|
+
const contents = res.docs.map((d) => d.title + "\n" + d.content);
|
|
57
|
+
const reranked = await emb.rerank(query, contents, limit);
|
|
58
|
+
if (reranked.length > 0) {
|
|
59
|
+
const reorderedDocs = [];
|
|
60
|
+
for (const r of reranked) if (r.index >= 0 && r.index < res.docs.length) reorderedDocs.push(res.docs[r.index]);
|
|
61
|
+
res.docs = reorderedDocs;
|
|
62
|
+
res.searchMethod = "vector+rerank";
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
res.warnings.push(`rerank failed: ${err}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.warnings.push(`vector embedding failed: ${err}`);
|
|
70
|
+
}
|
|
71
|
+
if (res.docs.length === 0) {
|
|
72
|
+
res.searchMethod = "fts5";
|
|
73
|
+
try {
|
|
74
|
+
res.docs = searchKnowledgeFTS(store, query, limit);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
res.warnings.push(`fts5 search failed: ${err}`);
|
|
77
|
+
res.searchMethod = "keyword";
|
|
78
|
+
try {
|
|
79
|
+
res.docs = searchKnowledgeKeyword(store, query, limit);
|
|
80
|
+
} catch (err2) {
|
|
81
|
+
res.warnings.push(`keyword search failed: ${err2}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
res.docs = applyRecencySignal(res.docs, /* @__PURE__ */ new Date());
|
|
86
|
+
if (res.docs.length > limit) res.docs = res.docs.slice(0, limit);
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
function trackHitCounts(store, docs) {
|
|
90
|
+
if (docs.length === 0) return;
|
|
91
|
+
incrementHitCount(store, docs.filter((d) => d.id > 0).map((d) => d.id));
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { trackHitCounts as n, truncate as r, searchPipeline as t };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/store/knowledge.ts
|
|
4
|
+
function contentHash(content) {
|
|
5
|
+
return createHash("sha256").update(content).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
function upsertKnowledge(store, row) {
|
|
8
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9
|
+
if (!row.createdAt) row.createdAt = now;
|
|
10
|
+
row.updatedAt = now;
|
|
11
|
+
row.contentHash = contentHash(row.content);
|
|
12
|
+
const existing = store.db.prepare("SELECT id, content_hash FROM knowledge_index WHERE project_remote = ? AND project_path = ? AND file_path = ?").get(row.projectRemote, row.projectPath, row.filePath);
|
|
13
|
+
if (existing && existing.content_hash === row.contentHash) {
|
|
14
|
+
row.id = existing.id;
|
|
15
|
+
return {
|
|
16
|
+
id: existing.id,
|
|
17
|
+
changed: false
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const result = store.db.prepare(`
|
|
21
|
+
INSERT INTO knowledge_index
|
|
22
|
+
(file_path, content_hash, title, content, sub_type,
|
|
23
|
+
project_remote, project_path, project_name, branch,
|
|
24
|
+
created_at, updated_at, hit_count, last_accessed, enabled)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, '', 1)
|
|
26
|
+
ON CONFLICT(project_remote, project_path, file_path) DO UPDATE SET
|
|
27
|
+
content_hash = excluded.content_hash,
|
|
28
|
+
title = excluded.title,
|
|
29
|
+
content = excluded.content,
|
|
30
|
+
sub_type = excluded.sub_type,
|
|
31
|
+
project_name = excluded.project_name,
|
|
32
|
+
branch = excluded.branch,
|
|
33
|
+
updated_at = excluded.updated_at
|
|
34
|
+
`).run(row.filePath, row.contentHash, row.title, row.content, row.subType, row.projectRemote, row.projectPath, row.projectName, row.branch, row.createdAt, row.updatedAt);
|
|
35
|
+
const id = Number(result.lastInsertRowid);
|
|
36
|
+
row.id = id;
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
changed: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function getKnowledgeByID(store, id) {
|
|
43
|
+
const row = store.db.prepare(`
|
|
44
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
45
|
+
project_remote, project_path, project_name, branch,
|
|
46
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
47
|
+
FROM knowledge_index WHERE id = ?
|
|
48
|
+
`).get(id);
|
|
49
|
+
return row ? mapRow(row) : void 0;
|
|
50
|
+
}
|
|
51
|
+
function getKnowledgeByIDs(store, ids) {
|
|
52
|
+
if (ids.length === 0) return [];
|
|
53
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
54
|
+
return store.db.prepare(`
|
|
55
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
56
|
+
project_remote, project_path, project_name, branch,
|
|
57
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
58
|
+
FROM knowledge_index WHERE id IN (${placeholders})
|
|
59
|
+
`).all(...ids).map(mapRow);
|
|
60
|
+
}
|
|
61
|
+
function listAllKnowledge(store, projectRemote, projectPath, limit) {
|
|
62
|
+
return store.db.prepare(`
|
|
63
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
64
|
+
project_remote, project_path, project_name, branch,
|
|
65
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
66
|
+
FROM knowledge_index
|
|
67
|
+
WHERE project_remote = ? AND project_path = ?
|
|
68
|
+
ORDER BY updated_at DESC LIMIT ?
|
|
69
|
+
`).all(projectRemote, projectPath, limit).map(mapRow);
|
|
70
|
+
}
|
|
71
|
+
function setKnowledgeEnabled(store, id, enabled) {
|
|
72
|
+
store.db.prepare("UPDATE knowledge_index SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, id);
|
|
73
|
+
}
|
|
74
|
+
function incrementHitCount(store, ids) {
|
|
75
|
+
if (ids.length === 0) return;
|
|
76
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
77
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
78
|
+
store.db.prepare(`UPDATE knowledge_index SET hit_count = hit_count + 1, last_accessed = ?
|
|
79
|
+
WHERE id IN (${placeholders})`).run(now, ...ids);
|
|
80
|
+
}
|
|
81
|
+
function promoteSubType(store, id, newSubType) {
|
|
82
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
83
|
+
if (store.db.prepare("UPDATE knowledge_index SET sub_type = ?, updated_at = ? WHERE id = ? AND enabled = 1").run(newSubType, now, id).changes === 0) throw new Error(`store: promote sub_type: knowledge ${id} not found or disabled`);
|
|
84
|
+
}
|
|
85
|
+
function getPromotionCandidates(store) {
|
|
86
|
+
return store.db.prepare(`
|
|
87
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
88
|
+
project_remote, project_path, project_name, branch,
|
|
89
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
90
|
+
FROM knowledge_index
|
|
91
|
+
WHERE enabled = 1
|
|
92
|
+
AND ((sub_type = 'general' AND hit_count >= 5)
|
|
93
|
+
OR (sub_type = 'pattern' AND hit_count >= 15))
|
|
94
|
+
ORDER BY hit_count DESC
|
|
95
|
+
`).all().map(mapRow);
|
|
96
|
+
}
|
|
97
|
+
function getKnowledgeStats(store) {
|
|
98
|
+
const agg = store.db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(hit_count), 0) as avg_hits FROM knowledge_index WHERE enabled = 1").get();
|
|
99
|
+
const bySubType = {};
|
|
100
|
+
const subtypeRows = store.db.prepare("SELECT sub_type, COUNT(*) as cnt FROM knowledge_index WHERE enabled = 1 GROUP BY sub_type").all();
|
|
101
|
+
for (const r of subtypeRows) bySubType[r.sub_type] = r.cnt;
|
|
102
|
+
const topRows = store.db.prepare(`
|
|
103
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
104
|
+
project_remote, project_path, project_name, branch,
|
|
105
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
106
|
+
FROM knowledge_index WHERE enabled = 1
|
|
107
|
+
ORDER BY hit_count DESC LIMIT 5
|
|
108
|
+
`).all();
|
|
109
|
+
return {
|
|
110
|
+
total: agg?.total ?? 0,
|
|
111
|
+
avgHitCount: agg?.avg_hits ?? 0,
|
|
112
|
+
bySubType,
|
|
113
|
+
topAccessed: topRows.map(mapRow)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function searchKnowledgeKeyword(store, query, limit) {
|
|
117
|
+
const escaped = escapeLIKEContains(query);
|
|
118
|
+
return store.db.prepare(`
|
|
119
|
+
SELECT id, file_path, content_hash, title, content, sub_type,
|
|
120
|
+
project_remote, project_path, project_name, branch,
|
|
121
|
+
created_at, updated_at, hit_count, last_accessed, enabled
|
|
122
|
+
FROM knowledge_index
|
|
123
|
+
WHERE enabled = 1 AND (content LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\')
|
|
124
|
+
ORDER BY hit_count DESC LIMIT ?
|
|
125
|
+
`).all(escaped, escaped, limit).map(mapRow);
|
|
126
|
+
}
|
|
127
|
+
function countKnowledge(store, projectRemote, projectPath) {
|
|
128
|
+
return store.db.prepare("SELECT COUNT(*) as cnt FROM knowledge_index WHERE project_remote = ? AND project_path = ? AND enabled = 1").get(projectRemote, projectPath).cnt;
|
|
129
|
+
}
|
|
130
|
+
function escapeLIKEContains(s) {
|
|
131
|
+
s = s.replaceAll("\\", "\\\\");
|
|
132
|
+
s = s.replaceAll("%", "\\%");
|
|
133
|
+
s = s.replaceAll("_", "\\_");
|
|
134
|
+
return `%${s}%`;
|
|
135
|
+
}
|
|
136
|
+
function mapRow(r) {
|
|
137
|
+
return {
|
|
138
|
+
id: r.id,
|
|
139
|
+
filePath: r.file_path,
|
|
140
|
+
contentHash: r.content_hash,
|
|
141
|
+
title: r.title,
|
|
142
|
+
content: r.content,
|
|
143
|
+
subType: r.sub_type,
|
|
144
|
+
projectRemote: r.project_remote,
|
|
145
|
+
projectPath: r.project_path,
|
|
146
|
+
projectName: r.project_name,
|
|
147
|
+
branch: r.branch,
|
|
148
|
+
createdAt: r.created_at,
|
|
149
|
+
updatedAt: r.updated_at,
|
|
150
|
+
hitCount: r.hit_count,
|
|
151
|
+
lastAccessed: r.last_accessed,
|
|
152
|
+
enabled: r.enabled === 1
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { getPromotionCandidates as a, mapRow as c, setKnowledgeEnabled as d, upsertKnowledge as f, getKnowledgeStats as i, promoteSubType as l, getKnowledgeByID as n, incrementHitCount as o, getKnowledgeByIDs as r, listAllKnowledge as s, countKnowledge as t, searchKnowledgeKeyword as u };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as readActive, t as SpecDir } from "./types-C3butmI8.mjs";
|
|
3
|
+
import { n as searchKnowledgeFTS } from "./fts-BDdUbNfM.mjs";
|
|
4
|
+
import { emitAdditionalContext, extractSection, notifyUser } from "./dispatcher-BzOdcjaa.mjs";
|
|
5
|
+
import { openDefaultCached } from "./store-Clcihees.mjs";
|
|
6
|
+
import { r as truncate } from "./helpers-BsdW4kgn.mjs";
|
|
7
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
//#region src/hooks/post-tool.ts
|
|
11
|
+
const EXPLORE_COUNTER_PATH = join(tmpdir(), "alfred-explore-count");
|
|
12
|
+
function readExploreCount() {
|
|
13
|
+
try {
|
|
14
|
+
return parseInt(readFileSync(EXPLORE_COUNTER_PATH, "utf-8"), 10) || 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeExploreCount(n) {
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(EXPLORE_COUNTER_PATH, String(n));
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
async function postToolUse(ev, _signal) {
|
|
25
|
+
if (!ev.cwd || !ev.tool_name) return;
|
|
26
|
+
if (ev.tool_name === "Read" || ev.tool_name === "Grep" || ev.tool_name === "Glob") {
|
|
27
|
+
const count = readExploreCount() + 1;
|
|
28
|
+
writeExploreCount(count);
|
|
29
|
+
if (count >= 5) try {
|
|
30
|
+
readActive(ev.cwd);
|
|
31
|
+
} catch {
|
|
32
|
+
notifyUser("tip: 5+ consecutive %s calls without a spec. Consider `/alfred:survey` to reverse-engineer a spec from the code.", ev.tool_name);
|
|
33
|
+
writeExploreCount(0);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
writeExploreCount(0);
|
|
38
|
+
if (ev.tool_name === "Bash") await handleBashResult(ev);
|
|
39
|
+
}
|
|
40
|
+
async function handleBashResult(ev) {
|
|
41
|
+
const response = ev.tool_response;
|
|
42
|
+
if (!response) return;
|
|
43
|
+
if (response.exitCode && response.exitCode !== 0 && response.stderr) {
|
|
44
|
+
const errorText = typeof response.stderr === "string" ? response.stderr : "";
|
|
45
|
+
if (errorText.length > 10) await searchErrorContext(ev.cwd, errorText);
|
|
46
|
+
}
|
|
47
|
+
if (response.exitCode === 0) autoCheckNextSteps(ev.cwd, response.stdout ?? "");
|
|
48
|
+
}
|
|
49
|
+
async function searchErrorContext(projectPath, errorText) {
|
|
50
|
+
let store;
|
|
51
|
+
try {
|
|
52
|
+
store = openDefaultCached();
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const query = errorText.slice(0, 200);
|
|
57
|
+
try {
|
|
58
|
+
const docs = searchKnowledgeFTS(store, query, 3);
|
|
59
|
+
if (docs.length > 0) emitAdditionalContext("PostToolUse", `Related knowledge for this error:\n${docs.map((d) => `- ${d.title}: ${truncate(d.content, 150)}`).join("\n")}`);
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
function autoCheckNextSteps(projectPath, stdout) {
|
|
63
|
+
try {
|
|
64
|
+
const sd = new SpecDir(projectPath, readActive(projectPath));
|
|
65
|
+
const session = sd.readFile("session.md");
|
|
66
|
+
const nextStepsSection = extractSection(session, "## Next Steps");
|
|
67
|
+
if (!nextStepsSection) return;
|
|
68
|
+
const lines = nextStepsSection.split("\n");
|
|
69
|
+
let changed = false;
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
const match = line.match(/^- \[ \] (.+)$/);
|
|
73
|
+
if (!match) continue;
|
|
74
|
+
const description = match[1].toLowerCase();
|
|
75
|
+
if (stdout && description.split(/\s+/).some((word) => word.length > 3 && stdout.toLowerCase().includes(word))) {
|
|
76
|
+
lines[i] = line.replace("- [ ]", "- [x]");
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (changed) {
|
|
81
|
+
const updatedSection = lines.join("\n");
|
|
82
|
+
const updatedSession = session.replace(nextStepsSection, updatedSection);
|
|
83
|
+
sd.writeFile("session.md", updatedSession);
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { postToolUse };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
//#region src/postinstall.ts
|
|
5
|
+
async function main() {
|
|
6
|
+
const home = homedir();
|
|
7
|
+
const dbDir = join(home, ".claude-alfred");
|
|
8
|
+
try {
|
|
9
|
+
mkdirSync(dbDir, { recursive: true });
|
|
10
|
+
} catch {}
|
|
11
|
+
try {
|
|
12
|
+
const Database = (await import("better-sqlite3")).default;
|
|
13
|
+
const { migrate } = await import("./schema-CcIFwr_0.mjs");
|
|
14
|
+
const dbPath = join(dbDir, "alfred.db");
|
|
15
|
+
const db = new Database(dbPath);
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
migrate(db);
|
|
18
|
+
db.close();
|
|
19
|
+
console.log("[alfred] database ready:", dbPath);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error("[alfred] warning: database setup skipped:", err);
|
|
22
|
+
}
|
|
23
|
+
const rulesDir = join(home, ".claude", "rules");
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
26
|
+
if (!readdirSync(rulesDir).some((f) => f.startsWith("alfred-"))) {
|
|
27
|
+
writeFileSync(join(rulesDir, "alfred.md"), `# alfred MCP Tools
|
|
28
|
+
|
|
29
|
+
alfred's knowledge base contains extensive curated Claude Code docs and best practices with vector search.
|
|
30
|
+
|
|
31
|
+
## knowledge — Search docs and best practices
|
|
32
|
+
|
|
33
|
+
**ALWAYS call knowledge BEFORE** answering questions about Claude Code. Do not guess or rely on training data.
|
|
34
|
+
|
|
35
|
+
Call when the user's question or task involves ANY of:
|
|
36
|
+
- Hooks, skills, rules, agents, plugins, MCP servers, CLAUDE.md, memory
|
|
37
|
+
- Permissions, settings, compaction, CLI features, IDE integrations
|
|
38
|
+
- Best practices for Claude Code configuration or workflow
|
|
39
|
+
- Evaluating whether code follows Claude Code conventions
|
|
40
|
+
|
|
41
|
+
Do NOT call for: general programming, project-specific code, non-Claude-Code topics.
|
|
42
|
+
`);
|
|
43
|
+
console.log("[alfred] rules installed:", rulesDir);
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
main().catch(() => {});
|
|
48
|
+
//#endregion
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as readActive, r as completeTask, s as readActiveState, t as SpecDir } from "./types-C3butmI8.mjs";
|
|
3
|
+
import { o as syncTaskStatus } from "./epic-CdRKNGvP.mjs";
|
|
4
|
+
import { f as upsertKnowledge } from "./knowledge-CCCixwb8.mjs";
|
|
5
|
+
import { t as detectProject } from "./project-CpgK3fwQ.mjs";
|
|
6
|
+
import { notifyUser } from "./dispatcher-BzOdcjaa.mjs";
|
|
7
|
+
import { openDefaultCached } from "./store-Clcihees.mjs";
|
|
8
|
+
import { t as appendAudit } from "./audit-DujZ6YAy.mjs";
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
//#region src/hooks/pre-compact.ts
|
|
12
|
+
async function preCompact(ev, _signal) {
|
|
13
|
+
if (!ev.cwd) return;
|
|
14
|
+
let store;
|
|
15
|
+
try {
|
|
16
|
+
store = openDefaultCached();
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const projectPath = ev.cwd;
|
|
21
|
+
const proj = detectProject(projectPath);
|
|
22
|
+
if (ev.transcript_path) try {
|
|
23
|
+
const decisions = extractDecisions(readFileSync(ev.transcript_path, "utf-8"));
|
|
24
|
+
if (decisions.length > 0) {
|
|
25
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
|
|
26
|
+
for (let idx = 0; idx < decisions.length; idx++) {
|
|
27
|
+
const dec = decisions[idx];
|
|
28
|
+
const row = {
|
|
29
|
+
id: 0,
|
|
30
|
+
filePath: `decisions/compact/${ts}-${idx}`,
|
|
31
|
+
contentHash: "",
|
|
32
|
+
title: dec.title,
|
|
33
|
+
content: dec.content,
|
|
34
|
+
subType: "decision",
|
|
35
|
+
projectRemote: proj.remote,
|
|
36
|
+
projectPath: proj.path,
|
|
37
|
+
projectName: proj.name,
|
|
38
|
+
branch: proj.branch,
|
|
39
|
+
createdAt: "",
|
|
40
|
+
updatedAt: "",
|
|
41
|
+
hitCount: 0,
|
|
42
|
+
lastAccessed: "",
|
|
43
|
+
enabled: true
|
|
44
|
+
};
|
|
45
|
+
upsertKnowledge(store, row);
|
|
46
|
+
}
|
|
47
|
+
notifyUser("extracted %d decisions from transcript", decisions.length);
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
try {
|
|
51
|
+
const taskSlug = readActive(projectPath);
|
|
52
|
+
const sd = new SpecDir(projectPath, taskSlug);
|
|
53
|
+
if (sd.exists()) {
|
|
54
|
+
const session = sd.readFile("session.md");
|
|
55
|
+
const chapterNum = (session.match(/## Compact Marker \[/g) ?? []).length + 1;
|
|
56
|
+
const title = `${proj.name} > ${taskSlug} > chapter-${chapterNum} > session-state`;
|
|
57
|
+
const row = {
|
|
58
|
+
id: 0,
|
|
59
|
+
filePath: `chapters/${taskSlug}/chapter-${chapterNum}`,
|
|
60
|
+
contentHash: "",
|
|
61
|
+
title,
|
|
62
|
+
content: session.slice(0, 2e3),
|
|
63
|
+
subType: "general",
|
|
64
|
+
projectRemote: proj.remote,
|
|
65
|
+
projectPath: proj.path,
|
|
66
|
+
projectName: proj.name,
|
|
67
|
+
branch: proj.branch,
|
|
68
|
+
createdAt: "",
|
|
69
|
+
updatedAt: "",
|
|
70
|
+
hitCount: 0,
|
|
71
|
+
lastAccessed: "",
|
|
72
|
+
enabled: true
|
|
73
|
+
};
|
|
74
|
+
upsertKnowledge(store, row);
|
|
75
|
+
const breadcrumb = {
|
|
76
|
+
claude_session_id: process.env["CLAUDE_SESSION_ID"] ?? "",
|
|
77
|
+
task_slug: taskSlug,
|
|
78
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
79
|
+
};
|
|
80
|
+
writeFileSync(join(projectPath, ".alfred", ".pending-compact.json"), JSON.stringify(breadcrumb));
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
try {
|
|
84
|
+
const taskSlug = readActive(projectPath);
|
|
85
|
+
if (isSessionCompleted(new SpecDir(projectPath, taskSlug).readFile("session.md"))) {
|
|
86
|
+
completeTask(projectPath, taskSlug);
|
|
87
|
+
syncTaskStatus(projectPath, taskSlug, "completed");
|
|
88
|
+
appendAudit(projectPath, {
|
|
89
|
+
action: "spec.complete",
|
|
90
|
+
target: taskSlug,
|
|
91
|
+
detail: "auto-completed during compact",
|
|
92
|
+
user: "auto"
|
|
93
|
+
});
|
|
94
|
+
notifyUser("auto-completed task '%s'", taskSlug);
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
try {
|
|
98
|
+
const state = readActiveState(projectPath);
|
|
99
|
+
for (const task of state.tasks) if (task.status === "completed") syncTaskStatus(projectPath, task.slug, "completed");
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
const DECISION_KEYWORDS = [
|
|
103
|
+
"decided",
|
|
104
|
+
"決定した",
|
|
105
|
+
"going with",
|
|
106
|
+
"we'll",
|
|
107
|
+
"chose",
|
|
108
|
+
"chosen",
|
|
109
|
+
"architecture",
|
|
110
|
+
"アーキテクチャ",
|
|
111
|
+
"design choice",
|
|
112
|
+
"decided to"
|
|
113
|
+
];
|
|
114
|
+
const RATIONALE_SIGNALS = [
|
|
115
|
+
"because",
|
|
116
|
+
"since",
|
|
117
|
+
"reason",
|
|
118
|
+
"rationale",
|
|
119
|
+
"なぜなら",
|
|
120
|
+
"理由"
|
|
121
|
+
];
|
|
122
|
+
const ALTERNATIVE_SIGNALS = [
|
|
123
|
+
"instead of",
|
|
124
|
+
"rather than",
|
|
125
|
+
"alternative",
|
|
126
|
+
"considered",
|
|
127
|
+
"代わりに"
|
|
128
|
+
];
|
|
129
|
+
const ARCH_TERMS = [
|
|
130
|
+
"component",
|
|
131
|
+
"module",
|
|
132
|
+
"layer",
|
|
133
|
+
"service",
|
|
134
|
+
"interface",
|
|
135
|
+
"pattern",
|
|
136
|
+
"migration"
|
|
137
|
+
];
|
|
138
|
+
function extractDecisions(transcript) {
|
|
139
|
+
const decisions = [];
|
|
140
|
+
const lines = transcript.split("\n");
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
let entry;
|
|
143
|
+
try {
|
|
144
|
+
entry = JSON.parse(line);
|
|
145
|
+
} catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const text = typeof entry.content === "string" ? entry.content : typeof entry.message?.content === "string" ? entry.message.content : "";
|
|
149
|
+
if (!text) continue;
|
|
150
|
+
if ((entry.role ?? entry.message?.role) !== "assistant") continue;
|
|
151
|
+
const lower = text.toLowerCase();
|
|
152
|
+
let score = 0;
|
|
153
|
+
for (const kw of DECISION_KEYWORDS) if (lower.includes(kw)) {
|
|
154
|
+
score = .35;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (score === 0) continue;
|
|
158
|
+
if (RATIONALE_SIGNALS.some((s) => lower.includes(s))) score += .15;
|
|
159
|
+
if (ALTERNATIVE_SIGNALS.some((s) => lower.includes(s))) score += .15;
|
|
160
|
+
for (const term of ARCH_TERMS) if (lower.includes(term)) {
|
|
161
|
+
score += .05;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (score < .4) continue;
|
|
165
|
+
const firstSentence = text.split(/[.!?\n]/)[0]?.trim() ?? "Decision";
|
|
166
|
+
decisions.push({
|
|
167
|
+
title: firstSentence.slice(0, 100),
|
|
168
|
+
content: text.slice(0, 1e3)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return decisions;
|
|
172
|
+
}
|
|
173
|
+
function isSessionCompleted(session) {
|
|
174
|
+
const lower = session.toLowerCase();
|
|
175
|
+
if (lower.includes("status: completed") || lower.includes("status: done")) return true;
|
|
176
|
+
const nextSteps = session.match(/^- \[[ x]\] .+$/gm);
|
|
177
|
+
if (!nextSteps || nextSteps.length === 0) return false;
|
|
178
|
+
return nextSteps.every((step) => step.startsWith("- [x]"));
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { preCompact };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
//#region src/store/project.ts
|
|
5
|
+
function detectProject(dirPath) {
|
|
6
|
+
const absPath = resolve(dirPath);
|
|
7
|
+
const info = {
|
|
8
|
+
path: absPath,
|
|
9
|
+
name: basename(absPath),
|
|
10
|
+
remote: "",
|
|
11
|
+
branch: ""
|
|
12
|
+
};
|
|
13
|
+
info.remote = detectGitRemote(absPath);
|
|
14
|
+
info.branch = detectGitBranch(absPath);
|
|
15
|
+
if (info.remote) {
|
|
16
|
+
const name = repoNameFromRemote(info.remote);
|
|
17
|
+
if (name) info.name = name;
|
|
18
|
+
}
|
|
19
|
+
return info;
|
|
20
|
+
}
|
|
21
|
+
function detectGitRemote(dir) {
|
|
22
|
+
try {
|
|
23
|
+
return normalizeRemoteURL(execFileSync("git", [
|
|
24
|
+
"-C",
|
|
25
|
+
dir,
|
|
26
|
+
"remote",
|
|
27
|
+
"get-url",
|
|
28
|
+
"origin"
|
|
29
|
+
], {
|
|
30
|
+
timeout: 500,
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
stdio: [
|
|
33
|
+
"ignore",
|
|
34
|
+
"pipe",
|
|
35
|
+
"ignore"
|
|
36
|
+
]
|
|
37
|
+
}).trim());
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function detectGitBranch(dir) {
|
|
43
|
+
try {
|
|
44
|
+
return execFileSync("git", [
|
|
45
|
+
"-C",
|
|
46
|
+
dir,
|
|
47
|
+
"rev-parse",
|
|
48
|
+
"--abbrev-ref",
|
|
49
|
+
"HEAD"
|
|
50
|
+
], {
|
|
51
|
+
timeout: 500,
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
stdio: [
|
|
54
|
+
"ignore",
|
|
55
|
+
"pipe",
|
|
56
|
+
"ignore"
|
|
57
|
+
]
|
|
58
|
+
}).trim();
|
|
59
|
+
} catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function normalizeRemoteURL(raw) {
|
|
64
|
+
let s = raw;
|
|
65
|
+
if (s.startsWith("git@")) {
|
|
66
|
+
s = s.slice(4);
|
|
67
|
+
s = s.replace(":", "/");
|
|
68
|
+
}
|
|
69
|
+
s = s.replace(/^https?:\/\//, "");
|
|
70
|
+
s = s.replace(/\.git$/, "");
|
|
71
|
+
s = s.replace(/\/$/, "");
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
function repoNameFromRemote(remote) {
|
|
75
|
+
const parts = remote.split("/");
|
|
76
|
+
return parts[parts.length - 1] ?? "";
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { detectProject as t };
|