superclawd 0.1.3 → 0.1.4
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/dist/index.js +2 -2
- package/dist/mcp-server.mjs +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -239,7 +239,7 @@ ${C}[?7l${C}[?25l`),i(!0),t.setRawMode(!0),t.resume();let a=!1,l=()=>{if(!a){a=!
|
|
|
239
239
|
`;let T=`v${t}`;m+=`${y}[${Math.max(1,_-1)};${Math.max(1,S-T.length-1)}H${W}${T}${R}`,I(m)},A=(u,S,_)=>{let h=process.stdout.columns||80,m="Select your workspace \u2191/\u2193 \xB7 enter launches \xB7 d sets default \xB7 esc cancels",$=Math.max(0,Math.floor((h-m.length)/2)),T=L=>` ${L.name} ${L.slug} \xB7 ${L.role}`,D=Math.max(...u.map(L=>T(L).length)),M=Math.max(0,Math.floor((h-D)/2)),N=[`${K($)}${Y}${m}${R}`,""];for(let L=0;L<u.length;L++){let j=u[L],X=L===S,z=X?`${W}\u276F `:" ",ge=X?`${W}${j.name}${R}`:j.name,he=j.slug===_?` ${W}\u2605 default${R}`:"";N.push(`${K(M)}${z}${ge} ${Y}${j.slug} \xB7 ${j.role}${R}${he}`)}d(N)};return{phase:(u,S)=>f(u-1,S,_r),ready:u=>f(e,u,Pr),message(u){let S=process.stdout.columns||80,_=Math.max(0,Math.floor((S-u.length)/2));d([`${K(_)}${Y}${u}${R}`]),r=Date.now()},selectWorkspace(u,S){return new Promise(_=>{let h=S?.currentSlug?u.findIndex(D=>D.slug===S.currentSlug):-1,m=h>=0?h:0,$=S?.defaultSlug;s&&process.stdin.off("data",i),A(u,m,$);let T=D=>{let M=D.toString();M===""||M===y?(process.stdin.off("data",T),_(null)):M==="\r"||M===`
|
|
240
240
|
`?(process.stdin.off("data",T),process.stdin.on("data",i),_(u[m].slug)):M==="d"||M==="D"?($=u[m].slug,S?.onSetDefault?.($),A(u,m,$)):M===`${y}[A`||M==="k"?(m=(m-1+u.length)%u.length,A(u,m,$)):(M===`${y}[B`||M==="j")&&(m=(m+1)%u.length,A(u,m,$))};process.stdin.on("data",T)})},fail(u){o&&c(),o=!1,w(),process.stderr.write(`${fe}\u2717 ${u}${R}
|
|
241
241
|
`)},note(){},handoff(){s&&(process.stdin.off("data",i),process.stdin.pause(),s=!1),I(Ie+Le),process.off("SIGINT",g),process.off("SIGTERM",g)},finish(){o&&c(),o=!1,w()}}}function xr(){return process.platform!=="win32"?!0:!!(process.env.WT_SESSION||process.env.TERM_PROGRAM||process.env.TERM)}function $r(){return{async phase(){},message(){},async selectWorkspace(e,t){return t?.currentSlug??e[0]?.slug??null},async ready(){},fail(e){process.stderr.write(`${fe}\u2717 ${e}${R}
|
|
242
|
-
`)},note(){},handoff(){},finish(){}}}function zt(e){if(e.quiet)return $r();let t=process.stdout.isTTY===!0,n=e.version??"";return e.plain||!t||!xr()?Ar(e.total,n):Mr(e.total,n)}var Lr=["low","medium","high","xhigh","max"];function Qt(e){let t=!1,n="",r=null,o=!1,s=!1,i=null,a=!1,l=!1,c="board",g=null,E=null,w=null;for(let p=0;p<e.length;p++){let f=e[p],d=e[p+1];f==="-p"||f==="--print"?(t=!0,d&&!d.startsWith("-")&&(n=d,p++)):f==="--workspace"||f==="-w"?d&&!d.startsWith("-")&&(r=d,p++):f==="--no-capture"?o=!0:f==="--resume"||f==="-r"?(s=!0,d&&!d.startsWith("-")&&(i=d,p++)):f==="--pipelines"?a=!0:f==="--pipelines-tools"?l=!0:f.startsWith("--pipelines-tools=")?(l=!0,c=f.slice(18)==="restricted"?"restricted":"board"):f==="--session-id"?d&&!d.startsWith("-")&&(g=d,p++):f==="--model"?d&&!d.startsWith("-")&&(E=d,p++):f==="--effort"&&d&&Lr.includes(d)&&(w=d,p++)}return{printMode:t,printPrompt:n,workspace:r,noCapture:o,resume:s,resumeSessionId:i,pipelines:a,pipelinesTools:l,pipelinesToolsMode:c,sessionId:g,model:E,effort:w}}var Zt={name:"superclawd",version:"0.1.
|
|
242
|
+
`)},note(){},handoff(){},finish(){}}}function zt(e){if(e.quiet)return $r();let t=process.stdout.isTTY===!0,n=e.version??"";return e.plain||!t||!xr()?Ar(e.total,n):Mr(e.total,n)}var Lr=["low","medium","high","xhigh","max"];function Qt(e){let t=!1,n="",r=null,o=!1,s=!1,i=null,a=!1,l=!1,c="board",g=null,E=null,w=null;for(let p=0;p<e.length;p++){let f=e[p],d=e[p+1];f==="-p"||f==="--print"?(t=!0,d&&!d.startsWith("-")&&(n=d,p++)):f==="--workspace"||f==="-w"?d&&!d.startsWith("-")&&(r=d,p++):f==="--no-capture"?o=!0:f==="--resume"||f==="-r"?(s=!0,d&&!d.startsWith("-")&&(i=d,p++)):f==="--pipelines"?a=!0:f==="--pipelines-tools"?l=!0:f.startsWith("--pipelines-tools=")?(l=!0,c=f.slice(18)==="restricted"?"restricted":"board"):f==="--session-id"?d&&!d.startsWith("-")&&(g=d,p++):f==="--model"?d&&!d.startsWith("-")&&(E=d,p++):f==="--effort"&&d&&Lr.includes(d)&&(w=d,p++)}return{printMode:t,printPrompt:n,workspace:r,noCapture:o,resume:s,resumeSessionId:i,pipelines:a,pipelinesTools:l,pipelinesToolsMode:c,sessionId:g,model:E,effort:w}}var Zt={name:"superclawd",version:"0.1.4",description:"Claude Code, supercharged \u2014 your team's standards, every session. The superclawd CLI loads your workspace's skills, agents, commands, workflows, and live directives into Claude Code as a plugin.",author:"SuperClawd <support@superclawd.com>",license:"SEE LICENSE IN LICENSE",homepage:"https://superclawd.com",repository:{type:"git",url:"https://github.com/superclawd-ai/superclawd"},bugs:{url:"https://github.com/superclawd-ai/superclawd/issues"},keywords:["superclawd","claude-code","claude","plugin","skills","agents","mcp","ai-assistant","coding-standards","model-context-protocol"],engines:{node:">=20"},type:"module",main:"dist/index.js",bin:{superclawd:"dist/index.js"},files:["dist","README.md","LICENSE"],scripts:{build:"node build.mjs",dev:"node build.mjs && tsx src/index.ts",typecheck:"tsc --noEmit",lint:"eslint 'src/**/*.{ts,tsx}'","lint:fix":"eslint 'src/**/*.{ts,tsx}' --fix",format:"prettier --check 'src/**/*.{ts,tsx}'","format:fix":"prettier --write 'src/**/*.{ts,tsx}'",selftest:"tsx test/mcp-selftest.ts","test:guardrails":"tsx test/guardrails-eval.test.ts",test:"node build.mjs && tsx test/mcp-selftest.ts && tsx test/pipelines-tools.test.ts && tsx test/guardrails-eval.test.ts && tsx test/hooks-capture.test.ts && tsx test/translator-teams.test.ts && tsx test/run-args.test.ts && tsx test/pipelines.test.ts && tsx test/update-check.test.ts && tsx test/retrieve.test.ts && tsx test/config-private-memory.test.ts && tsx test/config-choose-workspace.test.ts && tsx test/config.test.ts && tsx test/translator-basics.test.ts && tsx test/hook-helper-guardrails.test.ts && tsx test/capture-prompts.test.ts && tsx test/mcp-backend-real.test.ts && tsx test/catalog.test.ts",prepublishOnly:"npm run build","publish:npm":"npm publish","version:bump":"npm version patch --no-git-tag-version"},dependencies:{"@modelcontextprotocol/sdk":"^1.7.0",zod:"^3.24.2"},devDependencies:{"@eslint/js":"^9.39.1","@types/node":"^22.10.0","@typescript-eslint/eslint-plugin":"^8.44.1","@typescript-eslint/parser":"^8.44.1",esbuild:"^0.24.0",eslint:"^9.36.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4",prettier:"^3.6.2",tsx:"^4.19.0",typescript:"^5.7.0"}};var De=Zt.version,me=process.env.SUPERCLAWD_DRY_RUN==="1"||process.argv.includes("--dry-run");function Nr(){try{let e=Or("git remote get-url origin",{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim();if(e)return e.replace(/\.git$/,"").toLowerCase()}catch{}return`dir:${Tr(process.cwd())}`}function Gr(){if(process.env.SUPERCLAWD_MCP_SERVER_PATH)return process.env.SUPERCLAWD_MCP_SERVER_PATH;let e=en(new URL("./mcp-server.mjs",import.meta.url));return nn(e)?e:en(new URL("../dist/mcp-server.mjs",import.meta.url))}function jr(){return`superclawd \u2014 Claude Code, supercharged with your SuperClawd workspace.
|
|
243
243
|
|
|
244
244
|
Usage:
|
|
245
245
|
superclawd Launch Claude with your skills/agents/directives loaded
|
|
@@ -262,7 +262,7 @@ Print-mode flags (with -p):
|
|
|
262
262
|
--no-capture Don't write learned memories for this run (handy for behavior tests)
|
|
263
263
|
|
|
264
264
|
Not authenticated yet? Run \`superclawd login\` first.
|
|
265
|
-
`}function Fr(){try{return!Dr("claude",["--version"],{stdio:"ignore"}).error}catch{return!1}}function Kr(e,t=[],n,r,o,s,i){let a=["--plugin-dir",e.pluginDir];a.push("--allowedTools",dt);for(let l of t)a.push("--allowedTools",`mcp__plugin_superclawd_${l}__*`);return(Ne()||n)&&a.push("--dangerously-skip-permissions"),r?(a.push("--resume"),r.sessionId&&a.push(r.sessionId)):o&&a.push("--session-id",o),s&&a.push("--model",s),i&&a.push("--effort",i),n&&a.push("-p",n.prompt),new Promise(l=>{let c=tn("claude",a,{stdio:"inherit",env:{...process.env,CLAUDE_CODE_DISABLE_AUTO_MEMORY:"1"}});c.on("error",g=>l({code:127,error:`Could not launch claude: ${g.message}. Is Claude Code installed and on PATH?`})),c.on("close",g=>l({code:g??
|
|
265
|
+
`}function Fr(){try{return!Dr("claude",["--version"],{stdio:"ignore"}).error}catch{return!1}}function Kr(e,t=[],n,r,o,s,i){let a=["--plugin-dir",e.pluginDir];a.push("--allowedTools",dt);for(let l of t)a.push("--allowedTools",`mcp__plugin_superclawd_${l}__*`);return(Ne()||n)&&a.push("--dangerously-skip-permissions"),r?(a.push("--resume"),r.sessionId&&a.push(r.sessionId)):o&&a.push("--session-id",o),s&&a.push("--model",s),i&&a.push("--effort",i),n&&a.push("-p",n.prompt),new Promise(l=>{let c=tn("claude",a,{stdio:"inherit",env:{...process.env,CLAUDE_CODE_DISABLE_AUTO_MEMORY:"1"}});c.on("error",g=>l({code:127,error:`Could not launch claude: ${g.message}. Is Claude Code installed and on PATH?`})),c.on("close",g=>l({code:g??137}))})}function Yr(){return process.stdout.write(`Updating superclawd (npm i -g superclawd@latest)\u2026
|
|
266
266
|
`),new Promise(e=>{let t=tn("npm install -g superclawd@latest",{stdio:"inherit",shell:!0});t.on("error",n=>{process.stderr.write(`\u2717 Could not run npm: ${n.message}. Is npm on your PATH?
|
|
267
267
|
`),e(127)}),t.on("close",n=>{n===0?process.stdout.write("\u2713 Updated. Run `superclawd version` to confirm.\n"):process.stderr.write(`\u2717 Update failed (npm exited ${n}).
|
|
268
268
|
`),e(n??1)})})}async function Br(e){if(e.printMode&&!e.printPrompt.trim())return process.stderr.write(`Provide a prompt: superclawd -p "<your prompt>"
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -73,7 +73,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
73
73
|
`);if(e===-1)return null;let r=this._buffer.toString("utf8",0,e).replace(/\r$/,"");return this._buffer=this._buffer.subarray(e+1),eE(r)}clear(){this._buffer=void 0}};function eE(t){return jp.parse(JSON.parse(t))}function Sg(t){return JSON.stringify(t)+`
|
|
74
74
|
`}var ri=class{constructor(e=zg.stdin,r=zg.stdout){this._stdin=e,this._stdout=r,this._readBuffer=new ti,this._started=!1,this._ondata=n=>{this._readBuffer.append(n),this.processReadBuffer()},this._onerror=n=>{var o;(o=this.onerror)===null||o===void 0||o.call(this,n)}}async start(){if(this._started)throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");this._started=!0,this._stdin.on("data",this._ondata),this._stdin.on("error",this._onerror)}processReadBuffer(){for(var e,r;;)try{let n=this._readBuffer.readMessage();if(n===null)break;(e=this.onmessage)===null||e===void 0||e.call(this,n)}catch(n){(r=this.onerror)===null||r===void 0||r.call(this,n)}}async close(){var e;this._stdin.off("data",this._ondata),this._stdin.off("error",this._onerror),this._stdin.listenerCount("data")===0&&this._stdin.pause(),this._readBuffer.clear(),(e=this.onclose)===null||e===void 0||e.call(this)}send(e){return new Promise(r=>{let n=Sg(e);this._stdout.write(n)?r():this._stdout.once("drain",r)})}};async function Dg(){return(process.env.SUPERCLAWD_BACKEND||"real").toLowerCase()==="mock"?(await Promise.resolve().then(()=>(Cg(),Ng))).mockBackend:(await Promise.resolve().then(()=>(jg(),Mg))).realBackend}ni();var ai="superclawd",bE="core",xE=`mcp__plugin_${ai}_${bE}__`;var PC=`${xE}*`,TC=`${ai}:`;import{existsSync as El}from"node:fs";import{homedir as wE}from"node:os";import{spawn as $E,execSync as kE}from"node:child_process";var SE="sonnet",zE=2e4;function EE(){let t=process.env.SUPERCLAWD_CLAUDE_BIN;if(t&&El(t))return t;try{let r=process.platform==="win32"?"where claude":"command -v claude",n=kE(r,{encoding:"utf8"}).trim().split(`
|
|
75
75
|
`)[0];if(n&&El(n))return n}catch{}let e=[`${wE()}/.local/bin/claude`,"/opt/homebrew/bin/claude","/usr/local/bin/claude","/usr/bin/claude"];for(let r of e)if(El(r))return r;return null}function PE(t){let e=t.indexOf("{"),r=t.lastIndexOf("}");return e>=0&&r>e?t.slice(e,r+1):t}function TE(t){let{system:e,user:r}=t,n=t.model||SE,o=t.timeoutMs??zE,s=EE();return s?new Promise(i=>{let a="",c=!1,u=h=>{c||(c=!0,i(h))},l;try{l=$E(s,["-p","--model",n,"--output-format","json","--strict-mcp-config","--setting-sources","user","--tools","","--append-system-prompt",e],{stdio:["pipe","pipe","ignore"],env:{...process.env,SUPERCLAWD_POLICY_JUDGE:"1"}})}catch{return u(null)}let d=setTimeout(()=>{try{l.kill("SIGKILL")}catch{}u(null)},o);l.stdout?.on("data",h=>a+=h),l.on("error",()=>{clearTimeout(d),u(null)}),l.on("close",()=>{if(clearTimeout(d),t.onRaw)try{t.onRaw(a)}catch{}u(a)});try{l.stdin?.write(r),l.stdin?.end()}catch{}}):Promise.resolve(null)}function Lg(t){return TE(t).then(e=>{if(!e)return null;try{let r=e;try{let n=JSON.parse(e);typeof n.result=="string"&&(r=n.result)}catch{}return JSON.parse(PE(r))}catch{return null}})}var AC=["You maintain the durable memory for ONE repository. You review a FINISHED coding session and RECONCILE what it taught against the memories ALREADY SAVED (shown below with ids), keeping the set accurate, current, and non-redundant for the NEXT session. ","SAVE or UPDATE a fact when it would save a newcomer real time or prevent a wrong turn \u2014 for example: the build/test/run command (especially anything non-default, e.g. a subdirectory or required env flags); where key code or config lives when the layout is non-obvious (e.g. the app is in a subdirectory, or content is data-driven from one file); a setup step or arrangement needed to make things work (e.g. ESM-only deps must be mocked for tests to run); a correction to a reasonable wrong assumption (e.g. the repo uses npm despite a pnpm convention); or a non-obvious gotcha and its fix. A fact STILL qualifies even if it 'could be figured out by reading the files'. ","NEVER save: secrets or credentials; code dumps; transient state (a test that merely happened to pass or fail this run); one-off task details; or truly trivial facts (e.g. 'this is a React project', or that a file exists). ","NEVER bake VOLATILE SuperClawd identifiers into a fact: a workflow/skill/command/agent SLUG or id, or a tool-invocation recipe like 'call get_workflow with <slug>'. These break the moment the entity is renamed. Refer to an entity by its human NAME (e.g. the 'Code Review Process' workflow), or omit the identifier entirely. ","CRITICAL \u2014 the ALREADY-SAVED list is the ONLY knowledge future sessions have. If it is NONE, this repo has ZERO saved knowledge: nobody knows the build command, the package manager, or where the app lives \u2014 so the session's basic orientation facts (build/test command, package manager, where the app actually lives, key setup steps) are exactly what the first memories should record. Never call a fact 'already known' or 'standard' \u2014 judge only against the literal list. ","Emit OPERATIONS against the existing set: ",'\u2022 "create" \u2014 a genuinely NEW fact not represented by any existing memory. ','\u2022 "update" \u2014 the session REFINES, SHARPENS, or CORRECTS an existing memory: cite its id and rewrite its COMPLETE body to the now-correct version. Prefer update over create whenever an existing memory is about the same thing (the package manager, the app location, the build command, \u2026), even if the wording differs. A correction (e.g. the app moved from apps/web to apps/app) is an UPDATE to that memory, NOT a new one. When you correct a fact, also UPDATE any OTHER memory that still references the now-stale version. ',"\u2022 Omit any fact already captured accurately (no op). ","A memory tagged [LOCKED] is read-only: never update it; you may only create a complementary fact. ","Keep the set MINIMAL and CURRENT: never leave two memories about the same thing, and never leave an outdated fact beside its correction. ",'CRITICAL \u2014 within THIS single response, every fact must end up in EXACTLY ONE memory: never emit a "create" whose content is already covered by (or overlaps) a memory you keep or "update" in the same response. If the session adds a fact related to an existing memory, FOLD it into that memory with ONE "update" \u2014 do NOT also create a separate memory for the same fact. ',"This is a ONE-SHOT reconciliation of an already-finished session \u2014 do NOT continue the work, ask questions, or take any action. ",`Every op MUST include a "title": a distilled, human one-line SUMMARY of its body (<=120 chars) \u2014 a real summary, NOT the body's first words copied verbatim. `,'Respond with ONLY a strict JSON object {"ops":[ ... ],"reason":"<short overall why>"} and nothing else. Each op is {"op":"create","title":"<=120-char summary","body":"<self-contained 1-4 sentence note with concrete commands/paths + the gotcha & its fix>"} or {"op":"update","id":"<existing id>","title":"<=120-char summary","body":"<rewritten complete note>"}. If the session taught nothing durable, return {"ops":[],"reason":"..."}.'].join(""),ZC=["You review a FINISHED coding session and RECONCILE what it taught against the memories ALREADY SAVED (shown below with ids), keeping each memory set accurate, current, and non-redundant for the NEXT session. You maintain TWO memory sets and route every op to the correct one: ",'\u2022 "repo" \u2014 durable knowledge about THIS repository, SHARED with the whole team: the build/test/run command (especially non-default ones, e.g. a subdirectory or required env flags); where key code/config lives when non-obvious; a required setup step (e.g. an ESM-only dep must be mocked for tests); a correction to a wrong assumption; or a non-obvious gotcha and its fix. A rule about the codebase, the product, or how the TEAM works is "repo" EVEN IF the user phrases it as "I want\u2026", "always\u2026", or "going forward\u2026". A fact STILL qualifies even if it could be figured out by reading the files. ',`\u2022 "private" \u2014 a preference PRIVATE to THIS individual user about how THEY themselves want to work or be treated: tone/verbosity, what to call them, their experience level, editor/workflow habits, how the assistant should behave toward them specifically. NEVER shared with the team. ONLY for preferences about the person themselves \u2014 never about the code or the team's process. `,'Route each fact by what it is ABOUT \u2014 the WORK/team (repo) or the PERSON (private). Do NOT require the user to label something "private"; infer it. When genuinely unsure whether a stated preference is a team convention or a private one, treat it as "repo" (shared): never leak a team rule into a private memory, and never put a genuinely private preference into the shared repo set. ',"NEVER save (either set): secrets or credentials; code dumps; transient state (a test that merely passed/failed this run); one-off task details; trivia. ","NEVER bake VOLATILE SuperClawd identifiers into a fact: a workflow/skill/command/agent SLUG or id, or a tool-invocation recipe like 'call get_workflow with <slug>'. These break the moment the entity is renamed. Refer to an entity by its human NAME, or omit the identifier entirely. ","CRITICAL \u2014 the ALREADY-SAVED lists are the ONLY knowledge future sessions have. If a set is NONE it has ZERO saved knowledge (nobody knows the build command, package manager, or where the app lives), so the session's basic orientation facts are exactly what its first memories should record. Judge 'already known' only against the literal lists, never against what is 'standard'. ","Emit OPERATIONS, each tagged with its scope: ",'\u2022 {"op":"create","scope":"repo"|"private","body":"<self-contained note>"} \u2014 a genuinely NEW fact/preference not represented by any existing memory in that set. ','\u2022 {"op":"update","scope":"repo","id":"<existing repo id>","body":"<rewritten COMPLETE body>"} \u2014 the session REFINES, SHARPENS, or CORRECTS an existing REPO memory: prefer update over create whenever an existing repo memory is about the same thing (the package manager, the app location, \u2026); a correction (e.g. the app moved from apps/web to apps/app) is an UPDATE, not a new memory; also update any other repo memory that still references a now-stale fact. UPDATE applies to the repo set ONLY. ','\u2022 For the PRIVATE set, emit ONLY "create" for a genuinely new preference \u2014 never update or restate one already saved (the user curates their private memories themselves); omit (no op) any private preference already captured. ',"\u2022 Omit any fact already captured accurately (no op). ","A memory tagged [LOCKED] is read-only: never update it; you may only create a complementary fact. ","Keep each set MINIMAL and CURRENT: never leave two memories about the same thing, and never leave an outdated fact beside its correction. ",'CRITICAL \u2014 within THIS single response, every fact must end up in EXACTLY ONE memory of its set: never emit a "create" whose content is already covered by (or overlaps) a memory you keep or "update" in the same response. If the session adds a fact related to an existing memory, FOLD it into that memory with ONE "update" \u2014 do NOT also create a separate memory for the same fact. ',"This is a ONE-SHOT reconciliation of an already-finished session \u2014 do NOT continue the work, ask questions, or take any action. ",`Every op MUST include a "title": a distilled, human one-line SUMMARY of its body (<=120 chars) \u2014 a real summary, NOT the body's first words copied verbatim. `,'Respond with ONLY a strict JSON object {"ops":[ ... ],"reason":"<short overall why>"} and nothing else. Each op is {"op":"create","scope":"repo"|"private","title":"<=120-char summary","body":"..."} or {"op":"update","scope":"repo","id":"<existing id>","title":"<=120-char summary","body":"..."}. If the session taught nothing durable, return {"ops":[],"reason":"..."}.'].join(""),MC=["You maintain the durable lessons for a single reusable AGENT (a specialist invoked to do a job). You review a FINISHED run and RECONCILE what it learned about HOW TO PERFORM ITS ROLE against the lessons ALREADY SAVED (shown below with ids), keeping the set accurate, current, and non-redundant for the agent's NEXT invocation (in ANY repository). ","SAVE or UPDATE a lesson when it would make this agent do its job better next time: a standing preference or convention the user/team established for its work, a correction to how it should operate, or a recurring gotcha in its domain and how to handle it. Lessons are about the ROLE, not a codebase \u2014 they qualify regardless of which repo this run happened in. ","NEVER save: secrets or credentials; code dumps; one-off task details specific only to THIS single run; transient state; or truly trivial facts. ","NEVER record a lesson about TOOL AVAILABILITY \u2014 e.g. that a tool 'does not exist', 'is unavailable', 'is not in the toolset', or that a call failed because the tool wasn't callable. Which tools a run can call is an environment/config detail set per-invocation (this agent runs with a RESTRICTED toolset that omits tools the main session has), NOT a durable fact about the role: the tool may well exist and be available in another context, so such a lesson is misleading and goes stale. Treat a 'no such tool' / 'tool not found' error as a transient environment artifact, never a lesson. ","The ALREADY-SAVED list is the ONLY knowledge the agent carries between runs \u2014 treat anything NOT listed as new; never claim a lesson is 'already known' unless it literally appears. ","IMPORTANT \u2014 the agent runs under workspace SKILLS, instructions, and guardrails that you CANNOT see in this transcript. So: do NOT treat an asserted convention, rule, or guideline as fabricated or 'hallucinated' merely because the transcript shows no basis for it \u2014 it may come from the agent's hidden instructions. NEVER record a lesson that tells the agent to stop asserting conventions, to second-guess its guidance, or that is based on YOUR OWN judgment that the agent made a mistake this run. Capture a lesson ONLY when the run itself shows the USER establishing or correcting a standing preference, or a concrete reproducible domain gotcha the agent itself confirmed. ","Emit OPERATIONS against the existing set: ",'\u2022 "create" \u2014 a genuinely NEW lesson not represented by any existing one. ','\u2022 "update" \u2014 the run REFINES or CORRECTS an existing lesson: cite its id and rewrite its COMPLETE body. Prefer update over create whenever an existing lesson is about the same thing, even if the wording differs. ',"\u2022 Omit any lesson already captured accurately (no op). ","A lesson tagged [LOCKED] is read-only: never update it; you may only create a complementary lesson. ","Keep the set MINIMAL and CURRENT: never leave two lessons about the same thing, and never leave an outdated lesson beside its correction. ",'CRITICAL \u2014 within THIS single response, every lesson must end up exactly ONCE: never emit a "create" whose content is already covered by (or overlaps) a lesson you keep or "update" in the same response. If the run adds something related to an existing lesson, FOLD it in with ONE "update" \u2014 do NOT also create a separate lesson for the same thing. ',"This is a ONE-SHOT reconciliation of an already-finished run \u2014 do NOT continue the work, ask questions, or take any action. ",`Every op MUST include a "title": a distilled, human one-line SUMMARY of its lesson (<=120 chars) \u2014 a real summary, NOT the lesson's first words copied verbatim. `,'Respond with ONLY a strict JSON object {"ops":[ ... ],"reason":"<short overall why>"} and nothing else. Each op is {"op":"create","title":"<=120-char summary","body":"<self-contained instruction the agent can apply next time>"} or {"op":"update","id":"<existing id>","title":"<=120-char summary","body":"<rewritten complete lesson>"}. If the run taught nothing durable, return {"ops":[],"reason":"..."}.'].join(""),qg=`You are given the CONTENT of a saved memory. Return ONLY strict JSON {"title":"..."}: a concise, self-contained one-line summary of the content, MAX 120 characters, that is NOT merely the content's first words copied verbatim. Nothing else but the JSON object.`,jC=["You decide whether a code change makes a remembered lesson STALE (no longer true).",'Respond ONLY strict JSON {"verdict":"stale"|"valid","reason":"<one line>","confidence":<0..1>}.'].join(" ");import{homedir as RE}from"node:os";import{join as go}from"node:path";import{readFileSync as Ug,writeFileSync as IE,mkdirSync as OE,chmodSync as VC,existsSync as FC}from"node:fs";var HC=(process.env.SUPERCLAWD_BACKEND||"real").toLowerCase();var WC=process.env.SUPERCLAWD_MCP_API_URL||"https://mcp-api.superclawd.com",BC=process.env.SUPERCLAWD_API_URL||"https://api.superclawd.com",Vg=process.env.SUPERCLAWD_HOME||go(RE(),".superclawd"),NE=go(Vg,"sessions"),CE=go(Vg,"config.json");function AE(t){if(!t||typeof t!="object")return{};let e=t,r=e.credentials??{...typeof e.keyId=="string"?{keyId:e.keyId}:{},...typeof e.secret=="string"?{secret:e.secret}:{},...typeof e.workspace=="string"?{workspace:e.workspace}:{}},n=e.preferences??{...typeof e.skipPermissions=="boolean"?{skipPermissions:e.skipPermissions}:{},...typeof e.notifications=="boolean"?{notifications:e.notifications}:{},...typeof e.chooseWorkspaceAtStartup=="boolean"?{chooseWorkspaceAtStartup:e.chooseWorkspaceAtStartup}:{},...typeof e.experimental=="boolean"?{experimental:e.experimental}:{}},o={};return Object.keys(r).length&&(o.credentials=r),Object.keys(n).length&&(o.preferences=n),typeof e.machineId=="string"&&(o.machineId=e.machineId),o}function ZE(){try{return AE(JSON.parse(Ug(CE,"utf8")))}catch{return null}}function ME(){return ZE()?.preferences?.notifications===!0}function Fg(t){return go(NE,t)}function Hg(t){return go(t,"config.json")}function Wg(t){try{return JSON.parse(Ug(Hg(t),"utf8"))}catch{return null}}function jE(t,e){try{OE(t,{recursive:!0}),IE(Hg(t),`${JSON.stringify(e,null,2)}
|
|
76
|
-
`)}catch{}}function Bg(t){let e=t?Wg(Fg(t)):null;return e&&typeof e.notifications=="boolean"?e.notifications:ME()}function Kg(t,e){if(!t)return e;let r=Fg(t),n=Wg(r)??{};return jE(r,{...n,notifications:e}),e}var KC=1e3*60*60*24*3;var Jg=(process.env.SUPERCLAWD_PIPELINES_URL||"").replace(/\/+$/,""),DE=process.env.SUPERCLAWD_PIPELINES_TICKET||"",zt=t=>({content:[{type:"text",text:t}]}),yo=t=>zt(JSON.stringify(t,null,2)),se=t=>({isError:!0,content:[{type:"text",text:`<superclawd_error>${t}</superclawd_error>`}]});async function xe(t,e){if(!Jg)throw new Error("SUPERCLAWD_PIPELINES_URL is not set");let r=await fetch(`${Jg}${t}`,{...e,headers:{"Content-Type":"application/json",...e?.headers??{}}});if(!r.ok){let n=await r.text().catch(()=>""),o="";try{let s=JSON.parse(n);typeof s.error=="string"&&(o=s.error,typeof s.unresolved=="number"&&(o+=` (${s.unresolved} unresolved open question${s.unresolved===1?"":"s"})`))}catch{}throw new Error(o||`${e?.method??"GET"} ${t} \u2192 ${r.status}${n?`: ${n.slice(0,300)}`:""}`)}return r.status===204?void 0:await r.json()}function jt(t){return t&&t.trim()||DE||null}var Dt="No ticketId given and no ticket is bound to this session \u2014 pass a ticketId (use list_tickets to find one).";function Gg(t,e="board"){t.tool("list_boards","List the SuperClawd Pipelines boards in this workspace (a Local board plus any connected vendor boards like Linear). Use this to find a board id \u2014 e.g. the Local board to create a ticket on, or a board to list its tickets. `lastSyncedAt` shows how fresh a vendor board's data is (there is no way to sync from here).",{},async()=>{try{let n=await xe("/api/connections");return yo(n.filter(o=>o.linkStatus==="linked").map(o=>({boardId:o.id,name:o.displayName||o.providerAccountName,provider:o.provider,isLocal:o.provider==="local",workspaceId:o.workspaceId,workspaceName:o.workspaceName,ticketCount:o.ticketCount,lastSyncedAt:o.lastSyncedAt})))}catch(n){return se(String(n))}}),t.tool("list_tickets","List the tickets on a Pipelines board (by boardId from list_boards). Returns each ticket's id, identifier, title, priority and whether it's blocked \u2014 use read_ticket for the full detail. Workflow status/column is intentionally omitted.",{boardId:O.string().describe("The board id (from list_boards).")},async({boardId:n})=>{try{let o=await xe(`/api/board?connectionId=${encodeURIComponent(n)}`);return yo(o.map(({ticket:s})=>({ticketId:s.id,identifier:s.externalIdentifier,title:s.title,priority:s.priority,assignee:s.assignee?.name,blocked:!!s.blockedReason})))}catch(o){return se(String(o))}}),t.tool("read_ticket","Read a Pipelines ticket in full \u2014 description, additional context, plan, conclusion, open questions and notes, plus whether it's blocked (and why). For a board run you can omit ticketId to read the ticket this session is working on. Workflow status is intentionally omitted; `lastSyncedAt` shows how fresh vendor data is (no sync is possible from here).",{ticketId:O.string().optional().describe("The Pipelines ticket id. Omit on a board run to read the bound ticket.")},async({ticketId:n})=>{let o=jt(n);if(!o)return se(Dt);try{let[s,i,a,c]=await Promise.all([xe(`/api/tickets/${encodeURIComponent(o)}`),xe(`/api/tickets/${encodeURIComponent(o)}/open-questions`),xe(`/api/tickets/${encodeURIComponent(o)}/notes`),xe("/api/connections").catch(()=>[])]),u=s.ticket,l=c.find(d=>d.id===u.connectionId);return yo({ticketId:u.id,identifier:u.externalIdentifier,title:u.title,description:u.description??null,provider:u.provider,project:u.project??null,parent:u.parent??null,assignee:u.assignee?.name??null,priority:u.priority??null,labels:u.labels?.map(d=>d.name)??[],connectionName:s.connectionName,workspaceName:s.workspaceName,workingDir:u.workingDir??null,model:u.model??null,effort:u.effort??null,blocked:!!u.blockedReason,blockedReason:u.blockedReason??null,additionalContext:u.additionalContext??null,plan:u.plan??null,conclusion:u.conclusion??null,lastSyncedAt:l?.lastSyncedAt??null,openQuestions:i,notes:a.map(d=>({id:d.id,author:d.author,body:d.body}))})}catch(s){return se(String(s))}}),t.tool("create_ticket","Create a new ticket on a workspace's LOCAL board (vendor boards are read-only from here). Omit boardId to use the workspace's Local board when there's exactly one; otherwise pass a local boardId from list_boards.",{title:O.string().describe("The ticket title (required)."),description:O.string().optional().describe("Optional description (markdown)."),boardId:O.string().optional().describe("A LOCAL board id (from list_boards). Omit to use the sole Local board.")},async({title:n,description:o,boardId:s})=>{try{let i=s;if(!i){let u=(await xe("/api/connections")).filter(l=>l.provider==="local"&&l.linkStatus==="linked");if(u.length===0)return se("No local board found in this workspace.");if(u.length>1)return se("Multiple local boards exist \u2014 pass a boardId (see list_boards).");i=u[0].id}let a=await xe(`/api/board/${encodeURIComponent(i)}/tickets`,{method:"POST",body:JSON.stringify({title:n,description:o})});return zt(`Created ${a.externalIdentifier} \u2014 "${a.title}" (ticketId ${a.id}).`)}catch(i){return se(String(i))}});let r=[{tool:"set_plan",field:"plan",label:"plan",desc:"Write (or replace) the ticket's Plan \u2014 how you'll approach the task, as markdown (it needn't be technical). Prefer this over writing a plan file in the repo."},{tool:"set_conclusion",field:"conclusion",label:"conclusion",desc:"Write (or replace) the ticket's Conclusion \u2014 a short wrap-up of what you did and any results/findings, as markdown. Write this once you've fully completed what the ticket asked."},{tool:"set_additional_context",field:"additionalContext",label:"additional context",desc:"Write (or replace) the ticket's Additional context \u2014 extra context the vendor description can't carry, as markdown."}];for(let n of r)t.tool(n.tool,n.desc+" For a board run you can omit ticketId to target the bound ticket. Pass empty markdown to clear it.",{markdown:O.string().describe(`The ${n.label} content (markdown).`),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({markdown:o,ticketId:s})=>{let i=jt(s);if(!i)return se(Dt);try{return await xe(`/api/tickets/${encodeURIComponent(i)}`,{method:"PATCH",body:JSON.stringify({[n.field]:o})}),zt(`Saved the ${n.label} for ${i}.`)}catch(a){return se(String(a))}});t.tool("add_open_question","Add an open question to a ticket \u2014 something that needs an answer before/while working it. Use this for answerable questions; use block_ticket when you truly can't proceed. Note: if the ticket is (or becomes) blocked, every open question on it must be resolved before it can be unblocked. For a board run you can omit ticketId.",{question:O.string().describe("The question to record."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({question:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/open-questions`,{method:"POST",body:JSON.stringify({question:n})});return zt(`Added open question (id ${i.id}).`)}catch(i){return se(String(i))}}),t.tool("update_open_question","Answer and/or resolve an existing open question (by its id, from read_ticket). Pass an answer, a resolved flag, or both.",{questionId:O.string().describe("The open question id."),answer:O.string().optional().describe("The answer text."),resolved:O.boolean().optional().describe("Mark the question resolved (true) or reopened (false).")},async({questionId:n,answer:o,resolved:s})=>{if(o===void 0&&s===void 0)return se("Pass an answer and/or a resolved flag.");try{return await xe(`/api/open-questions/${encodeURIComponent(n)}`,{method:"PATCH",body:JSON.stringify({answer:o,resolved:s})}),zt(`Updated open question ${n}.`)}catch(i){return se(String(i))}}),t.tool("add_note","Add a note to a ticket (recorded as authored by the assistant). Use for durable findings or decisions worth keeping on the ticket. For a board run you can omit ticketId.",{body:O.string().describe("The note text."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({body:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/notes`,{method:"POST",body:JSON.stringify({body:n,author:"assistant"})});return zt(`Added note (id ${i.id}).`)}catch(i){return se(String(i))}}),t.tool("edit_note","Edit the body of an existing note (by its id, from read_ticket). Notes cannot be deleted.",{noteId:O.string().describe("The note id."),body:O.string().describe("The revised note text.")},async({noteId:n,body:o})=>{try{return await xe(`/api/notes/${encodeURIComponent(n)}`,{method:"PATCH",body:JSON.stringify({body:o})}),zt(`Edited note ${n}.`)}catch(s){return se(String(s))}}),t.tool("block_ticket","Mark a ticket as blocked with a reason \u2014 an escalation signal that you can't proceed and a human needs to look. Prefer add_open_question for questions you can still work around. If you record open questions on a blocked ticket, they must all be resolved before it can be unblocked. For a board run you can omit ticketId.",{reason:O.string().describe("Why the ticket is blocked (required)."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({reason:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);if(!n.trim())return se("A blocked reason is required.");try{return await xe(`/api/tickets/${encodeURIComponent(s)}`,{method:"PATCH",body:JSON.stringify({blockedReason:n})}),zt(`Blocked ${s}: ${n}`)}catch(i){return se(String(i))}}),t.tool("unblock_ticket","Clear a ticket's blocked state. Drops a note (authored by the assistant) recording why, so there's a trail. You can only unblock once EVERY open question on the ticket is resolved \u2014 answer/resolve them with update_open_question first, or this will fail. For a board run you can omit ticketId.",{reason:O.string().optional().describe("Why you're unblocking it (recorded as a note)."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({reason:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{await xe(`/api/tickets/${encodeURIComponent(s)}`,{method:"PATCH",body:JSON.stringify({blockedReason:null})});let i=`Unblocked${n&&n.trim()?`: ${n.trim()}`:"."}`;return await xe(`/api/tickets/${encodeURIComponent(s)}/notes`,{method:"POST",body:JSON.stringify({body:i,author:"assistant"})}).catch(()=>{}),zt(`Unblocked ${s}.`)}catch(i){return se(String(i))}}),t.tool("list_repos","List the git repositories available in this ticket's working directory. The working dir is often an umbrella project folder holding several repos in subfolders \u2014 use this to see what you can work on. For a board run you can omit ticketId.",{ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({ticketId:n})=>{let o=jt(n);if(!o)return se(Dt);try{let s=await xe(`/api/tickets/${encodeURIComponent(o)}/repos`);return yo(s)}catch(s){return se(String(s))}}),e==="board"&&(t.tool("start_repo_work","Claim a repo to modify \u2014 creates an isolated git worktree on a fresh branch and returns its path. You MUST make ALL changes to that repo inside the returned `worktreePath`, and commit there \u2014 NEVER edit the repo in the working directory itself (other runs may be working on it concurrently). Call once per repo you need (a task may touch several). Returns { worktreePath, branch, base, compareUrl }. When done: commit, push the branch, then record the PR with add_pr_link \u2014 if you can't open a PR (e.g. no `gh`), just push and share the compareUrl. For a board run you can omit ticketId.",{repo:O.string().optional().describe("Which repo (folder name from list_repos). Omit only if the working dir is a single repo."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({repo:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/worktree`,{method:"POST",body:JSON.stringify({repo:n})});return yo(i)}catch(i){return se(String(i))}}),t.tool("add_pr_link","Record a pull-request link on the ticket. A ticket can have several PRs (one per repo/branch), so call this once per PR you open. If you couldn't open the PR yourself, still push the branch and (optionally) record the compare URL from start_repo_work so a human can open it. For a board run you can omit ticketId.",{url:O.string().describe("The PR (or compare) URL."),title:O.string().optional().describe("Optional short label for the PR."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({url:n,title:o,ticketId:s})=>{let i=jt(s);if(!i)return se(Dt);try{return await xe(`/api/tickets/${encodeURIComponent(i)}/pr-links`,{method:"POST",body:JSON.stringify({url:n,title:o})}),zt(`Recorded PR link on ${i}.`)}catch(a){return se(String(a))}}))}var Ve=await Dg();async function LE(t){if(process.env.SUPERCLAWD_BACKEND!=="mock")try{let r=((await Lg({system:qg,user:`CONTENT:
|
|
76
|
+
`)}catch{}}function Bg(t){let e=t?Wg(Fg(t)):null;return e&&typeof e.notifications=="boolean"?e.notifications:ME()}function Kg(t,e){if(!t)return e;let r=Fg(t),n=Wg(r)??{};return jE(r,{...n,notifications:e}),e}var KC=1e3*60*60*24*3;var Jg=(process.env.SUPERCLAWD_PIPELINES_URL||"").replace(/\/+$/,""),DE=process.env.SUPERCLAWD_PIPELINES_TICKET||"",zt=t=>({content:[{type:"text",text:t}]}),yo=t=>zt(JSON.stringify(t,null,2)),se=t=>({isError:!0,content:[{type:"text",text:`<superclawd_error>${t}</superclawd_error>`}]});async function xe(t,e){if(!Jg)throw new Error("SUPERCLAWD_PIPELINES_URL is not set");let r=await fetch(`${Jg}${t}`,{...e,headers:{"Content-Type":"application/json",...e?.headers??{}}});if(!r.ok){let n=await r.text().catch(()=>""),o="";try{let s=JSON.parse(n);typeof s.error=="string"&&(o=s.error,typeof s.unresolved=="number"&&(o+=` (${s.unresolved} unresolved open question${s.unresolved===1?"":"s"})`))}catch{}throw new Error(o||`${e?.method??"GET"} ${t} \u2192 ${r.status}${n?`: ${n.slice(0,300)}`:""}`)}return r.status===204?void 0:await r.json()}function jt(t){return t&&t.trim()||DE||null}var Dt="No ticketId given and no ticket is bound to this session \u2014 pass a ticketId (use list_tickets to find one).";function Gg(t,e="board"){t.tool("list_boards","List the SuperClawd Pipelines boards in this workspace (a Local board plus any connected vendor boards like Linear). Use this to find a board id \u2014 e.g. the Local board to create a ticket on, or a board to list its tickets. `lastSyncedAt` shows how fresh a vendor board's data is (there is no way to sync from here).",{},async()=>{try{let n=await xe("/api/connections");return yo(n.filter(o=>o.linkStatus==="linked").map(o=>({boardId:o.id,name:o.displayName||o.providerAccountName,provider:o.provider,isLocal:o.provider==="local",workspaceId:o.workspaceId,workspaceName:o.workspaceName,ticketCount:o.ticketCount,lastSyncedAt:o.lastSyncedAt})))}catch(n){return se(String(n))}}),t.tool("list_tickets","List the tickets on a Pipelines board (by boardId from list_boards). Returns each ticket's id, identifier, title, priority and whether it's blocked \u2014 use read_ticket for the full detail. Workflow status/column is intentionally omitted.",{boardId:O.string().describe("The board id (from list_boards).")},async({boardId:n})=>{try{let o=await xe(`/api/board?connectionId=${encodeURIComponent(n)}`);return yo(o.map(({ticket:s})=>({ticketId:s.id,identifier:s.externalIdentifier,title:s.title,priority:s.priority,assignee:s.assignee?.name,blocked:!!s.blockedReason})))}catch(o){return se(String(o))}}),t.tool("read_ticket","Read a Pipelines ticket in full \u2014 description, vendor comments (the ticket's discussion, synced read-only from the provider \u2014 often where the real acceptance criteria live), additional context, plan, conclusion, open questions and notes, plus whether it's blocked (and why). For a board run you can omit ticketId to read the ticket this session is working on. Workflow status is intentionally omitted; `lastSyncedAt` shows how fresh vendor data is (no sync is possible from here).",{ticketId:O.string().optional().describe("The Pipelines ticket id. Omit on a board run to read the bound ticket.")},async({ticketId:n})=>{let o=jt(n);if(!o)return se(Dt);try{let[s,i,a,c]=await Promise.all([xe(`/api/tickets/${encodeURIComponent(o)}`),xe(`/api/tickets/${encodeURIComponent(o)}/open-questions`),xe(`/api/tickets/${encodeURIComponent(o)}/notes`),xe("/api/connections").catch(()=>[])]),u=s.ticket,l=c.find(d=>d.id===u.connectionId);return yo({ticketId:u.id,identifier:u.externalIdentifier,title:u.title,description:u.description??null,provider:u.provider,project:u.project??null,parent:u.parent??null,assignee:u.assignee?.name??null,priority:u.priority??null,labels:u.labels?.map(d=>d.name)??[],connectionName:s.connectionName,workspaceName:s.workspaceName,workingDir:u.workingDir??null,model:u.model??null,effort:u.effort??null,blocked:!!u.blockedReason,blockedReason:u.blockedReason??null,additionalContext:u.additionalContext??null,plan:u.plan??null,conclusion:u.conclusion??null,lastSyncedAt:l?.lastSyncedAt??null,openQuestions:i,comments:(u.comments??[]).map(d=>({id:d.id,author:d.author??null,body:d.body,createdAt:d.createdAt??null})),notes:a.map(d=>({id:d.id,author:d.author,body:d.body}))})}catch(s){return se(String(s))}}),t.tool("create_ticket","Create a new ticket on a workspace's LOCAL board (vendor boards are read-only from here). Omit boardId to use the workspace's Local board when there's exactly one; otherwise pass a local boardId from list_boards.",{title:O.string().describe("The ticket title (required)."),description:O.string().optional().describe("Optional description (markdown)."),boardId:O.string().optional().describe("A LOCAL board id (from list_boards). Omit to use the sole Local board.")},async({title:n,description:o,boardId:s})=>{try{let i=s;if(!i){let u=(await xe("/api/connections")).filter(l=>l.provider==="local"&&l.linkStatus==="linked");if(u.length===0)return se("No local board found in this workspace.");if(u.length>1)return se("Multiple local boards exist \u2014 pass a boardId (see list_boards).");i=u[0].id}let a=await xe(`/api/board/${encodeURIComponent(i)}/tickets`,{method:"POST",body:JSON.stringify({title:n,description:o})});return zt(`Created ${a.externalIdentifier} \u2014 "${a.title}" (ticketId ${a.id}).`)}catch(i){return se(String(i))}});let r=[{tool:"set_plan",field:"plan",label:"plan",desc:"Write (or replace) the ticket's Plan \u2014 how you'll approach the task, as markdown (it needn't be technical). Prefer this over writing a plan file in the repo."},{tool:"set_conclusion",field:"conclusion",label:"conclusion",desc:"Write (or replace) the ticket's Conclusion \u2014 a short wrap-up of what you did and any results/findings, as markdown. Write this once you've fully completed what the ticket asked."},{tool:"set_additional_context",field:"additionalContext",label:"additional context",desc:"Write (or replace) the ticket's Additional context \u2014 extra context the vendor description can't carry, as markdown."}];for(let n of r)t.tool(n.tool,n.desc+" For a board run you can omit ticketId to target the bound ticket. Pass empty markdown to clear it.",{markdown:O.string().describe(`The ${n.label} content (markdown).`),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({markdown:o,ticketId:s})=>{let i=jt(s);if(!i)return se(Dt);try{return await xe(`/api/tickets/${encodeURIComponent(i)}`,{method:"PATCH",body:JSON.stringify({[n.field]:o})}),zt(`Saved the ${n.label} for ${i}.`)}catch(a){return se(String(a))}});t.tool("add_open_question","Add an open question to a ticket \u2014 something that needs an answer before/while working it. Use this for answerable questions; use block_ticket when you truly can't proceed. Note: if the ticket is (or becomes) blocked, every open question on it must be resolved before it can be unblocked. For a board run you can omit ticketId.",{question:O.string().describe("The question to record."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({question:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/open-questions`,{method:"POST",body:JSON.stringify({question:n})});return zt(`Added open question (id ${i.id}).`)}catch(i){return se(String(i))}}),t.tool("update_open_question","Answer and/or resolve an existing open question (by its id, from read_ticket). Pass an answer, a resolved flag, or both.",{questionId:O.string().describe("The open question id."),answer:O.string().optional().describe("The answer text."),resolved:O.boolean().optional().describe("Mark the question resolved (true) or reopened (false).")},async({questionId:n,answer:o,resolved:s})=>{if(o===void 0&&s===void 0)return se("Pass an answer and/or a resolved flag.");try{return await xe(`/api/open-questions/${encodeURIComponent(n)}`,{method:"PATCH",body:JSON.stringify({answer:o,resolved:s})}),zt(`Updated open question ${n}.`)}catch(i){return se(String(i))}}),t.tool("add_note","Add a note to a ticket (recorded as authored by the assistant). Use for durable findings or decisions worth keeping on the ticket. For a board run you can omit ticketId.",{body:O.string().describe("The note text."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({body:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/notes`,{method:"POST",body:JSON.stringify({body:n,author:"assistant"})});return zt(`Added note (id ${i.id}).`)}catch(i){return se(String(i))}}),t.tool("edit_note","Edit the body of an existing note (by its id, from read_ticket). Notes cannot be deleted.",{noteId:O.string().describe("The note id."),body:O.string().describe("The revised note text.")},async({noteId:n,body:o})=>{try{return await xe(`/api/notes/${encodeURIComponent(n)}`,{method:"PATCH",body:JSON.stringify({body:o})}),zt(`Edited note ${n}.`)}catch(s){return se(String(s))}}),t.tool("block_ticket","Mark a ticket as blocked with a reason \u2014 an escalation signal that you can't proceed and a human needs to look. Prefer add_open_question for questions you can still work around. If you record open questions on a blocked ticket, they must all be resolved before it can be unblocked. For a board run you can omit ticketId.",{reason:O.string().describe("Why the ticket is blocked (required)."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({reason:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);if(!n.trim())return se("A blocked reason is required.");try{return await xe(`/api/tickets/${encodeURIComponent(s)}`,{method:"PATCH",body:JSON.stringify({blockedReason:n})}),zt(`Blocked ${s}: ${n}`)}catch(i){return se(String(i))}}),t.tool("unblock_ticket","Clear a ticket's blocked state. Drops a note (authored by the assistant) recording why, so there's a trail. You can only unblock once EVERY open question on the ticket is resolved \u2014 answer/resolve them with update_open_question first, or this will fail. For a board run you can omit ticketId.",{reason:O.string().optional().describe("Why you're unblocking it (recorded as a note)."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({reason:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{await xe(`/api/tickets/${encodeURIComponent(s)}`,{method:"PATCH",body:JSON.stringify({blockedReason:null})});let i=`Unblocked${n&&n.trim()?`: ${n.trim()}`:"."}`;return await xe(`/api/tickets/${encodeURIComponent(s)}/notes`,{method:"POST",body:JSON.stringify({body:i,author:"assistant"})}).catch(()=>{}),zt(`Unblocked ${s}.`)}catch(i){return se(String(i))}}),t.tool("list_repos","List the git repositories available in this ticket's working directory. The working dir is often an umbrella project folder holding several repos in subfolders \u2014 use this to see what you can work on. For a board run you can omit ticketId.",{ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({ticketId:n})=>{let o=jt(n);if(!o)return se(Dt);try{let s=await xe(`/api/tickets/${encodeURIComponent(o)}/repos`);return yo(s)}catch(s){return se(String(s))}}),e==="board"&&(t.tool("start_repo_work","Claim a repo to modify \u2014 creates an isolated git worktree on a fresh branch and returns its path. You MUST make ALL changes to that repo inside the returned `worktreePath`, and commit there \u2014 NEVER edit the repo in the working directory itself (other runs may be working on it concurrently). Call once per repo you need (a task may touch several). Returns { worktreePath, branch, base, compareUrl }. When done: commit, push the branch, then record the PR with add_pr_link \u2014 if you can't open a PR (e.g. no `gh`), just push and share the compareUrl. For a board run you can omit ticketId.",{repo:O.string().optional().describe("Which repo (folder name from list_repos). Omit only if the working dir is a single repo."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({repo:n,ticketId:o})=>{let s=jt(o);if(!s)return se(Dt);try{let i=await xe(`/api/tickets/${encodeURIComponent(s)}/worktree`,{method:"POST",body:JSON.stringify({repo:n})});return yo(i)}catch(i){return se(String(i))}}),t.tool("add_pr_link","Record a pull-request link on the ticket. A ticket can have several PRs (one per repo/branch), so call this once per PR you open. If you couldn't open the PR yourself, still push the branch and (optionally) record the compare URL from start_repo_work so a human can open it. For a board run you can omit ticketId.",{url:O.string().describe("The PR (or compare) URL."),title:O.string().optional().describe("Optional short label for the PR."),ticketId:O.string().optional().describe("The ticket id; omit on a board run to use the bound one.")},async({url:n,title:o,ticketId:s})=>{let i=jt(s);if(!i)return se(Dt);try{return await xe(`/api/tickets/${encodeURIComponent(i)}/pr-links`,{method:"POST",body:JSON.stringify({url:n,title:o})}),zt(`Recorded PR link on ${i}.`)}catch(a){return se(String(a))}}))}var Ve=await Dg();async function LE(t){if(process.env.SUPERCLAWD_BACKEND!=="mock")try{let r=((await Lg({system:qg,user:`CONTENT:
|
|
77
77
|
${t}`,model:"haiku",timeoutMs:15e3}))?.title||"").trim();return r?r.slice(0,120):void 0}catch{return}}var Ce=new ei({name:ai,version:"0.1.0"});Ce.tool("get_directives","Fetch and apply the authoritative SuperClawd instructions for one or more skills. Call this whenever an SuperClawd skill activates, BEFORE acting, passing the skill's skillId. If several skills activate at the same time, pass them together in `skills` to fetch them in one (cheaper) call.",{skillId:O.string().optional().describe("A single activated skill's skillId."),action:O.string().optional().describe("Action context for `skillId`, e.g. code/test/review."),skills:O.array(O.object({skillId:O.string().describe("The activated skill's skillId."),action:O.string().optional().describe("Action context, e.g. code/test/review.")})).optional().describe("Batch form: multiple activated skills to fetch in a single call.")},async({skillId:t,action:e,skills:r})=>{let n=r?.length?r:t?[{skillId:t,action:e}]:[];if(n.length===0)return{isError:!0,content:[{type:"text",text:'<superclawd_error status="400">Provide a skillId, or a non-empty skills array.</superclawd_error>'}]};let o=await Ve.getDirectives(n);return o.ok?{content:[{type:"text",text:o.text??""}]}:{isError:!0,content:[{type:"text",text:`<superclawd_error status="${o.status}">${o.message}</superclawd_error>`}]}});function qE(){try{let t=JSON.parse(process.env.SUPERCLAWD_RESOURCES??"[]");return Array.isArray(t)?t:[]}catch{return[]}}var Pl=qE();function UE(){return Pl.length===0?"":`
|
|
78
78
|
|
|
79
79
|
Available resources (fetch by name when relevant to the task):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superclawd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Claude Code, supercharged — your team's standards, every session. The superclawd CLI loads your workspace's skills, agents, commands, workflows, and live directives into Claude Code as a plugin.",
|
|
5
5
|
"author": "SuperClawd <support@superclawd.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|