84 Commits

Author SHA1 Message Date
Sean Goedecke
f347eae8eb Merge pull request #91 from JessRudder/secure-tmp-files
Uses tmp library to ensure more secure tmp file creation
2025-08-14 07:15:18 +10:00
Jess Rudder
07fe2f30ad Merge branch 'main' into secure-tmp-files 2025-08-13 14:11:23 -07:00
Jess Rudder
1843310df4 Add license info 2025-08-13 21:07:21 +00:00
Sean Goedecke
c72cb2ef9c Merge pull request #90 from garman/pin-to-sha
Pin two imported actions to a set sha
2025-08-14 06:58:03 +10:00
Jessica Rudder
a2fd223fcf Properly clean up tmp files 2025-08-12 14:31:05 -07:00
Jessica Rudder
3ba8e1b39d Replace manual tmp file creation with tmp library which uses security best practices 2025-08-12 13:49:47 -07:00
Daniel Garman
52e5222a82 pin to a sha 2025-08-12 15:04:16 -04:00
Sean Goedecke
a62dfeda7b Merge pull request #79 from salmanmkc/node24
Node 24
2025-08-11 21:13:39 +10:00
Salman Chishti
48235f7026 Merge branch 'main' into node24 2025-08-11 11:52:36 +01:00
Sean Goedecke
b81b2afb83 Merge pull request #88 from actions/sgoedecke/force-exit-once-inference-finishes
Force exit once inference finishes
2025-08-06 11:01:14 +10:00
Sean Goedecke
9133f81330 package 2025-08-06 00:54:19 +00:00
Sean Goedecke
7923b92ef8 Merge pull request #89 from actions/sgoedecke/ensure-mcp-loops-output-desired-response-format
Ensure MCP loops output the right response format
2025-08-06 10:41:02 +10:00
Sean Goedecke
e44da102bf fixup format parsing 2025-08-05 22:21:28 +00:00
Sean Goedecke
866ae2b5d7 Ensure MCP loops output the right response format
In a tool loop, you can't set response_format because the model needs to
be able to think in plain English. But you still need the final response
to be in the desired format, so we add response_format only on the last
iteration.
2025-08-05 22:06:49 +00:00
Sean Goedecke
4685e0dcd4 Force exit once inference finishes in case we are holding any connections open 2025-08-05 21:42:07 +00:00
Sean Goedecke
0cbed4a106 Merge pull request #86 from actions/sgoedecke/use-openai-sdk
Use the OpenAI SDK
2025-08-05 14:19:47 +10:00
Sean Goedecke
009d5e6e28 Update error 2025-08-05 02:52:11 +00:00
Sean Goedecke
18367df745 Merge branch 'main' into sgoedecke/use-openai-sdk 2025-08-05 02:49:44 +00:00
Sean Goedecke
3c6ec33d64 Merge pull request #85 from actions/sgoedecke/file-inputs
Allow templating variables from files
2025-08-05 12:19:39 +10:00
Sean Goedecke
0347935cb1 licensed 2025-08-05 02:17:25 +00:00
Sean Goedecke
8c9e538880 package 2025-08-05 02:17:03 +00:00
Sean Goedecke
de436346ec Fixup error messages 2025-08-05 02:11:43 +00:00
Sean Goedecke
4b5bb5c538 Use OpenAI SDK to avoid setting apiVersion manually 2025-08-05 02:09:17 +00:00
Sean Goedecke
ea4e7d8bb9 package 2025-08-05 01:52:46 +00:00
Sean Goedecke
aaf9c5af33 Update src/prompt.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 11:51:50 +10:00
Sean Goedecke
15868b88f4 Allow templating variables from files 2025-08-05 01:32:32 +00:00
Sean Goedecke
c37f296c98 Merge pull request #84 from actions/sgoedecke/better-error-logging
Log specific error even if it is not an Error
2025-08-05 09:28:24 +10:00
Sean Goedecke
e7ddc840ba npm run package 2025-08-04 23:00:34 +00:00
Sean Goedecke
fa321d4c78 Update src/main.ts
Co-authored-by: Marais Rossouw <me@marais.co>
2025-08-05 08:59:43 +10:00
Sean Goedecke
3b5da63917 update tests 2025-08-04 22:44:17 +00:00
Sean Goedecke
a620b9fa98 Force exit on error 2025-08-04 22:40:30 +00:00
Sean Goedecke
a6d2a86ab3 Log specific error even if it is not an Error 2025-08-04 22:28:10 +00:00
Sean Goedecke
4b591cc529 Merge pull request #83 from actions/sgoedecke/separate-mcp
Separate out MCP token
2025-08-04 15:03:54 +10:00
Sean Goedecke
ea24ec2ed4 Update README.md
Co-authored-by: Yuzuki <36879321+Yuzuki-S@users.noreply.github.com>
2025-08-04 13:52:21 +10:00
Sean Goedecke
b9f9444fb7 update docs 2025-08-04 03:41:34 +00:00
Sean Goedecke
419f171f16 Separate out MCP token 2025-08-04 03:06:53 +00:00
Salman Muin Kayser Chishti
9bbcef8fa4 node 24 2025-08-01 12:13:15 +01:00
Yumin Wong
fc8527d1d9 Merge pull request #74 from actions/dependabot/github_actions/actions-minor-e893b3f303
chore(deps): bump actions/publish-action from 0.2.2 to 0.3.0 in the actions-minor group
2025-07-29 14:00:29 +08:00
Yumin Wong
719349dfcc Merge branch 'main' into dependabot/github_actions/actions-minor-e893b3f303 2025-07-29 13:30:19 +08:00
Yumin Wong
2762750922 Merge pull request #76 from actions/dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.46.0
chore(deps): bump @rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
2025-07-29 13:20:25 +08:00
dependabot[bot]
9386906af5 chore(deps): bump @rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
Bumps [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) from 4.45.1 to 4.46.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.45.1...v4.46.0)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:48:49 +00:00
dependabot[bot]
ca9eff7051 chore(deps): bump actions/publish-action in the actions-minor group
Bumps the actions-minor group with 1 update: [actions/publish-action](https://github.com/actions/publish-action).


Updates `actions/publish-action` from 0.2.2 to 0.3.0
- [Commits](https://github.com/actions/publish-action/compare/v0.2.2...v0.3.0)

---
updated-dependencies:
- dependency-name: actions/publish-action
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:05:06 +00:00
Marais Rossouw
6bef1d0031 Merge pull request #72 from actions/mr/linters 2025-07-24 19:50:25 +10:00
Marais Rossouw
a5af2ca963 chore: bundles do change a tiny bit now 2025-07-24 19:14:33 +10:00
Marais Rossouw
7e2aa19f3b chore: use github's shared prettier-config 2025-07-24 19:11:15 +10:00
Marais Rossouw
a2235c5511 chore: move superlinter files to .github/linters 2025-07-24 19:11:03 +10:00
Marais Rossouw
b1fc21bd19 Merge pull request #70 from actions/mr/to-vitest 2025-07-24 19:06:21 +10:00
Marais Rossouw
305e9d3933 chore: trigger ci 2025-07-24 19:05:42 +10:00
licensed-ci
b1c0a96f18 Auto-update license files 2025-07-24 08:21:55 +00:00
Marais Rossouw
ea289a3b79 chore: move dev deps to dev deps 2025-07-24 18:20:00 +10:00
Marais Rossouw
77f8afc857 chore: drop coverage, for now 2025-07-24 18:17:25 +10:00
Marais Rossouw
4ba8e6bc1e feat: moves project to using vitest 2025-07-24 18:08:26 +10:00
Marais Rossouw
64cbe74d35 Merge pull request #69 from actions/mr/cleanup-package.json
Tidy up package.json
2025-07-24 08:19:08 +10:00
Marais Rossouw
d045ae4018 chore: tidy up package.json 2025-07-24 08:04:24 +10:00
Yuzuki
0b15edbb56 Merge pull request #66 from actions/dependabot/npm_and_yarn/multi-da3791aed2
Bump jest and @types/jest
2025-07-23 15:38:35 +10:00
Yuzuki
8726487e22 Merge branch 'main' into dependabot/npm_and_yarn/multi-da3791aed2 2025-07-23 15:34:29 +10:00
Yuzuki
79c7fc388f Merge pull request #65 from actions/dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.45.1
Bump @rollup/rollup-linux-x64-gnu from 4.43.0 to 4.45.1
2025-07-22 11:34:41 +10:00
dependabot[bot]
e43f4c40d0 Bump jest and @types/jest
---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
- dependency-name: "@types/jest"
  dependency-version: 30.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:53:19 +00:00
dependabot[bot]
7396fddf1d Bump @rollup/rollup-linux-x64-gnu from 4.43.0 to 4.45.1
---
updated-dependencies:
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:53:04 +00:00
Yuzuki
afe6f4df95 Merge pull request #63 from actions/dependabot/npm_and_yarn/github/local-action-5.1.0
Bump @github/local-action from 3.2.1 to 5.1.0
2025-07-21 15:51:49 +10:00
dependabot[bot]
a915345307 Bump @github/local-action from 3.2.1 to 5.1.0
---
updated-dependencies:
- dependency-name: "@github/local-action"
  dependency-version: 5.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:16:08 +00:00
Sean Goedecke
9693b137b6 Merge pull request #61 from actions/sgoedecke/prompt-file
Support .prompt.yml files
2025-07-21 15:14:53 +10:00
Sean Goedecke
d0b2f23c43 Merge branch 'main' into sgoedecke/prompt-file 2025-07-21 15:01:31 +10:00
Sean Goedecke
0df96479bc Merge pull request #60 from actions/sgoedecke/update-readme
Update readme to say MCP needs a PAT
2025-07-21 15:00:56 +10:00
Sean Goedecke
446f075e3b Merge branch 'main' into sgoedecke/update-readme 2025-07-21 15:00:09 +10:00
Sean Goedecke
ce58b26ac7 Merge pull request #59 from actions/sgoedecke-patch-1
Add GitHub Actions workflow for releasing new version
2025-07-21 14:57:40 +10:00
Sean Goedecke
1cf96b0212 Merge branch 'main' into sgoedecke/update-readme 2025-07-21 14:57:19 +10:00
Sean Goedecke
f79e4e11cb regenerate dist 2025-07-21 04:56:41 +00:00
Sean Goedecke
72102e50bf Update src/prompt.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 14:55:49 +10:00
Sean Goedecke
2bc30a525a regenerate dist 2025-07-21 04:35:30 +00:00
Sean Goedecke
8f64ac1284 Fixup types and tests 2025-07-21 04:31:06 +00:00
Sean Goedecke
1f89e942aa licensed cache 2025-07-21 04:25:01 +00:00
Sean Goedecke
77a7cbe11b Merge branch 'main' into sgoedecke-patch-1 2025-07-21 14:18:05 +10:00
Sean Goedecke
fcc8550115 lint 2025-07-21 04:01:22 +00:00
Sean Goedecke
29b5f08d0f Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:50:38 +10:00
Sean Goedecke
6f7bd88d1d lint 2025-07-21 03:49:57 +00:00
Yuzuki
8ae5306787 Merge pull request #62 from actions/dependabot/github_actions/super-linter/super-linter-8.0.0
Bump super-linter/super-linter from 7.4.0 to 8.0.0
2025-07-21 13:13:50 +10:00
dependabot[bot]
9445295106 Bump super-linter/super-linter from 7.4.0 to 8.0.0
Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 7.4.0 to 8.0.0.
- [Release notes](https://github.com/super-linter/super-linter/releases)
- [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md)
- [Commits](12150456a7...5119dcd801)

---
updated-dependencies:
- dependency-name: super-linter/super-linter
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 02:52:40 +00:00
Sean Goedecke
e385879671 Merge branch 'main' into sgoedecke/prompt-file 2025-07-21 00:21:07 +00:00
Sean Goedecke
1780121e3b Support .prompt.yml files 2025-07-21 00:11:26 +00:00
Sean Goedecke
b002da2928 Add GitHub Actions workflow for releasing new version 2025-07-21 09:41:47 +10:00
Sean Goedecke
ba509f9275 update readme 2025-07-16 07:51:13 +00:00
Sean Goedecke
c05344404e Merge pull request #58 from actions/sgoedecke/fixup-bundle
Fixup bundle
2025-07-16 17:33:26 +10:00
Sean Goedecke
aff9eb000b Fixup bundle 2025-07-16 07:30:35 +00:00
45 changed files with 40688 additions and 13333 deletions

View File

@@ -43,7 +43,7 @@ jobs:
- name: Test
id: npm-ci-test
run: npm run ci-test
run: npm run test
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -83,8 +83,7 @@ jobs:
run: echo "hello" > prompt.txt
- name: Create System Prompt File
run:
echo "You are a helpful AI assistant for testing." > system-prompt.txt
run: echo "You are a helpful AI assistant for testing." > system-prompt.txt
- name: Test Local Action with Prompt File
id: test-action-prompt-file

View File

@@ -42,11 +42,11 @@ jobs:
- name: Setup Ruby
id: setup-ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4
with:
ruby-version: ruby
- uses: licensee/setup-licensed@v1.3.2
- uses: licensee/setup-licensed@0d52e575b3258417672be0dff2f115d7db8771d8
with:
version: 4.x
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -38,7 +38,7 @@ jobs:
- name: Lint Codebase
id: super-linter
uses: super-linter/super-linter/slim@12150456a73e248bdc94d0794898f94e23127c88
uses: super-linter/super-linter/slim@5119dcd8011e92182ce8219d9e9efc82f16fddb6
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*

View File

@@ -0,0 +1,21 @@
name: Release new action version
on:
release:
types: [released]
env:
TAG_NAME: ${{ github.event.release.tag_name }}
permissions:
contents: write
jobs:
update_tag:
name: Update the major tag to include the ${{ github.event.release.tag_name }}
changes
runs-on: ubuntu-latest
steps:
- name: Update the ${{ env.TAG_NAME }} tag
uses: actions/publish-action@v0.3.0
with:
source-tag: ${{ env.TAG_NAME }}

View File

@@ -1,32 +0,0 @@
---
name: "@rollup/pluginutils"
version: 5.1.4
type: npm
summary: A set of utility functions commonly used by Rollup plugins
homepage: https://github.com/rollup/plugins/tree/master/packages/pluginutils#readme
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
notices: []

View File

@@ -1,9 +1,9 @@
---
name: "@types/estree"
version: 1.0.7
name: "@types/tmp"
version: 0.2.6
type: npm
summary: TypeScript definitions for estree
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree
summary: TypeScript definitions for tmp
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/tmp
license: mit
licenses:
- sources: LICENSE

View File

@@ -0,0 +1,265 @@
---
name: argparse
version: 2.0.1
type: npm
summary: CLI arguments parser. Native port of python's argparse.
homepage:
license: other
licenses:
- sources: LICENSE
text: |
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see http://www.opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

View File

@@ -1,20 +0,0 @@
---
name: estree-walker
version: 2.0.2
type: npm
summary: Traverse an ESTree-compliant AST
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright (c) 2015-20 [these people](https://github.com/Rich-Harris/estree-walker/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- sources: README.md
text: MIT
notices: []

View File

@@ -1,16 +1,16 @@
---
name: "@rollup/plugin-json"
version: 6.1.0
name: js-yaml
version: 4.1.0
type: npm
summary: Convert .json files to ES6 modules
homepage: https://github.com/rollup/plugins/tree/master/packages/json#readme
summary: YAML 1.2 parser and serializer
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
(The MIT License)
Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)
Copyright (C) 2011-2015 by Vitaly Puzrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,212 @@
---
name: openai
version: 5.11.0
type: npm
summary: The official TypeScript library for the OpenAI API
homepage:
license: apache-2.0
licenses:
- sources: LICENSE
text: |2
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 OpenAI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
notices: []

View File

@@ -1,38 +0,0 @@
---
name: picomatch
version: 4.0.2
type: npm
summary: Blazing fast and accurate glob matcher written in JavaScript, with no dependencies
and full support for standard and extended Bash glob features, including braces,
extglobs, POSIX brackets, and regular expressions.
homepage: https://github.com/micromatch/picomatch
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
Copyright (c) 2017-present, Jon Schlinkert.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
- sources: README.md
text: |-
Copyright © 2017-present, [Jon Schlinkert](https://github.com/jonschlinkert).
Released under the [MIT License](LICENSE).
notices: []

View File

@@ -1,438 +0,0 @@
---
name: rollup
version: 4.43.0
type: npm
summary: Next-generation ES module bundler
homepage: https://rollupjs.org/
license: other
licenses:
- sources: LICENSE.md
text: "# Rollup core license\nRollup is released under the MIT license:\n\nThe MIT
License (MIT)\n\nCopyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)\n\nPermission
is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the \"Software\"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following
conditions:\n\nThe above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE
IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n# Licenses of bundled dependencies\nThe
published Rollup artifact additionally contains code with the following licenses:\nMIT,
ISC, 0BSD\n\n# Bundled dependencies:\n## @jridgewell/sourcemap-codec\nLicense:
MIT\nBy: Rich Harris\nRepository: git+https://github.com/jridgewell/sourcemap-codec.git\n\n>
The MIT License\n> \n> Copyright (c) 2015 Rich Harris\n> \n> Permission is hereby
granted, free of charge, to any person obtaining a copy\n> of this software and
associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
@rollup/pluginutils\nLicense: MIT\nBy: Rich Harris\nRepository: rollup/plugins\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in\n> all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## anymatch\nLicense:
ISC\nBy: Elan Shanker\nRepository: https://github.com/micromatch/anymatch\n\n>
The ISC License\n> \n> Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)\n>
\n> Permission to use, copy, modify, and/or distribute this software for any\n>
purpose with or without fee is hereby granted, provided that the above\n> copyright
notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE IS
PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS. IN
NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR\n> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n---------------------------------------\n\n##
binary-extensions\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/binary-extensions\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
Copyright (c) Paul Miller (https://paulmillr.com)\n> \n> Permission is hereby
granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the \"Software\"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:\n>
\n> The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
braces\nLicense: MIT\nBy: Jon Schlinkert, Brian Woodward, Elan Shanker, Eugene
Sharygin, hemanth.hm\nRepository: micromatch/braces\n\n> The MIT License (MIT)\n>
\n> Copyright (c) 2014-present, Jon Schlinkert.\n> \n> Permission is hereby granted,
free of charge, to any person obtaining a copy\n> of this software and associated
documentation files (the \"Software\"), to deal\n> in the Software without restriction,
including without limitation the rights\n> to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell\n> copies of the Software, and to permit persons
to whom the Software is\n> furnished to do so, subject to the following conditions:\n>
\n> The above copyright notice and this permission notice shall be included in\n>
all copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
builtin-modules\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/builtin-modules\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
chokidar\nLicense: MIT\nBy: Paul Miller, Elan Shanker\nRepository: git+https://github.com/paulmillr/chokidar.git\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com),
Elan Shanker\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the “Software”),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## date-time\nLicense:
MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/date-time\n\n> MIT License\n>
\n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
fill-range\nLicense: MIT\nBy: Jon Schlinkert, Edo Rivai, Paul Miller, Rouven Weßling\nRepository:
jonschlinkert/fill-range\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2014-present,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## flru\nLicense:
MIT\nBy: Luke Edwards\nRepository: lukeed/flru\n\n> MIT License\n> \n> Copyright
(c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)\n> \n> Permission is
hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the \"Software\"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following
conditions:\n> \n> The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the Software.\n> \n> THE
SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
glob-parent\nLicense: ISC\nBy: Gulp Team, Elan Shanker, Blaine Bublitz\nRepository:
gulpjs/glob-parent\n\n> The ISC License\n> \n> Copyright (c) 2015, 2019 Elan Shanker\n>
\n> Permission to use, copy, modify, and/or distribute this software for any\n>
purpose with or without fee is hereby granted, provided that the above\n> copyright
notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE IS
PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS. IN
NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR\n> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n---------------------------------------\n\n##
is-binary-path\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/is-binary-path\n\n>
MIT License\n> \n> Copyright (c) 2019 Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com),
Paul Miller (https://paulmillr.com)\n> \n> Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated documentation
files (the \"Software\"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom
the Software is furnished to do so, subject to the following conditions:\n> \n>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
is-extglob\nLicense: MIT\nBy: Jon Schlinkert\nRepository: jonschlinkert/is-extglob\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-2016, Jon Schlinkert\n> \n> Permission
is hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
is-glob\nLicense: MIT\nBy: Jon Schlinkert, Brian Woodward, Daniel Perez\nRepository:
micromatch/is-glob\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2014-2017,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## is-number\nLicense:
MIT\nBy: Jon Schlinkert, Olsten Larck, Rouven Weßling\nRepository: jonschlinkert/is-number\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-present, Jon Schlinkert.\n> \n>
Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in\n> all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## is-reference\nLicense:
MIT\nBy: Rich Harris\nRepository: git+https://github.com/Rich-Harris/is-reference.git\n\n---------------------------------------\n\n##
locate-character\nLicense: MIT\nBy: Rich Harris\nRepository: git+https://gitlab.com/Rich-Harris/locate-character.git\n\n---------------------------------------\n\n##
magic-string\nLicense: MIT\nBy: Rich Harris\nRepository: https://github.com/rich-harris/magic-string\n\n>
Copyright 2018 Rich Harris\n> \n> Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and associated documentation files
(the \"Software\"), to deal in the Software without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.\n\n---------------------------------------\n\n## normalize-path\nLicense:
MIT\nBy: Jon Schlinkert, Blaine Bublitz\nRepository: jonschlinkert/normalize-path\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-2018, Jon Schlinkert.\n> \n> Permission
is hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
parse-ms\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/parse-ms\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
picocolors\nLicense: ISC\nBy: Alexey Raspopov\nRepository: alexeyraspopov/picocolors\n\n>
ISC License\n> \n> Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov,
Anton Verinov\n> \n> Permission to use, copy, modify, and/or distribute this software
for any\n> purpose with or without fee is hereby granted, provided that the above\n>
copyright notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE
IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO
THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
ACTION, ARISING OUT OF\n> OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE.\n\n---------------------------------------\n\n## picomatch\nLicense:
MIT\nBy: Jon Schlinkert\nRepository: micromatch/picomatch\n\n> The MIT License
(MIT)\n> \n> Copyright (c) 2017-present, Jon Schlinkert.\n> \n> Permission is
hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
pretty-bytes\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/pretty-bytes\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
pretty-ms\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/pretty-ms\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
readdirp\nLicense: MIT\nBy: Thorsten Lorenz, Paul Miller\nRepository: git://github.com/paulmillr/readdirp.git\n\n>
MIT License\n> \n> Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in all\n> copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE\n> SOFTWARE.\n\n---------------------------------------\n\n## signal-exit\nLicense:
ISC\nBy: Ben Coe\nRepository: https://github.com/tapjs/signal-exit.git\n\n> The
ISC License\n> \n> Copyright (c) 2015-2023 Benjamin Coe, Isaac Z. Schlueter, and
Contributors\n> \n> Permission to use, copy, modify, and/or distribute this software\n>
for any purpose with or without fee is hereby granted, provided\n> that the above
copyright notice and this permission notice\n> appear in all copies.\n> \n> THE
SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\n> OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE\n> LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES\n> OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS,\n> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION,\n> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
OF THIS SOFTWARE.\n\n---------------------------------------\n\n## time-zone\nLicense:
MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/time-zone\n\n> MIT License\n>
\n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
to-regex-range\nLicense: MIT\nBy: Jon Schlinkert, Rouven Weßling\nRepository:
micromatch/to-regex-range\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2015-present,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## tslib\nLicense:
0BSD\nBy: Microsoft Corp.\nRepository: https://github.com/Microsoft/tslib.git\n\n>
Copyright (c) Microsoft Corporation.\n> \n> Permission to use, copy, modify, and/or
distribute this software for any\n> purpose with or without fee is hereby granted.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH\n> REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n>
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n>
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n>
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n>
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n> PERFORMANCE
OF THIS SOFTWARE.\n\n---------------------------------------\n\n## yargs-parser\nLicense:
ISC\nBy: Ben Coe\nRepository: https://github.com/yargs/yargs-parser.git\n\n> Copyright
(c) 2016, Contributors\n> \n> Permission to use, copy, modify, and/or distribute
this software\n> for any purpose with or without fee is hereby granted, provided\n>
that the above copyright notice and this permission notice\n> appear in all copies.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n>
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\n> OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE\n> LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES\n> OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS,\n> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION,\n> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
OF THIS SOFTWARE.\n"
- sources: README.md
text: "[MIT](https://github.com/rollup/rollup/blob/master/LICENSE.md)"
notices: []

View File

@@ -1,14 +1,16 @@
---
name: "@rollup/rollup-linux-x64-musl"
version: 4.43.0
name: tmp
version: 0.2.5
type: npm
summary: Native bindings for Rollup
homepage: https://rollupjs.org/
summary: Temporary file and directory creator
homepage: http://github.com/raszi/node-tmp
license: mit
licenses:
- sources: Auto-generated MIT license text
- sources: LICENSE
text: |
MIT License
The MIT License (MIT)
Copyright (c) 2014 KARASZI István
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1 +1 @@
20.9.0
24.4.0

View File

@@ -1,16 +0,0 @@
# See: https://prettier.io/docs/en/configuration
printWidth: 80
tabWidth: 2
useTabs: false
semi: false
singleQuote: true
quoteProps: as-needed
jsxSingleQuote: false
trailingComma: none
bracketSpacing: true
bracketSameLine: true
arrowParens: always
proseWrap: always
htmlWhitespaceSensitivity: css
endOfLine: lf

128
README.md
View File

@@ -4,7 +4,6 @@
![CI](https://github.com/actions/typescript-action/actions/workflows/ci.yml/badge.svg)
[![Check dist/](https://github.com/actions/typescript-action/actions/workflows/check-dist.yml/badge.svg)](https://github.com/actions/typescript-action/actions/workflows/check-dist.yml)
[![CodeQL](https://github.com/actions/typescript-action/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/actions/typescript-action/actions/workflows/codeql-analysis.yml)
[![Coverage](./badges/coverage.svg)](./badges/coverage.svg)
Use AI models from [GitHub Models](https://github.com/marketplace/models) in
your workflows.
@@ -36,17 +35,94 @@ jobs:
### Using a prompt file
You can also provide a prompt file instead of an inline prompt:
You can also provide a prompt file instead of an inline prompt. The action
supports both plain text files and structured `.prompt.yml` files:
```yaml
steps:
- name: Run AI Inference with Prompt File
- name: Run AI Inference with Text File
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './path/to/prompt.txt'
```
### Using GitHub prompt.yml files
For more advanced use cases, you can use structured `.prompt.yml` files that
support templating, custom models, and JSON schema responses:
```yaml
steps:
- name: Run AI Inference with Prompt YAML
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './.github/prompts/sample.prompt.yml'
input: |
var1: hello
var2: ${{ steps.some-step.outputs.output }}
var3: |
Lorem Ipsum
Hello World
file_input: |
var4: ./path/to/long-text.txt
var5: ./path/to/config.json
```
#### Simple prompt.yml example
```yaml
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o
```
#### Prompt.yml with JSON schema support
```yaml
messages:
- role: system
content: You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
Use JSON format as specified in the response schema
model: openai/gpt-4o
responseFormat: json_schema
jsonSchema: |-
{
"name": "describe_animal",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the animal"
},
"habitat": {
"type": "string",
"description": "The habitat the animal lives in"
}
},
"additionalProperties": false,
"required": [
"name",
"habitat"
]
}
}
```
Variables in prompt.yml files are templated using `{{variable}}` format and are
supplied via the `input` parameter in YAML format. Additionally, you can
provide file-based variables via `file_input`, where each key maps to a file
path.
### Using a system prompt file
In addition to the regular prompt, you can provide a system prompt file instead
@@ -90,34 +166,50 @@ repository management, issue tracking, and pull request operations.
steps:
- name: AI Inference with GitHub Tools
id: inference
uses: actions/ai-inference@v1
uses: actions/ai-inference@v1.2
with:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.USER_PAT }}
```
If you want, you can use separate tokens for the AI inference endpoint
and the GitHub MCP server:
```yaml
steps:
- name: AI Inference with Separate MCP Token
id: inference
uses: actions/ai-inference@v1.2
with:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.GITHUB_TOKEN }}
github-mcp-token: ${{ secrets.USER_PAT }}
```
When MCP is enabled, the AI model will have access to GitHub tools and can
perform actions like searching issues and PRs.
**Note:** MCP integration requires your workflow token to have appropriate
GitHub permissions for the operations the AI will perform.
## Inputs
Various inputs are defined in [`action.yml`](action.yml) to let you configure
the action:
| Name | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `prompt-file` | Path to a file containing the prompt. If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4.1` |
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| Name | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). Use a separate PAT for tighter security | `""` |
## Outputs

View File

@@ -1,11 +1,11 @@
import type * as core from '@actions/core'
import { jest } from '@jest/globals'
import {vi} from 'vitest'
export const debug = jest.fn<typeof core.debug>()
export const error = jest.fn<typeof core.error>()
export const info = jest.fn<typeof core.info>()
export const getInput = jest.fn<typeof core.getInput>()
export const getBooleanInput = jest.fn<typeof core.getBooleanInput>()
export const setOutput = jest.fn<typeof core.setOutput>()
export const setFailed = jest.fn<typeof core.setFailed>()
export const warning = jest.fn<typeof core.warning>()
export const debug = vi.fn<typeof core.debug>()
export const error = vi.fn<typeof core.error>()
export const info = vi.fn<typeof core.info>()
export const getInput = vi.fn<typeof core.getInput>()
export const getBooleanInput = vi.fn<typeof core.getBooleanInput>()
export const setOutput = vi.fn<typeof core.setOutput>()
export const setFailed = vi.fn<typeof core.setFailed>()
export const warning = vi.fn<typeof core.warning>()

View File

@@ -0,0 +1,40 @@
messages:
- role: system
content: You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
Use JSON format as specified in the response schema
model: openai/gpt-4o
responseFormat: json_schema
jsonSchema: |-
{
"name": "describe_animal",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the animal"
},
"habitat": {
"type": "string",
"description": "The habitat the animal lives in"
},
"characteristics": {
"type": "array",
"items": {
"type": "string"
},
"description": "Key characteristics of the animal"
}
},
"additionalProperties": false,
"required": [
"name",
"habitat",
"characteristics"
]
}
}

View File

@@ -0,0 +1,6 @@
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o

View File

@@ -1,3 +1,3 @@
import { jest } from '@jest/globals'
import {vi} from 'vitest'
export const wait = jest.fn<typeof import('../src/wait.js').wait>()
export const wait = vi.fn<typeof import('../src/wait.js').wait>()

View File

@@ -0,0 +1,157 @@
import {describe, it, expect} from 'vitest'
import {buildMessages, buildResponseFormat, buildInferenceRequest} from '../src/helpers'
import {PromptConfig} from '../src/prompt'
describe('helpers.ts - inference request building', () => {
describe('buildMessages', () => {
it('should build messages from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
}
const result = buildMessages(promptConfig)
expect(result).toEqual([
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
])
})
it('should build messages from legacy format', () => {
const result = buildMessages(undefined, 'System prompt', 'User prompt')
expect(result).toEqual([
{role: 'system', content: 'System prompt'},
{role: 'user', content: 'User prompt'},
])
})
it('should use default system prompt when none provided', () => {
const result = buildMessages(undefined, undefined, 'User prompt')
expect(result).toEqual([
{role: 'system', content: 'You are a helpful assistant'},
{role: 'user', content: 'User prompt'},
])
})
})
describe('buildResponseFormat', () => {
it('should build JSON schema response format', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: {type: 'object'},
}),
}
const result = buildResponseFormat(promptConfig)
expect(result).toEqual({
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: {type: 'object'},
},
})
})
it('should return undefined for text format', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'text',
}
const result = buildResponseFormat(promptConfig)
expect(result).toBeUndefined()
})
it('should return undefined when no response format specified', () => {
const promptConfig: PromptConfig = {
messages: [],
}
const result = buildResponseFormat(promptConfig)
expect(result).toBeUndefined()
})
it('should throw error for invalid JSON schema', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'json_schema',
jsonSchema: 'invalid json',
}
expect(() => buildResponseFormat(promptConfig)).toThrow('Invalid JSON schema')
})
})
describe('buildInferenceRequest', () => {
it('should build complete inference request from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: {type: 'object'},
}),
}
const result = buildInferenceRequest(
promptConfig,
undefined,
undefined,
'gpt-4',
100,
'https://api.test.com',
'test-token',
)
expect(result).toEqual({
messages: [
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token',
responseFormat: {
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: {type: 'object'},
},
},
})
})
it('should build inference request from legacy format', () => {
const result = buildInferenceRequest(
undefined,
'System prompt',
'User prompt',
'gpt-4',
100,
'https://api.test.com',
'test-token',
)
expect(result).toEqual({
messages: [
{role: 'system', content: 'System prompt'},
{role: 'user', content: 'User prompt'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token',
responseFormat: undefined,
})
})
})
})

