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.
Files changed (44) hide show
  1. package/README.ja.md +283 -0
  2. package/README.md +283 -0
  3. package/content/hooks/hooks.json +41 -0
  4. package/content/mcp/.mcp.json +12 -0
  5. package/dist/audit-DujZ6YAy.mjs +18 -0
  6. package/dist/cli.mjs +509 -0
  7. package/dist/dispatcher-BzOdcjaa.mjs +93 -0
  8. package/dist/embedder-BshPIMrW.mjs +215 -0
  9. package/dist/epic-CdRKNGvP.mjs +227 -0
  10. package/dist/fts-BDdUbNfM.mjs +195 -0
  11. package/dist/helpers-BsdW4kgn.mjs +94 -0
  12. package/dist/knowledge-CCCixwb8.mjs +156 -0
  13. package/dist/post-tool-qemgso2b.mjs +88 -0
  14. package/dist/postinstall.mjs +49 -0
  15. package/dist/pre-compact-Cmg9kprV.mjs +181 -0
  16. package/dist/project-CpgK3fwQ.mjs +79 -0
  17. package/dist/schema-CcIFwr_0.mjs +289 -0
  18. package/dist/server-DF7CXxKi.mjs +2635 -0
  19. package/dist/server-Dsf47Pd4.mjs +19220 -0
  20. package/dist/session-start-DUYF6E0V.mjs +209 -0
  21. package/dist/store-Clcihees.mjs +338 -0
  22. package/dist/types-C3butmI8.mjs +6823 -0
  23. package/dist/user-prompt-BDeST0mR.mjs +144 -0
  24. package/dist/vectors-DvuAqDeO.mjs +83 -0
  25. package/package.json +46 -0
  26. package/web/dist/assets/activity-UyW12k7Z.js +1 -0
  27. package/web/dist/assets/api-BI8AW-mC.js +1 -0
  28. package/web/dist/assets/dist-BHj_gZG8.js +1 -0
  29. package/web/dist/assets/dist-DDZSXOC-.js +1 -0
  30. package/web/dist/assets/index-B9C85vN2.js +10 -0
  31. package/web/dist/assets/index-bIyYMf1a.css +1 -0
  32. package/web/dist/assets/knowledge-DmvXTX67.js +5 -0
  33. package/web/dist/assets/link-BSgD_zxQ.js +1 -0
  34. package/web/dist/assets/matchContext-CO01nzZ3.js +1 -0
  35. package/web/dist/assets/progress-DBmt_Ww6.js +6 -0
  36. package/web/dist/assets/routes-zEN1XNFl.js +1 -0
  37. package/web/dist/assets/scroll-area-DPCDB42s.js +45 -0
  38. package/web/dist/assets/separator-5sy8HYz5.js +1 -0
  39. package/web/dist/assets/skeleton-D7GRd6oJ.js +1 -0
  40. package/web/dist/assets/tabs-VSkG1f0-.js +1 -0
  41. package/web/dist/assets/tasks-CKNc1U7M.js +1 -0
  42. package/web/dist/assets/tasks._slug-DPzi78wf.js +8 -0
  43. package/web/dist/assets/utils-Dw49HYRP.js +1 -0
  44. 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 };