Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!package.json
!server.js
26 changes: 26 additions & 0 deletions .github/workflows/call-reusable-agent-readiness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Agent Readiness — synced from seankoji-com/.github (panel/call-reusable-agent-readiness.yml)
# Ratchet on agentic token cost: reports what a PR makes worse — a new
# CLAUDE.md/AGENTS.md split, a file pushed over the line limit, a doc or
# .claude/ command now pointing at a missing path. Existing debt never fails.
# Advisory unless this repo's .agent-readiness.json sets "enforce": true.
# Standard, check list, and config: seankoji-com/.github docs/agent-readiness.md
name: Agent Readiness

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read

concurrency:
group: agent-readiness-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
agent-readiness:
# Fork heads do not run on the organization runner.
if: github.event.pull_request.head.repo.full_name == github.repository
uses: seankoji-com/.github/.github/workflows/reusable-agent-readiness.yml@main
with:
runner-json: '["ubuntu-latest"]'
36 changes: 36 additions & 0 deletions .github/workflows/call-reusable-dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Dependabot Auto-Merge — synced from seankoji-com/.github (panel/call-reusable-dependabot-automerge.yml)
# Enables auto-merge for dependabot PRs after CI + reviews pass.
# Major version bumps are excluded (require human approval).
# Uses the org-wide SEANKOJI_CI_APP_ID / SEANKOJI_CI_PRIVATE_KEY App identity
# (Contents:Write and PullRequests:Write) — no per-repo secrets required.
name: Dependabot Auto-Merge

# Trigger is pull_request_target, NOT pull_request. Dependabot-triggered
# `pull_request` runs get a read-only GITHUB_TOKEN and cannot read Actions
# secrets, so the reusable workflow's create-github-app-token step failed with
# "private-key ... must be set to a non-empty string". That red check was then
# propagated by gatekeeper / all-checks-passed, blocking every Dependabot PR in
# the fleet. pull_request_target runs in the base-repo context with secrets
# available. This workflow never checks out or executes PR code, so it is safe
# on that trigger.
on:
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
contents: read

# A rebase or retry push to an open Dependabot PR fires another `synchronize`
# run before the prior one finishes. Without this, both runs evaluate
# auto-merge eligibility independently and the stale one can still act on an
# outdated diff after the newer push landed.
concurrency:
group: dependabot-automerge-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
auto-merge:
uses: seankoji-com/.github/.github/workflows/reusable-dependabot-automerge.yml@main
with:
runner-json: '["ubuntu-latest"]'
secrets: inherit
124 changes: 124 additions & 0 deletions .github/workflows/call-reusable-pr-gatekeeper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# PR Gatekeeper — synced from seankoji-com/.github (panel/call-reusable-pr-gatekeeper.yml)
# Posts the single aggregator check `gatekeeper / all-checks-passed`, required
# org-wide, which blocks on ANY red check rather than only the ones this repo
# happens to list as required.
# Uses this repository's own GITHUB_TOKEN to post the check run — never an App
# identity, so the ruleset's context match stays unambiguous.
name: PR Gatekeeper

on:
# The wildcard is deliberate and load-bearing: `workflows` is a REQUIRED key
# with minItems 1, so it cannot be omitted, and an enumerated list would go
# stale the moment a repo adds a workflow. check_run/check_suite cannot be
# used at all — GitHub suppresses both for any head SHA that ran an Actions
# workflow, which is every PR here.
workflow_run:
workflows: ['*']
types: [completed]
pull_request_review:
types: [submitted, edited, dismissed]
# Seed the required context the moment a PR opens or its head moves.
# pull_request_target, NOT pull_request: on a fork, pull_request hands the
# reusable a read-only token, the check-run POST 403s, and this job goes red —
# pinning the required context red permanently, which nothing can clear.
# pull_request_target runs this base-branch workflow with the base repo's
# write token and executes no PR code: the reusable checks nothing out, it
# only curls the evaluator from this org's raw URL at the reusable's own
# commit. The caller job's own composite check run already carries the
# required name and the evaluator drops every run with that name, so the
# seed cannot block on itself. The queued job's check run is what makes the
# gate visible before any evaluation, even with every runner busy.
pull_request_target:
types: [opened, synchronize, reopened]
# The reconciler's path, for a PR the event route never noticed.
workflow_dispatch:
inputs:
head_sha:
description: 'Commit SHA to evaluate and report the gate against.'
required: true
type: string
persona_state:
description: 'Private controller signal for the Grumpy review job.'
type: string
default: ''
persona_pr:
description: 'PR number for the exact-head persona signal.'
type: string
default: ''

