@lineman-io/mcp 2.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 (45) hide show
  1. package/.claude-plugin/plugin.json +27 -0
  2. package/README.md +86 -0
  3. package/dist/main.js +2 -0
  4. package/hooks/core/build-snapshot.mjs +1 -0
  5. package/hooks/core/extract-events.mjs +1 -0
  6. package/hooks/core/ingest-claude-md.mjs +1 -0
  7. package/hooks/core/mcp-ready.mjs +1 -0
  8. package/hooks/core/narrative.mjs +1 -0
  9. package/hooks/core/routing.mjs +1 -0
  10. package/hooks/core/security/chain-split.mjs +1 -0
  11. package/hooks/core/security/decide.mjs +1 -0
  12. package/hooks/core/security/patterns.mjs +1 -0
  13. package/hooks/core/security/policies.mjs +1 -0
  14. package/hooks/core/security/shell-escape.mjs +1 -0
  15. package/hooks/core/session-cleanup.mjs +1 -0
  16. package/hooks/core/session-db.mjs +1 -0
  17. package/hooks/core/stdin.mjs +1 -0
  18. package/hooks/core/summary-envelope.mjs +1 -0
  19. package/hooks/core/tool-aliases.mjs +1 -0
  20. package/hooks/core/tool-naming.mjs +1 -0
  21. package/hooks/core/truncate.mjs +1 -0
  22. package/hooks/ensure-deps.mjs +1 -0
  23. package/hooks/file-changed.mjs +2 -0
  24. package/hooks/hooks.json +230 -0
  25. package/hooks/lineman-call.mjs +1 -0
  26. package/hooks/metrics.mjs +2 -0
  27. package/hooks/permission-request.mjs +2 -0
  28. package/hooks/plugin-paths.mjs +1 -0
  29. package/hooks/post-compact.mjs +2 -0
  30. package/hooks/post-tool-use-failure.mjs +2 -0
  31. package/hooks/post-tool-use.mjs +2 -0
  32. package/hooks/pre-compact.mjs +2 -0
  33. package/hooks/pre-tool-use.mjs +2 -0
  34. package/hooks/runpod-state-reader.mjs +1 -0
  35. package/hooks/sentry-hook.mjs +1 -0
  36. package/hooks/session-start.mjs +2 -0
  37. package/hooks/settings.json +95 -0
  38. package/hooks/stop.mjs +2 -0
  39. package/hooks/suppress-stderr.mjs +1 -0
  40. package/hooks/user-prompt-submit.mjs +2 -0
  41. package/package.json +47 -0
  42. package/skills/doctor/SKILL.md +19 -0
  43. package/skills/lineman/SKILL.md +23 -0
  44. package/skills/stats/SKILL.md +13 -0
  45. package/start.mjs +31 -0
