@gitkraken/agent-tools 0.0.0-stage → 0.1.1
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 +20 -0
- package/LICENSE +19 -0
- package/README.md +139 -2
- package/dist/agents.d.ts +52 -0
- package/dist/agents.js +1 -0
- package/dist/config.d.ts +68 -0
- package/dist/config.js +1 -0
- package/dist/file-change.d.ts +28 -0
- package/dist/file-change.js +1 -0
- package/dist/hooks/claude.d.ts +16 -0
- package/dist/hooks/claude.js +3 -0
- package/dist/hooks/errors.d.ts +10 -0
- package/dist/hooks/errors.js +1 -0
- package/dist/hooks/events.d.ts +73 -0
- package/dist/hooks/events.js +1 -0
- package/dist/hooks/install.d.ts +15 -0
- package/dist/hooks/install.js +1 -0
- package/dist/hooks/results.d.ts +30 -0
- package/dist/hooks/results.js +1 -0
- package/dist/hooks/templates.d.ts +15 -0
- package/dist/hooks/templates.js +278 -0
- package/dist/hooks/writers.d.ts +18 -0
- package/dist/hooks/writers.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli-install-command.d.ts +27 -0
- package/dist/mcp/cli-install-command.js +1 -0
- package/dist/mcp/clients.d.ts +87 -0
- package/dist/mcp/clients.js +1 -0
- package/dist/mcp/config-entry.d.ts +13 -0
- package/dist/mcp/config-entry.js +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/installer.d.ts +69 -0
- package/dist/mcp/installer.js +3 -0
- package/dist/mcp/jsonc-edit.d.ts +41 -0
- package/dist/mcp/jsonc-edit.js +3 -0
- package/dist/mcp/toml-read.d.ts +11 -0
- package/dist/mcp/toml-read.js +1 -0
- package/dist/mcp/toml-safety.d.ts +7 -0
- package/dist/mcp/toml-safety.js +1 -0
- package/dist/mcp/toml-splice.d.ts +16 -0
- package/dist/mcp/toml-splice.js +11 -0
- package/dist/mcp/uninstaller.d.ts +41 -0
- package/dist/mcp/uninstaller.js +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +1 -0
- package/dist/skills/install.d.ts +126 -0
- package/dist/skills/install.js +1 -0
- package/dist/skills/manifest.d.ts +56 -0
- package/dist/skills/manifest.js +1 -0
- package/dist/skills/skill-dirs.d.ts +22 -0
- package/dist/skills/skill-dirs.js +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/memory-fs.d.ts +22 -0
- package/dist/testing/memory-fs.js +1 -0
- package/package.json +39 -3
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{applyEdits as e,findNodeAtLocation as t,getNodeValue as n,modify as r,parseTree as i,visit as a}from"jsonc-parser";const o={tabSize:2,insertSpaces:!0,eol:`
|
|
2
|
+
`},s={allowTrailingComma:!0};function c(e){return e.startsWith(``)?{bom:``,body:e.slice(1)}:{bom:``,body:e}}function l(e){let t=[];return i(c(e).body,t,s),t.length===0}function u(e,t){if(!l(e))throw Error(`invalid JSON in ${t}`)}function d(e){return e?e.split(`/`).slice(1).map(e=>e.replace(/~1/g,`/`).replace(/~0/g,`~`)):[]}function f(e,n){let r=[],i=e;for(let e of n){let n=i?.type===`array`&&/^(0|[1-9]\d*)$/.test(e)?Number(e):e;r.push(n),i=i?t(i,[n]):void 0}return r}function p(e,r){let a=[],o=i(c(e).body,a,s);if(!o||a.length>0)return;let l=t(o,f(o,d(r)));return l?n(l):void 0}function m(n,a,s,l){u(n,l);let{bom:p,body:m}=c(n),h=d(a),g=m;for(let n=1;n<h.length;n++){let a=i(g),s=a?f(a,h.slice(0,n)):h.slice(0,n);a&&t(a,s)||(g=e(g,r(g,s,{},{formattingOptions:o})))}let _=i(g),v=_?f(_,h):h;return p+e(g,r(g,v,s,{formattingOptions:o}))}function h(n,a){let c=i(n,void 0,s),l=c?f(c,a):a,u=c?t(c,l):void 0;if(!(a.length===0||!c||!u))return u.parent?.type===`array`&&typeof l.at(-1)==`number`?g(n,u.parent,u):v(e(n,r(n,l,void 0,{formattingOptions:o})))}function g(t,n,r){let i=n.children??[],a=i.indexOf(r),o=[{offset:r.offset,length:r.length,content:``}];if(a<i.length-1){let e=_(t,r.offset+r.length,i[a+1].offset);e!==void 0&&o.push({offset:e,length:1,content:``})}else if(a>0){let e=i[a-1],n=_(t,e.offset+e.length,r.offset);n!==void 0&&o.push({offset:n,length:1,content:``})}else{let e=_(t,r.offset+r.length,n.offset+n.length-1);e!==void 0&&o.push({offset:e,length:1,content:``})}return e(t,o.sort((e,t)=>e.offset-t.offset))}function _(e,t,n){for(let r=t;r<n;r++)if(e.startsWith(`//`,r))for(;r<n&&e[r]!==`
|
|
3
|
+
`;)r++;else if(e.startsWith(`/*`,r)){let t=e.indexOf(`*/`,r+2);if(t<0||t>=n)return;r=t+1}else if(e[r]===`,`)return r}function v(e){let t={justOpened:!1},n=()=>{t.justOpened=!1};a(e,{onObjectBegin:()=>{t.justOpened=!0},onSeparator:(e,r)=>{t.justOpened&&e===`,`&&(t.commaOffset??=r),n()},onObjectProperty:n,onObjectEnd:n},s);let{commaOffset:r}=t;return r===void 0?e:e.slice(0,r)+e.slice(r+1)}function y(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=h(i,d(t));return a===void 0?{text:e,removed:!1}:{text:r+a,removed:!0}}function b(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=d(t),o=h(i,a);if(o===void 0)return{text:e,removed:!1};let s=a.slice(0,-1);return s.length>0&&x(o,s)&&(o=h(o,s)??o),{text:r+o,removed:!0}}function x(e,n){let r=i(e,void 0,s);if(!r)return!1;let a=t(r,f(r,n));return!a||a.type!==`object`?!1:(a.children?.length??0)===0}export{m as addAtPointer,p as findAtPointer,l as isValidJsonc,y as removeAtPointer,b as removeAtPointerAndEmptyParent};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/mcp/toml-read.d.ts
|
|
2
|
+
/** Parses TOML `text`, or returns `undefined` on any parse error (mirrors `toml.Unmarshal`'s error check in gkcli). */
|
|
3
|
+
declare function parseTomlSafe(text: string): Record<string, unknown> | undefined;
|
|
4
|
+
/** Throws when `text` isn't valid TOML; `path` names its file in the message. */
|
|
5
|
+
declare function assertValidToml(text: string, path: string): void;
|
|
6
|
+
/** Reads Codex's legacy literal-key table (`['mcp_servers.GitKraken']`), which decodes to a single flat top-level key. */
|
|
7
|
+
declare function tomlLiteralKey(table: Record<string, unknown> | undefined, key: string): unknown;
|
|
8
|
+
/** Walks a dotted key (`a.b.c`) through nested TOML tables. */
|
|
9
|
+
declare function tomlDottedPath(table: Record<string, unknown> | undefined, dottedKey: string): unknown;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { assertValidToml, parseTomlSafe, tomlDottedPath, tomlLiteralKey };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{parse as e}from"smol-toml";function t(t){try{return e(t)}catch{return}}function n(e,n){if(!t(e))throw Error(`invalid TOML in ${n}`)}function r(e,t){return e?e[t]:void 0}function i(e,t){let n=e;for(let e of t.split(`.`))if(typeof n!=`object`||!n||Array.isArray(n)||(n=n[e],n===void 0))return;return n}export{n as assertValidToml,t as parseTomlSafe,i as tomlDottedPath,r as tomlLiteralKey};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/mcp/toml-safety.d.ts
|
|
2
|
+
/** Removes Codex's entry from a parsed document and gives an absent parent the same shape as an empty table. */
|
|
3
|
+
declare function withoutCodexEntry(table: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
4
|
+
/** Compares parsed TOML values without treating distinct Date values as equal empty objects. */
|
|
5
|
+
declare function tomlSemanticEqual(a: unknown, b: unknown): boolean;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { tomlSemanticEqual, withoutCodexEntry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t={...e??{}};delete t[`mcp_servers.GitKraken`];let r=t.mcp_servers;if(n(r)){let e={...r};delete e.GitKraken,t.mcp_servers=e}else r===void 0&&(t.mcp_servers={});return t}function t(e,n){if(e===n)return!0;if(typeof e!=typeof n||e===null||n===null||typeof e!=`object`)return!1;if(e instanceof Date||n instanceof Date)return e instanceof Date&&n instanceof Date&&e.valueOf()===n.valueOf();if(Array.isArray(e)||Array.isArray(n))return Array.isArray(e)&&Array.isArray(n)&&e.length===n.length&&e.every((e,r)=>t(e,n[r]));let r=e,i=n,a=Object.keys(r);return a.length===Object.keys(i).length&&a.every(e=>e in i&&t(r[e],i[e]))}function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&!(e instanceof Date)}export{t as tomlSemanticEqual,e as withoutCodexEntry};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/mcp/toml-splice.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Splices `block` over the existing `[tableName]` block (replacing only the
|
|
4
|
+
* leaf, preserving any `[tableName.*]` subtables) or appends it. `block` must
|
|
5
|
+
* start with its own header line.
|
|
6
|
+
*/
|
|
7
|
+
declare function replaceOrAppendBlock(original: string, tableName: string, block: string): string;
|
|
8
|
+
/** Removes every `[tableName]` block and any `[tableName.*]` subtables, so hand-added subtables don't become orphans. */
|
|
9
|
+
declare function removeBlock(original: string, tableName: string): {
|
|
10
|
+
text: string;
|
|
11
|
+
removed: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** Collapses trailing blank lines to a single newline, so removing an end-of-file block doesn't leave an extra blank line behind. */
|
|
14
|
+
declare function trimTrailingBlankLines(text: string): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { removeBlock, replaceOrAppendBlock, trimTrailingBlankLines };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{parse as e}from"smol-toml";function t(e,t,n){let{bom:c,body:l}=r(e),u=i(l),d=a(o(n),u)+u,f=s(l,t,!1);if(f.length===0)return w(c,l,d,u);let p=c,m=0;return f.forEach((e,t)=>{p+=l.slice(m,e.start),t===0&&(p+=d),m=e.end}),p+=l.slice(m),p}function n(e,t){let{bom:n,body:i}=r(e),a=s(i,t,!0);if(a.length===0)return{text:e,removed:!1};let o=``,c=0;for(let e of a)o+=i.slice(c,e.start),c=e.end;return o+=i.slice(c),o.length===0?{text:``,removed:!0}:{text:n+o,removed:!0}}function r(e){return e.startsWith(``)?{bom:``,body:e.slice(1)}:{bom:``,body:e}}function i(e){return e.includes(`\r
|
|
2
|
+
`)?`\r
|
|
3
|
+
`:`
|
|
4
|
+
`}function a(e,t){let n=e.replace(/\r\n/g,`
|
|
5
|
+
`);return t===`\r
|
|
6
|
+
`?n.replace(/\n/g,`\r
|
|
7
|
+
`):n}function o(e){return e.replace(/[\r\n]+$/,``)}function s(e,t,n){let r=y(e);if(r.length===0)return[];let i=d(t),{headerKey:a,isHeader:o,lineStartsInString:s}=l(r),u=[],f=-1,_=e=>{if(f<0)return;let t=e;for(;t>f+1&&b(r[t-1]);)t--;u.push({start:c(r,f),end:c(r,t)}),f=-1};for(let e=0;e<r.length;e++){if(s[e]||!o[e])continue;let r=a[e],c=p(r.segments,i)||m(r.segments,t),l=n&&(h(r.segments,i)||g(r.segments,t));if(f>=0){if(c||l){!n&&c&&(_(e),f=e);continue}_(e)}(c||l)&&(f=e)}return _(r.length),u}function c(e,t){let n=0;for(let r=0;r<t&&r<e.length;r++)n+=e[r].length;return n}function l(e){let t=Array(e.length).fill({segments:[]}),n=Array(e.length).fill(!1),r=Array(e.length).fill(!1),i=new x;for(let a=0;a<e.length;a++){if(r[a]=i.inString(),!r[a]){let r=u(e[a]);r!==void 0&&(t[a]=r,n[a]=!0)}i.advance(v(e[a]))}return{headerKey:t,isHeader:n,lineStartsInString:r}}function u(t){let n=v(t).trim();if(!n.startsWith(`[`))return;let r=_(n);if(!(r<0)&&!(n.startsWith(`[[`)&&!n.slice(r+1).startsWith(`]`)))try{return{segments:f(e(`${n}\n`))}}catch{return}}function d(t){try{return f(e(`[${t}]\n`))}catch{return t.split(`.`)}}function f(e){let t=[],n=e;for(;n&&typeof n==`object`&&!Array.isArray(n);){let e=Object.entries(n);if(e.length!==1)break;let[r,i]=e[0];if(t.push(r),n=Array.isArray(i)?i[i.length-1]:i,n&&typeof n==`object`&&!Array.isArray(n)&&Object.keys(n).length===0)break}return t}function p(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function m(e,t){return e.length===1&&e[0]===t}function h(e,t){return e.length>t.length&&t.every((t,n)=>e[n]===t)}function g(e,t){return e.length>1&&e[0]===t}function _(e){let t=!1,n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(t)i===`'`&&(t=!1);else if(n)i===`\\`?r++:i===`"`&&(n=!1);else if(i===`'`)t=!0;else if(i===`"`)n=!0;else if(i===`]`)return r}return-1}function v(e){return e.replace(/\r?\n$/,``)}function y(e){if(e.length===0)return[];let t=[],n=0;for(let r=0;r<e.length;r++)e[r]===`
|
|
8
|
+
`&&(t.push(e.slice(n,r+1)),n=r+1);return n<e.length&&t.push(e.slice(n)),t}function b(e){let t=v(e).trim();return t===``||t.startsWith(`#`)}var x=class{inMultiBasic=!1;inMultiLiteral=!1;inString(){return this.inMultiBasic||this.inMultiLiteral}advance(e){let t=0;for(;t<e.length;)if(this.inMultiBasic){if(e[t]===`\\`&&t+1<e.length){t+=2;continue}if(e.startsWith(`"""`,t)){this.inMultiBasic=!1,t+=3;continue}t++}else if(this.inMultiLiteral){if(e.startsWith(`'''`,t)){this.inMultiLiteral=!1,t+=3;continue}t++}else if(e[t]===`#`)return;else e.startsWith(`"""`,t)?(this.inMultiBasic=!0,t+=3):e.startsWith(`'''`,t)?(this.inMultiLiteral=!0,t+=3):e[t]===`"`?t=S(e,t+1):e[t]===`'`?t=C(e,t+1):t++}};function S(e,t){let n=t;for(;n<e.length;){if(e[n]===`\\`&&n+1<e.length){n+=2;continue}if(e[n]===`"`)return n+1;n++}return n}function C(e,t){let n=t;for(;n<e.length;){if(e[n]===`'`)return n+1;n++}return n}function w(e,t,n,r){let i=e+t;return t.length===0?i+n:(t.endsWith(`
|
|
9
|
+
`)||(i+=r),T(t)||(i+=r),i+n)}function T(e){if(!e.endsWith(`
|
|
10
|
+
`))return!1;let t=e.slice(0,-1);return t.endsWith(`\r`)&&(t=t.slice(0,-1)),t.endsWith(`
|
|
11
|
+
`)}function E(e){let t=e.replace(/[ \t\r\n]+$/,``);return t===``?``:`${t}\n`}export{n as removeBlock,t as replaceOrAppendBlock,E as trimTrailingBlankLines};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { Client } from "./clients.js";
|
|
3
|
+
|
|
4
|
+
//#region src/mcp/uninstaller.d.ts
|
|
5
|
+
interface McpUninstallRequest {
|
|
6
|
+
client: Client;
|
|
7
|
+
/**
|
|
8
|
+
* Clears this path instead of every path {@link clientConfigPaths} resolves
|
|
9
|
+
* (Go's `--file-path`). When set, only this one file is touched — e.g. the
|
|
10
|
+
* antigravity legacy path is not additionally swept.
|
|
11
|
+
*/
|
|
12
|
+
configPath?: string;
|
|
13
|
+
}
|
|
14
|
+
interface McpUninstallFileResult {
|
|
15
|
+
path: string;
|
|
16
|
+
/** MCP uninstall edits a config file but never deletes it. */
|
|
17
|
+
change: 'updated' | 'unchanged';
|
|
18
|
+
/** The pointers or table keys removed from this file. */
|
|
19
|
+
removedLocations: string[];
|
|
20
|
+
/** Why nothing was removed; set when `change` is `unchanged`. */
|
|
21
|
+
reason?: 'file-missing' | 'file-empty' | 'no-entry';
|
|
22
|
+
}
|
|
23
|
+
interface McpUninstallResult {
|
|
24
|
+
client: Client;
|
|
25
|
+
/** Whether a GitKraken entry was actually removed from at least one config path. */
|
|
26
|
+
removed: boolean;
|
|
27
|
+
/** One entry per path swept, in sweep order. */
|
|
28
|
+
files: McpUninstallFileResult[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clears the client's GitKraken MCP entry from every config path it may live
|
|
32
|
+
* in (mirrors gkcli's uninstall sweep across `mcp.Client.Paths()`), including
|
|
33
|
+
* OpenCode's v1/v2 pointer pair and Codex's comment-preserving TOML splice.
|
|
34
|
+
* A missing or empty config is a no-op, matching Go's "already uninstalled".
|
|
35
|
+
*
|
|
36
|
+
* Throws, before writing anything, for an unknown client or a config file that
|
|
37
|
+
* does not parse, so a sweep never leaves some files edited and others not.
|
|
38
|
+
*/
|
|
39
|
+
declare function uninstallMcp(request: McpUninstallRequest, config: AgentHostConfig): Promise<McpUninstallResult>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{removeAtPointer as e,removeAtPointerAndEmptyParent as t}from"./jsonc-edit.js";import{assertValidToml as n,parseTomlSafe as r,tomlDottedPath as i,tomlLiteralKey as a}from"./toml-read.js";import{OPEN_CODE_JSON_PATHS as o,clientConfigPaths as s,clientJSONPath as c,isSupported as l}from"./clients.js";import{writeFileTracked as u}from"../file-change.js";import{tomlSemanticEqual as d,withoutCodexEntry as f}from"./toml-safety.js";import{removeBlock as p,trimTrailingBlankLines as m}from"./toml-splice.js";function h(e,t){return{file:{path:e,change:`unchanged`,removedLocations:[],reason:t}}}async function g(e,t){let{client:n}=e;if(!l(n))throw Error(`unsupported client: ${n}`);let r=e.configPath?[e.configPath]:await s(n,t),i=[];for(let e of r)i.push(await _(n,e,t));let a=[];for(let{file:e,text:n}of i)n!==void 0&&await u(t.fs,e.path,n,[]),a.push(e);return{client:n,removed:a.some(e=>e.removedLocations.length>0),files:a}}async function _(e,t,n){let r=await n.fs.readFile(t);if(r===null)return h(t,`file-missing`);if(r.trim()===``)return h(t,`file-empty`);let{text:i,removedLocations:a}=e===`codex`?y(r,t):v(r,e,t);return a.length===0?h(t,`no-entry`):{file:{path:t,change:`updated`,removedLocations:a},text:i}}function v(n,r,i){let a=r===`opencode`,s=a?o:[c(r)],l=n,u=[];for(let n of s){let r=a?t(l,n,i):e(l,n,i);r.removed&&(l=r.text,u.push(n))}return{text:l,removedLocations:u}}function y(e,t){n(e,t);let o=c(`codex`),s=r(e);if(a(s,o)===void 0&&i(s,o)===void 0)return{text:e,removedLocations:[]};let{text:l,removed:u}=p(e,o);if(!u){if(a(s,o)!==void 0||i(s,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: unsupported TOML representation`);return{text:e,removedLocations:[]}}let h=m(l),g=r(h);if(a(g,o)!==void 0||i(g,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: target remains after splice`);if(!g||!d(f(g),f(s)))throw Error(`cannot safely remove Codex MCP entry in ${t}: splice would change another TOML value`);return{text:h,removedLocations:[o]}}export{g as uninstallMcp};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest } from "./manifest.js";
|
|
2
|
+
import { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir } from "./skill-dirs.js";
|
|
3
|
+
import { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill } from "./install.js";
|
|
4
|
+
export { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, type SkillAgent, SkillConflictError, type SkillFiles, type SkillInstallAction, type SkillInstallOptions, type SkillInstallResult, type SkillManifest, type SkillOperation, type SkillReason, type SkillState, type SkillStatus, type SkillUninstallAction, type SkillUninstallOptions, type SkillUninstallResult, SkillUnsupportedManifestError, allSkillAgents, inspectSkillStatus, installSkill, isSkillAgentSupported, skillDir, uninstallSkill };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SKILL_MANIFEST_FILE_NAME as e,SKILL_MANIFEST_SCHEMA_VERSION as t}from"./manifest.js";import{allSkillAgents as n,isSkillAgentSupported as r,skillDir as i}from"./skill-dirs.js";import{SkillConflictError as a,SkillUnsupportedManifestError as o,inspectSkillStatus as s,installSkill as c,uninstallSkill as l}from"./install.js";export{e as SKILL_MANIFEST_FILE_NAME,t as SKILL_MANIFEST_SCHEMA_VERSION,a as SkillConflictError,o as SkillUnsupportedManifestError,n as allSkillAgents,s as inspectSkillStatus,c as installSkill,r as isSkillAgentSupported,i as skillDir,l as uninstallSkill};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { FileChange } from "../file-change.js";
|
|
3
|
+
import { SkillManifest } from "./manifest.js";
|
|
4
|
+
import { SkillAgent } from "./skill-dirs.js";
|
|
5
|
+
|
|
6
|
+
//#region src/skills/install.d.ts
|
|
7
|
+
interface SkillFiles {
|
|
8
|
+
agent: SkillAgent;
|
|
9
|
+
/** A nonempty portable directory name (one path segment, not `.`/`..`). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Safe relative slash paths (e.g. `'SKILL.md'`) -> file content. Must be nonempty and exclude the reserved manifest path. */
|
|
12
|
+
files: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* How the skill's on-disk state compares to the bundle passed in. Mirrors
|
|
16
|
+
* gkcli's `SkillStatus`/`InstallState`/reason constants
|
|
17
|
+
* (`internal/actions/aiskill/skill.go`), minus the fields this library has no
|
|
18
|
+
* way to observe: `removalBlocked`'s `symlink` half (no `FileSystemPort`
|
|
19
|
+
* primitive reports a symlink) and project-scope provenance (`AgentHostConfig`
|
|
20
|
+
* has no git-worktree concept, so every install/uninstall is Go's "global"
|
|
21
|
+
* scope).
|
|
22
|
+
*/
|
|
23
|
+
type SkillState = 'missing' | 'current' | 'older-release' | 'modified';
|
|
24
|
+
type SkillReason = 'absent' | 'unowned' | 'invalid-manifest' | 'unsupported-manifest' | 'content-changed' | 'different-bundle' | 'stale-stamp';
|
|
25
|
+
interface SkillStatus {
|
|
26
|
+
state: SkillState;
|
|
27
|
+
/** The skill's directory, `<skillDir>/<name>`. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Why the skill is not plainly current or missing. Absent when it is current and clean. */
|
|
30
|
+
reason?: SkillReason;
|
|
31
|
+
/** Whether `force` could repair this status — always `false` for `'unsupported-manifest'`. */
|
|
32
|
+
requiresForce: boolean;
|
|
33
|
+
/** Whether a valid, matching manifest vouches for this skill (mirrors `SkillStatus.Owned`). */
|
|
34
|
+
owned: boolean;
|
|
35
|
+
/** Whether the on-disk files are byte-identical to the bundle passed in. */
|
|
36
|
+
matchesBundle: boolean;
|
|
37
|
+
manifestVersion?: number;
|
|
38
|
+
installedAt?: string;
|
|
39
|
+
updatedAt?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Inspects the on-disk state against a bundle without writing any files. */
|
|
42
|
+
declare function inspectSkillStatus(request: SkillFiles, config: AgentHostConfig): Promise<SkillStatus>;
|
|
43
|
+
type SkillInstallAction = 'create' | 'replace' | 'unchanged';
|
|
44
|
+
type SkillUninstallAction = 'remove' | 'absent';
|
|
45
|
+
type SkillOperation = 'install' | 'uninstall';
|
|
46
|
+
/** A skill's on-disk content conflicts with the bundle, and `force` was not set (or cannot repair it). */
|
|
47
|
+
declare class SkillConflictError extends Error {
|
|
48
|
+
readonly code: 'conflict' | 'invalid-manifest' | 'modified';
|
|
49
|
+
readonly operation: SkillOperation;
|
|
50
|
+
/** The skill's directory. */
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly status: SkillStatus;
|
|
53
|
+
constructor(status: SkillStatus, operation: SkillOperation);
|
|
54
|
+
private static codeFor;
|
|
55
|
+
private static buildMessage;
|
|
56
|
+
}
|
|
57
|
+
/** A skill's manifest was written by a newer release. Neither install nor uninstall may discard state this version cannot interpret, even with `force`. */
|
|
58
|
+
declare class SkillUnsupportedManifestError extends Error {
|
|
59
|
+
readonly code = "unsupported-manifest";
|
|
60
|
+
readonly operation: SkillOperation;
|
|
61
|
+
/** The skill's directory. */
|
|
62
|
+
readonly path: string;
|
|
63
|
+
readonly status: SkillStatus;
|
|
64
|
+
constructor(status: SkillStatus, operation: SkillOperation);
|
|
65
|
+
}
|
|
66
|
+
interface SkillInstallOptions {
|
|
67
|
+
/** Overwrite content this library cannot otherwise prove it owns. Never lifts a `SkillUnsupportedManifestError`. */
|
|
68
|
+
force?: boolean;
|
|
69
|
+
/** Recorded as the manifest's `gkVersion`. Defaults to `'dev'`, the value gkcli dev builds record. */
|
|
70
|
+
version?: string;
|
|
71
|
+
/** Injectable for tests; defaults to `new Date()`. */
|
|
72
|
+
now?: Date;
|
|
73
|
+
}
|
|
74
|
+
interface SkillInstallResult {
|
|
75
|
+
action: SkillInstallAction;
|
|
76
|
+
/** The skill's status before the action. */
|
|
77
|
+
status: SkillStatus;
|
|
78
|
+
/** The skill's directory. */
|
|
79
|
+
path: string;
|
|
80
|
+
/** What the install did to each content file and the manifest. Empty when `action` is `'unchanged'` and nothing needed repair. */
|
|
81
|
+
files: FileChange[];
|
|
82
|
+
/** The manifest now on disk. Absent when nothing was written. */
|
|
83
|
+
manifest?: SkillManifest;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Writes a bundled skill's files under `<skillDir>/<name>/` for `agent`, and
|
|
87
|
+
* the `.gk-skill.json` ownership manifest beside them. Mirrors gkcli's
|
|
88
|
+
* `aiskill.Install`: a fresh install (`'create'`), a no-op when the bundle is
|
|
89
|
+
* already current (`'unchanged'`, still repairing a missing or stale manifest
|
|
90
|
+
* when applicable), an automatic upgrade over an owned older release
|
|
91
|
+
* (`'replace'`), and — with `force` — a repair over content this library does not
|
|
92
|
+
* otherwise recognize. Without `force`, content that conflicts with the
|
|
93
|
+
* bundle throws {@link SkillConflictError}; a manifest from a newer release
|
|
94
|
+
* always throws {@link SkillUnsupportedManifestError}. `name` must be one
|
|
95
|
+
* portable directory segment; `files` must be a nonempty map of safe relative
|
|
96
|
+
* slash paths without traversal, collisions, or the reserved manifest path.
|
|
97
|
+
* The result lists each file created, updated or left unchanged.
|
|
98
|
+
*/
|
|
99
|
+
declare function installSkill(request: SkillFiles, config: AgentHostConfig, options?: SkillInstallOptions): Promise<SkillInstallResult>;
|
|
100
|
+
interface SkillUninstallOptions {
|
|
101
|
+
/** Remove content this library cannot otherwise prove it owns. Never lifts a `SkillUnsupportedManifestError`. */
|
|
102
|
+
force?: boolean;
|
|
103
|
+
}
|
|
104
|
+
interface SkillUninstallResult {
|
|
105
|
+
action: SkillUninstallAction;
|
|
106
|
+
/** The skill's status before the action. */
|
|
107
|
+
status: SkillStatus;
|
|
108
|
+
/** The skill's directory. */
|
|
109
|
+
path: string;
|
|
110
|
+
/** The content files removed, the manifest when it was owned and removable, and the directory once it was left empty. Empty when `action` is `'absent'`. */
|
|
111
|
+
files: FileChange[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Removes a bundled skill's files for `agent`, mirroring gkcli's
|
|
115
|
+
* `aiskill.Uninstall`. A skill whose content is byte-identical to the bundle
|
|
116
|
+
* passed in is always removed; an owned older release is removed without
|
|
117
|
+
* `force`; other content requires `force`. The manifest is removed alongside
|
|
118
|
+
* the content only when it is owned (a valid, matching manifest) and either
|
|
119
|
+
* `force` was set or the content wasn't hand-edited after install (mirrors
|
|
120
|
+
* `removeSkillFile`'s `removeManifest` rule: a foreign or mismatched-identity
|
|
121
|
+
* manifest is left in place even though the content beside it is removable).
|
|
122
|
+
* The skill's directory is removed too, but only once it is left empty.
|
|
123
|
+
*/
|
|
124
|
+
declare function uninstallSkill(request: SkillFiles, config: AgentHostConfig, options?: SkillUninstallOptions): Promise<SkillUninstallResult>;
|
|
125
|
+
//#endregion
|
|
126
|
+
export { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{SKILL_MANIFEST_FILE_NAME as r,buildSkillManifest as i,digestFiles as a,readSkillManifest as o,serializeSkillManifest as s}from"./manifest.js";import{skillDir as c}from"./skill-dirs.js";function l(e){let t=e.name;if(typeof t!=`string`||t.length===0||t===`.`||t===`..`||/[\\/:<>"|?*]/.test(t)||u(t)||/[ .]$/.test(t)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(t))throw TypeError(`skill name must be a nonempty portable directory name`);if(typeof e.files!=`object`||e.files===null||Array.isArray(e.files))throw TypeError(`skill files must be a nonempty file map`);let n=Object.keys(e.files);if(n.length===0)throw TypeError(`skill files must be a nonempty file map`);let i=new Set;for(let t of n){if(typeof e.files[t]!=`string`||t.length===0||t.startsWith(`/`)||t.includes(`\\`)||t.includes(`:`)||t.split(`/`).some(e=>e===``||e===`.`||e===`..`||/[<>"|?*]/.test(e)||u(e)||/[ .]$/.test(e)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(e)))throw TypeError(`skill file path must be a safe relative slash path: ${t}`);let n=t.toLowerCase(),a=r.toLowerCase();if(n===a||n.startsWith(`${a}/`))throw TypeError(`skill files cannot include ${r}`);if(i.has(n))throw TypeError(`skill files contain a case-insensitive path collision: ${t}`);i.add(n)}for(let e of i){let t=e.split(`/`);for(let n=1;n<t.length;n++)if(i.has(t.slice(0,n).join(`/`)))throw TypeError(`skill files contain a file and parent path collision: ${e}`)}}function u(e){return[...e].some(e=>e.charCodeAt(0)<32||e.charCodeAt(0)===127)}function d(e){return e!==`unsupported-manifest`}function f(e){return e===`unowned`||e===`stale-stamp`}async function p(t,n,r){let i=Object.create(null);for(let a of n){let n=await r.readFile(e(t,a));n!==null&&(i[a]=n)}return i}async function m(t,n){let r=Object.create(null),i=[],a=async(t,o)=>{let s;try{s=await n.readFile(t)}catch(e){if(e?.code!==`EISDIR`)throw e;s=null}if(s!==null){o!==`.gk-skill.json`&&(r[o]=s);return}let c;try{c=await n.readDir(t)}catch(e){if(e?.code===`ENOENT`&&o===``)return;throw e}if(c!==null){i.push(t);for(let n of c)await a(e(t,n),o?`${o}/${n}`:n)}};return await a(t,``),{files:r,directories:i}}function h(t,n){let r=new Set([t]);for(let i of n){let n=i.split(`/`);for(let i=1;i<n.length;i++)r.add(e(t,n.slice(0,i).join(`/`)))}return[...r]}function g(e,t){let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>e[n]===t[n]):!1}async function _(t,n){let i=e(c(t.agent,n),t.name),s=e(i,r),l=Object.keys(t.files),{manifest:u,reason:f}=await o(s,t.name,t.agent,`global`,n.fs.readFile.bind(n.fs)),_,v=async()=>(_??=await m(i,n.fs),_),y=(e,t=[])=>({status:e,ownedPaths:t,directories:_?.directories??h(i,t)});if(f===`unsupported-manifest`)return y({state:`modified`,path:i,reason:`unsupported-manifest`,requiresForce:!1,owned:!1,matchesBundle:!1});let b=await p(i,l,n.fs),x=Object.keys(b),S=x.length>0,C=x.length===l.length,w=C&&g(b,t.files);if(!S){if(u===void 0){if(f!==`unowned`){let e=f??`absent`;return y({state:`missing`,path:i,reason:e,requiresForce:d(e),owned:!1,matchesBundle:!1})}return y({state:`missing`,path:i,reason:`absent`,requiresForce:!1,owned:!1,matchesBundle:!1})}let e=await v(),t=Object.keys(e.files);return(t.length>0?await a(e.files):void 0)===u.digest?y({state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,owned:!0,matchesBundle:!1,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt},t):t.length>0?y({state:`modified`,path:i,reason:`content-changed`,requiresForce:!0,owned:!0,matchesBundle:!1,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt}):y({state:`missing`,path:i,reason:`absent`,requiresForce:!1,owned:!1,matchesBundle:!1})}if(u===void 0){let e=f??`invalid-manifest`;return e===`unowned`&&w?y({state:`current`,path:i,reason:`unowned`,requiresForce:!1,owned:!1,matchesBundle:w},l):y({state:`modified`,path:i,reason:e,requiresForce:d(e),owned:!1,matchesBundle:w},x)}let T={owned:!0,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt};if((C?await a(b):void 0)===u.digest)return y(w?{state:`current`,path:i,requiresForce:!1,matchesBundle:w,...T}:{state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,matchesBundle:w,...T},l);let E=await v(),D=Object.keys(E.files);return(D.length>0?await a(E.files):void 0)===u.digest?y({state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,matchesBundle:w,...T},D):w&&g(E.files,t.files)?y({state:`current`,path:i,reason:`stale-stamp`,requiresForce:!1,matchesBundle:w,...T},l):y({state:`modified`,path:i,reason:`content-changed`,requiresForce:d(`content-changed`),matchesBundle:w,...T},x)}async function v(e,t,n,r){for(let i of[...t].sort((e,t)=>t.length-e.length))i!==r&&(await e.readDir(i))?.length===0&&(await e.remove(i),n.push({path:i,change:`removed`}))}async function y(e,t){return l(e),(await _(e,t)).status}function b(e,t){switch(e.state){case`missing`:switch(e.reason){case void 0:case`absent`:case`unowned`:return`create`;default:return t&&d(e.reason)?`replace`:`conflict`}case`current`:return t?`replace`:`unchanged`;case`older-release`:return`replace`;default:return t&&d(e.reason)?`replace`:`conflict`}}function x(e,t){return e.state===`missing`?`absent`:e.reason===`unsupported-manifest`?`unsupported-manifest`:e.matchesBundle||t||e.state===`older-release`?`remove`:`modified`}var S=class e extends Error{code;operation;path;status;constructor(t,n){let r=e.codeFor(t,n);super(e.buildMessage(r,t,n)),this.name=`SkillConflictError`,this.code=r,this.operation=n,this.path=t.path,this.status=t}static codeFor(e,t){return t===`uninstall`?`modified`:e.reason===`invalid-manifest`?`invalid-manifest`:`conflict`}static buildMessage(e,t,n){let r=n===`install`?`replace`:`remove`;return e===`invalid-manifest`?`skill at ${t.path} has invalid ownership metadata; pass \`force\` to ${r} it`:`skill at ${t.path} differs from the bundle (${t.reason}); pass \`force\` to ${r} it`}},C=class extends Error{code=`unsupported-manifest`;operation;path;status;constructor(e,t){super(`skill metadata at ${e.path} was written by a newer release and can't be interpreted; nothing was changed`),this.name=`SkillUnsupportedManifestError`,this.operation=t,this.path=e.path,this.status=e}};async function w(a,o,c={}){l(a);let u=await _(a,o),{status:d}=u,p=b(d,c.force??!1);if(p===`conflict`)throw d.reason===`unsupported-manifest`?new C(d,`install`):new S(d,`install`);let m=d.path,h=e(m,r),g=p===`unchanged`&&f(d.reason);if(p===`unchanged`&&!g)return{action:p,status:d,path:m,files:[]};let y=[];if(!g){for(let[t,r]of Object.entries(a.files))await n(o.fs,e(m,t),r,y,{mode:420});if(d.state===`older-release`){let n=new Set(Object.keys(a.files));for(let r of u.ownedPaths)n.has(r)||await t(o.fs,e(m,r),y);await v(o.fs,u.directories,y,m)}}let x=await i(a.name,a.agent,a.files,d.installedAt,c.version,c.now??new Date);return await n(o.fs,h,s(x),y,{mode:420}),{action:p,status:d,path:m,files:y,manifest:x}}async function T(n,i,a={}){l(n);let o=await _(n,i),{status:s}=o,c=a.force??!1,u=x(s,c);if(u===`unsupported-manifest`)throw new C(s,`uninstall`);if(u===`modified`)throw new S(s,`uninstall`);if(u===`absent`)return{action:u,status:s,path:s.path,files:[]};let d=s.path,f=[];for(let n of o.ownedPaths)await t(i.fs,e(d,n),f);return s.owned&&(c||s.reason!==`content-changed`)&&await t(i.fs,e(d,r),f),await v(i.fs,o.directories,f,void 0),{action:u,status:s,path:d,files:f}}export{S as SkillConflictError,C as SkillUnsupportedManifestError,y as inspectSkillStatus,w as installSkill,T as uninstallSkill};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/skills/manifest.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The ownership manifest gkcli's `aiskill` package writes beside a skill's
|
|
4
|
+
* content (`.gk-skill.json`), and the read/validate/digest logic that backs
|
|
5
|
+
* `installSkill`/`uninstallSkill`'s state machine. Direct port of
|
|
6
|
+
* `internal/actions/aiskill/manifest.go`.
|
|
7
|
+
*
|
|
8
|
+
* This is deliberately local bookkeeping, not a publisher signature: it lets
|
|
9
|
+
* `installSkill`/`uninstallSkill` tell "gk installed this, byte-identical to
|
|
10
|
+
* what it wrote" apart from "someone else's file happens to be here."
|
|
11
|
+
*/
|
|
12
|
+
declare const SKILL_MANIFEST_FILE_NAME = ".gk-skill.json";
|
|
13
|
+
declare const SKILL_MANIFEST_SCHEMA_VERSION = 1;
|
|
14
|
+
/** Persisted shape of `.gk-skill.json`. See `## Persisted data` in `ARCHITECTURE.md`. */
|
|
15
|
+
interface SkillManifest {
|
|
16
|
+
schemaVersion: number;
|
|
17
|
+
owner: string;
|
|
18
|
+
skill: string;
|
|
19
|
+
client: string;
|
|
20
|
+
scope: string;
|
|
21
|
+
/** `sha256:<64 lowercase hex chars>`. */
|
|
22
|
+
digest: string;
|
|
23
|
+
gkVersion?: string;
|
|
24
|
+
/** ISO 8601 / RFC 3339. */
|
|
25
|
+
installedAt?: string;
|
|
26
|
+
/** ISO 8601 / RFC 3339. */
|
|
27
|
+
updatedAt?: string;
|
|
28
|
+
}
|
|
29
|
+
/** `sha256:<hex>` of a single string, compatible with gkcli's manifest digest so either tool can verify ownership. */
|
|
30
|
+
declare function digestContent(content: string): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* `sha256:<hex>` over every file in a skill (sorted by path), for a skill
|
|
33
|
+
* bundle with more than one file. gkcli's bundled skills are always exactly
|
|
34
|
+
* one file (`SKILL.md`), where this reduces to {@link digestContent} of that
|
|
35
|
+
* file's bytes; multi-file skills have no Go equivalent to stay byte-compatible
|
|
36
|
+
* with.
|
|
37
|
+
*/
|
|
38
|
+
declare function digestFiles(files: Readonly<Record<string, string>>): Promise<string>;
|
|
39
|
+
/** Why a manifest read didn't return a valid, matching manifest — see `SkillReason` in `install.ts`. */
|
|
40
|
+
type ManifestReadReason = 'unowned' | 'invalid-manifest' | 'unsupported-manifest';
|
|
41
|
+
interface ManifestReadResult {
|
|
42
|
+
manifest?: SkillManifest;
|
|
43
|
+
/** Set only when `manifest` is `undefined`. */
|
|
44
|
+
reason?: ManifestReadReason;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads and validates the manifest beside `target`. The host filesystem port
|
|
48
|
+
* does not expose symlink identity, so symlink handling is the host's
|
|
49
|
+
* responsibility (see `ARCHITECTURE.md`).
|
|
50
|
+
*/
|
|
51
|
+
declare function readSkillManifest(manifestPath: string, name: string, client: string, scope: string, readFile: (path: string) => Promise<string | null>): Promise<ManifestReadResult>;
|
|
52
|
+
/** Builds the manifest `installSkill` writes. Mirrors `marshalManifest`; `scope` is always `'global'` (see `readSkillManifest`'s doc comment). */
|
|
53
|
+
declare function buildSkillManifest(name: string, client: string, files: Readonly<Record<string, string>>, previousInstalledAt: string | undefined, version: string | undefined, now: Date): Promise<SkillManifest>;
|
|
54
|
+
declare function serializeSkillManifest(manifest: SkillManifest): string;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ManifestReadReason, ManifestReadResult, SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest, buildSkillManifest, digestContent, digestFiles, readSkillManifest, serializeSkillManifest };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=`.gk-skill.json`,t=1,n=`gitkraken`;async function r(e){let t=new TextEncoder().encode(e),n=await globalThis.crypto.subtle.digest(`SHA-256`,t);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function i(e){return`sha256:${await r(e)}`}async function a(e){let t=Object.keys(e).sort();return t.length===1?i(e[t[0]]):i(t.map(t=>`${t}\u0000${e[t]}`).join(`\0`))}const o=/^sha256:[0-9a-f]{64}$/;function s(e){return typeof e==`string`&&o.test(e)}function c(e){if(typeof e!=`string`)return!1;let t=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(e);if(!t)return!1;let[,n,r,i,a,o,s,c]=t,l=Number(n),u=Number(r),d=Number(i),f=Number(a),p=Number(o),m=Number(s),h=[31,l%4==0&&(l%100!=0||l%400==0)?29:28,31,30,31,30,31,31,30,31,30,31][u-1];if(u<1||u>12||d<1||d>h||f>23||p>59||m>59)return!1;if(c!==`Z`){let e=/[+-](\d{2}):(\d{2})$/.exec(c);if(!e||Number(e[1])>23||Number(e[2])>59)return!1}return!0}function l(e,t,r,i){return e.schemaVersion!==1||e.owner!==n||e.skill!==t||e.client!==r||e.scope!==i||!s(e.digest)?!1:typeof e.installedAt==`string`&&c(e.installedAt)&&typeof e.updatedAt==`string`&&c(e.updatedAt)&&(e.gkVersion===void 0||typeof e.gkVersion==`string`)}async function u(e,t,n,r,i){let a=await i(e);if(a===null)return{reason:`unowned`};let o;try{o=JSON.parse(a)}catch{return{reason:`invalid-manifest`}}if(typeof o!=`object`||!o)return{reason:`invalid-manifest`};let s=o;return s.schemaVersion===1?l(s,t,n,r)?{manifest:s}:{reason:`invalid-manifest`}:{reason:`unsupported-manifest`}}function d(e){return e.toISOString().replace(/\.\d{3}Z$/,`Z`)}async function f(e,t,r,i,o,s){let c=d(s);return{schemaVersion:1,owner:n,skill:e,client:t,scope:`global`,digest:await a(r),gkVersion:o||`dev`,installedAt:i||c,updatedAt:c}}function p(e){return`${JSON.stringify(e)}\n`}export{e as SKILL_MANIFEST_FILE_NAME,t as SKILL_MANIFEST_SCHEMA_VERSION,f as buildSkillManifest,i as digestContent,a as digestFiles,u as readSkillManifest,p as serializeSkillManifest};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/skills/skill-dirs.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Hook-registry client ids gkcli can install bundled skills for (a subset of
|
|
6
|
+
* the full aihook registry — every client that has a skill install
|
|
7
|
+
* directory). Ids match gkcli's `aihook.Client` values, which differ from
|
|
8
|
+
* the MCP registry's (`'claude-code'` here vs. `'claude-cli'` in
|
|
9
|
+
* `src/mcp`); the two registries are unified only by gkcli's own
|
|
10
|
+
* `internal/agents` package, which is out of this lane's P6.1/P6.2 scope.
|
|
11
|
+
*/
|
|
12
|
+
type SkillAgent = 'claude-code' | 'opencode' | 'cursor' | 'codex' | 'copilot' | 'antigravity' | 'pi' | 'augment' | 'grok';
|
|
13
|
+
declare function allSkillAgents(): SkillAgent[];
|
|
14
|
+
declare function isSkillAgentSupported(agent: string): agent is SkillAgent;
|
|
15
|
+
/**
|
|
16
|
+
* The global skill install directory for `agent` (bundled skills are written
|
|
17
|
+
* one subdirectory per skill name beneath it). Direct port of gkcli's
|
|
18
|
+
* `aiskill.skillDirs`.
|
|
19
|
+
*/
|
|
20
|
+
declare function skillDir(agent: SkillAgent, config: AgentHostConfig): string;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{joinPath as e}from"../config.js";const t=[`claude-code`,`opencode`,`cursor`,`codex`,`copilot`,`antigravity`,`pi`,`augment`,`grok`];function n(){return[...t]}function r(e){return t.includes(e)}function i(t){return t.paths.piAgentDir||e(t.paths.home,`.pi`,`agent`)}function a(t){return t.paths.grokHome||e(t.paths.home,`.grok`)}function o(t,n){switch(t){case`claude-code`:return n.paths.claudeConfigDir?e(n.paths.claudeConfigDir,`skills`):e(n.paths.home,`.claude`,`skills`);case`opencode`:return e(n.paths.configHome,`opencode`,`skills`);case`cursor`:return e(n.paths.home,`.cursor`,`skills`);case`codex`:return e(n.paths.home,`.agents`,`skills`);case`copilot`:return n.paths.copilotHome?e(n.paths.copilotHome,`skills`):e(n.paths.home,`.copilot`,`skills`);case`antigravity`:return e(n.paths.home,`.gemini`,`config`,`skills`);case`pi`:return e(i(n),`skills`);case`augment`:return e(n.paths.home,`.augment`,`skills`);case`grok`:return e(a(n),`skills`);default:throw Error(`"${t}" is not a supported client`)}}export{n as allSkillAgents,r as isSkillAgentSupported,o as skillDir};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{MemoryFileSystem as e}from"./memory-fs.js";export{e as MemoryFileSystem};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FileSystemPort } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/memory-fs.d.ts
|
|
4
|
+
/** In-memory {@link FileSystemPort}, for a host's own tests driving `installMcp`/`installSkill` and friends without touching a real filesystem. */
|
|
5
|
+
declare class MemoryFileSystem implements FileSystemPort {
|
|
6
|
+
private readonly files;
|
|
7
|
+
private readonly directories;
|
|
8
|
+
constructor(seed?: Record<string, string>);
|
|
9
|
+
readFile(path: string): Promise<string | null>;
|
|
10
|
+
writeFile(path: string, content: string, _options?: {
|
|
11
|
+
mode?: number;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
remove(path: string, options?: {
|
|
14
|
+
recursive?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
readDir(path: string): Promise<string[] | null>;
|
|
17
|
+
/** Full path -> content snapshot, for assertions in a host's own tests. Not part of {@link FileSystemPort}. */
|
|
18
|
+
snapshot(): Record<string, string>;
|
|
19
|
+
private addParentDirectories;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { MemoryFileSystem };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class{files=new Map;directories=new Set([`/`]);constructor(e){if(e)for(let[n,r]of Object.entries(e)){let e=t(n);this.files.set(e,r),this.addParentDirectories(e)}}async readFile(e){return this.files.get(t(e))??null}async writeFile(e,n,r){let i=t(e);this.files.set(i,n),this.addParentDirectories(i)}async remove(e,n){let r=t(e),i=`${r}/`;if(this.files.has(r)){this.files.delete(r);return}if(!this.directories.has(r))return;if(!n?.recursive){if((await this.readDir(r))?.length)throw Error(`directory not empty: ${r}`);r!==`/`&&this.directories.delete(r);return}let a=r===`/`?`/`:i;for(let e of this.files.keys())e.startsWith(a)&&this.files.delete(e);for(let e of this.directories)(e===r||e.startsWith(a))&&e!==`/`&&this.directories.delete(e)}async readDir(e){let n=t(e);if(!this.directories.has(n))return null;let r=n===`/`?`/`:`${n}/`,i=new Set;for(let e of[...this.files.keys(),...this.directories])if(e.startsWith(r)){let t=e.slice(r.length);t&&i.add(t.split(`/`)[0])}return[...i]}snapshot(){return Object.fromEntries(this.files)}addParentDirectories(e){for(let t=0;t<e.length-1;t++)e[t]===`/`&&(t===0?this.directories.add(`/`):(t===2&&/^[A-Za-z]:$/.test(e.slice(0,t)),this.directories.add(e.slice(0,t))))}};function t(e){let t=e;for(;t.endsWith(`/`)&&t.length>1;)t=t.slice(0,-1);return t}export{e as MemoryFileSystem};
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/agent-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Host configuration and path helpers for AI coding agent integrations, with an in-memory filesystem for testing.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Axosoft, LLC dba GitKraken",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">= 22"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./testing": {
|
|
18
|
+
"types": "./dist/testing/index.d.ts",
|
|
19
|
+
"default": "./dist/testing/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"jsonc-parser": "^3.3.1",
|
|
28
|
+
"smol-toml": "^1.9.0",
|
|
29
|
+
"@gitkraken/shared-tools": "^0.3.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^6.0.2",
|
|
34
|
+
"vitest": "^4.1.1"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown --config-loader=unrun",
|
|
38
|
+
"build:dev": "tsdown --config tsdown.dev.config.ts --config-loader=unrun",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run"
|
|
41
|
+
}
|
|
6
42
|
}
|