# Deliberately NOT `contents: read` alone. A reusable workflow can never be
# granted more than its caller holds, so narrowing here would 403 the check-run
# POST in every repo — invisibly, since a resolution failure produces no check
# run to go red.
permissions:
actions: read
checks: write
statuses: read
pull-requests: read
contents: read

jobs:
recover-persona:
# An independent event path; the reusable sends only PR identity to the
# private controller and never checks out PR code.
# No continue-on-error and no secrets in this `if`: GitHub rejects both on
# a reusable-workflow caller job, and the whole file then fails to parse,
# so the required gate never posts. The reusable skips itself when the
# key is absent and marks every step advisory, so this job cannot go red.
if: github.event_name == 'pull_request_target'
# Must cover the called job's own grant (contents + pull-requests read): a
# called workflow can only downgrade permissions, so a narrower caller is
# also a load-time failure that actionlint cannot see for a remote reusable.
permissions:
contents: read
pull-requests: read
uses: seankoji-com/.github/.github/workflows/reusable-persona-recovery-request.yml@main
with:
target_repository: ${{ github.repository }}
pr_number: ${{ format('{0}', github.event.pull_request.number) }}
head_sha: ${{ github.event.pull_request.head.sha }}
secrets:
SEANKOJI_CI_PRIVATE_KEY: ${{ secrets.SEANKOJI_CI_PRIVATE_KEY }}

review-event:
# Reviews on forks have read-only tokens. This signal finishes without
# fetching PR code; workflow_run then evaluates with default-branch code.
if: github.event_name == 'pull_request_review'
permissions:
contents: read
uses: seankoji-com/.github/.github/workflows/reusable-review-event.yml@main

gatekeeper:
# Head, merge, and review-event evaluations publish to overlapping commits.
# Serialize repository-wide so an older approval cannot overtake a dismissal.
# queue:max preserves completion events when several PRs finish together.
# Only gate writers enter this queue. Skipped workflow events and independent
# recovery/review signals must not occupy the bounded writer queue.
concurrency:
group: pr-gatekeeper-${{ github.repository }}
cancel-in-progress: false
queue: max
# Guarding at the job level, not inside a step: an in-job early exit still
# claims a self-hosted runner first, and fleet capacity is scarce.
# - Skipping the gate's own runs stops it re-triggering itself, which
# would burn workflow_run's three-level chain cap.
# - `pull_requests` is empty on fork runs, so the event allowlist is what
# keeps fork PRs gated rather than ignored.
# - On `pull_request_target` the job's own queued composite check run is
# the pending seed: it carries the required name with no runner needed.
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'workflow_run' &&
(github.event.workflow_run.name != 'PR Gatekeeper' ||
github.event.workflow_run.event == 'pull_request_review') &&
(github.event.workflow_run.pull_requests[0] != null ||
contains(fromJSON('["pull_request", "pull_request_target", "pull_request_review", "dynamic"]'), github.event.workflow_run.event)))
uses: seankoji-com/.github/.github/workflows/reusable-pr-gatekeeper.yml@main
with:
head_sha: ${{ github.event.workflow_run.pull_requests[0].head.sha || github.event.workflow_run.head_sha || github.event.pull_request.head.sha || inputs.head_sha }}
review_run_id: ${{ github.event.workflow_run.event == 'pull_request_review' && format('{0}', github.event.workflow_run.id) || '' }}
runner-json: '["ubuntu-latest"]'
seed: ${{ github.event_name == 'pull_request_target' }}
persona_state: ${{ inputs.persona_state || '' }}
persona_pr: ${{ inputs.persona_pr || '' }}
27 changes: 27 additions & 0 deletions .github/workflows/call-reusable-static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Static Analysis — synced from seankoji-com/.github (panel/call-reusable-static-analysis.yml)
# Runs Semgrep and zizmor against every PR via the reusable workflow. Both
# scanners are non-blocking (continue-on-error) on first release — see
# .github/workflows/reusable-static-analysis.yml in seankoji-com/.github.

name: Static Analysis

on:
pull_request:
types: [opened, synchronize, reopened]

