@lineman-io/mcp 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -9
- package/dist/main.js +1 -1
- package/hooks/core/lineman-config.mjs +1 -0
- package/hooks/core/tool-naming.mjs +1 -1
- package/hooks/lineman-call.mjs +1 -1
- package/hooks/post-tool-use.mjs +1 -1
- package/hooks/pre-tool-use.mjs +1 -1
- package/hooks/runpod-state-reader.mjs +1 -1
- package/package.json +2 -15
- package/skills/auth/SKILL.md +55 -0
- package/skills/lineman/SKILL.md +1 -2
- package/hooks/core/security/chain-split.mjs +0 -1
- package/hooks/core/security/decide.mjs +0 -1
- package/hooks/core/security/patterns.mjs +0 -1
- package/hooks/core/security/policies.mjs +0 -1
- package/hooks/core/security/shell-escape.mjs +0 -1
- package/hooks/ensure-deps.mjs +0 -1
|
@@ -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
|
|
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`;
|
package/hooks/lineman-call.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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}}
|
package/hooks/post-tool-use.mjs
CHANGED
|
@@ -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
|
|
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()}}();
|
package/hooks/pre-tool-use.mjs
CHANGED
|
@@ -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{
|
|
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:
|
|
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}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lineman-io/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Lineman MCP server — AI-powered code intelligence for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,16 +14,10 @@
|
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
15
15
|
"@sentry/node": "^10.47.0",
|
|
16
16
|
"better-sqlite3": "^11.8.1",
|
|
17
|
+
"open": "^10.2.0",
|
|
17
18
|
"turndown": "^7.2.2",
|
|
18
19
|
"zod": "^3.25.76"
|
|
19
20
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"hooks",
|
|
23
|
-
"skills",
|
|
24
|
-
".claude-plugin",
|
|
25
|
-
"start.mjs"
|
|
26
|
-
],
|
|
27
21
|
"publishConfig": {
|
|
28
22
|
"access": "restricted",
|
|
29
23
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -32,14 +26,7 @@
|
|
|
32
26
|
"node": ">=20"
|
|
33
27
|
},
|
|
34
28
|
"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
29
|
"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
30
|
"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
31
|
"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
32
|
"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.
|
package/skills/lineman/SKILL.md
CHANGED
|
@@ -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`, `
|
|
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}}
|
package/hooks/ensure-deps.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function ensureDeps(){}
|