@lineman-io/mcp 2.1.1 → 2.3.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.
@@ -0,0 +1 @@
1
+ import{readFileSync as o}from"node:fs";import{homedir as e}from"node:os";import{join as n}from"node:path";const r=n(e(),".goodex","lineman.json");export function loadLinemanConfig(){let e={};try{e=JSON.parse(o(r,"utf-8"))}catch{}const n=process.env.LINEMAN_SERVER_URL??("string"==typeof e.serverUrl?e.serverUrl:null),t=process.env.LINEMAN_API_TOKEN??("string"==typeof e.apiToken?e.apiToken:null);return n&&t?{serverUrl:n,apiToken:t}:null}
@@ -1 +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";
1
+ export const PLUGIN_NAME="lineman";export const MCP_SERVER_KEY="core";export const MCP_SERVER_NAME="plugin:lineman:core";export const MCP_TOOL_PREFIX="mcp__plugin_lineman_core__";export const MCP_TOOL_NAME=`${MCP_TOOL_PREFIX}assist`;export const MCP_TOOL_NAME_EDIT_FILE=`${MCP_TOOL_PREFIX}edit_file`;
@@ -1 +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}}
1
+ import{loadLinemanConfig as t}from"./core/lineman-config.mjs";import{MCP_SERVER_NAME as n,MCP_TOOL_NAME as e,MCP_TOOL_PREFIX as o}from"./core/tool-naming.mjs";function r(){return t()}export function loadHookConfig(){return r()}export function isEnabled(){return"false"!==process.env.LINEMAN_ENABLED}export async function callLineman(t,n,e={},o={}){const a=r();if(!a)return null;const i=o.timeoutMs??3e4;let l,s;try{l=await fetch(`${a.serverUrl}/lineman/execute`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a.apiToken}`},body:JSON.stringify({task_type:t,content:n,params:e}),signal:AbortSignal.timeout(i)})}catch{return null}if(!l.ok)return null;try{s=await l.json()}catch{return null}return"success"!==s?.metadata?.status?null:s.shaped_result??null}export function loadHooksConfig(){return{toolPrefix:o,toolName:e,serverName:n}}
@@ -1,2 +1,2 @@
1
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()}}();
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 o}from"./core/stdin.mjs";import{wrapSummary as n}from"./core/summary-envelope.mjs";import{canonicalize as r}from"./core/tool-aliases.mjs";import{MCP_TOOL_NAME as s}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 f(){console.log("{}"),process.exit(0)}function h(t,e,o){const n=o?o.slice(0,200):"",r=`[[LM_PASSTHROUGH:${t}]] tool=${e}${n?` cmd=${n}`:""}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:r}})),process.exit(0)}p("post-tool-use"),await async function(){if(!c())return f();const p=await o(),g=r(p.tool_name),d=p.tool_input??{},y=p.tool_response??p.tool_result??{},$=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 o="number"==typeof e.numFiles?e.numFiles:e.filenames.length;return"Grep"===t?`Found ${o} files\n${e.filenames.join("\n")}`:e.filenames.join("\n")}return Array.isArray(e?.matches)?e.matches.join("\n"):""}(g,y);if(function(o){try{const n=o.session_id,r=o.cwd??process.cwd();if(!n)return;const s=t(o);if(0===s.length)return;const i=e(n,r);try{for(const t of s)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),g===s){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}(y);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)),f()}switch(g){case"Bash":return async function(t,e){if(e.length<2e3)return f();const o=(t.command??"").trim(),r=i(e,1e5),s=await a("triage_build_output",r,{command:o});if(!s)return m("compression-failed-on-large-input",{sourceTool:"Bash",command:o,originalSize:e.length}),h("compression_failed","Bash",o);const c=function(t){const e=[];if("string"==typeof t?.summary&&t.summary.trim()&&e.push(t.summary.trim()),Array.isArray(t?.errors)&&t.errors.length>0){e.push(""),e.push("Errors:");for(const o of t.errors)e.push(`- ${o}`)}if(Array.isArray(t?.failing_tests)&&t.failing_tests.length>0){e.push(""),e.push("Failing tests:");for(const o of t.failing_tests)e.push(`- ${o}`)}return e.join("\n").trim()}(s);if(!c)return m("compression-empty-summary-on-large-input",{sourceTool:"Bash",command:o,originalSize:e.length}),h("empty_summary","Bash",o);m("Bash output compressed",{command:o,originalSize:e.length,compressedSize:c.length,ratio:(c.length/e.length).toFixed(2)});const l=`Key findings from \`${o}\`:\n${c}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:n(l,{sourceTool:"Bash",sourceCommand:o,taskTypeForVerbatim:"triage_build_output"})}}))}(d,$).catch(async t=>{l(t,{tool:"Bash",command:d.command}),await u(),f()});case"Grep":return async function(t,e){const o=e.split("\n").filter(t=>t.trim());if(0===o.length&&/^\w+$/.test(t.pattern||"")){const e=t.pattern;try{const{execSync:t}=await import("node:child_process"),o=process.env.CWD||process.cwd();let n="";try{n=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:o,encoding:"utf-8",timeout:5e3}).trim()}catch{n=""}n?(console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:`"${e}" found in wider search:\n${n}`}})),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(o.length<20)return f();const r=t.pattern??t.query??"",s=i(e,5e4),c=await a("grep_filter",s,{query:r});if(!c)return m("compression-failed-on-large-input",{sourceTool:"Grep",query:r,originalMatches:o.length}),h("compression_failed","Grep",r);const l=function(t){return(Array.isArray(t?.ranked_matches)?t.ranked_matches:[]).slice(0,10).map(t=>"string"==typeof t?.match?t.match:"").filter(Boolean).join("\n")}(c);if(!l)return m("compression-empty-summary-on-large-input",{sourceTool:"Grep",query:r,originalMatches:o.length}),h("empty_summary","Grep",r);m("Grep results filtered",{query:r,originalMatches:o.length,compressedSize:l.length});const u=`Top ${Math.min(10,o.length)} of ${o.length} grep matches for \`${r}\`:\n${l}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:n(u,{sourceTool:"Grep",sourceCommand:r,taskTypeForVerbatim:"grep_filter"})}}))}(d,$).catch(async t=>{l(t,{tool:"Grep",pattern:d.pattern}),await u(),f()});case"Glob":return async function(t){const e=t.split("\n").filter(t=>t.trim());if(e.length<100)return f();const o={};for(const t of e){const e=t.split("/"),n=e.length>1?e.slice(0,-1).join("/"):".";o[n]=(o[n]??0)+1}const r=Object.entries(o).sort((t,e)=>e[1]-t[1]).map(([t,e])=>` ${t}/ (${e} files)`).join("\n"),s=`Grouped ${e.length} glob results by directory:\n${r}`;console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:n(s,{sourceTool:"Glob",sourceCommand:"<the original glob pattern>"})}}))}($).catch(async t=>{l(t,{tool:"Glob"}),await u(),f()});default:return f()}}();
@@ -1,2 +1,2 @@
1
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()});
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{readJsonStdin as r}from"./core/stdin.mjs";import{canonicalize as s}from"./core/tool-aliases.mjs";import{isEnabled as c}from"./lineman-call.mjs";import{captureHookError as a,flushHookSentry as l,hookBreadcrumb as p,initHookSentry as m}from"./sentry-hook.mjs";function u(){console.log("{}"),process.exit(0)}function d(o){if("continue"===o.action)return u();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)}m("pre-tool-use"),async function(){if(!c())return u();const a=await r(),l=s(a.tool_name),m=a.tool_input??{},f=a.cwd??process.cwd(),h=i();if("Read"===l){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}}}(m,f),r=n({tool_name:l,tool_input:m},{mcpReady:h,fileStats:i});return"block"===r.action&&i&&p(`Read denied: ${i.lineCount} lines`,{filePath:m.file_path??m.path,lineCount:i.lineCount}),d(r)}if("Edit"===l){const o=n({tool_name:l,tool_input:m},{mcpReady:h});return"block"===o.action&&p("Edit redirected to edit_file",{filePath:m.file_path}),d(o)}if("Bash"===l){const o=n({tool_name:l,tool_input:m},{mcpReady:h});return"modify"===o.action&&p("Bash echo-rewrite applied",{original:m?.command?.slice(0,200)}),d(o)}return u()}().catch(async o=>{a(o,{}),await l(),u()});
@@ -1 +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}}
1
+ import{loadLinemanConfig as t}from"./core/lineman-config.mjs";export async function readRunpodState({fetchImpl:r=globalThis.fetch,timeoutMs:e=1500,loadConfigImpl:n=t}={}){const o=n();if(!o)return null;const l=`${o.serverUrl.replace(/\/+$/,"")}/v1/pod-state`;let a,i;try{a=await r(l,{method:"GET",headers:{Authorization:`Bearer ${o.apiToken}`},signal:AbortSignal.timeout(e)})}catch{return null}if(!a.ok)return null;try{i=await a.json()}catch{return null}return(u=i)&&"object"==typeof u?"string"!=typeof u.podId||"string"!=typeof u.proxyUrl||"string"!=typeof u.status?null:u:null;var u}
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/user-prompt-submit.mjs",
8
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/user-prompt-submit.mjs",
9
9
  "timeout": 5
10
10
  }
11
11
  ]
@@ -17,7 +17,7 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/pre-tool-use.mjs",
20
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/pre-tool-use.mjs",
21
21
  "timeout": 10,
22
22
  "statusMessage": "Lineman checking file size..."
23
23
  }
@@ -28,7 +28,7 @@
28
28
  "hooks": [
29
29
  {
30
30
  "type": "command",
31
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/pre-tool-use.mjs",
31
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/pre-tool-use.mjs",
32
32
  "timeout": 30,
33
33
  "statusMessage": "Lineman validating patch target..."
34
34
  }
@@ -41,7 +41,7 @@
41
41
  "hooks": [
42
42
  {
43
43
  "type": "command",
44
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
44
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/post-tool-use.mjs",
45
45
  "timeout": 30,
46
46
  "statusMessage": "Lineman compressing output..."
47
47
  }
@@ -52,7 +52,7 @@
52
52
  "hooks": [
53
53
  {
54
54
  "type": "command",
55
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
55
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/post-tool-use.mjs",
56
56
  "timeout": 30,
57
57
  "statusMessage": "Lineman filtering results..."
58
58
  }
@@ -63,7 +63,7 @@
63
63
  "hooks": [
64
64
  {
65
65
  "type": "command",
66
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-tool-use.mjs",
66
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/post-tool-use.mjs",
67
67
  "timeout": 5
68
68
  }
69
69
  ]
@@ -74,7 +74,7 @@
74
74
  "hooks": [
75
75
  {
76
76
  "type": "command",
77
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/post-compact.mjs",
77
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/post-compact.mjs",
78
78
  "timeout": 2
79
79
  }
80
80
  ]
@@ -85,7 +85,7 @@
85
85
  "hooks": [
86
86
  {
87
87
  "type": "command",
88
- "command": "node $CLAUDE_PROJECT_DIR/apps/lineman-mcp/hooks/stop.mjs",
88
+ "command": "node $CLAUDE_PROJECT_DIR/apps/lineman/mcp/hooks/stop.mjs",
89
89
  "timeout": 5
90
90
  }
91
91
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lineman-io/mcp",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "Lineman MCP server — AI-powered code intelligence for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,18 +12,13 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@modelcontextprotocol/sdk": "^1.12.0",
15
- "@sentry/node": "^10.47.0",
15
+ "@sentry/node": "10.47.0",
16
+ "@sentry/node-native": "10.47.0",
16
17
  "better-sqlite3": "^11.8.1",
18
+ "open": "^10.2.0",
17
19
  "turndown": "^7.2.2",
18
20
  "zod": "^3.25.76"
19
21
  },
20
- "files": [
21
- "dist",
22
- "hooks",
23
- "skills",
24
- ".claude-plugin",
25
- "start.mjs"
26
- ],
27
22
  "publishConfig": {
28
23
  "access": "restricted",
29
24
  "registry": "https://registry.npmjs.org/"
@@ -32,14 +27,7 @@
32
27
  "node": ">=20"
33
28
  },
34
29
  "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
30
  "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
31
  "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
32
  "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
33
  "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)\""
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: auth
3
+ description: "Authenticate Lineman from inside a Claude Code session — runs the device-code flow that writes an API token to ~/.goodex/lineman.json. Use when the user types '/lineman:auth', asks to 'sign in to lineman', 'authenticate lineman', 'lineman login', 'set up lineman', 'connect lineman', or hits a not_authenticated error from any other Lineman tool."
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Lineman auth
8
+
9
+ Run the device-code flow that swaps a short user-entered code for an API token. The flow has two phases — start and poll — and the model orchestrates the wait.
10
+
11
+ ## Phase 1 — start
12
+
13
+ Call `mcp__plugin_lineman_core__auth` with an empty `{}` argument.
14
+
15
+ The response will be JSON shaped like:
16
+
17
+ ```json
18
+ {
19
+ "status": "started",
20
+ "verification_uri": "https://lineman.sh/auth/verify",
21
+ "verification_uri_complete": "https://lineman.sh/auth/verify?code=ABCD-1234",
22
+ "user_code": "ABCD-1234",
23
+ "device_code": "<opaque>",
24
+ "expires_in": 600,
25
+ "interval": 5
26
+ }
27
+ ```
28
+
29
+ Show the user three things, in order:
30
+
31
+ 1. **The complete URL** (`verification_uri_complete`) — clickable, this is the one-step path.
32
+ 2. **The bare URL + code** (`verification_uri` and `user_code`) — fallback for users who can't click.
33
+ 3. A short note that you'll wait while they confirm in the browser.
34
+
35
+ Render the URL as plain text (not a markdown link) so terminals that don't auto-link still show it cleanly.
36
+
37
+ ## Phase 2 — poll
38
+
39
+ Hold onto the `device_code` from phase 1 and poll until done:
40
+
41
+ - Call `mcp__plugin_lineman_core__auth` with `{ "device_code": "<the device_code>" }`.
42
+ - Wait `interval` seconds (typically 5) between calls.
43
+ - The response is one of:
44
+ - `{ "status": "pending", "message": "..." }` — user hasn't confirmed yet; wait `interval` seconds and call again.
45
+ - `{ "status": "complete", "user_id": "...", "tier": "free|pro|enterprise", "monthly_limit": <bytes> }` — done. Show the user a one-line confirmation including their plan tier and monthly limit (formatted as `<n>M tokens/month`).
46
+ - `{ "status": "expired", "message": "..." }` — the code expired before they confirmed. Tell the user, and offer to start again (re-invoke `/lineman:auth`).
47
+ - `{ "status": "error", "message": "..." }` — surface the message verbatim. Don't retry on `error`.
48
+
49
+ Stop polling after at most `Math.ceil(expires_in / interval)` attempts (≈120 attempts at the default 10-minute window). If you hit that limit, treat it as expired.
50
+
51
+ ## Rules
52
+
53
+ - The token is written to `~/.goodex/lineman.json` by the handler on `complete`. You do NOT need to write it yourself, and you do NOT need to restart the MCP server — subsequent `mcp__plugin_lineman_core__assist` calls will pick the token up automatically.
54
+ - Never echo the `device_code` to the user — it's an opaque session credential, not the human-readable `user_code`.
55
+ - If the user already has a working token (any other tool succeeds without `not_authenticated`), do not re-run `/lineman:auth` unprompted.
@@ -13,8 +13,7 @@ Use `mcp__plugin_lineman_core__assist` with the appropriate `task_type`:
13
13
 
14
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
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`
16
+ - Build/test/git: `triage_build_output`, `summarize_test_results`, `summarize_diff` — run the command via Bash; the post-tool hook auto-summarises long output.
18
17
 
19
18
  ## Rules
20
19
 
@@ -1 +0,0 @@
1
- export function splitChainedCommands(n){const e=[];if("string"!=typeof n||0===n.length)return e;let t="",i=!1,o=!1,s=!1,c=0;const f=n.length,l=n=>{const i=t.trim();i.length>0&&e.push({command:i,separator:n}),t=""};for(;c<f;){const e=n[c];if("\\"===e&&c+1<f)t+=n[c]+n[c+1],c+=2;else if(o||s||"'"!==e)if(i||s||'"'!==e)if(i||o||"`"!==e){if(!i&&!o&&!s){const t=n.slice(c,c+2);if("&&"===t||"||"===t){l(t),c+=2;continue}if(";"===e){l(";"),c+=1;continue}if("|"===e){"|"===n[c+1]?(l("||"),c+=2):(l("|"),c+=1);continue}}t+=e,c+=1}else s=!s,t+=e,c+=1;else o=!o,t+=e,c+=1;else i=!i,t+=e,c+=1}return l(null),e}export function commandsOf(n){return splitChainedCommands(n).map(n=>n.command)}
@@ -1 +0,0 @@
1
- import{splitChainedCommands as o}from"./chain-split.mjs";import{compilePatterns as t,matchPattern as n}from"./patterns.mjs";import{scanCommand as e}from"./shell-escape.mjs";function c(o){const t=[];let n=0;const e=o.length;for(;n<e;){const c=o[n];if("\\"===c&&n+1<e)n+=2;else{if("$"===c&&"("===o[n+1]){let c=1,s=n+2;for(;s<e&&c>0;){const t=o[s];"\\"===t&&s+1<e?s+=2:("("===t?c++:")"===t&&c--,c>0&&s++)}if(0===c){const e=o.slice(n+2,s);e.length>0&&t.push(e),n=s+1;continue}}if("`"===c){let c=n+1;for(;c<e;)if("\\"===o[c]&&c+1<e)c+=2;else{if("`"===o[c])break;c++}if(c<e){const e=o.slice(n+1,c);e.length>0&&t.push(e),n=c+1;continue}}n++}}return t}export function compilePolicies(o){return{deny:t(o.deny??[]),ask:t(o.ask??[]),allow:t(o.allow??[])}}function s(o,t,e){for(const c of o)if(n(c,t,e))return c;return null}export function enumerateSubjects(t){if("Bash"!==t.tool)return t.subject?[t.subject]:[];const n=t.command??"",s=o(n).map(o=>o.command),r=[],a=new Set;for(;s.length>0;){const t=s.shift();if(!t||a.has(t))continue;a.add(t),r.push(t);const n=e(t);for(const t of n.commands){const n=o(t).map(o=>o.command);s.push(...n.length>0?n:[t])}const i=c(t);for(const t of i){const n=o(t).map(o=>o.command);s.push(...n.length>0?n:[t])}}return r}export function decide(o,t){const n=enumerateSubjects(o);for(const e of n){const n=s(t.deny,o.tool,e);if(n)return{action:"deny",reason:`Denied by policy "${n.raw}" — matched ${o.tool} subject ${JSON.stringify(e)}`,matched:{subject:e,pattern:n.raw,tool:o.tool}}}for(const e of n){const n=s(t.ask,o.tool,e);if(n)return{action:"ask",reason:`Requires confirmation by policy "${n.raw}" — matched ${JSON.stringify(e)}`,matched:{subject:e,pattern:n.raw,tool:o.tool}}}for(const e of n){const n=s(t.allow,o.tool,e);if(n)return{action:"allow",reason:`Allowed by policy "${n.raw}" — matched ${JSON.stringify(e)}`,matched:{subject:e,pattern:n.raw,tool:o.tool}}}return{action:"allow",reason:"No matching policy — default allow"}}
@@ -1 +0,0 @@
1
- export function parsePattern(e){if("string"!=typeof e||0===e.length)return null;if("*"===e)return{raw:e,tool:"*",body:"*",bodyKind:"plain",regex:/^.*$/s};const n=e.indexOf("(");if(n<=0||!e.endsWith(")"))return null;const t=e.slice(0,n);if(!/^[A-Za-z_][\w-]*$|^\*$/.test(t))return null;const r=e.slice(n+1,-1);if(0===r.length)return null;const o=function(e){const n=e.indexOf(":"),t=e.indexOf(" "),r=t>=0,o=n>=0&&(!r||n<t),l=r&&!o;if(o){const t=e.slice(0,n),r=e.slice(n+1);return{regex:new RegExp(`^${globToRegex(t)}(?:\\s+${globToRegex(r)})?$`,"s"),kind:"colon"}}if(l){const n=e.slice(0,t),r=e.slice(t+1);return{regex:new RegExp(`^${globToRegex(n)}\\s+${globToRegex(r)}$`,"s"),kind:"space"}}return{regex:new RegExp(`^${globToRegex(e)}$`,"s"),kind:"plain"}}(r);return{raw:e,tool:t,body:r,bodyKind:o.kind,regex:o.regex}}export function globToRegex(e){const n="\0GS_SLASH\0",t="\0GS\0",r="\0STAR\0";let o=e.replace(/\*\*\//g,n).replace(/\*\*/g,t).replace(/\*/g,r).replace(/\?/g,"\0Q\0");return o=o.replace(/[.+^${}()|[\]\\/]/g,"\\$&"),o=o.replace(new RegExp(n,"g"),"(?:.*/)?").replace(new RegExp(t,"g"),".*").replace(new RegExp(r,"g"),".*").replace(new RegExp("\0Q\0","g"),"."),o}export function matchPattern(e,n,t){return("*"===e.tool||e.tool===n)&&"string"==typeof t&&e.regex.test(t)}export function compilePatterns(e){const n=[];for(const t of e){const e=parsePattern(t);e&&n.push(e)}return n}
@@ -1 +0,0 @@
1
- import{existsSync as r,readFileSync as o}from"node:fs";import{homedir as n}from"node:os";import{join as e}from"node:path";export function loadPolicies(o){const c=o.homeDir??n(),a=[e(o.projectDir,".claude","settings.local.json"),e(o.projectDir,".claude","settings.json"),e(c,".claude","settings.json")],f=[],u=[],p=[],l=[];for(const o of a){if(!r(o))continue;const n=t(o);if(n.error){l.push({path:o,reason:n.error});continue}const e=s(n.contents);if(e.error){l.push({path:o,reason:e.error});continue}const c=e.value?.permissions??{};i(f,c.deny),i(u,c.ask),i(p,c.allow)}return{deny:f,ask:u,allow:p,errors:l}}function t(r){try{return{contents:o(r,"utf-8")}}catch(r){return{error:r instanceof Error?r.message:String(r)}}}function s(r){try{return{value:JSON.parse(r)}}catch(r){return{error:r instanceof Error?r.message:"parse failed"}}}function i(r,o){if(Array.isArray(o))for(const n of o)"string"==typeof n&&n.length>0&&r.push(n)}
@@ -1 +0,0 @@
1
- const e={python:[/os\.system\(\s*(['"])(.*?)\1\s*\)/g,/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*(['"])(.*?)\1/g],javascript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],typescript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],ruby:[/\bsystem\(\s*(['"])(.*?)\1/g,/`(.*?)`/g],go:[/exec\.Command\(\s*(['"`])(.*?)\1/g],php:[/shell_exec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])\bexec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])\bsystem\(\s*(['"`])(.*?)\1/g,/passthru\(\s*(['"`])(.*?)\1/g,/proc_open\(\s*(['"`])(.*?)\1/g],rust:[/Command::new\(\s*(['"`])(.*?)\1/g],shell:[/^([\s\S]*)$/g]},t=/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*(?:args\s*=\s*)?[[(]\s*((?:['"][^\\'"]*(?:\\.[^\\'"]*)*['"]\s*,?\s*)+)\s*[\])]/g,s=/(['"])((?:[^\\]|\\.)*?)\1/g;export function detectLanguage(e){if("string"!=typeof e||0===e.length)return null;const t=e.trim().split(/\s+/)[0];if(!t)return null;const s=t.replace(/^.*\//,"");return/^python3?(?:\.\d+)?$/.test(s)?"python":/^node$/.test(s)?"javascript":/^tsx?$/.test(s)||/^ts-node$/.test(s)?"typescript":/^ruby$/.test(s)?"ruby":/^go$/.test(s)&&/\s+run\b/.test(e)?"go":/^php$/.test(s)?"php":/^cargo$/.test(s)&&/\s+run\b/.test(e)?"rust":/^(?:bash|sh|zsh|dash|fish|ksh)$/.test(s)?"shell":null}export function extractEmbeddedCommands(n,c){if("string"!=typeof n||0===n.length)return[];const o=[];for(const t of e[c]){t.lastIndex=0;let e=t.exec(n);for(;null!==e;){const s=e[2]??e[1];s&&s.length>0&&o.push(s),e=t.exec(n)}}if("python"===c){t.lastIndex=0;let e=t.exec(n);for(;null!==e;){const c=e[1],l=[];s.lastIndex=0;let r=s.exec(c);for(;null!==r;)l.push(r[2]),r=s.exec(c);l.length>0&&o.push(l.join(" ")),e=t.exec(n)}}return o}export function extractInlinePayloads(e){const t=/(?:\s|^)(?:-c|-e|--command)\s*(['"`])((?:[^\\]|\\.)*?)\1/g,s=[];let n=t.exec(e);for(;null!==n;)s.push(n[2]),n=t.exec(e);return s}export function extractInlinePayload(e){return extractInlinePayloads(e)[0]??null}export function scanCommand(e){const t=detectLanguage(e);if(!t)return{lang:null,commands:[]};const s=extractInlinePayloads(e);if(0===s.length)return{lang:t,commands:[]};const n=[];for(const e of s)"shell"===t?e.length>0&&n.push(e):n.push(...extractEmbeddedCommands(e,t));return{lang:t,commands:n}}
@@ -1 +0,0 @@
1
- export function ensureDeps(){}