# Every push to a PR queues a fresh scan; without this, rapid successive
# pushes (force-push loops, auto-rebase branches) stack up several full runs
# of the same diff on the shared self-hosted pool. Cancel in favor of the
# latest push — an in-flight scan of a superseded commit has no value.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
static-analysis:
uses: seankoji-com/.github/.github/workflows/reusable-static-analysis.yml@main
with:
runner-json: '["ubuntu-latest"]'
56 changes: 56 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: CodeQL analysis

# The org's Universal ruleset requires code scanning results on main, and a
# repository with no analyses can never satisfy it: every pull request sits at
# mergeable_state "blocked" with nothing to point at.
#
# Scans the proxy's JavaScript (it parses untrusted upstream NDJSON and client
# request bodies) and the GitHub Actions workflows.

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '23 7 * * 1' # Mondays 07:23 UTC
workflow_dispatch:

permissions:
contents: read
actions: read
security-events: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
analyze:
name: Analyze
# Skip forks: they cannot write security events, so the job would only ever
# fail noisily on an outside contributor's pull request.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
language: [actions, javascript-typescript]

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
upload: true
category: "/language:${{ matrix.language }}"
58 changes: 58 additions & 0 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Test and publish image
# Every branch push publishes an immutable sha-<commit> tag; deployments pin the
# digest, so a pre-merge branch build is as deployable as a main build.
on:
pull_request:
push:
branches: ['**']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: image-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
# Public repo: the org's self-hosted runner groups refuse public repos, so
# this runs on GitHub-hosted runners (no minutes charge for public repos).
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- run: npm test
image:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
id: image
with:
context: .
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/seankoji-com/proxy-commandcode:sha-${{ github.sha }}
cache-from: type=gha,scope=proxy-commandcode
cache-to: type=gha,scope=proxy-commandcode,mode=max
- if: github.event_name != 'pull_request'
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
run: echo "Deploy ghcr.io/seankoji-com/proxy-commandcode@${IMAGE_DIGEST}" >> "$GITHUB_STEP_SUMMARY"
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json server.js ./
ENV NODE_ENV=production PCMC_PORT=3456
USER node
EXPOSE 3456
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+process.env.PCMC_PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ A transparent reverse proxy that translates **OpenAI-compatible** requests (`/v1

Use any CommandCode model (including the Go plan) with **ZCode**, **9router**, **Cursor**, **Continue**, **Aider**, and any editor that supports custom OpenAI endpoints.

## Fork changes (seankoji-com)

Forked from [nasrulhadi/proxy-commandcode](https://github.com/nasrulhadi/proxy-commandcode) at `1a95fbb` to run as a LiteLLM sidecar in a container. Differences:

| Area | Change |
|---|---|
| Reasoning | Streamed and returned as `reasoning_content`, not mixed into `content` |
| Errors | An upstream `error` event before any output returns HTTP 502 (so a router can fail over); later events are ignored instead of writing to an ended response; network errors carry the code/addresses instead of an empty message |
| Usage | `finish-step`/`finish` usage mapped to OpenAI `usage` (sync body; extra stream chunk with empty `choices`) |
| Finish reason | `length` / `tool_calls` / `content_filter` mapped from the upstream reason |
| Request shape | Tool-call `input` sent as an object; consecutive tool results merged; `developer` role folded into `system`; image parts use `{image, mediaType}`; `temperature` and `max_completion_tokens` passed through; `x-session-id` and `x-cli-environment` headers sent |
| CLI version | `x-command-code-version` tracks `command-code@latest` on npm (checked at start and every 6 h, default `1.66.0`); set `PCMC_VERSION` to pin |
| Runtime | Logs to stdout only (no `proxy.log`, no ANSI without a TTY); Windows `netstat`/`taskkill` port handling removed; graceful `SIGTERM`; `Dockerfile` + `npm test` |

Image: `ghcr.io/seankoji-com/proxy-commandcode:sha-<commit>`, built by `.github/workflows/image.yml` on every branch push. Deploy by digest.

## Why

CommandCode has two API surfaces:
Expand Down Expand Up @@ -48,7 +64,7 @@ Zero dependencies. Node.js 18+ only.
| Variable | Default |
|---|---|
| `PCMC_PORT` | `3456` |
| `PCMC_VERSION` | `1.54.0` |
| `PCMC_VERSION` | unset — tracks npm `command-code@latest` (floor `1.66.0`); set to pin |
| `PCMC_DEBUG` | off (set `1` to enable) |

### Enabling debug mode
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
"dev": "node --watch server.js",
"test": "node --test"
},
"dependencies": {}
}
Loading
Loading