@cesar-richard/git-connector-sdk
v1.45.0
Published
TypeScript SDK for the git-connector v1 API (work items + iterations aggregated from GitHub/GitLab). Version published on npm tracks server releases.
Readme
@cesar-richard/git-connector-sdk
TypeScript SDK for git-connector — a server that aggregates GitHub + GitLab activity (PRs, MRs, issues, commits, comments, reviews, CI runs) and exposes a unified, ticket-correlated read API.
This SDK is a thin, fully-typed wrapper around the server's /v1/* HTTP API, generated from its OpenAPI spec via openapi-fetch.
Version policy: the SDK version always tracks the git-connector server release it was generated against. Pinning the same major+minor as your server is recommended.
Install
npm install @cesar-richard/git-connector-sdk
# or
pnpm add @cesar-richard/git-connector-sdk
# or
bun add @cesar-richard/git-connector-sdkQuick start
import { createGitConnectorClient } from "@cesar-richard/git-connector-sdk";
const client = createGitConnectorClient({
baseUrl: "https://git-connector.example.com",
token: process.env.GIT_CONNECTOR_TOKEN!, // gck_…
});
const { data, error } = await client.GET("/v1/work-items", {
params: { query: { state: "open", iteration: "current" } },
});
if (error) throw new Error(`git-connector error: ${JSON.stringify(error)}`);
for (const item of data.items) {
console.log(`${item.source}/${item.projectKey}#${item.number} — ${item.title}`);
for (const pr of item.linkedActivities) {
console.log(` ↳ ${pr.type.toUpperCase()} ${pr.url} (${pr.state})`);
}
}The client is fully typed: request params, query strings, and response bodies are all derived from the server's OpenAPI schema. Hover any field in your editor to see its shape.
Authentication
The SDK sends Authorization: Bearer <token> on every request. Mint a token from the git-connector control UI or via:
curl -X POST https://git-connector.example.com/admin/api-keys \
-H "Content-Type: application/json" \
-d '{"name":"my-integration"}'
# → { "token": "gck_…", "id": 12, "name": "my-integration", "createdAt": "…" }Tokens with the gck_ prefix are immutable once minted; revoke and reissue via the same endpoint.
Activity tracking / CRA workflow
The /v1/* surface is designed to power activity reports (CRA, billable-time sheets, "what did I do this month"). The primitives below let you slice your data per-user, per-day, per-type.
What did I do today?
const today = new Date().toISOString().slice(0, 10);
const { data } = await client.GET("/v1/events", {
params: { query: { from: today, to: today, user: "cesar" } },
});
for (const e of data.events) {
console.log(`[${e.day}] ${e.type} — ${e.title} (${e.repo})`);
}List my open PRs / MRs
const { data } = await client.GET("/v1/activities", {
params: {
query: { author: "cesar", state: "open,draft", type: "pr,mr" },
},
});
for (const a of data.items) {
console.log(`${a.source}/${a.repo}#${a.number} — ${a.title} (${a.state})`);
}List issues I opened
const { data } = await client.GET("/v1/work-items", {
params: { query: { author: "cesar", state: "open" } },
});Incremental polling (every 5 minutes)
Use ?since=<ISO> to fetch only events newer than your last cursor:
let cursor = "2026-05-18T00:00:00Z";
const { data } = await client.GET("/v1/events", {
params: {
query: { from: "2026-05-01", to: "2026-05-31", since: cursor },
},
});
if (data.events.length > 0) {
cursor = data.events[data.events.length - 1].occurredAt;
}Monthly CRA: count events per day
const { data } = await client.GET("/v1/events", {
params: {
query: {
from: "2026-05-01",
to: "2026-05-31",
user: "cesar",
type: "commit,review,comment,state-transition",
},
},
});
const byDay: Record<string, number> = {};
for (const e of data.events) {
byDay[e.day] = (byDay[e.day] ?? 0) + 1;
}Reliability of the author field
author (on Activity and Event) is the GitHub or GitLab account login (e.g. cesar, never César Richard). If the underlying provider payload doesn't expose a login — rare cases like a git commit by an email not linked to any account — author is null. The git-config commit-author name is never used for author; it lives in meta.commitAuthorName when available, for display purposes only.
Filters like ?author=cesar (on /v1/activities and /v1/work-items) and ?user=cesar (on /v1/events) match the login exactly (case-insensitive). They do NOT fuzzy-match display names. This is a deliberate design choice for auditable activity tracking.
GitLab resolver: commits ingested via polling are resolved server-side by mapping commit.author_email → GL user login via the GitLab Users API. The result is cached (default TTL 90 days). If the email cannot be resolved (anonymous commit, email not associated with any GL user, private email on a self-hosted instance), author stays null and the git-config name lives in meta.commitAuthorName. Webhook-ingested GL commits use the pusher's username directly (no resolver needed).
Structured author access via authorResolved: commit events also carry an authorResolved blob with {login, name, email} (each nullable) — a single typed accessor that aggregates everything we know about the author:
for (const e of data.events) {
if (e.type !== "commit") continue;
const { login, name, email } = e.authorResolved ?? {};
console.log(`${login ?? "anonymous"} (${name ?? "?"} <${email ?? "?"}>)`);
}Available on every commit event regardless of provider or ingestion path. event.author is always equal to event.authorResolved?.login.
User resolution on work-items (assigneesResolved, authorResolved, reviewerResolved)
Beyond commit events, /v1/work-items exposes resolved user identities on
work-item assignees, linked-activity authors, and reviewers. Each is a
ResolvedUser shape:
type ResolvedUser = {
id: string; // "gl:<user_id>" or "gh:<login>" — stable across login renames
login: string;
name: string | null;
email: string | null;
};The id is stable: two references to the same GitLab user retain the same
gl:<user_id> even after the user's username changes (a common pattern in
self-hosted GitLab where matricule-based logins get rewritten to friendly
names later). GitHub usernames don't typically change in a way that affects
this, so id is gh:<login> for GitHub users.
const { data } = await client.GET("/v1/work-items");
for (const wi of data.items) {
for (const a of wi.assigneesResolved ?? []) {
console.log(`${wi.title} assigned to ${a.name ?? a.login} (id=${a.id})`);
}
for (const pr of wi.linkedActivities) {
if (pr.authorResolved) {
console.log(`PR ${pr.url} by ${pr.authorResolved.name ?? pr.authorResolved.login}`);
}
for (const rev of pr.reviews.entries) {
console.log(` review by ${rev.reviewerResolved?.name ?? rev.reviewer}`);
}
}
}Link traceability (linkedActivities[].linkSource)
Each entry in linkedActivities carries an optional linkSource object with the
raw hintSource persisted on the link row and a human-readable kind derived
from it (body-ref, title-ref, pr-comment-ref, issue-comment-ref,
unknown). Consumers can use this to surface WHY a link was created — useful
for debugging missing or unexpected links in the Explorer UI.
for (const pr of wi.linkedActivities) {
if (pr.linkSource) {
console.log(` ↳ ${pr.url} linked via ${pr.linkSource.kind} (${pr.linkSource.hintSource})`);
}
}GitLab resolution uses a 90-day cache (configurable via GITLAB_USER_CACHE_TTL_DAYS).
Issue ↔ PR/MR link detection patterns
WorkItem.linkedActivities is populated by scanning PR/MR bodies, commit
messages, notes, AND issue comments for references to PR/MR numbers or URLs.
The recognized patterns are:
Canonical (in PR/MR body / commits / notes — forward direction):
Fixes #1234,Fix #1234,Closes #1234,Close #1234,Resolves #1234,Resolve #1234(English short refs).https://github.com/<owner>/<repo>/issues/<n>orhttps://<gitlab>/.../-/issues/<n>(full URLs).<owner>/<repo>#<n>(cross-repo GitHub).<alias>#<n>and<alias>!<n>(project-alias cross-repo).- Short refs
#<n>and!<n>(resolved against the active provider/project).
Issue-comment reverse direction (hint_source="issue_comment"):
The above PR/MR URLs and short refs are all recognized in issue comments as evidence that the cited PR/MR is linked to the issue.
Additionally, the following explicit phrase prefixes are recognized, case-insensitively, to catch informal "this issue was delivered" comments:
| Phrase | Example | Behavior |
|---|---|---|
| Fixed by …, Fixed in … | Fixed by 1214, Fixed by https://…/pull/1214 | Treat the cited ref as a linked PR/MR |
| Fixes by …, Fix by … | Fix by #99 | Same |
| Resolved by …, Resolves by …, Resolve by … | Resolved by 42 | Same |
| Closed by …, Closes by …, Close by … | Closed by !99 | Same |
| FR: Corrigé par …, Corrigée par …, Corrige par … | Corrigé par 42 | Same |
| FR: Résolu par …, Resolu par … | Résolu par !99 | Same |
| FR: Fermé par …, Ferme par … | Fermé par #1214 | Same |
| FR: … dans <n> variant | Corrigé dans 42 | Same |
The phrase prefix lets the SDK detect refs even when the number is bare
(no # or ! prefix), e.g. Fixed by 1214 instead of Fixed by #1214.
Code blocks (fenced ``` and inline `) are stripped before scanning.
PR review status (reviewStatus)
Each LinkedActivity carries an optional reviewStatus field with 8 possible
values, computed server-side from the PR's state, draft flag, requested
reviewers, and review history:
| Value | Meaning |
|---|---|
| draft | PR is a draft — not yet ready for review |
| open-no-review | Open, no reviewer requested |
| open-awaiting-my-review | Open, you (the viewer) are a requested reviewer |
| open-awaiting-other-review | Open, waiting on someone else's review |
| open-changes-requested | Open, at least one reviewer requested changes (and that verdict hasn't been superseded by an approval) |
| open-approved | Open and approved, no outstanding change requests |
| merged | Merged |
| closed | Closed without merging |
Pass ?viewer=<login> (case-insensitive) on GET /v1/work-items or
GET /v1/work-items/{source}/{projectKey}/{number} to drive the
open-awaiting-my-review vs open-awaiting-other-review split. Without
viewer, awaiting-review PRs always show as open-awaiting-other-review.
const { data } = await client.GET("/v1/work-items", {
params: { query: { state: "open", viewer: "cesar" } },
});
for (const wi of data.items) {
for (const pr of wi.linkedActivities) {
if (pr.reviewStatus === "open-awaiting-my-review") {
console.log(`Needs your review: ${pr.url}`);
}
}
}API surface
The /v1/* API exposes five resources: work items, iterations, work-item details, the unified events stream, and raw activities.
GET /v1/work-items — list aggregated work items
Issues from GitHub + GitLab, with linked PRs/MRs resolved server-side via ticket-reference correlation. Filter by state, iteration (id or "current"), assignee, label, source.
const { data } = await client.GET("/v1/work-items", {
params: {
query: {
state: "open",
iteration: "current",
assignee: "alice",
},
},
});
// data: { items: WorkItem[]; total: number; limit: number }Each WorkItem includes its linkedActivities (PRs/MRs with full enrichment: reviews summary, unresolved threads, volume, status history, reviewRequestedAt, mergedAt).
GET /v1/work-items/{source}/{projectKey}/{number} — detail
const { data } = await client.GET(
"/v1/work-items/{source}/{projectKey}/{number}",
{ params: { path: { source: "gitlab", projectKey: "acme/ops", number: 42 } } },
);GET /v1/iterations — list iterations
const { data } = await client.GET("/v1/iterations");
// data: IterationWithCount[] — each carries workItemCount across all linked WorkItemsGET /v1/events — unified activity stream
Daily-bucketed (Paris-TZ) chronological event stream. Mixes commits, comments, reviews, state-transitions, and CI runs across both providers. Cursor-paginated.
const { data } = await client.GET("/v1/events", {
params: {
query: {
from: "2026-05-01",
to: "2026-05-17",
type: "ci-run,review", // CSV
user: "alice",
limit: "200",
},
},
});
// data: { events: GitEvent[]; total: number; nextCursor: string | null }
// Paginate:
if (data.nextCursor) {
const { data: next } = await client.GET("/v1/events", {
params: { query: { from: "2026-05-01", to: "2026-05-17", cursor: data.nextCursor } },
});
}Event types in the stream:
| type | Source | parentActivityId | Notable meta keys |
|--------|--------|--------------------|---------------------|
| commit | Default branch | null | fullMessage |
| comment | Issue/PR/MR notes | resolves to PR/MR/issue | body |
| review | GH PR review / GL MR approval | resolves to PR/MR | state, reviewer |
| state-transition | Derived from activity state diff | resolves to PR/MR/issue | from, to |
| ci-run | GH Actions job / GL pipeline job | resolves to PR/MR via head SHA | runId, jobName, conclusion, durationSec, sha, branch |
Schema as a separate import
If you need the raw OpenAPI types without the client (e.g. for codegen or for non-openapi-fetch consumers), import directly:
import type { components } from "@cesar-richard/git-connector-sdk/schema";
type WorkItem = components["schemas"]["WorkItem"];
type GitEvent = components["schemas"]["GitEvent"];The full OpenAPI document is also published in the package — see openapi.json for the contract.
Error handling
openapi-fetch never throws on HTTP-level errors — it returns { data, error, response }. Always check error before using data:
const { data, error, response } = await client.GET("/v1/work-items");
if (error) {
if (response.status === 401) throw new Error("Missing/invalid gck_ token");
if (response.status === 403) throw new Error("Token revoked");
throw new Error(`git-connector ${response.status}: ${JSON.stringify(error)}`);
}
// data is non-null and fully typed from hereNetwork errors (DNS, timeout, connection refused) reject the promise as usual.
Custom fetch
Pass your own fetch implementation to wire up retries, telemetry, or a non-global fetch (e.g. inside Cloudflare Workers):
import { fetch as undiciFetch } from "undici";
const client = createGitConnectorClient({
baseUrl: "https://git-connector.example.com",
token: process.env.GIT_CONNECTOR_TOKEN!,
fetch: undiciFetch,
});Compatibility
- Node ≥ 18 (uses global
fetch). - Bun ≥ 1.0.
- Modern browsers (no Node-only APIs).
- ESM only.
Related
- Server / control UI: git-connector
- OpenAPI spec: ships with this package as
openapi.json— generated server-side, the SDK types derive from it viaopenapi-typescript. - Releases: SDK and server release in lockstep via semantic-release on every merge to
main.