View File

@@ -1,26 +1,21 @@
/**
* Unit tests for the helpers module, src/helpers.ts
*/
import { jest } from '@jest/globals'
import {vi, it, expect, beforeEach, describe} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock fs module
const mockExistsSync = jest.fn()
const mockReadFileSync = jest.fn()
const mockExistsSync = vi.fn()
const mockReadFileSync = vi.fn()
jest.unstable_mockModule('fs', () => ({
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync
readFileSync: mockReadFileSync,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { loadContentFromFileOrInput } = await import('../src/helpers.js')
const {loadContentFromFileOrInput} = await import('../src/helpers.js')
describe('helpers.ts', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('loadContentFromFileOrInput', () => {
@@ -108,11 +103,7 @@ describe('helpers.ts', () => {
core.getInput.mockImplementation(() => '')
const result = loadContentFromFileOrInput(
'file-input',
'content-input',
defaultValue
)
const result = loadContentFromFileOrInput('file-input', 'content-input', defaultValue)
expect(result).toBe(defaultValue)
expect(mockExistsSync).not.toHaveBeenCalled()
@@ -136,11 +127,7 @@ describe('helpers.ts', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
core.getInput.mockImplementation(() => undefined as any)
const result = loadContentFromFileOrInput(
'file-input',
'content-input',
defaultValue
)
const result = loadContentFromFileOrInput('file-input', 'content-input', defaultValue)
expect(result).toBe(defaultValue)
})

View File

@@ -1,114 +1,142 @@
/**
* Unit tests for the inference module, src/inference.ts
*/
import { jest } from '@jest/globals'
import {vi, type MockedFunction, beforeEach, expect, describe, it} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock Azure AI Inference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockPost = jest.fn() as jest.MockedFunction<any>
const mockPath = jest.fn(() => ({ post: mockPost }))
const mockClient = jest.fn(() => ({ path: mockPath }))
jest.unstable_mockModule('@azure-rest/ai-inference', () => ({
default: mockClient,
isUnexpected: jest.fn(() => false)
const mockCreate = vi.fn() as MockedFunction<any>
const mockCompletions = {create: mockCreate}
const mockChat = {completions: mockCompletions}
const mockOpenAIClient = vi.fn(() => ({
chat: mockChat,
}))
jest.unstable_mockModule('@azure/core-auth', () => ({
AzureKeyCredential: jest.fn()
vi.mock('openai', () => ({
default: mockOpenAIClient,
}))
// Mock MCP functions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockExecuteToolCalls = jest.fn() as jest.MockedFunction<any>
jest.unstable_mockModule('../src/mcp.js', () => ({
executeToolCalls: mockExecuteToolCalls
const mockExecuteToolCalls = vi.fn() as MockedFunction<any>
vi.mock('../src/mcp.js', () => ({
executeToolCalls: mockExecuteToolCalls,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { simpleInference, mcpInference } = await import('../src/inference.js')
const {simpleInference, mcpInference} = await import('../src/inference.js')
describe('inference.ts', () => {
const mockRequest = {
systemPrompt: 'You are a test assistant',
prompt: 'Hello, AI!',
messages: [
{role: 'system' as const, content: 'You are a test assistant'},
{role: 'user' as const, content: 'Hello, AI!'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token'
token: 'test-token',
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('simpleInference', () => {
it('performs simple inference without tools', async () => {
const mockResponse = {
body: {
choices: [
{
message: {
content: 'Hello, user!'
}
}
]
}
choices: [
{
message: {
content: 'Hello, user!',
},
},
],
}
mockPost.mockResolvedValue(mockResponse)
mockCreate.mockResolvedValue(mockResponse)
const result = await simpleInference(mockRequest)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'Running simple inference without tools'
)
expect(core.info).toHaveBeenCalledWith('Running simple inference without tools')
expect(core.info).toHaveBeenCalledWith('Model response: Hello, user!')
// Verify the request structure
expect(mockPost).toHaveBeenCalledWith({
body: {
messages: [
{
role: 'system',
content: 'You are a test assistant'
},
{
role: 'user',
content: 'Hello, AI!'
}
],
max_tokens: 100,
model: 'gpt-4'
}
expect(mockCreate).toHaveBeenCalledWith({
messages: [
{
role: 'system',
content: 'You are a test assistant',
},
{
role: 'user',
content: 'Hello, AI!',
},
],
max_tokens: 100,
model: 'gpt-4',
})
})
it('handles null response content', async () => {
const mockResponse = {
body: {
choices: [
{
message: {
content: null
}
}
]
}
choices: [
{
message: {
content: null,
},
},
],
}
mockPost.mockResolvedValue(mockResponse)
mockCreate.mockResolvedValue(mockResponse)
const result = await simpleInference(mockRequest)
expect(result).toBeNull()
expect(core.info).toHaveBeenCalledWith(
'Model response: No response content'
)
expect(core.info).toHaveBeenCalledWith('Model response: No response content')
})
it('includes response format when specified', async () => {
const requestWithResponseFormat = {
...mockRequest,
responseFormat: {
type: 'json_schema' as const,
json_schema: {type: 'object'},
},
}
const mockResponse = {
choices: [
{
message: {
content: '{"result": "success"}',
},
},
],
}
mockCreate.mockResolvedValue(mockResponse)
const result = await simpleInference(requestWithResponseFormat)
expect(result).toBe('{"result": "success"}')
// Verify response format was included in the request
expect(mockCreate).toHaveBeenCalledWith({
messages: [
{
role: 'system',
content: 'You are a test assistant',
},
{
role: 'user',
content: 'Hello, AI!',
},
],
max_tokens: 100,
model: 'gpt-4',
response_format: requestWithResponseFormat.responseFormat,
})
})
})
@@ -122,47 +150,42 @@ describe('inference.ts', () => {
function: {
name: 'test-tool',
description: 'A test tool',
parameters: { type: 'object' }
}
}
]
parameters: {type: 'object'},
},
},
],
}
it('performs inference without tool calls', async () => {
const mockResponse = {
body: {
choices: [
{
message: {
content: 'Hello, user!',
tool_calls: null
}
}
]
}
choices: [
{
message: {
content: 'Hello, user!',
tool_calls: null,
},
},
],
}
mockPost.mockResolvedValue(mockResponse)
mockCreate.mockResolvedValue(mockResponse)
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'Running GitHub MCP inference with tools'
)
expect(core.info).toHaveBeenCalledWith('Running GitHub MCP inference with tools')
expect(core.info).toHaveBeenCalledWith('MCP inference iteration 1')
expect(core.info).toHaveBeenCalledWith(
'No tool calls requested, ending GitHub MCP inference loop'
)
expect(core.info).toHaveBeenCalledWith('No tool calls requested, ending GitHub MCP inference loop')
// The MCP inference loop will always add the assistant message, even when there are no tool calls
// So we don't check the exact messages, just that tools were included
expect(mockPost).toHaveBeenCalledTimes(1)
expect(mockCreate).toHaveBeenCalledTimes(1)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs = mockPost.mock.calls[0][0] as any
expect(callArgs.body.tools).toEqual(mockMcpClient.tools)
expect(callArgs.body.model).toBe('gpt-4')
expect(callArgs.body.max_tokens).toBe(100)
const callArgs = mockCreate.mock.calls[0][0] as any
expect(callArgs.tools).toEqual(mockMcpClient.tools)
expect(callArgs.response_format).toBeUndefined()
expect(callArgs.model).toBe('gpt-4')
expect(callArgs.max_tokens).toBe(100)
})
it('executes tool calls and continues conversation', async () => {
@@ -171,9 +194,9 @@ describe('inference.ts', () => {
id: 'call-123',
function: {
name: 'test-tool',
arguments: '{"param": "value"}'
}
}
arguments: '{"param": "value"}',
},
},
]
const toolResults = [
@@ -181,60 +204,51 @@ describe('inference.ts', () => {
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'Tool result'
}
content: 'Tool result',
},
]
// First response with tool calls
const firstResponse = {
body: {
choices: [
{
message: {
content: 'I need to use a tool.',
tool_calls: toolCalls
}
}
]
}
choices: [
{
message: {
content: 'I need to use a tool.',
tool_calls: toolCalls,
},
},
],
}
// Second response after tool execution
const secondResponse = {
body: {
choices: [
{
message: {
content: 'Here is the final answer.',
tool_calls: null
}
}
]
}
choices: [
{
message: {
content: 'Here is the final answer.',
tool_calls: null,
},
},
],
}
mockPost
.mockResolvedValueOnce(firstResponse)
.mockResolvedValueOnce(secondResponse)
mockCreate.mockResolvedValueOnce(firstResponse).mockResolvedValueOnce(secondResponse)
mockExecuteToolCalls.mockResolvedValue(toolResults)
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Here is the final answer.')
expect(mockExecuteToolCalls).toHaveBeenCalledWith(
mockMcpClient.client,
toolCalls
)
expect(mockPost).toHaveBeenCalledTimes(2)
expect(mockExecuteToolCalls).toHaveBeenCalledWith(mockMcpClient.client, toolCalls)
expect(mockCreate).toHaveBeenCalledTimes(2)
// Verify the second call includes the conversation history
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const secondCall = mockPost.mock.calls[1][0] as any
expect(secondCall.body.messages).toHaveLength(5) // system, user, assistant, tool, assistant
expect(secondCall.body.messages[2].role).toBe('assistant')
expect(secondCall.body.messages[2].tool_calls).toEqual(toolCalls)
expect(secondCall.body.messages[3]).toEqual(toolResults[0])
const secondCall = mockCreate.mock.calls[1][0] as any
expect(secondCall.messages).toHaveLength(5) // system, user, assistant, tool, assistant
expect(secondCall.messages[2].role).toBe('assistant')
expect(secondCall.messages[2].tool_calls).toEqual(toolCalls)
expect(secondCall.messages[3]).toEqual(toolResults[0])
})
it('handles maximum iteration limit', async () => {
@@ -243,9 +257,9 @@ describe('inference.ts', () => {
id: 'call-123',
function: {
name: 'test-tool',
arguments: '{}'
}
}
arguments: '{}',
},
},
]
const toolResults = [
@@ -253,58 +267,50 @@ describe('inference.ts', () => {
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'Tool result'
}
content: 'Tool result',
},
]
// Always respond with tool calls to trigger infinite loop
const responseWithToolCalls = {
body: {
choices: [
{
message: {
content: 'Using tool again.',
tool_calls: toolCalls
}
}
]
}
choices: [
{
message: {
content: 'Using tool again.',
tool_calls: toolCalls,
},
},
],
}
mockPost.mockResolvedValue(responseWithToolCalls)
mockCreate.mockResolvedValue(responseWithToolCalls)
mockExecuteToolCalls.mockResolvedValue(toolResults)
const result = await mcpInference(mockRequest, mockMcpClient)
expect(mockPost).toHaveBeenCalledTimes(5) // Max iterations reached
expect(core.warning).toHaveBeenCalledWith(
'GitHub MCP inference loop exceeded maximum iterations (5)'
)
expect(mockCreate).toHaveBeenCalledTimes(5) // Max iterations reached
expect(core.warning).toHaveBeenCalledWith('GitHub MCP inference loop exceeded maximum iterations (5)')
expect(result).toBe('Using tool again.') // Last assistant message
})
it('handles empty tool calls array', async () => {
const mockResponse = {
body: {
choices: [
{
message: {
content: 'Hello, user!',
tool_calls: []
}
}
]
}
choices: [
{
message: {
content: 'Hello, user!',
tool_calls: [],
},
},
],
}
mockPost.mockResolvedValue(mockResponse)
mockCreate.mockResolvedValue(mockResponse)
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'No tool calls requested, ending GitHub MCP inference loop'
)
expect(core.info).toHaveBeenCalledWith('No tool calls requested, ending GitHub MCP inference loop')
expect(mockExecuteToolCalls).not.toHaveBeenCalled()
})
@@ -312,52 +318,232 @@ describe('inference.ts', () => {
const toolCalls = [
{
id: 'call-123',
function: { name: 'test-tool', arguments: '{}' }
}
function: {name: 'test-tool', arguments: '{}'},
},
]
const firstResponse = {
body: {
choices: [
{
message: {
content: 'First message',
tool_calls: toolCalls
}
}
]
}
choices: [
{
message: {
content: 'First message',
tool_calls: toolCalls,
},
},
],
}
const secondResponse = {
body: {
choices: [
{
message: {
content: 'Second message',
tool_calls: toolCalls
}
}
]
}
choices: [
{
message: {
content: 'Second message',
tool_calls: toolCalls,
},
},
],
}
mockPost
.mockResolvedValueOnce(firstResponse)
.mockResolvedValue(secondResponse)
mockCreate.mockResolvedValueOnce(firstResponse).mockResolvedValue(secondResponse)
mockExecuteToolCalls.mockResolvedValue([
{
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'result'
}
content: 'result',
},
])
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Second message')
})
it('makes additional loop with response format when no tool calls are made', async () => {
const requestWithResponseFormat = {
...mockRequest,
responseFormat: {
type: 'json_schema' as const,
json_schema: {type: 'object'},
},
}
// First response without tool calls
const firstResponse = {
choices: [
{
message: {
content: 'First response',
tool_calls: null,
},
},
],
}
// Second response with response format applied
const secondResponse = {
choices: [
{
message: {
content: '{"result": "formatted response"}',
tool_calls: null,
},
},
],
}
mockCreate.mockResolvedValueOnce(firstResponse).mockResolvedValueOnce(secondResponse)
const result = await mcpInference(requestWithResponseFormat, mockMcpClient)
expect(result).toBe('{"result": "formatted response"}')
expect(mockCreate).toHaveBeenCalledTimes(2)
expect(core.info).toHaveBeenCalledWith('Making one more MCP loop with the requested response format...')
// First call should have tools but no response format
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const firstCall = mockCreate.mock.calls[0][0] as any
expect(firstCall.tools).toEqual(mockMcpClient.tools)
expect(firstCall.response_format).toBeUndefined()
// Second call should have response format but no tools
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const secondCall = mockCreate.mock.calls[1][0] as any
expect(secondCall.tools).toBeUndefined()
expect(secondCall.response_format).toEqual(requestWithResponseFormat.responseFormat)
// Second call should include the user message requesting JSON format
expect(secondCall.messages).toHaveLength(5) // system, user, assistant, user, assistant
expect(secondCall.messages[3].role).toBe('user')
expect(secondCall.messages[3].content).toContain('Please provide your response in the exact')
})
it('uses response format only on final iteration after tool calls', async () => {
const requestWithResponseFormat = {
...mockRequest,
responseFormat: {
type: 'json_schema' as const,
json_schema: {type: 'object'},
},
}
const toolCalls = [
{
id: 'call-123',
function: {
name: 'test-tool',
arguments: '{"param": "value"}',
},
},
]
const toolResults = [
{
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'Tool result',
},
]
// First response with tool calls
const firstResponse = {
choices: [
{
message: {
content: 'Using tool',
tool_calls: toolCalls,
},
},
],
}
// Second response without tool calls, but should trigger final message loop
const secondResponse = {
choices: [
{
message: {
content: 'Intermediate result',
tool_calls: null,
},
},
],
}
// Third response with response format
const thirdResponse = {
choices: [
{
message: {
content: '{"final": "result"}',
tool_calls: null,
},
},
],
}
mockCreate
.mockResolvedValueOnce(firstResponse)
.mockResolvedValueOnce(secondResponse)
.mockResolvedValueOnce(thirdResponse)
mockExecuteToolCalls.mockResolvedValue(toolResults)
const result = await mcpInference(requestWithResponseFormat, mockMcpClient)
expect(result).toBe('{"final": "result"}')
expect(mockCreate).toHaveBeenCalledTimes(3)
// First call: tools but no response format
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const firstCall = mockCreate.mock.calls[0][0] as any
expect(firstCall.tools).toEqual(mockMcpClient.tools)
expect(firstCall.response_format).toBeUndefined()
// Second call: tools but no response format (after tool execution)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const secondCall = mockCreate.mock.calls[1][0] as any
expect(secondCall.tools).toEqual(mockMcpClient.tools)
expect(secondCall.response_format).toBeUndefined()
// Third call: response format but no tools (final message)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const thirdCall = mockCreate.mock.calls[2][0] as any
expect(thirdCall.tools).toBeUndefined()
expect(thirdCall.response_format).toEqual(requestWithResponseFormat.responseFormat)
})
it('returns immediately when response format is set and finalMessage is already true', async () => {
const requestWithResponseFormat = {
...mockRequest,
responseFormat: {
type: 'json_schema' as const,
json_schema: {type: 'object'},
},
}
// Response without tool calls on what would be the final message iteration
const mockResponse = {
choices: [
{
message: {
content: '{"immediate": "result"}',
tool_calls: null,
},
},
],
}
mockCreate.mockResolvedValue(mockResponse)
// We need to test a scenario where finalMessage would already be true
// This happens when we're already in the final iteration
const result = await mcpInference(requestWithResponseFormat, mockMcpClient)
// The function should make two calls: one normal, then one with response format
expect(mockCreate).toHaveBeenCalledTimes(2)
expect(result).toBe('{"immediate": "result"}')
})
})
})

View File

@@ -0,0 +1,236 @@
import {describe, it, expect, beforeEach, vi, type MockedFunction, type Mock} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Create fs mocks
const mockExistsSync = vi.fn()
const mockReadFileSync = vi.fn()
const mockWriteFileSync = vi.fn()
// Create inference mocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockSimpleInference = vi.fn() as MockedFunction<any>
const mockMcpInference = vi.fn()
// Create MCP mocks
const mockConnectToGitHubMCP = vi.fn()
// Mock fs module
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync,
}))
// Mock the inference functions
vi.mock('../src/inference.js', () => ({
simpleInference: mockSimpleInference,
mcpInference: mockMcpInference,
}))
// Mock the MCP connection
vi.mock('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP,
}))
vi.mock('@actions/core', () => core)
// Mock process.exit to prevent it from actually exiting during tests
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
// Prevent actual exit, but don't throw - just return
return undefined as never
})
// The module being tested should be imported dynamically. This ensures that the
// mocks are used in place of any actual dependencies.
const {run} = await import('../src/main.js')
describe('main.ts - prompt.yml integration', () => {
beforeEach(() => {
vi.clearAllMocks()
mockProcessExit.mockClear()
// Mock environment variables
process.env['GITHUB_TOKEN'] = 'test-token'
// Mock core.getInput to return appropriate values
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
// Mock core.getBooleanInput
const mockGetBooleanInput = core.getBooleanInput as Mock
mockGetBooleanInput.mockReturnValue(false)
// Mock fs.readFileSync for prompt file
mockReadFileSync.mockReturnValue(`
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o
`)
// Mock fs.writeFileSync
mockWriteFileSync.mockImplementation(() => {})
// Mock simpleInference
mockSimpleInference.mockResolvedValue('Mocked AI response')
})
it('should handle prompt YAML files with template variables', async () => {
mockExistsSync.mockReturnValue(true)
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'prompt-file':
return 'test.prompt.yml'
case 'input':
return 'a: cats\nb: dogs'
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
// Expect the run function to complete successfully
await run()
// Verify process.exit was called with code 0 (success)
expect(mockProcessExit).toHaveBeenCalledWith(0)
// Verify simpleInference was called with the correct message structure
expect(mockSimpleInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: 'Be as concise as possible',
},
{
role: 'user',
content: 'Compare cats and dogs, please',
},
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token',
}),
)
// Verify outputs were set
expect(core.setOutput).toHaveBeenCalledWith('response', 'Mocked AI response')
expect(core.setOutput).toHaveBeenCalledWith('response-file', expect.any(String))
})
it('supports file_input variables to load file contents', async () => {
mockExistsSync.mockReturnValue(true)
// First call: reading the prompt file. Second call: reading file_input referenced file contents.
const externalFilePath = 'vars.txt'
mockReadFileSync.mockImplementation((path: string) => {
if (path === 'test.prompt.yml') {
return `messages:\n - role: user\n content: 'Here is the data: {{blob}}'\nmodel: openai/gpt-4o\n`
}
if (path === externalFilePath) {
return 'FILE_CONTENTS'
}
return ''
})
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'prompt-file':
return 'test.prompt.yml'
case 'file_input':
return `blob: ${externalFilePath}`
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
await run()
expect(mockSimpleInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [{role: 'user', content: 'Here is the data: FILE_CONTENTS'}],
}),
)
// Verify process.exit was called with code 0 (success)
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('should fall back to legacy format when not using prompt YAML', async () => {
mockExistsSync.mockReturnValue(false)
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'prompt':
return 'Hello, world!'
case 'system-prompt':
return 'You are helpful'
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
await run()
// Verify simpleInference was called with converted message format
expect(mockSimpleInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: 'You are helpful',
},
{
role: 'user',
content: 'Hello, world!',
},
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token',
}),
)
// Verify process.exit was called with code 0 (success)
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
})