@@ -0,0 +1 @@
1
+ import{createHash as e}from"node:crypto";import{existsSync as s,mkdirSync as n}from"node:fs";import{createRequire as t}from"node:module";import{tmpdir as o}from"node:os";import{dirname as r,join as i,resolve as a}from"node:path";const E=t(import.meta.url);function _(s){const n=process.env.CLAUDE_PLUGIN_DATA??i(o(),"lineman"),t=e("sha256").update(a(s)).digest("hex").slice(0,16);return i(n,"sessions",`${t}.db`)}function T(e){e.pragma("journal_mode = WAL"),e.pragma("synchronous = NORMAL"),e.pragma("foreign_keys = ON"),e.exec("\n CREATE TABLE IF NOT EXISTS lineman_session_meta_ver (\n key TEXT PRIMARY KEY, value TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS session_meta (\n session_id TEXT PRIMARY KEY,\n project_dir TEXT NOT NULL,\n started_at TEXT NOT NULL DEFAULT (datetime('now')),\n last_event_at TEXT,\n event_count INTEGER NOT NULL DEFAULT 0,\n compact_count INTEGER NOT NULL DEFAULT 0\n );\n CREATE INDEX IF NOT EXISTS idx_session_meta_last_event ON session_meta(last_event_at);\n CREATE TABLE IF NOT EXISTS session_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n type TEXT NOT NULL,\n category TEXT NOT NULL,\n priority INTEGER NOT NULL DEFAULT 2,\n data TEXT NOT NULL,\n source_hook TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n data_hash TEXT NOT NULL DEFAULT '',\n FOREIGN KEY (session_id) REFERENCES session_meta(session_id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_events_session ON session_events(session_id);\n CREATE INDEX IF NOT EXISTS idx_events_session_type ON session_events(session_id, type);\n CREATE INDEX IF NOT EXISTS idx_events_session_priority ON session_events(session_id, priority);\n CREATE INDEX IF NOT EXISTS idx_events_session_hash ON session_events(session_id, type, data_hash);\n CREATE INDEX IF NOT EXISTS idx_events_created_at ON session_events(created_at);\n CREATE TABLE IF NOT EXISTS session_resume (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL UNIQUE,\n snapshot TEXT NOT NULL,\n event_count INTEGER NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n consumed INTEGER NOT NULL DEFAULT 0,\n narrative TEXT,\n narrative_event_count INTEGER,\n FOREIGN KEY (session_id) REFERENCES session_meta(session_id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_resume_consumed ON session_resume(consumed);\n ");const s=e.prepare("SELECT value FROM lineman_session_meta_ver WHERE key = 'schema_version'").get();if(!s)return void e.prepare("INSERT INTO lineman_session_meta_ver (key, value) VALUES ('schema_version', ?)").run(String(2));const n=Number.parseInt(s.value,10);if(2!==n){if(1!==n)throw new Error(`Lineman session DB schema mismatch: on disk is v${n}, hook expects v2`);!function(e){const s=e.prepare("PRAGMA table_info(session_resume)").all(),n=e=>s.some(s=>s.name===e);e.transaction(()=>{n("narrative")||e.exec("ALTER TABLE session_resume ADD COLUMN narrative TEXT"),n("narrative_event_count")||e.exec("ALTER TABLE session_resume ADD COLUMN narrative_event_count INTEGER"),e.prepare("UPDATE lineman_session_meta_ver SET value = ? WHERE key = 'schema_version'").run(String(2))})()}(e)}}export async function openSessionDB(e,s){const t=_(s);n(r(t),{recursive:!0});const{default:o}=await import("better-sqlite3"),i=new o(t);try{T(i)}catch(e){try{i.close()}catch{}throw e}return c(i,e,s)}export function openSessionDBSync(e,s){const t=_(s);n(r(t),{recursive:!0});const o=new(E("better-sqlite3"))(t);return T(o),c(o,e,s)}function c(s,n,t){const o=s.prepare("INSERT INTO session_meta (session_id, project_dir) VALUES (?, ?) ON CONFLICT(session_id) DO NOTHING"),r=s.prepare("UPDATE session_meta SET last_event_at = datetime('now'), event_count = event_count + 1 WHERE session_id = ?"),i=s.prepare("INSERT INTO session_events (session_id, type, category, priority, data, source_hook, data_hash) VALUES (?, ?, ?, ?, ?, ?, ?)"),a=s.prepare("SELECT 1 FROM session_events\n WHERE session_id = ? AND type = ? AND data_hash = ?\n AND id > (SELECT COALESCE(MAX(id), 0) - ? FROM session_events WHERE session_id = ?)\n LIMIT 1"),E=s.prepare("SELECT COUNT(*) AS n FROM session_events WHERE session_id = ?"),_=s.prepare("DELETE FROM session_events\n WHERE id IN (\n SELECT id FROM session_events WHERE session_id = ?\n ORDER BY priority DESC, id ASC LIMIT ?\n )"),T=s.prepare("SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash\n FROM session_events\n WHERE session_id = ?\n ORDER BY created_at ASC, id ASC\n LIMIT ?"),c=s.prepare("SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count\n FROM session_meta WHERE session_id = ?"),u=s.prepare("INSERT INTO session_resume (session_id, snapshot, event_count, consumed)\n VALUES (?, ?, ?, 0)\n ON CONFLICT(session_id) DO UPDATE\n SET snapshot = excluded.snapshot,\n event_count = excluded.event_count,\n consumed = 0,\n created_at = datetime('now')"),N=s.prepare("SELECT snapshot, event_count, consumed, narrative, narrative_event_count\n FROM session_resume WHERE session_id = ?"),p=s.prepare("UPDATE session_resume SET consumed = 1 WHERE session_id = ?"),m=s.prepare("UPDATE session_resume\n SET narrative = ?, narrative_event_count = ?\n WHERE session_id = ?"),L=s.prepare("UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),v=s.prepare("SELECT session_id FROM session_meta\n WHERE last_event_at IS NOT NULL AND last_event_at < datetime('now', ? || ' days')"),R=s.prepare("DELETE FROM session_meta WHERE session_id = ?");return o.run(n,t),{insertEvent:(t,o)=>{if(!t?.type||!t?.category)return!1;const T=t.priority??2,c=JSON.stringify(t.data??null),d=e("sha256").update(c).digest("hex").slice(0,16);return s.transaction(()=>{if(a.get(n,t.type,d,50,n))return!1;i.run(n,t.type,t.category,T,c,o,d),r.run(n);const{n:e}=E.get(n);return e>1e3&&_.run(n,e-1e3),!0})()},getEvents:(e=500)=>{const s=Math.min(Math.max(1,Number(e)||500),2e3);return T.all(n,s).map(e=>({id:e.id,sessionId:e.session_id,type:e.type,category:e.category,priority:e.priority,data:d(e.data),sourceHook:e.source_hook,createdAt:e.created_at,dataHash:e.data_hash}))},getMeta:()=>{const e=c.get(n);return e?{sessionId:e.session_id,projectDir:e.project_dir,startedAt:e.started_at,lastEventAt:e.last_event_at,eventCount:e.event_count,compactCount:e.compact_count}:null},upsertResume:(e,s)=>{if("string"!=typeof e)return;const t=Math.max(0,Number(s)||0);u.run(n,e,t)},getResume:()=>{const e=N.get(n);return e?{snapshot:e.snapshot,eventCount:e.event_count,consumed:1===e.consumed,narrative:e.narrative??null,narrativeEventCount:e.narrative_event_count??null}:null},markResumeConsumed:()=>{p.run(n)},upsertNarrative:(e,s)=>{if("string"!=typeof e)return!1;const t=Math.max(0,Number(s)||0);return m.run(e,t,n).changes>0},incrementCompactCount:()=>{L.run(n)},cleanupOldSessions:(e=7)=>{if(e<=0)throw new Error("cleanupOldSessions: days must be positive");const n=`-${e}`,t=v.all(n);return s.transaction(()=>{for(const e of t)R.run(e.session_id)})(),{sessionsDeleted:t.length}},close:()=>{try{s.close()}catch{}}}}function d(e){try{return JSON.parse(e)}catch{return e}}export{_ as sessionDbPath};
@@ -0,0 +1 @@
1
+ export function readStdin(){return new Promise((n,s)=>{const e=[];process.stdin.setEncoding("utf8"),process.stdin.on("data",n=>e.push(n)),process.stdin.on("end",()=>{let s=e.join("");65279===s.charCodeAt(0)&&(s=s.slice(1)),n(s)}),process.stdin.on("error",s),process.stdin.resume()})}export async function readJsonStdin(){const n=await readStdin();return n.trim()?JSON.parse(n):{}}
@@ -0,0 +1 @@
1
+ import{MCP_TOOL_NAME as e}from"./tool-naming.mjs";const t="[[LM_SUMMARY]]";export function wrapSummary(a,r){const o=function(t){const{sourceTool:a,sourcePath:r,sourceCommand:o,taskTypeForVerbatim:n}=t??{};return"Read"===a||"Edit"===a?`Generated by Lineman (summary-first; native ${a} was intercepted because the file is large). For exact bytes, call \`${e}\` with \`{ task_type: "${n??"read_file_full"}", path: "${r??"<original path>"}" }\` (supports \`offset\`/\`limit\` for line-range slices).`:"WebFetch"===a?`Generated by Lineman (summary-first). For a more targeted view of this page, call \`${e}\` with \`{ task_type: "analyze_web_page", url: "${r??"<original url>"}", query: "<what you need>" }\`. For raw HTML, retry the native WebFetch with a focused prompt.`:`Generated by Lineman (summary-first; ${a} output was compressed because it exceeded the size threshold). For raw output, re-run \`${o??"<original command>"}\` natively. For a differently-shaped summary, call \`${e}\` with \`{ task_type: "${n??"triage_build_output"}", content: "<paste output>" }\`.`}(r);return`${t} ${a}\n\n--\n${o}`}export const LINEMAN_SUMMARY_TAG=t;
@@ -0,0 +1 @@
1
+ const e=Object.freeze({run_shell_command:"Bash",read_file:"Read",grep_search:"Grep",web_fetch:"WebFetch",write_file:"Write",bash:"Bash",view:"Read",fetch:"WebFetch",shell:"Bash",grep_files:"Grep",run_in_terminal:"Bash",fs_read:"Read",execute_bash:"Bash"});export function canonicalize(a){return"string"!=typeof a?a:e[a]??a}export function aliasTable(){return e}
@@ -0,0 +1 @@
1
+ export const MCP_SERVER_NAME="plugin:lineman:core";export const MCP_TOOL_PREFIX="mcp__plugin_lineman_core__";export const MCP_TOOL_NAME="mcp__plugin_lineman_core__assist";export const MCP_TOOL_NAME_EDIT_FILE="mcp__plugin_lineman_core__edit_file";
@@ -0,0 +1 @@
1
+ const t=.6,e=.4;export function smartTruncate(n,r,f={}){const{headRatio:i=t,tailRatio:o=e,graceFactor:s=1.1}=f;if("string"!=typeof n||r<=0)return n??"";const u=Buffer.byteLength(n,"utf-8");if(u<=r)return n;if(u<=r*s)return n;const h=n.split(/\r?\n/);if(h.length<=1)return byteSafePrefix(n,r);const l=n.includes("\r\n")?2:1,a=Math.floor(r*i),g=Math.floor(r*o),c=[];let y=0;for(const t of h){const e=Buffer.byteLength(t,"utf-8")+l;if(y+e>a)break;c.push(t),y+=e}const b=[];let d=0;for(let t=h.length-1;t>=c.length;t--){const e=Buffer.byteLength(h[t],"utf-8")+l;if(d+e>g)break;b.unshift(h[t]),d+=e}const L=h.length-c.length-b.length;if(0===c.length&&0===b.length)return byteSafePrefix(n,r);const p=function({omittedLines:t,omittedBytes:e,headLines:n,tailLines:r}){return`... [${t} lines / ${(e/1024).toFixed(1)}KB truncated — showing first ${n} + last ${r} lines] ...`}({omittedLines:L,omittedBytes:u-y-d,headLines:c.length,tailLines:b.length}),B=[];return c.length>0&&B.push(c.join("\n")),B.push(p),b.length>0&&B.push(b.join("\n")),B.join("\n")}export function byteSafePrefix(t,e){if("string"!=typeof t)return"";if(e<=0)return"";if(Buffer.byteLength(t,"utf-8")<=e)return t;const n=Buffer.from(t,"utf-8");let r=Math.min(e,n.length);for(;r>0&&128==(192&n[r]);)r--;return n.subarray(0,r).toString("utf-8")}
@@ -0,0 +1 @@
1
+ export function ensureDeps(){}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{readFileSync as e}from"node:fs";import{join as n}from"node:path";import{readJsonStdin as o}from"./core/stdin.mjs";import{isEnabled as t}from"./lineman-call.mjs";await async function(){t()||(console.log("{}"),process.exit(0));try{const t=await o(),i=t.file??t.path??"",s=t.cwd??process.cwd();if(i.includes("package.json"))try{const o=e(n(s,i),"utf-8"),t=JSON.parse(o),a=Object.keys(t.dependencies??{}).length,c=Object.keys(t.devDependencies??{}).length;console.log(JSON.stringify({additionalContext:`FILE CHANGED: ${i} — ${a} dependencies, ${c} devDependencies. Use lineman with task_type='summarize_content' if you need to analyze the changes.`}))}catch{console.log(JSON.stringify({additionalContext:`FILE CHANGED: ${i} was modified.`}))}else i.includes(".env")?console.log(JSON.stringify({additionalContext:`FILE CHANGED: ${i} — environment variables may have changed. Do NOT read or display .env contents.`})):i.includes("tsconfig")?console.log(JSON.stringify({additionalContext:`FILE CHANGED: ${i} — TypeScript configuration may have changed. This could affect compilation behavior.`})):console.log("{}")}catch{console.log("{}")}}();
@@ -0,0 +1,230 @@
1
+ {
2
+ "description": "Lineman plugin hooks — secondary-LLM-powered context savings and safety checks for Claude Code.",
3
+ "hooks": {
4
+ "UserPromptSubmit": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.mjs",
10
+ "timeout": 5
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "PreToolUse": [
16
+ {
17
+ "matcher": "Read",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
22
+ "timeout": 10,
23
+ "statusMessage": "Lineman checking file size..."
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "matcher": "Edit",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
33
+ "timeout": 30,
34
+ "statusMessage": "Lineman validating patch target..."
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "Grep",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
44
+ "timeout": 3
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "matcher": "Bash",
50
+ "hooks": [
51
+ {
52
+ "type": "command",
53
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
54
+ "timeout": 3
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "matcher": "WebFetch",
60
+ "hooks": [
61
+ {
62
+ "type": "command",
63
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
64
+ "timeout": 3,
65
+ "statusMessage": "Lineman checking..."
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ "PostToolUse": [
71
+ {
72
+ "matcher": "Bash",
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs",
77
+ "timeout": 30,
78
+ "statusMessage": "Lineman extracting key findings..."
79
+ }
80
+ ]
81
+ },
82
+ {
83
+ "matcher": "Grep",
84
+ "hooks": [
85
+ {
86
+ "type": "command",
87
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs",
88
+ "timeout": 30,
89
+ "statusMessage": "Lineman ranking results..."
90
+ }
91
+ ]
92
+ },
93
+ {
94
+ "matcher": "Glob",
95
+ "hooks": [
96
+ {
97
+ "type": "command",
98
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs",
99
+ "timeout": 5
100
+ }
101
+ ]
102
+ }
103
+ ],
104
+ "PostToolUseFailure": [
105
+ {
106
+ "hooks": [
107
+ {
108
+ "type": "command",
109
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use-failure.mjs",
110
+ "timeout": 5
111
+ }
112
+ ]
113
+ }
114
+ ],
115
+ "PreCompact": [
116
+ {
117
+ "hooks": [
118
+ {
119
+ "type": "command",
120
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.mjs",
121
+ "timeout": 10
122
+ }
123
+ ]
124
+ }
125
+ ],
126
+ "PostCompact": [
127
+ {
128
+ "hooks": [
129
+ {
130
+ "type": "command",
131
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-compact.mjs",
132
+ "timeout": 2
133
+ }
134
+ ]
135
+ }
136
+ ],
137
+ "Stop": [
138
+ {
139
+ "hooks": [
140
+ {
141
+ "type": "command",
142
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/stop.mjs",
143
+ "timeout": 5
144
+ },
145
+ {
146
+ "type": "command",
147
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/metrics.mjs",
148
+ "timeout": 5
149
+ }
150
+ ]
151
+ }
152
+ ],
153
+ "SessionStart": [
154
+ {
155
+ "matcher": "startup",
156
+ "hooks": [
157
+ {
158
+ "type": "command",
159
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs",
160
+ "timeout": 10
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ "matcher": "resume",
166
+ "hooks": [
167
+ {
168
+ "type": "command",
169
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs",
170
+ "timeout": 3
171
+ }
172
+ ]
173
+ },
174
+ {
175
+ "matcher": "compact",
176
+ "hooks": [
177
+ {
178
+ "type": "command",
179
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs",
180
+ "timeout": 3
181
+ }
182
+ ]
183
+ }
184
+ ],
185
+ "PermissionRequest": [
186
+ {
187
+ "matcher": "mcp__plugin_lineman_core__*",
188
+ "hooks": [
189
+ {
190
+ "type": "command",
191
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/permission-request.mjs",
192
+ "timeout": 3
193
+ }
194
+ ]
195
+ }
196
+ ],
197
+ "FileChanged": [
198
+ {
199
+ "matcher": "package.json",
200
+ "hooks": [
201
+ {
202
+ "type": "command",
203
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/file-changed.mjs",
204
+ "timeout": 5
205
+ }
206
+ ]
207
+ },
208
+ {
209
+ "matcher": ".env*",
210
+ "hooks": [
211
+ {
212
+ "type": "command",
213
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/file-changed.mjs",
214
+ "timeout": 3
215
+ }
216
+ ]
217
+ },
218
+ {
219
+ "matcher": "tsconfig*",
220
+ "hooks": [
221
+ {
222
+ "type": "command",
223
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/file-changed.mjs",
224
+ "timeout": 3
225
+ }
226
+ ]
227
+ }
228
+ ]
229
+ }
230
+ }
@@ -0,0 +1 @@
1
+ import{readFileSync as o}from"node:fs";import{homedir as e}from"node:os";import{join as t}from"node:path";import{MCP_SERVER_NAME as n,MCP_TOOL_NAME as r,MCP_TOOL_PREFIX as a}from"./core/tool-naming.mjs";import{readRunpodState as s}from"./runpod-state-reader.mjs";const i=t(e(),".goodex","lineman.json"),l={url:"http://localhost:11434",model:"",timeout:3e4,maxTokens:2e3,contextWindow:32768,enabled:!0};function m(){try{const e=o(i,"utf-8"),t=JSON.parse(e);return{...l,...t}}catch{return l}}export function loadConfig(){return m()}let c=null;export async function loadConfigAsync(){if(c)return c;const o=m();try{const e=await s();if(e&&"string"==typeof e.proxyUrl&&"RUNNING"===e.status)return c={...o,url:e.proxyUrl,model:o.model||e.modelName},c}catch{}return c=o,c}export function _resetConfigCache(){c=null}export function isEnabled(){return!1!==loadConfig().enabled}export async function callLineman(o,e){const t=await loadConfigAsync(),n="openai"===t.backend,r=n?"/v1/chat/completions":"/api/chat",a=n?{model:t.model,messages:[{role:"user",content:o}],max_tokens:e??t.maxTokens,temperature:0,stream:!1}:{model:t.model,messages:[{role:"user",content:o}],options:{num_predict:e??t.maxTokens,temperature:0,num_ctx:t.contextWindow},think:!1,stream:!1},s=await fetch(`${t.url}${r}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(t.timeout)});if(!s.ok)return null;const i=await s.json();return n?i.choices?.[0]?.message?.content??null:i.message?.content??null}export function loadHooksConfig(){return{toolPrefix:a,toolName:r,serverName:n}}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{appendFileSync as t,mkdirSync as o,readFileSync as e}from"node:fs";import{readJsonStdin as n}from"./core/stdin.mjs";import{MCP_TOOL_PREFIX as s}from"./core/tool-naming.mjs";import{pluginDataDir as a,pluginDataPath as r}from"./plugin-paths.mjs";const c=r("lineman-metrics.jsonl");await async function(){try{o(a(),{recursive:!0});const r=await n(),i=r.transcript_path,l=r.session_id??"unknown";let p;i||(console.log("{}"),process.exit(0));try{p=e(i,"utf-8").split("\n").filter(t=>t.trim())}catch{console.log("{}"),process.exit(0)}let m=0,u=0,_=0,d=0,h=0;const f={};let g=0,w=0,k=0,y=0,j=0,v="unknown";for(const t of p){let o;try{o=JSON.parse(t)}catch{continue}if("assistant"!==o.type)continue;const e=o.message??{},n=e.usage;n&&(m+=n.input_tokens??0,u+=n.output_tokens??0,_+=n.cache_read_input_tokens??0,d+=n.cache_creation_input_tokens??0,h++),e.model&&(v=e.model);const a=e.content??[];for(const t of Array.isArray(a)?a:[]){if("tool_use"!==t.type)continue;const o=t.name??"unknown";f[o]=(f[o]??0)+1,o.startsWith(s)?g++:"Read"===o?w++:"Grep"===o?k++:"Bash"===o?y++:"Glob"===o&&j++}}const b=Object.values(f).reduce((t,o)=>t+o,0),O={timestamp:(new Date).toISOString(),session_id:l,cwd:r.cwd??process.cwd(),model:v,turns:h,tokens:{input:m,output:u,cache_read:_,cache_creation:d,total:m+u},tools:{total_calls:b,lineman_calls:g,native_read:w,native_grep:k,native_bash:y,native_glob:j,breakdown:f},ratios:{lineman_share:b>0?Math.round(g/b*100):0,cache_hit_rate:m+_+d>0?Math.round(_/(m+_+d)*100):0}};t(c,`${JSON.stringify(O)}\n`)}catch{}console.log("{}")}();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{readJsonStdin as o}from"./core/stdin.mjs";import{MCP_TOOL_NAME as s,MCP_TOOL_PREFIX as e}from"./core/tool-naming.mjs";await async function(){((await o()).tool_name??"").startsWith(e)||(console.log("{}"),process.exit(0)),console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PermissionRequest",decision:{behavior:"allow",updatedPermissions:[{type:"addRules",rules:[{toolName:s}],behavior:"allow",destination:"localSettings"}]}}}))}();
@@ -0,0 +1 @@
1
+ import{tmpdir as n}from"node:os";import{join as r}from"node:path";export function pluginDataDir(){return process.env.CLAUDE_PLUGIN_DATA??r(n(),"lineman")}export function pluginDataPath(...n){return r(pluginDataDir(),...n)}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";console.log("{}");
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{extractFailureEvents as t}from"./core/extract-events.mjs";import{openSessionDBSync as o}from"./core/session-db.mjs";import{readJsonStdin as r}from"./core/stdin.mjs";import{captureHookError as s,flushHookSentry as e,hookBreadcrumb as n,initHookSentry as i}from"./sentry-hook.mjs";function c(){console.log("{}"),process.exit(0)}i("post-tool-use-failure"),await async function(){let i;try{i=await r()}catch(t){return s(t,{hook:"post-tool-use-failure",stage:"stdin"}),await e(),c()}return function(r){try{const s=r.session_id,e=r.cwd??process.cwd();if(!s)return;const n=t(r);if(0===n.length)return;const i=o(s,e);try{for(const t of n)i.insertEvent(t,"post-tool-use-failure")}finally{i.close()}}catch(t){n("captureSessionEvents (failure) failed",{errType:t instanceof Error?t.constructor.name:typeof t,errMessage:t instanceof Error?t.message:String(t)})}}(i),c()}();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{extractToolEvents as t}from"./core/extract-events.mjs";import{openSessionDBSync as e}from"./core/session-db.mjs";import{readJsonStdin as n}from"./core/stdin.mjs";import{wrapSummary as o}from"./core/summary-envelope.mjs";import{canonicalize as s}from"./core/tool-aliases.mjs";import{MCP_TOOL_NAME as r}from"./core/tool-naming.mjs";import{smartTruncate as i}from"./core/truncate.mjs";import{callLineman as a,isEnabled as c}from"./lineman-call.mjs";import{captureHookError as l,flushHookSentry as u,hookBreadcrumb as m,initHookSentry as p}from"./sentry-hook.mjs";function d(){console.log("{}"),process.exit(0)}p("post-tool-use"),await async function(){if(!c())return d();const p=await n(),f=s(p.tool_name),h=p.tool_input??{},g=p.tool_response??p.tool_result??{},y=function(t,e){if("string"==typeof e?.stdout)return e.stdout;if("string"==typeof e?.output)return e.output;if("string"==typeof e?.content?.[0]?.text)return e.content[0].text;if("string"==typeof e?.content)return e.content;if(Array.isArray(e?.filenames)){const n="number"==typeof e.numFiles?e.numFiles:e.filenames.length;return"Grep"===t?`Found ${n} files\n${e.filenames.join("\n")}`:e.filenames.join("\n")}return Array.isArray(e?.matches)?e.matches.join("\n"):""}(f,g);if(function(n){try{const o=n.session_id,s=n.cwd??process.cwd();if(!o)return;const r=t(n);if(0===r.length)return;const i=e(o,s);try{for(const t of r)i.insertEvent(t,"post-tool-use")}finally{i.close()}}catch(t){m("captureSessionEvents failed",{errType:t instanceof Error?t.constructor.name:typeof t,errMessage:t instanceof Error?t.message:String(t)})}}(p),f===r){const t=function(t){if(t?.result?.fallback_suggestion)return t.result;const e=t?.content?.[0]?.text;if("string"==typeof e)try{const t=JSON.parse(e);return t.result??t}catch{}if("string"==typeof t?.output)try{const e=JSON.parse(t.output);return e.result??e}catch{}return null}(g);return"native_tools"===t?.fallback_suggestion&&(console.log(JSON.stringify({additionalContext:"Lineman quota exhausted for this month. For the rest of this session, use native Read/Grep/Bash tools directly rather than calling the Lineman assist tool. The assist tool will be available again after the monthly reset."})),process.exit(0)),d()}switch(f){case"Bash":return async function(t,e){if(e.length<2e3)return d();const n=(t.command??"").trim();let s="Extract the key information, errors, and actionable items";/^git\s+(diff|log|blame|show)/.test(n)?s="Summarize the changes: what files changed, key additions/removals, and the purpose of the changes":/^(npm|pnpm|yarn)\s+(test|vitest|jest)/.test(n)?s="Extract: total tests, passed/failed counts, failing test names with error messages, and skip count":/^(npm|pnpm|yarn)\s+(build|tsc)/.test(n)||/^tsc/.test(n)?s="Extract all TypeScript/build errors with file paths and line numbers. Ignore warnings and successful compilations":/^(npm|pnpm|yarn)\s+(install|add)/.test(n)?s="Summarize: packages added/updated, any warnings or peer dependency issues, total install time":/^(npm|pnpm|yarn)\s+audit/.test(n)?s="List vulnerabilities by severity (critical/high/moderate/low) with affected packages":/^docker\s+(build|logs)/.test(n)?s="Extract errors, warnings, and the final build status. Ignore intermediate build steps that succeeded":/^(eslint|biome)\s/.test(n)?s="List all lint errors/warnings grouped by file, with rule names and line numbers":/^curl\s/.test(n)?s="Extract the HTTP status code, response headers of interest, and summarize the response body":/^kubectl\s/.test(n)?s="Summarize the resource status, any errors or warnings, and key metrics":/^terraform\s+plan/.test(n)&&(s="Summarize: resources to add/change/destroy, any errors, and the plan summary");const r=i(e,1e5),c=await a(`You are a command output compressor. Summarize this output concisely.\n\nCommand: ${n}\nFocus: ${s}\n\nOutput:\n\`\`\`\n${r}\n\`\`\`\n\nRespond with a concise summary (not JSON, just plain text).`,500);if(!c)return d();m("Bash output compressed",{command:n,originalSize:e.length,compressedSize:c.length,ratio:(c.length/e.length).toFixed(2)});const l=`Key findings from \`${n}\`:\n${c}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:o(l,{sourceTool:"Bash",sourceCommand:n,taskTypeForVerbatim:"triage_build_output"})}}))}(h,y).catch(async t=>{l(t,{tool:"Bash",command:h.command}),await u(),d()});case"Grep":return async function(t,e){const n=e.split("\n").filter(t=>t.trim());if(0===n.length&&/^\w+$/.test(t.pattern||"")){const e=t.pattern;try{const{execSync:t}=await import("node:child_process"),n=process.env.CWD||process.cwd();let o="";try{o=t(`grep -rn "def ${e}\\|class ${e}\\|func ${e}\\|function ${e}\\|const ${e}\\|type ${e}" . --include="*.py" --include="*.ts" --include="*.js" --include="*.go" --include="*.rs" --include="*.java" 2>/dev/null | head -5`,{cwd:n,encoding:"utf-8",timeout:5e3}).trim()}catch{o=""}o?(console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:`"${e}" found in wider search:\n${o}`}})),process.exit(0)):(console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:`Symbol "${e}" is not defined anywhere in this project. If the task requires this function/class, you need to CREATE it — stop searching.`}})),process.exit(0))}catch{}}if(n.length<20)return d();const s=t.pattern??t.query??"",r=i(e,5e4),c=await a(`You are a search result analyst. Rank these grep matches by relevance to the query. Return only the top 10 most relevant matches.\n\nQuery: ${s}\n\nMatches:\n\`\`\`\n${r}\n\`\`\`\n\nRespond with the top 10 matches, most relevant first (plain text, one per line).`,500);if(!c)return d();m("Grep results filtered",{query:s,originalMatches:n.length,compressedSize:c.length});const l=`Top ${Math.min(10,n.length)} of ${n.length} grep matches for \`${s}\`:\n${c}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:o(l,{sourceTool:"Grep",sourceCommand:s,taskTypeForVerbatim:"grep_filter"})}}))}(h,y).catch(async t=>{l(t,{tool:"Grep",pattern:h.pattern}),await u(),d()});case"Glob":return async function(t){const e=t.split("\n").filter(t=>t.trim());if(e.length<100)return d();const n={};for(const t of e){const e=t.split("/"),o=e.length>1?e.slice(0,-1).join("/"):".";n[o]=(n[o]??0)+1}const s=Object.entries(n).sort((t,e)=>e[1]-t[1]).map(([t,e])=>` ${t}/ (${e} files)`).join("\n"),r=`Grouped ${e.length} glob results by directory:\n${s}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:o(r,{sourceTool:"Glob",sourceCommand:"<the original glob pattern>"})}}))}(y).catch(async t=>{l(t,{tool:"Glob"}),await u(),d()});default:return d()}}();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{buildResumeSnapshot as e}from"./core/build-snapshot.mjs";import{generateNarrative as o}from"./core/narrative.mjs";import{openSessionDBSync as t}from"./core/session-db.mjs";import{readJsonStdin as r}from"./core/stdin.mjs";import{isEnabled as s}from"./lineman-call.mjs";import{flushHookSentry as n,hookBreadcrumb as c,initHookSentry as i}from"./sentry-hook.mjs";i("pre-compact"),await async function(){if(s())if("1"!==process.env.LINEMAN_DISABLE_SNAPSHOT){try{const s=await r(),n=s.session_id,i=s.cwd??process.cwd();if(!n)return void console.log("{}");const a=t(n,i);let m=null,l=0;try{const o=a.getEvents(500),t=a.getMeta()??{};a.incrementCompactCount(),l=Number(t.eventCount??0);const r=e(o,{projectDir:t.projectDir,eventCount:l,compactCount:(t.compactCount??0)+1});if(r&&r.length>0){a.upsertResume(r,l);const e=a.getResume();e?.narrative&&e?.narrativeEventCount===l||(m=r)}}finally{a.close()}if(m){const e=await o(m);if(e){const o=t(n,i);try{o.upsertNarrative(e,l)||c("pre-compact narrative-write-missed-resume-row",{cumulativeEventCount:l})}finally{o.close()}}}}catch(e){c("pre-compact failed",{errType:e instanceof Error?e.constructor.name:typeof e,errMessage:e instanceof Error?e.message:String(e)})}finally{await n()}console.log("{}")}else console.log("{}");else console.log("{}")}();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{readFileSync as o,statSync as t}from"node:fs";import{resolve as e}from"node:path";import{isMcpReady as i}from"./core/mcp-ready.mjs";import{routeHookDecision as n}from"./core/routing.mjs";import{compilePolicies as r,decide as s}from"./core/security/decide.mjs";import{loadPolicies as a}from"./core/security/policies.mjs";import{readJsonStdin as c}from"./core/stdin.mjs";import{canonicalize as l}from"./core/tool-aliases.mjs";import{isEnabled as u}from"./lineman-call.mjs";import{captureHookError as m,flushHookSentry as p,hookBreadcrumb as d,initHookSentry as f}from"./sentry-hook.mjs";function h(){console.log("{}"),process.exit(0)}function y(o){if("continue"===o.action)return h();if("modify"===o.action){const t={hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",updatedInput:o.updatedInput}};return console.log(JSON.stringify(t)),void process.exit(0)}const t={decision:"block",reason:o.reason,hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:o.redirect?.additionalContext}};console.log(JSON.stringify(t)),process.exit(0)}f("pre-tool-use"),async function(){if(!u())return h();const m=await c(),p=l(m.tool_name),f=m.tool_input??{},g=m.cwd??process.cwd(),k=i(),_=function(o,t,e){let i;try{i=r(a({projectDir:e}))}catch(o){return d("security-gate load failed",{errMessage:o instanceof Error?o.message:String(o)}),null}const n="Bash"===o?void 0:t.file_path??t.path??t.url,c="Bash"===o?t.command??"":void 0,l=s({tool:o,subject:n,command:c},i);return"allow"===l.action?null:"deny"===l.action?{action:"block",reason:l.reason,redirect:{additionalContext:`Tool call blocked by local policy. ${l.reason}\n\nThis is Stage 1 of the Lineman security gate — see your .claude/settings{,.local}.json files for the deny/ask/allow rules that govern it.`}}:{action:"ask",reason:l.reason}}(p,f,g);if(_){if("block"===_.action)return y(_);if("ask"===_.action){const o={hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:_.reason}};return console.log(JSON.stringify(o)),void process.exit(0)}}if("Read"===p){const i=function(i,n){const r=i.file_path??i.path;if(!r)return null;try{const i=e(n,r);return t(i).size<12288?{exists:!0,lineCount:0}:{exists:!0,lineCount:o(i,"utf-8").split("\n").length}}catch{return{exists:!1,lineCount:0}}}(f,g),r=n({tool_name:p,tool_input:f},{mcpReady:k,fileStats:i});return"block"===r.action&&i&&d(`Read denied: ${i.lineCount} lines`,{filePath:f.file_path??f.path,lineCount:i.lineCount}),y(r)}if("Edit"===p){const o=n({tool_name:p,tool_input:f},{mcpReady:k});return"block"===o.action&&d("Edit redirected to edit_file",{filePath:f.file_path}),y(o)}if("Bash"===p){const o=n({tool_name:p,tool_input:f},{mcpReady:k});return"modify"===o.action&&d("Bash echo-rewrite applied",{original:f?.command?.slice(0,200)}),y(o)}return h()}().catch(async o=>{m(o,{}),await p(),h()});
@@ -0,0 +1 @@
1
+ export async function readRunpodState({fetchImpl:t=globalThis.fetch,timeoutMs:n=1500,env:e=process.env}={}){const r=e.UPSTASH_REDIS_REST_URL,o=e.UPSTASH_REDIS_REST_TOKEN;if(!r||!o)return null;const s=e.RUNPOD_STATE_STORE_KEY??"lineman:runpod:state";try{const e=await t(`${r}/get/${encodeURIComponent(s)}`,{method:"GET",headers:{Authorization:`Bearer ${o}`},signal:AbortSignal.timeout(n)});if(!e.ok)return null;const l=await e.json(),u=l?.result;return"string"!=typeof u||0===u.length?null:function(t){try{const n=JSON.parse(t);return"string"!=typeof n?.podId||"string"!=typeof n?.proxyUrl||"string"!=typeof n?.status?null:n}catch{return null}}(u)}catch{return null}}
@@ -0,0 +1 @@
1
+ let e=null;try{e=await import("@sentry/node")}catch{}let o=!1;export function initHookSentry(t){if(!e)return;const n=process.env.SENTRY_DSN;n&&(e.init({dsn:n,environment:process.env.SENTRY_ENVIRONMENT??"development",tracesSampleRate:1,initialScope:{tags:{service:"lineman-hook",hook:t,source:"hook",benchmark_run_id:process.env.LINEMAN_BENCHMARK_RUN_ID??"",benchmark_task_id:process.env.LINEMAN_BENCHMARK_TASK_ID??""}}}),o=!0)}export function captureHookError(t,n={}){o&&e.captureException(t,{extra:n})}export function hookBreadcrumb(t,n={}){o&&e.addBreadcrumb({category:"hook",message:t,data:n})}export async function flushHookSentry(t=1e3){o&&await e.flush(t)}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{readdirSync as t,statSync as s}from"node:fs";import{extname as n,join as e}from"node:path";import{ingestClaudeMd as o}from"./core/ingest-claude-md.mjs";import{degradationMarker as r,getFullHealth as c}from"./core/mcp-ready.mjs";import{runCleanupIfDue as i}from"./core/session-cleanup.mjs";import{openSessionDBSync as a}from"./core/session-db.mjs";import{readJsonStdin as l}from"./core/stdin.mjs";import{isEnabled as p}from"./lineman-call.mjs";await async function(){p()||(console.log("{}"),process.exit(0));try{const p=await l(),m=p.source??"startup",f=p.cwd??process.cwd(),h=p.session_id;h&&i({openDb:a,sessionId:h,projectDir:f});const d=[];if("resume"===m||"compact"===m)try{const t=await c(),s=r(t);s&&d.push(s)}catch{}const g="1"===process.env.LINEMAN_DISABLE_SNAPSHOT,y="1"===process.env.LINEMAN_FORCE_RESUME;if(("compact"===m||"resume"===m||y)&&!g){const t=function(t,s){if(!t)return null;try{const n=a(t,s);try{const t=n.getResume();return!t||t.consumed||"string"!=typeof t.snapshot||0===t.snapshot.length?null:(n.markResumeConsumed(),{snapshot:t.snapshot,narrative:"string"==typeof t.narrative&&t.narrative.length>0?t.narrative:null})}finally{n.close()}}catch{return null}}(h,f);t&&(d.length>0&&d.push(""),d.push(t.snapshot),t.narrative&&d.push("",`<session_narrative>\n${u=t.narrative,String(u).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}\n</session_narrative>`))}if("startup"===m){const r=function(o){try{const r=[],c=new Set;function i(a,l){if(!(l>3||c.has(a))){c.add(a);try{const c=t(a);for(const t of c){if(["node_modules",".git","dist",".next",".turbo","coverage",".claude"].includes(t))continue;const c=e(a,t);try{const e=s(c),a=c.replace(`${o}/`,"");if(e.isDirectory())r.push(`${a}/`),i(c,l+1);else if(l<=2){const s=n(t);[".ts",".tsx",".js",".jsx",".json",".md",".yaml",".yml"].includes(s)&&r.push(a)}}catch{}}}catch{}}}if(i(o,0),0===r.length)return null;if(r.length>100){const a={};for(const l of r){const p=l.split("/")[0];a[p]=(a[p]??0)+1}return Object.entries(a).sort((t,s)=>s[1]-t[1]).map(([t,s])=>` ${t}/ (${s} items)`).join("\n")}return r.map(t=>` ${t}`).join("\n")}catch{return null}}(f);if(r&&(d.length>0&&d.push(""),d.push("PROJECT STRUCTURE (pre-scanned by Lineman):",r,""),d.push("Do NOT scan the project structure yourself — use the map above.")),h)try{const t=a(h,f);try{o({db:t,cwd:f})}finally{t.close()}}catch{}}if(0===d.length)return void console.log("{}");console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:d.join("\n")}}))}catch{console.log("{}")}var u}();
@@ -0,0 +1,95 @@
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/user-prompt-submit.mjs",
9
+ "timeout": 5
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": "Read",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/pre-tool-use.mjs",
21
+ "timeout": 10,
22
+ "statusMessage": "Lineman checking file size..."
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "matcher": "Edit",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/pre-tool-use.mjs",
32
+ "timeout": 30,
33
+ "statusMessage": "Lineman validating patch target..."
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "PostToolUse": [
39
+ {
40
+ "matcher": "Bash",
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
45
+ "timeout": 30,
46
+ "statusMessage": "Lineman compressing output..."
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "matcher": "Grep",
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
56
+ "timeout": 30,
57
+ "statusMessage": "Lineman filtering results..."
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "matcher": "Glob",
63
+ "hooks": [
64
+ {
65
+ "type": "command",
66
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
67
+ "timeout": 5
68
+ }
69
+ ]
70
+ }
71
+ ],
72
+ "PostCompact": [
73
+ {
74
+ "hooks": [
75
+ {
76
+ "type": "command",
77
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-compact.mjs",
78
+ "timeout": 2
79
+ }
80
+ ]
81
+ }
82
+ ],
83
+ "Stop": [
84
+ {
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/stop.mjs",
89
+ "timeout": 5
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ }
95
+ }
package/hooks/stop.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{appendFileSync as e}from"node:fs";import{homedir as t}from"node:os";import{join as o}from"node:path";import{readJsonStdin as s}from"./core/stdin.mjs";import{flushHookSentry as n,initHookSentry as i}from"./sentry-hook.mjs";i("stop");const a=o(t(),".goodex","lineman-missed.log");function r(t){const o=`[${(new Date).toISOString()}] ${t}\n`;try{e(a,o)}catch{}}(async()=>{try{const e=(await s()).last_assistant_message??"";e||(await n(),console.log("{}"),process.exit(0));const t=e.match(/```[\s\S]{500,}?```/g);if(t)for(const e of t)e.length>2e3&&r(`MISSED: Response contains ${e.length}-char code block — likely inline file content. Could have used lineman read_file.`);/error\s+(TS\d+|E\d+):/i.test(e)&&e.length>1e3&&!/lineman/i.test(e)&&r("MISSED: Response contains TypeScript/build error analysis without Lineman. Could have used triage_build_output."),/(?:I can see|looking at|from the file|the file contains|the file shows)/i.test(e)&&e.length>500&&!/lineman/i.test(e)&&r("MISSED: Response references file contents without Lineman. Could have used read_file.")}catch{}await n(),console.log("{}")})();
@@ -0,0 +1 @@
1
+ import{closeSync as o,openSync as r}from"node:fs";import{devNull as m}from"node:os";try{o(2),r(m,"w")}catch{}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import"./suppress-stderr.mjs";import{extractUserEvents as s}from"./core/extract-events.mjs";import{openSessionDBSync as o}from"./core/session-db.mjs";import{readJsonStdin as t}from"./core/stdin.mjs";import{isEnabled as r}from"./lineman-call.mjs";await async function(){r()||(console.log("{}"),process.exit(0));const c=await t();try{const t=c.session_id,r=c.cwd??process.cwd();if(t){const e=s(c);if(e.length>0){const s=o(t,r);try{for(const o of e)s.insertEvent(o,"user-prompt-submit")}finally{s.close()}}}}catch{}console.log("{}")}();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@lineman-io/mcp",
3
+ "version": "2.1.0",
4
+ "description": "Lineman MCP server — AI-powered code intelligence for Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "lineman-mcp": "./dist/main.js"
8
+ },
9
+ "main": "./dist/main.js",
10
+ "exports": {
11
+ ".": "./dist/main.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.12.0",
15
+ "@sentry/node": "^10.47.0",
16
+ "better-sqlite3": "^11.8.1",
17
+ "turndown": "^7.2.2",
18
+ "zod": "^3.25.76"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "hooks",
23
+ "skills",
24
+ ".claude-plugin",
25
+ "start.mjs"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "restricted",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "build:publish": "tsup && node scripts/obfuscate.mjs && node scripts/minify-hooks.mjs",
37
+ "sync-version": "node scripts/version-sync.mjs",
38
+ "version": "node scripts/version-sync.mjs && git add .claude-plugin/plugin.json ../../.claude-plugin/marketplace.json",
39
+ "start": "node dist/main.js",
40
+ "test:e2e": "vitest run --config vitest.config.e2e.ts",
41
+ "test:live": "vitest run --config vitest.config.live.ts",
42
+ "test:smoke": "vitest run --config vitest.config.smoke.ts",
43
+ "enable": "node -e \"const f=require('os').homedir()+'/.goodex/lineman.json';const c=JSON.parse(require('fs').readFileSync(f,'utf8'));c.enabled=true;require('fs').writeFileSync(f,JSON.stringify(c,null,2)+'\\n');console.log('Lineman ENABLED — restart Claude session')\"",
44
+ "disable": "node -e \"const f=require('os').homedir()+'/.goodex/lineman.json';const c=JSON.parse(require('fs').readFileSync(f,'utf8'));c.enabled=false;require('fs').writeFileSync(f,JSON.stringify(c,null,2)+'\\n');console.log('Lineman DISABLED — restart Claude session')\"",
45
+ "status": "node -e \"const f=require('os').homedir()+'/.goodex/lineman.json';const c=JSON.parse(require('fs').readFileSync(f,'utf8'));console.log('Lineman:',c.enabled===false?'DISABLED':'ENABLED','| Model:',c.model,'| URL:',c.url)\""
46
+ }
47
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: doctor
3
+ description: "Diagnose Lineman plugin health: verifies MCP server reachable, API token present and valid, tier recognized (free/pro/enterprise), hooks installed, channels capability available, and reports last-call latency. Use when Lineman appears broken, tool calls fail, results look empty, or the user asks 'is lineman working', 'why is lineman slow', 'check lineman status', 'lineman diagnose', 'lineman health', 'what's wrong with lineman'."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Lineman doctor
8
+
9
+ Call `mcp__plugin_lineman_core__doctor` with an empty `{}` argument.
10
+
11
+ Render the result verbatim as a checklist. Each entry in the `checks` array is one line:
12
+
13
+ - `ok` → green check
14
+ - `warn` → yellow dash
15
+ - `fail` → red cross
16
+
17
+ Follow the checklist with the `summary` field (e.g. "6/7 checks passed") and the reported `version`. If any check is `fail`, print its `detail` field so the user can act on it (for example: missing API token, server unreachable, channels not loaded).
18
+
19
+ Do not embellish or re-interpret — the handler already ran the probes.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: lineman
3
+ description: "Delegate data-heavy work to Lineman's secondary LLM instead of consuming context window tokens. Use whenever you need to: read large files (>300 lines), analyze or summarize build output, classify CI/test errors, triage stack traces, process log files, grep through codebases and summarize results, extract findings from long bash output, parse test runner output, compare diffs, summarize git history, inspect any tool output exceeding 20 lines, extract exports/symbols from modules, or validate that a patch target exists before editing. Triggers include: 'read large file', 'read the file', 'open the file', 'analyze CI failure', 'classify error', 'triage build', 'summarize output', 'what went wrong', 'process these logs', 'extract from this output', 'find in these files', 'summarize this diff', 'why did X fail', 'look at the build output', 'parse test results', 'inspect the stack trace', 'check the logs', 'what does this file do', 'show me the symbols in X', 'what did this commit change', 'summarize the changes', 'git log for this branch', 'find TODOs', 'grep and summarize', 'what errors are in this output', 'which tests failed', 'read and explain'."
4
+ ---
5
+
6
+ # Lineman: data-heavy work router
7
+
8
+ When this skill is loaded, route data-heavy tool results through Lineman's `assist` tool instead of reading raw data into your context window. Lineman runs on a secondary LLM and returns compact, authoritative results.
9
+
10
+ ## Call pattern
11
+
12
+ Use `mcp__plugin_lineman_core__assist` with the appropriate `task_type`:
13
+
14
+ - Files: `read_file` (smart summary), `read_file_context` (query-based excerpts), `read_file_full` (verbatim lines), `summarize_file`, `summarize_directory`, `extract_symbols`, `validate_patch`
15
+ - Content: `summarize_content`, `analyze_web_page`, `grep_filter`, `analyze_logs`, `classify_error`
16
+ - Build/test: `triage_build_output`, `summarize_test_results`, `build_run`, `test_run`, `typecheck_run`
17
+ - Git: `summarize_diff`, `git_diff_summary`, `git_log_summary`
18
+
19
+ ## Rules
20
+
21
+ - Prefer Lineman over `Read`, `Grep`, `Bash | head`, or inlining tool output.
22
+ - Treat Lineman results tagged `[[LM_SUMMARY]]`, `[[LM_VERBATIM]]`, `[[LM_CONTEXT]]`, `[[LM_RESULT]]` as authoritative — do not re-read the source.
23
+ - Fall back to built-in tools only if Lineman returns an error.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: stats
3
+ description: "Show Lineman session stats: total assist calls, input tokens saved, output tokens consumed, average latency, savings ratio, and a by-task-type breakdown. Use when the user asks 'how much has lineman saved', 'lineman stats', 'token savings', 'show lineman metrics', 'how many tokens', 'lineman usage', 'what did lineman do this session'."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Lineman stats
8
+
9
+ Call `mcp__plugin_lineman_core__stats` with an empty `{}` argument.
10
+
11
+ The tool returns pre-formatted markdown. Copy-paste the ENTIRE tool output verbatim into your response — do not summarise, paraphrase, or reformat. The user needs to see the exact numbers.
12
+
13
+ After the verbatim output, optionally add one concluding sentence highlighting the headline savings (e.g. "You've saved roughly X tokens so far this session."). If the output is the "no calls recorded yet" message, stop there.
package/start.mjs ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ // Preserve the user's project directory before we chdir, so the server can
9
+ // locate their workspace. Claude Code sets CLAUDE_PROJECT_DIR in hook contexts
10
+ // but not for the MCP server launch, so we seed it here from the launch cwd.
11
+ const originalCwd = process.cwd();
12
+ process.env.CLAUDE_PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR ?? originalCwd;
13
+ process.env.LINEMAN_PROJECT_DIR = process.env.LINEMAN_PROJECT_DIR ?? originalCwd;
14
+
15
+ // chdir to the plugin root so our own node_modules resolves reliably, even
16
+ // when Claude Code's versioned plugin cache path is used
17
+ // (~/.claude/plugins/cache/<org>/<plugin>/<version>/).
18
+ process.chdir(__dirname);
19
+
20
+ // Log the versioned cache path, if we appear to be running from one — helps
21
+ // diagnose stale-install issues without self-healing yet.
22
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? __dirname;
23
+ const cacheMatch = pluginRoot.match(/plugins\/cache\/([^/]+)\/([^/]+)\/([^/]+)/);
24
+ if (cacheMatch) {
25
+ process.stderr.write(
26
+ `[lineman] plugin cache: org=${cacheMatch[1]} plugin=${cacheMatch[2]} version=${cacheMatch[3]}\n`,
27
+ );
28
+ }
29
+
30
+ // Hand off to the compiled server entry.
31
+ await import(resolve(__dirname, "dist", "main.js"));