View File

@@ -1,31 +1,21 @@
/**
* Unit tests for the action's main functionality, src/main.ts
*/
import { jest } from '@jest/globals'
import {vi, describe, expect, it, beforeEach, type MockedFunction} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Default to throwing errors to catch unexpected calls
const mockExistsSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to existsSync - test should override this implementation'
)
const mockExistsSync = vi.fn().mockImplementation(() => {
throw new Error('Unexpected call to existsSync - test should override this implementation')
})
const mockReadFileSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to readFileSync - test should override this implementation'
)
const mockReadFileSync = vi.fn().mockImplementation(() => {
throw new Error('Unexpected call to readFileSync - test should override this implementation')
})
const mockWriteFileSync = jest.fn()
const mockWriteFileSync = vi.fn()
/**
* Helper function to mock file system operations for one or more files
* @param fileContents - Object mapping file paths to their contents
* @param nonExistentFiles - Array of file paths that should be treated as non-existent
*/
function mockFileContent(
fileContents: Record<string, string> = {},
nonExistentFiles: string[] = []
): void {
function mockFileContent(fileContents: Record<string, string> = {}, nonExistentFiles: string[] = []): void {
// Mock existsSync to return true for files that exist, false for those that don't
mockExistsSync.mockImplementation((...args: unknown[]): boolean => {
const [path] = args as [string]
@@ -55,11 +45,11 @@ function mockInputs(inputs: Record<string, string> = {}): void {
token: 'fake-token',
model: 'gpt-4',
'max-tokens': '100',
endpoint: 'https://api.test.com'
endpoint: 'https://api.test.com',
}
// Combine defaults with user-provided inputs
const allInputs: Record<string, string> = { ...defaultInputs, ...inputs }
const allInputs: Record<string, string> = {...defaultInputs, ...inputs}
core.getInput.mockImplementation((name: string) => {
return allInputs[name] || ''
@@ -76,46 +66,62 @@ function mockInputs(inputs: Record<string, string> = {}): void {
*/
function verifyStandardResponse(): void {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'response', 'Hello, user!')
expect(core.setOutput).toHaveBeenNthCalledWith(
2,
'response-file',
expect.stringContaining('modelResponse.txt')
)
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', expect.stringContaining('modelResponse-'))
}
jest.unstable_mockModule('fs', () => ({
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync
writeFileSync: mockWriteFileSync,
}))
// Mocks for tmp module to control temporary file creation and cleanup
const mockRemoveCallback = vi.fn()
const mockFileSync = vi.fn().mockReturnValue({
name: '/secure/temp/dir/modelResponse-abc123.txt',
removeCallback: mockRemoveCallback,
})
const mockSetGracefulCleanup = vi.fn()
vi.mock('tmp', () => ({
fileSync: mockFileSync,
setGracefulCleanup: mockSetGracefulCleanup,
}))
// Mock MCP and inference modules
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConnectToGitHubMCP = jest.fn() as jest.MockedFunction<any>
const mockConnectToGitHubMCP = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockSimpleInference = jest.fn() as jest.MockedFunction<any>
const mockSimpleInference = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockMcpInference = jest.fn() as jest.MockedFunction<any>
const mockMcpInference = vi.fn() as MockedFunction<any>
jest.unstable_mockModule('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP
vi.mock('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP,
}))
jest.unstable_mockModule('../src/inference.js', () => ({
vi.mock('../src/inference.js', () => ({
simpleInference: mockSimpleInference,
mcpInference: mockMcpInference
mcpInference: mockMcpInference,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Mock process.exit to prevent it from actually exiting during tests
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
// Prevent actual exit, but don't throw - just return
return undefined as never
})
// The module being tested should be imported dynamically. This ensures that the
// mocks are used in place of any actual dependencies.
const { run } = await import('../src/main.js')
const {run} = await import('../src/main.js')
describe('main.ts', () => {
// Reset all mocks before each test
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockProcessExit.mockClear()
// Remove any existing GITHUB_TOKEN
delete process.env.GITHUB_TOKEN
@@ -128,62 +134,65 @@ describe('main.ts', () => {
it('Sets the response output', async () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.'
'system-prompt': 'You are a test assistant.',
})
await run()
expect(core.setOutput).toHaveBeenCalled()
verifyStandardResponse()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('Sets a failed status when no prompt is set', async () => {
mockInputs({
prompt: '',
'prompt-file': ''
'prompt-file': '',
})
await run()
expect(core.setFailed).toHaveBeenNthCalledWith(
1,
'Neither prompt-file nor prompt was set'
)
expect(core.setFailed).toHaveBeenCalledWith('Neither prompt-file nor prompt was set')
expect(mockProcessExit).toHaveBeenCalledWith(1)
})
it('uses simple inference when MCP is disabled', async () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'false'
'enable-github-mcp': 'false',
})
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
systemPrompt: 'You are a test assistant.',
prompt: 'Hello, AI!',
messages: [
{role: 'system', content: 'You are a test assistant.'},
{role: 'user', content: 'Hello, AI!'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token'
token: 'fake-token',
responseFormat: undefined,
})
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
verifyStandardResponse()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('uses MCP inference when enabled and connection succeeds', async () => {
const mockMcpClient = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client: {} as any,
tools: [{ type: 'function', function: { name: 'test-tool' } }]
tools: [{type: 'function', function: {name: 'test-tool'}}],
}
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'true'
'enable-github-mcp': 'true',
})
mockConnectToGitHubMCP.mockResolvedValue(mockMcpClient)
@@ -193,21 +202,24 @@ describe('main.ts', () => {
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockMcpInference).toHaveBeenCalledWith(
expect.objectContaining({
systemPrompt: 'You are a test assistant.',
prompt: 'Hello, AI!',
token: 'fake-token'
messages: [
{role: 'system', content: 'You are a test assistant.'},
{role: 'user', content: 'Hello, AI!'},
],
token: 'fake-token',
}),
mockMcpClient
mockMcpClient,
)
expect(mockSimpleInference).not.toHaveBeenCalled()
verifyStandardResponse()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('falls back to simple inference when MCP connection fails', async () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'true'
'enable-github-mcp': 'true',
})
mockConnectToGitHubMCP.mockResolvedValue(null)
@@ -217,10 +229,9 @@ describe('main.ts', () => {
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockSimpleInference).toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
expect(core.warning).toHaveBeenCalledWith(
'MCP connection failed, falling back to simple inference'
)
expect(core.warning).toHaveBeenCalledWith('MCP connection failed, falling back to simple inference')
verifyStandardResponse()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('properly integrates with loadContentFromFileOrInput', async () => {
@@ -231,26 +242,30 @@ describe('main.ts', () => {
mockFileContent({
[promptFile]: promptContent,
[systemPromptFile]: systemPromptContent
[systemPromptFile]: systemPromptContent,
})
mockInputs({
'prompt-file': promptFile,
'system-prompt-file': systemPromptFile,
'enable-github-mcp': 'false'
'enable-github-mcp': 'false',
})
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
systemPrompt: systemPromptContent,
prompt: promptContent,
messages: [
{role: 'system', content: systemPromptContent},
{role: 'user', content: promptContent},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token'
token: 'fake-token',
responseFormat: undefined,
})
verifyStandardResponse()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('handles non-existent prompt-file with an error', async () => {
@@ -259,13 +274,51 @@ describe('main.ts', () => {
mockFileContent({}, [promptFile])
mockInputs({
'prompt-file': promptFile
'prompt-file': promptFile,
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
`File for prompt-file was not found: ${promptFile}`
)
expect(core.setFailed).toHaveBeenCalledWith(`File for prompt-file was not found: ${promptFile}`)
expect(mockProcessExit).toHaveBeenCalledWith(1)
})
it('creates secure temporary files with proper cleanup', async () => {
mockInputs({
prompt: 'Test prompt',
'system-prompt': 'You are a test assistant.',
})
await run()
expect(mockSetGracefulCleanup).toHaveBeenCalledOnce()
expect(mockFileSync).toHaveBeenCalledWith({
prefix: 'modelResponse-',
postfix: '.txt',
})
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', '/secure/temp/dir/modelResponse-abc123.txt')
expect(mockWriteFileSync).toHaveBeenCalledWith('/secure/temp/dir/modelResponse-abc123.txt', 'Hello, user!', 'utf-8')
expect(mockRemoveCallback).toHaveBeenCalledOnce()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('handles cleanup errors gracefully', async () => {
mockRemoveCallback.mockImplementationOnce(() => {
throw new Error('Cleanup failed')
})
mockInputs({
prompt: 'Test prompt',
'system-prompt': 'You are a test assistant.',
})
await run()
expect(mockRemoveCallback).toHaveBeenCalledOnce()
expect(core.warning).toHaveBeenCalledWith('Failed to cleanup temporary file: Error: Cleanup failed')
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
})

View File

@@ -1,45 +1,37 @@
/**
* Unit tests for the MCP module, src/mcp.ts
*/
import { jest } from '@jest/globals'
import {vi, type MockedFunction, describe, it, expect, beforeEach} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock MCP SDK
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConnect = jest.fn() as jest.MockedFunction<any>
const mockConnect = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockListTools = jest.fn() as jest.MockedFunction<any>
const mockListTools = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCallTool = jest.fn() as jest.MockedFunction<any>
const mockCallTool = vi.fn() as MockedFunction<any>
const mockClient = {
connect: mockConnect,
listTools: mockListTools,
callTool: mockCallTool
callTool: mockCallTool,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any
jest.unstable_mockModule('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: jest.fn(() => mockClient)
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: vi.fn(() => mockClient),
}))
jest.unstable_mockModule(
'@modelcontextprotocol/sdk/client/streamableHttp.js',
() => ({
StreamableHTTPClientTransport: jest.fn()
})
)
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
StreamableHTTPClientTransport: vi.fn(),
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { connectToGitHubMCP, executeToolCall, executeToolCalls } = await import(
'../src/mcp.js'
)
const {connectToGitHubMCP, executeToolCall, executeToolCalls} = await import('../src/mcp.js')
describe('mcp.ts', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('connectToGitHubMCP', () => {
@@ -49,20 +41,20 @@ describe('mcp.ts', () => {
{
name: 'test-tool-1',
description: 'Test tool 1',
inputSchema: { type: 'object', properties: {} }
inputSchema: {type: 'object', properties: {}},
},
{
name: 'test-tool-2',
description: 'Test tool 2',
inputSchema: {
type: 'object',
properties: { param: { type: 'string' } }
}
}
properties: {param: {type: 'string'}},
},
},
]
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: mockTools })
mockListTools.mockResolvedValue({tools: mockTools})
const result = await connectToGitHubMCP(token)
@@ -74,21 +66,13 @@ describe('mcp.ts', () => {
function: {
name: 'test-tool-1',
description: 'Test tool 1',
parameters: { type: 'object', properties: {} }
}
parameters: {type: 'object', properties: {}},
},
})
expect(core.info).toHaveBeenCalledWith(
'Connecting to GitHub MCP server...'
)
expect(core.info).toHaveBeenCalledWith(
'Successfully connected to GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 2 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 2 GitHub MCP tools for Azure AI Inference'
)
expect(core.info).toHaveBeenCalledWith('Connecting to GitHub MCP server...')
expect(core.info).toHaveBeenCalledWith('Successfully connected to GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Retrieved 2 tools from GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Mapped 2 GitHub MCP tools for Azure AI Inference')
})
it('returns null when connection fails', async () => {
@@ -100,27 +84,21 @@ describe('mcp.ts', () => {
const result = await connectToGitHubMCP(token)
expect(result).toBeNull()
expect(core.warning).toHaveBeenCalledWith(
'Failed to connect to GitHub MCP server: Error: Connection failed'
)
expect(core.warning).toHaveBeenCalledWith('Failed to connect to GitHub MCP server: Error: Connection failed')
})
it('handles empty tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: [] })
mockListTools.mockResolvedValue({tools: []})
const result = await connectToGitHubMCP(token)
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 0 GitHub MCP tools for Azure AI Inference'
)
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Mapped 0 GitHub MCP tools for Azure AI Inference')
})
it('handles undefined tools list', async () => {
@@ -133,9 +111,7 @@ describe('mcp.ts', () => {
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
})
})
@@ -146,11 +122,11 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'test-tool',
arguments: '{"param": "value"}'
}
arguments: '{"param": "value"}',
},
}
const toolResult = {
content: [{ type: 'text', text: 'Tool execution result' }]
content: [{type: 'text', text: 'Tool execution result'}],
}
mockCallTool.mockResolvedValue(toolResult)
@@ -159,20 +135,16 @@ describe('mcp.ts', () => {
expect(mockCallTool).toHaveBeenCalledWith({
name: 'test-tool',
arguments: { param: 'value' }
arguments: {param: 'value'},
})
expect(result).toEqual({
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: JSON.stringify(toolResult.content)
content: JSON.stringify(toolResult.content),
})
expect(core.info).toHaveBeenCalledWith(
'Executing GitHub MCP tool: test-tool with args: {"param": "value"}'
)
expect(core.info).toHaveBeenCalledWith(
'GitHub MCP tool test-tool executed successfully'
)
expect(core.info).toHaveBeenCalledWith('Executing GitHub MCP tool: test-tool with args: {"param": "value"}')
expect(core.info).toHaveBeenCalledWith('GitHub MCP tool test-tool executed successfully')
})
it('handles tool execution errors gracefully', async () => {
@@ -181,8 +153,8 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'failing-tool',
arguments: '{"param": "value"}'
}
arguments: '{"param": "value"}',
},
}
const toolError = new Error('Tool execution failed')
@@ -194,10 +166,10 @@ describe('mcp.ts', () => {
tool_call_id: 'call-456',
role: 'tool',
name: 'failing-tool',
content: 'Error: Error: Tool execution failed'
content: 'Error: Error: Tool execution failed',
})
expect(core.warning).toHaveBeenCalledWith(
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed'
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed',
)
})
@@ -207,8 +179,8 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'test-tool',
arguments: 'invalid-json'
}
arguments: 'invalid-json',
},
}
const result = await executeToolCall(mockClient, toolCall)
@@ -217,9 +189,7 @@ describe('mcp.ts', () => {
expect(result.role).toBe('tool')
expect(result.name).toBe('test-tool')
expect(result.content).toContain('Error:')
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining('Failed to execute GitHub MCP tool test-tool:')
)
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to execute GitHub MCP tool test-tool:'))
})
})
@@ -229,21 +199,21 @@ describe('mcp.ts', () => {
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
function: {name: 'tool-1', arguments: '{}'},
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{"param": "value"}' }
}
function: {name: 'tool-2', arguments: '{"param": "value"}'},
},
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
content: [{type: 'text', text: 'Result 1'}],
})
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 2' }]
content: [{type: 'text', text: 'Result 2'}],
})
const results = await executeToolCalls(mockClient, toolCalls)
@@ -266,18 +236,18 @@ describe('mcp.ts', () => {
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
function: {name: 'tool-1', arguments: '{}'},
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{}' }
}
function: {name: 'tool-2', arguments: '{}'},
},
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
content: [{type: 'text', text: 'Result 1'}],
})
.mockRejectedValueOnce(new Error('Tool 2 failed'))

139
__tests__/prompt.test.ts Normal file
View File

@@ -0,0 +1,139 @@
import {describe, it, expect} from 'vitest'
import * as path from 'path'
import {fileURLToPath} from 'url'
import {
parseTemplateVariables,
replaceTemplateVariables,
loadPromptFile,
isPromptYamlFile,
parseFileTemplateVariables,
} from '../src/prompt'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
describe('prompt.ts', () => {
describe('parseTemplateVariables', () => {
it('should parse simple YAML variables', () => {
const input = `
a: hello
b: world
`
const result = parseTemplateVariables(input)
expect(result).toEqual({a: 'hello', b: 'world'})
})
it('should parse multiline variables', () => {
const input = `
var1: hello
var2: |
This is a
multiline string
`
const result = parseTemplateVariables(input)
expect(result.var1).toBe('hello')
expect(result.var2).toContain('This is a')
expect(result.var2).toContain('multiline string')
})
it('should return empty object for empty input', () => {
const result = parseTemplateVariables('')
expect(result).toEqual({})
})
it('should throw error for invalid YAML', () => {
const input = 'invalid: yaml: content:'
expect(() => parseTemplateVariables(input)).toThrow()
})
})
describe('replaceTemplateVariables', () => {
it('should replace simple variables', () => {
const text = 'Hello {{name}}, welcome to {{place}}!'
const variables = {name: 'John', place: 'GitHub'}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to GitHub!')
})
it('should leave unreplaced variables as is', () => {
const text = 'Hello {{name}}, welcome to {{unknown}}!'
const variables = {name: 'John'}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to {{unknown}}!')
})
it('should handle no variables', () => {
const text = 'No variables here'
const variables = {}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('No variables here')
})
})
describe('isPromptYamlFile', () => {
it('should detect .prompt.yml files', () => {
expect(isPromptYamlFile('test.prompt.yml')).toBe(true)
expect(isPromptYamlFile('path/to/test.prompt.yml')).toBe(true)
})
it('should detect .prompt.yaml files', () => {
expect(isPromptYamlFile('test.prompt.yaml')).toBe(true)
expect(isPromptYamlFile('path/to/test.prompt.yaml')).toBe(true)
})
it('should reject other file types', () => {
expect(isPromptYamlFile('test.txt')).toBe(false)
expect(isPromptYamlFile('test.yml')).toBe(false)
expect(isPromptYamlFile('test.yaml')).toBe(false)
expect(isPromptYamlFile('test.prompt')).toBe(false)
})
})
describe('loadPromptFile', () => {
it('should load simple prompt file', () => {
const filePath = path.join(__dirname, '../__fixtures__/prompts/simple.prompt.yml')
const variables = {a: 'cats', b: 'dogs'}
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
expect(result.messages[0]).toEqual({
role: 'system',
content: 'Be as concise as possible',
})
expect(result.messages[1]).toEqual({
role: 'user',
content: 'Compare cats and dogs, please',
})
expect(result.model).toBe('openai/gpt-4o')
})
it('should load JSON schema prompt file', () => {
const filePath = path.join(__dirname, '../__fixtures__/prompts/json-schema.prompt.yml')
const variables = {animal: 'dog'}
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
expect(result.messages[1].content).toContain('Describe a dog')
expect(result.responseFormat).toBe('json_schema')
expect(result.jsonSchema).toBeDefined()
expect(result.jsonSchema).toContain('describe_animal')
})
it('should throw error for non-existent file', () => {
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow('Prompt file not found')
})
})
describe('parseFileTemplateVariables', () => {
it('reads file contents for variables', () => {
const configPath = path.join(__dirname, '../__fixtures__/prompts/json-schema.prompt.yml')
const data = parseFileTemplateVariables(`sample: ${configPath}`)
expect(data.sample).toContain('messages:')
expect(data.sample).toContain('responseFormat:')
})
it('errors on missing files', () => {
expect(() => parseFileTemplateVariables('x: ./does-not-exist.txt')).toThrow('was not found')
})
})
})

View File

@@ -14,7 +14,16 @@ inputs:
required: false
default: ''
prompt-file:
description: Path to a file containing the prompt
description: Path to a file containing the prompt (supports .txt and .prompt.yml
formats)
required: false
default: ''
input:
description: Template variables in YAML format for .prompt.yml files
required: false
default: ''
file_input:
description: Template variables in YAML format mapping variable names to file paths. The file contents will be used for templating.
required: false
default: ''
model:
@@ -45,6 +54,10 @@ inputs:
description: Enable Model Context Protocol integration with GitHub tools
required: false
default: 'false'
github-mcp-token:
description: The token to use for GitHub MCP server (defaults to GITHUB_TOKEN if not specified)
required: false
default: ''
# Define your outputs here.
outputs:
@@ -54,5 +67,5 @@ outputs:
description: The file path where the response is saved
runs:
using: node20
using: node24
main: dist/index.js

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 84.21%"><title>Coverage: 84.21%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">84.21%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">84.21%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

44910
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,50 +1,43 @@
// See: https://eslint.org/docs/latest/use/configure/configuration-files
import { fixupPluginRules } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
import {FlatCompat} from '@eslint/eslintrc'
import js from '@eslint/js'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import _import from 'eslint-plugin-import'
import jest from 'eslint-plugin-jest'
import prettier from 'eslint-plugin-prettier'
import globals from 'globals'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {fileURLToPath} from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
allConfig: js.configs.all,
})
export default [
{
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules']
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules'],
},
...compat.extends(
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
),
{
plugins: {
import: fixupPluginRules(_import),
jest,
prettier,
'@typescript-eslint': typescriptEslint
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
SharedArrayBuffer: 'readonly',
},
parser: tsParser,
@@ -53,17 +46,17 @@ export default [
parserOptions: {
project: ['tsconfig.eslint.json'],
tsconfigRootDir: '.'
}
tsconfigRootDir: '.',
},
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: 'tsconfig.eslint.json'
}
}
project: 'tsconfig.eslint.json',
},
},
},
rules: {
@@ -75,7 +68,7 @@ export default [
'no-console': 'off',
'no-shadow': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error'
}
}
'prettier/prettier': 'error',
},
},
]

View File

@@ -1,40 +0,0 @@
// See: https://jestjs.io/docs/configuration
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['./src/**'],
coverageDirectory: './coverage',
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
coverageReporters: ['json-summary', 'text', 'lcov'],
// Uncomment the below lines if you would like to enforce a coverage threshold
// for your action. This will fail the build if the coverage is below the
// specified thresholds.
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100
// }
// },
extensionsToTreatAsEsm: ['.ts'],
moduleFileExtensions: ['ts', 'js'],
preset: 'ts-jest',
reporters: ['default'],
resolver: 'ts-jest-resolver',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: 'tsconfig.eslint.json',
useESM: true
}
]
},
verbose: true
}

5599
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,58 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"name": "ai-inference",
"version": "1.0.0",
"author": "",
"type": "module",
"private": true,
"homepage": "https://github.com/actions/typescript-action",
"repository": {
"type": "git",
"url": "git+https://github.com/actions/typescript-action.git"
},
"bugs": {
"url": "https://github.com/actions/typescript-action/issues"
},
"keywords": [
"actions"
],
"exports": {
".": "./dist/index.js"
},
"engines": {
"node": ">=20"
"node": ">=24"
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
"format:write": "npx prettier --write .",
"format:check": "npx prettier --check .",
"lint": "npx eslint .",
"local-action": "npx @github/local-action . src/main.ts .env",
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
"package:watch": "npm run package -- --watch",
"test": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
"test": "vitest --run",
"test:watch": "vitest --watch",
"all": "npm run format:write && npm run lint && npm run test && npm run package"
},
"license": "MIT",
"prettier": "@github/prettier-config",
"dependencies": {
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.15.1",
"@rollup/plugin-json": "^6.1.0",
"pkce-challenge": "^5.0.0"
"@types/tmp": "^0.2.6",
"js-yaml": "^4.1.0",
"openai": "^5.11.0",
"pkce-challenge": "^5.0.0",
"tmp": "^0.2.4"
},
"devDependencies": {
"@azure-rest/ai-inference": "latest",
"@azure/core-auth": "latest",
"@azure/core-sse": "latest",
"@eslint/compat": "^1.3.0",
"@github/local-action": "^3.2.1",
"@jest/globals": "^30.0.2",
"@github/local-action": "^5.1.0",
"@github/prettier-config": "^0.0.6",
"@rollup/plugin-commonjs": "^28.0.5",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.31",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.14.0",
"eslint-plugin-prettier": "^5.4.1",
"jest": "^29.7.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.4.2",
"rollup": "^4.43.0",
"ts-jest": "^29.4.0",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^3"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"

View File

@@ -1,5 +1,5 @@
// See: https://rollupjs.org/introduction/
import { builtinModules } from 'node:module'
import {builtinModules} from 'node:module'
import commonjs from '@rollup/plugin-commonjs'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
@@ -11,26 +11,21 @@ const config = {
esModule: true,
file: 'dist/index.js',
format: 'es',
sourcemap: true
sourcemap: true,
},
external: [
...builtinModules,
/^node:/,
'@actions/core',
'@actions/github' // Those are preinstalled on the runner
],
external: [...builtinModules, /^node:/],
plugins: [
typescript(),
nodeResolve({
preferBuiltins: true,
browser: false,
exportConditions: ['node']
exportConditions: ['node'],
}),
commonjs({
include: /node_modules/
include: /node_modules/,
}),
json()
]
json(),
],
}
export default config

View File

@@ -1,6 +1,7 @@
import * as core from '@actions/core'
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
import * as fs from 'fs'
import {PromptConfig} from './prompt.js'
import {InferenceRequest} from './inference.js'
/**
* Helper function to load content from a file or use fallback input
@@ -9,11 +10,7 @@ import * as fs from 'fs'
* @param defaultValue - Default value to use if neither file nor content is provided
* @returns The loaded content
*/
export function loadContentFromFileOrInput(
filePathInput: string,
contentInput: string,
defaultValue?: string
): string {
export function loadContentFromFileOrInput(filePathInput: string, contentInput: string, defaultValue?: string): string {
const filePath = core.getInput(filePathInput)
const contentString = core.getInput(contentInput)
@@ -32,35 +29,72 @@ export function loadContentFromFileOrInput(
}
/**
* Helper function to handle unexpected responses from AI service
* @param response - The response object from the AI service
* @throws Error with appropriate error message based on response content
* Build messages array from either prompt config or legacy format
*/
export function handleUnexpectedResponse(
response: GetChatCompletionsDefaultResponse
): never {
// Extract x-ms-error-code from headers if available
const errorCode = response.headers['x-ms-error-code']
const errorCodeMsg = errorCode ? ` (error code: ${errorCode})` : ''
// Check if response body exists and contains error details
if (response.body && response.body.error) {
throw response.body.error
export function buildMessages(
promptConfig?: PromptConfig,
systemPrompt?: string,
prompt?: string,
): Array<{role: 'system' | 'user' | 'assistant' | 'tool'; content: string}> {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map(msg => ({
role: msg.role as 'system' | 'user' | 'assistant' | 'tool',
content: msg.content,
}))
} else {
// Use legacy format
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant',
},
{role: 'user', content: prompt || ''},
]
}
}
/**
* Build response format object for API from prompt config
*/
export function buildResponseFormat(
promptConfig?: PromptConfig,
): {type: 'json_schema'; json_schema: unknown} | undefined {
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
try {
const schema = JSON.parse(promptConfig.jsonSchema)
return {
type: 'json_schema',
json_schema: schema,
}
} catch (error) {
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
return undefined
}
/**
* Build complete InferenceRequest from prompt config and inputs
*/
export function buildInferenceRequest(
promptConfig: PromptConfig | undefined,
systemPrompt: string | undefined,
prompt: string | undefined,
modelName: string,
maxTokens: number,
endpoint: string,
token: string,
): InferenceRequest {
const messages = buildMessages(promptConfig, systemPrompt, prompt)
const responseFormat = buildResponseFormat(promptConfig)
return {
messages,
modelName,
maxTokens,
endpoint,
token,
responseFormat,
}
// Handle case where response body is missing
if (!response.body) {
throw new Error(
`Failed to get response from AI service (status: ${response.status})${errorCodeMsg}. ` +
'Please check network connection and endpoint configuration.'
)
}
// Handle other error cases
throw new Error(
`AI service returned error response (status: ${response.status})${errorCodeMsg}: ` +
(typeof response.body === 'string'
? response.body
: JSON.stringify(response.body))
)
}

View File

@@ -2,7 +2,7 @@
* The entrypoint for the action. This file simply imports and runs the action's
* main logic.
*/
import { run } from './main.js'
import {run} from './main.js'
/* istanbul ignore next */
run()

View File

@@ -1,16 +1,21 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import { GitHubMCPClient, executeToolCalls } from './mcp.js'
import { handleUnexpectedResponse } from './helpers.js'
import OpenAI from 'openai'
import {GitHubMCPClient, executeToolCalls, ToolCall} from './mcp.js'
interface ChatMessage {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | null
tool_calls?: ToolCall[]
tool_call_id?: string
}
export interface InferenceRequest {
systemPrompt: string
prompt: string
messages: Array<{role: 'system' | 'user' | 'assistant' | 'tool'; content: string}>
modelName: string
maxTokens: number
endpoint: string
token: string
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
}
export interface InferenceResponse {
@@ -28,43 +33,41 @@ export interface InferenceResponse {
/**
* Simple one-shot inference without tools
*/
export async function simpleInference(
request: InferenceRequest
): Promise<string | null> {
export async function simpleInference(request: InferenceRequest): Promise<string | null> {
core.info('Running simple inference without tools')
const client = ModelClient(
request.endpoint,
new AzureKeyCredential(request.token),
{
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
}
)
const requestBody = {
messages: [
{
role: 'system',
content: request.systemPrompt
},
{ role: 'user', content: request.prompt }
],
max_tokens: request.maxTokens,
model: request.modelName
}
const response = await client.path('/chat/completions').post({
body: requestBody
const client = new OpenAI({
apiKey: request.token,
baseURL: request.endpoint,
})
if (isUnexpected(response)) {
handleUnexpectedResponse(response)
const chatCompletionRequest: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
max_tokens: request.maxTokens,
model: request.modelName,
}
const modelResponse = response.body.choices[0].message.content
core.info(`Model response: ${modelResponse || 'No response content'}`)
// Add response format if specified
if (request.responseFormat) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chatCompletionRequest.response_format = request.responseFormat as any
}
return modelResponse
try {
const response = await client.chat.completions.create(chatCompletionRequest)
if ('choices' in response) {
const modelResponse = response.choices[0]?.message?.content
core.info(`Model response: ${modelResponse || 'No response content'}`)
return modelResponse || null
} else {
core.error(`Unexpected response format from API: ${JSON.stringify(response)}`)
return null
}
} catch (error) {
core.error(`API error: ${error}`)
throw error
}
}
/**
@@ -72,89 +75,106 @@ export async function simpleInference(
*/
export async function mcpInference(
request: InferenceRequest,
githubMcpClient: GitHubMCPClient
githubMcpClient: GitHubMCPClient,
): Promise<string | null> {
core.info('Running GitHub MCP inference with tools')
const client = ModelClient(
request.endpoint,
new AzureKeyCredential(request.token),
{
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
}
)
const client = new OpenAI({
apiKey: request.token,
baseURL: request.endpoint,
})
// Start with the initial conversation
const messages = [
{
role: 'system',
content: request.systemPrompt
},
{ role: 'user', content: request.prompt }
]
// Start with the pre-processed messages
const messages: ChatMessage[] = [...request.messages]
let iterationCount = 0
const maxIterations = 5 // Prevent infinite loops
// We want to use response_format (e.g. JSON) on the last iteration only, so the model can output
// the final result in the expected format without interfering with tool calls
let finalMessage = false
while (iterationCount < maxIterations) {
iterationCount++
core.info(`MCP inference iteration ${iterationCount}`)
const requestBody = {
messages: messages,
const chatCompletionRequest: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
max_tokens: request.maxTokens,
model: request.modelName,
tools: githubMcpClient.tools
}
const response = await client.path('/chat/completions').post({
body: requestBody
})
if (isUnexpected(response)) {
handleUnexpectedResponse(response)
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
if (finalMessage && request.responseFormat) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chatCompletionRequest.response_format = request.responseFormat as any
} else {
chatCompletionRequest.tools = githubMcpClient.tools as OpenAI.Chat.Completions.ChatCompletionTool[]
}
const assistantMessage = response.body.choices[0].message
const modelResponse = assistantMessage.content
const toolCalls = assistantMessage.tool_calls
try {
const response = await client.chat.completions.create(chatCompletionRequest)
core.info(`Model response: ${modelResponse || 'No response content'}`)
if (!('choices' in response)) {
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`)
}
messages.push({
role: 'assistant',
content: modelResponse || '',
...(toolCalls && { tool_calls: toolCalls })
})
const assistantMessage = response.choices[0]?.message
const modelResponse = assistantMessage?.content
const toolCalls = assistantMessage?.tool_calls
if (!toolCalls || toolCalls.length === 0) {
core.info('No tool calls requested, ending GitHub MCP inference loop')
return modelResponse
core.info(`Model response: ${modelResponse || 'No response content'}`)
messages.push({
role: 'assistant',
content: modelResponse || '',
...(toolCalls && {tool_calls: toolCalls as ToolCall[]}),
})
if (!toolCalls || toolCalls.length === 0) {
core.info('No tool calls requested, ending GitHub MCP inference loop')
// If we have a response format set and we haven't explicitly run one final message iteration,
// do another loop with the response format set
if (request.responseFormat && !finalMessage) {
core.info('Making one more MCP loop with the requested response format...')
// Add a user message requesting JSON format and try again
messages.push({
role: 'user',
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
})
finalMessage = true
// Continue the loop to get a properly formatted response
continue
} else {
return modelResponse || null
}
}
core.info(`Model requested ${toolCalls.length} tool calls`)
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[])
// Add tool results to the conversation
messages.push(...toolResults)
core.info('Tool results added, continuing conversation...')
} catch (error) {
core.error(`OpenAI API error: ${error}`)
throw error
}
core.info(`Model requested ${toolCalls.length} tool calls`)
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(
githubMcpClient.client,
toolCalls
)
// Add tool results to the conversation
messages.push(...toolResults)
core.info('Tool results added, continuing conversation...')
}
core.warning(
`GitHub MCP inference loop exceeded maximum iterations (${maxIterations})`
)
core.warning(`GitHub MCP inference loop exceeded maximum iterations (${maxIterations})`)
// Return the last assistant message content
const lastAssistantMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === 'assistant')
.find(msg => msg.role === 'assistant')
return lastAssistantMessage?.content || null
}

View File

@@ -1,12 +1,16 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { connectToGitHubMCP } from './mcp.js'
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
import { loadContentFromFileOrInput } from './helpers.js'
const RESPONSE_FILE = 'modelResponse.txt'
import * as tmp from 'tmp'
import {connectToGitHubMCP} from './mcp.js'
import {simpleInference, mcpInference} from './inference.js'
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js'
import {
loadPromptFile,
parseTemplateVariables,
isPromptYamlFile,
PromptConfig,
parseFileTemplateVariables,
} from './prompt.js'
/**
* The main function for the action.
@@ -14,39 +18,70 @@ const RESPONSE_FILE = 'modelResponse.txt'
* @returns Resolves when the action is complete.
*/
export async function run(): Promise<void> {
let responseFile: tmp.FileResult | null = null
// Set up graceful cleanup for temporary files on process exit
tmp.setGracefulCleanup()
try {
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
const promptFilePath = core.getInput('prompt-file')
const inputVariables = core.getInput('input')
const fileInputVariables = core.getInput('file_input')
const systemPrompt = loadContentFromFileOrInput(
'system-prompt-file',
'system-prompt',
'You are a helpful assistant'
)
let promptConfig: PromptConfig | undefined = undefined
let systemPrompt: string | undefined = undefined
let prompt: string | undefined = undefined
const modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
// Check if we're using a prompt YAML file
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
core.info('Using prompt YAML file format')
// Parse template variables from both string inputs and file-based inputs
const stringVars = parseTemplateVariables(inputVariables)
const fileVars = parseFileTemplateVariables(fileInputVariables)
const templateVariables = {...stringVars, ...fileVars}
// Load and process prompt file
promptConfig = loadPromptFile(promptFilePath, templateVariables)
} else {
// Use legacy format
core.info('Using legacy prompt format')
prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
systemPrompt = loadContentFromFileOrInput('system-prompt-file', 'system-prompt', 'You are a helpful assistant')
}
// Get common parameters
const modelName = promptConfig?.model || core.getInput('model')
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
if (token === undefined) {
throw new Error('GITHUB_TOKEN is not set')
}
const endpoint = core.getInput('endpoint')
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
const githubMcpToken = core.getInput('github-mcp-token') || token
const inferenceRequest: InferenceRequest = {
const endpoint = core.getInput('endpoint')
// Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(
promptConfig,
systemPrompt,
prompt,
modelName,
maxTokens,
endpoint,
token
}
token,
)
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
let modelResponse: string | null = null
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(token)
const mcpClient = await connectToGitHubMCP(githubMcpToken)
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient)
@@ -60,22 +95,37 @@ export async function run(): Promise<void> {
core.setOutput('response', modelResponse || '')
const responseFilePath = path.join(tempDir(), RESPONSE_FILE)
core.setOutput('response-file', responseFilePath)
// Create a secure temporary file instead of using the temp directory directly
responseFile = tmp.fileSync({
prefix: 'modelResponse-',
postfix: '.txt',
})
core.setOutput('response-file', responseFile.name)
if (modelResponse && modelResponse !== '') {
fs.writeFileSync(responseFilePath, modelResponse, 'utf-8')
fs.writeFileSync(responseFile.name, modelResponse, 'utf-8')
}
} catch (error) {
if (error instanceof Error) {
core.setFailed(error.message)
} else {
core.setFailed('An unexpected error occurred')
core.setFailed(`An unexpected error occurred: ${JSON.stringify(error, null, 2)}`)
}
// Force exit to prevent hanging on open connections
process.exit(1)
} finally {
// Explicit cleanup of temporary file if it was created
if (responseFile) {
try {
responseFile.removeCallback()
} catch (cleanupError) {
// Log cleanup errors but don't fail the action
core.warning(`Failed to cleanup temporary file: ${cleanupError}`)
}
}
}
}
function tempDir(): string {
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
return tempDirectory
// Force exit to prevent hanging on open connections
process.exit(0)
}

View File

@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import {Client} from '@modelcontextprotocol/sdk/client/index.js'
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js'
export interface ToolResult {
tool_call_id: string
@@ -35,9 +35,7 @@ export interface GitHubMCPClient {
/**
* Connect to the GitHub MCP server and retrieve available tools
*/
export async function connectToGitHubMCP(
token: string
): Promise<GitHubMCPClient | null> {
export async function connectToGitHubMCP(token: string): Promise<GitHubMCPClient | null> {
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
core.info('Connecting to GitHub MCP server...')
@@ -46,15 +44,15 @@ export async function connectToGitHubMCP(
requestInit: {
headers: {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true'
}
}
'X-MCP-Readonly': 'true',
},
},
})
const client = new Client({
name: 'ai-inference-action',
version: '1.0.0',
transport
transport,
})
try {
@@ -67,42 +65,35 @@ export async function connectToGitHubMCP(
core.info('Successfully connected to GitHub MCP server')
const toolsResponse = await client.listTools()
core.info(
`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`
)
core.info(`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`)
// Map GitHub MCP tools → Azure AI Inference tool definitions
const tools = (toolsResponse.tools || []).map((t) => ({
const tools = (toolsResponse.tools || []).map(t => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema
}
parameters: t.inputSchema,
},
}))
core.info(`Mapped ${tools.length} GitHub MCP tools for Azure AI Inference`)
return { client, tools }
return {client, tools}
}
/**
* Execute a single tool call via GitHub MCP
*/
export async function executeToolCall(
githubMcpClient: Client,
toolCall: ToolCall
): Promise<ToolResult> {
core.info(
`Executing GitHub MCP tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`
)
export async function executeToolCall(githubMcpClient: Client, toolCall: ToolCall): Promise<ToolResult> {
core.info(`Executing GitHub MCP tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`)
try {
const args = JSON.parse(toolCall.function.arguments)
const result = await githubMcpClient.callTool({
name: toolCall.function.name,
arguments: args
arguments: args,
})
core.info(`GitHub MCP tool ${toolCall.function.name} executed successfully`)
@@ -111,18 +102,16 @@ export async function executeToolCall(
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: JSON.stringify(result.content)
content: JSON.stringify(result.content),
}
} catch (toolError) {
core.warning(
`Failed to execute GitHub MCP tool ${toolCall.function.name}: ${toolError}`
)
core.warning(`Failed to execute GitHub MCP tool ${toolCall.function.name}: ${toolError}`)
return {
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: `Error: ${toolError}`
content: `Error: ${toolError}`,
}
}
}
@@ -130,10 +119,7 @@ export async function executeToolCall(
/**
* Execute all tool calls from a response via GitHub MCP
*/
export async function executeToolCalls(
githubMcpClient: Client,
toolCalls: ToolCall[]
): Promise<ToolResult[]> {
export async function executeToolCalls(githubMcpClient: Client, toolCalls: ToolCall[]): Promise<ToolResult[]> {
const toolResults: ToolResult[] = []
for (const toolCall of toolCalls) {

135
src/prompt.ts Normal file
View File

@@ -0,0 +1,135 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
export interface PromptMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export interface PromptConfig {
messages: PromptMessage[]
model?: string
responseFormat?: 'text' | 'json_schema'
jsonSchema?: string
}
export interface TemplateVariables {
[key: string]: string
}
/**
* Parse template variables from YAML input string
*/
export function parseTemplateVariables(input: string): TemplateVariables {
if (!input.trim()) {
return {}
}
try {
const parsed = yaml.load(input) as TemplateVariables
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('Template variables must be a YAML object')
}
return parsed
} catch (error) {
throw new Error(`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Parse file-based template variables from YAML input string. The YAML should map
* variable names to file paths. File contents are read and returned as variables.
*/
export function parseFileTemplateVariables(fileInput: string): TemplateVariables {
if (!fileInput.trim()) {
return {}
}
try {
const parsed = yaml.load(fileInput) as Record<string, unknown>
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('File template variables must be a YAML object')
}
const result: TemplateVariables = {}
for (const [key, value] of Object.entries(parsed)) {
if (typeof value !== 'string') {
throw new Error(`File template variable '${key}' must be a string file path`)
}
const filePath = value
if (!fs.existsSync(filePath)) {
throw new Error(`File for template variable '${key}' was not found: ${filePath}`)
}
try {
result[key] = fs.readFileSync(filePath, 'utf-8')
} catch (err) {
throw new Error(
`Failed to read file for template variable '${key}' at path '${filePath}': ${err instanceof Error ? err.message : 'Unknown error'}`,
)
}
}
return result
} catch (error) {
throw new Error(
`Failed to parse file template variables: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
/**
* Replace template variables in text using {{variable}} syntax
*/
export function replaceTemplateVariables(text: string, variables: TemplateVariables): string {
return text.replace(/\{\{([\w.-]+)\}\}/g, (match, variableName) => {
if (variableName in variables) {
return variables[variableName]
}
core.warning(`Template variable '${variableName}' not found in input variables`)
return match // Return the original placeholder if variable not found
})
}
/**
* Load and parse a prompt YAML file with template variable substitution
*/
export function loadPromptFile(filePath: string, templateVariables: TemplateVariables = {}): PromptConfig {
if (!fs.existsSync(filePath)) {
throw new Error(`Prompt file not found: ${filePath}`)
}
const fileContent = fs.readFileSync(filePath, 'utf-8')
// Apply template variable substitution
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
try {
const config = yaml.load(processedContent) as PromptConfig
if (!config.messages || !Array.isArray(config.messages)) {
throw new Error('Prompt file must contain a "messages" array')
}
// Validate messages
for (const message of config.messages) {
if (!message.role || !message.content) {
throw new Error('Each message must have "role" and "content" properties')
}
if (!['system', 'user', 'assistant'].includes(message.role)) {
throw new Error(`Invalid message role: ${message.role}`)
}
}
return config
} catch (error) {
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Check if a file is a prompt YAML file based on extension
*/
export function isPromptYamlFile(filePath: string): boolean {
return filePath.endsWith('.prompt.yml') || filePath.endsWith('.prompt.yaml')
}

View File

@@ -6,12 +6,5 @@
"noEmit": true
},
"exclude": ["dist", "node_modules"],
"include": [
"__fixtures__",
"__tests__",
"src",
"eslint.config.mjs",
"jest.config.js",
"rollup.config.ts"
]
"include": ["__fixtures__", "__tests__", "src", "eslint.config.mjs", "rollup.config.ts"]
}