@ibobbyts/dsh-git-commit 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ /** Change tokens keep browser polling cheap: Git is queried only on changes. */
2
+ import { watch } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { git } from './git.js';
5
+
6
+ const IGNORE = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', 'target', '.next', '.venv', '__pycache__']);
7
+ export class RepoWatcher {
8
+ constructor() { this.repos = new Map(); this.seed = 0; }
9
+ async token(root) {
10
+ let record = this.repos.get(root);
11
+ if (record) return record.token;
12
+ record = { token: ++this.seed, handles: [], timer: null, first: 0 };
13
+ this.repos.set(root, record);
14
+ const publish = () => { record.token += 1; record.first = 0; record.timer = null; };
15
+ const bump = () => {
16
+ const now = Date.now();
17
+ if (!record.first) record.first = now;
18
+ if (record.timer) clearTimeout(record.timer);
19
+ if (now - record.first > 1000) return publish();
20
+ record.timer = setTimeout(publish, 160);
21
+ record.timer.unref?.();
22
+ };
23
+ const listen = (path, options, filter) => {
24
+ try {
25
+ const handle = watch(path, options, (_, file) => { if (filter(file)) bump(); });
26
+ handle.on('error', () => bump());
27
+ handle.unref?.();
28
+ record.handles.push(handle);
29
+ } catch { /* A low-frequency client fallback still ensures eventual refresh. */ }
30
+ };
31
+ listen(root, { recursive: true }, path => {
32
+ if (!path) return true;
33
+ const first = String(path).replaceAll('\\', '/').split('/')[0];
34
+ return !IGNORE.has(first);
35
+ });
36
+ // Git worktrees have a .git FILE, and common refs may live in another checkout.
37
+ for (const path of ['HEAD', 'index', 'packed-refs', 'refs', 'MERGE_HEAD']) {
38
+ try {
39
+ const actual = (await git(root, ['rev-parse', '--git-path', path])).trim();
40
+ const full = resolve(root, actual);
41
+ listen(dirname(full), {}, name => {
42
+ if (!name) return true;
43
+ if (path === 'refs') return String(name) === 'refs';
44
+ return String(name) === path.split('/').at(-1);
45
+ });
46
+ } catch { /* Path may not exist on a freshly initialized repository. */ }
47
+ }
48
+ return record.token;
49
+ }
50
+ close() {
51
+ for (const record of this.repos.values()) {
52
+ if (record.timer) clearTimeout(record.timer);
53
+ for (const handle of record.handles) handle.close();
54
+ }
55
+ this.repos.clear();
56
+ }
57
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ /** Public host-facing API for DSH plugins. */
2
+ export interface GitCommitRequest {
3
+ /** Working directory inside the repository. Prefer an explicit cwd from another plugin. */
4
+ cwd?: string;
5
+ repositoryPath?: string;
6
+ sessionId?: string;
7
+ /** Optional caller-supplied message; skips AI generation but preserves scope/snapshot validation. */
8
+ message?: string;
9
+ signal?: AbortSignal;
10
+ }
11
+ /** Provider usage totals and local monotonic timings across at most two LLM calls. */
12
+ export interface GitCommitLlmMetrics {
13
+ calls: number;
14
+ /** Total input including uncached, cache-read, and cache-write tokens. Null if usage unavailable. */
15
+ inputTokens: number | null;
16
+ outputTokens: number | null;
17
+ cacheReadTokens: number | null;
18
+ cacheWriteTokens: number | null;
19
+ reasoningTokens: number | null;
20
+ /** Earliest content token across calls, relative to first request's start. */
21
+ latencyMs: number | null;
22
+ /** LLM phase elapsed wall-clock time. */
23
+ durationMs: number;
24
+ /** Total output tokens / sum of per-call decoding times. Null if data incomplete. */
25
+ outputSpeedTps: number | null;
26
+ }
27
+ export interface GitCommitResult {
28
+ success: boolean;
29
+ status: 'success' | 'failure';
30
+ scope: 'staged' | 'all' | null;
31
+ additions: number;
32
+ deletions: number;
33
+ message: string;
34
+ /** Exactly the first seven characters of the resulting commit OID, or null upon failure. */
35
+ hash: string | null;
36
+ fullHash?: string;
37
+ /** Present when an LLM was invoked; caller-supplied messages omit this. */
38
+ llm?: GitCommitLlmMetrics;
39
+ totalDurationMs?: number;
40
+ error?: string;
41
+ }
42
+ export interface GitCommitSettings {
43
+ globalInstruction: string;
44
+ repositoryInstruction: string;
45
+ inheritGlobal: boolean;
46
+ /** Global preference, defaults to false; affects floating panel only. */
47
+ autoHideNonGit: boolean;
48
+ /** Global setting; defaults to false. */
49
+ showTokenDetails: boolean;
50
+ /** Progress bar color in #rrggbb form; defaults to orange (#f59e0b). */
51
+ countdownColor: string;
52
+ /** Null means default lower-right anchor. Otherwise saved top/right coordinates in CSS pixels. */
53
+ floatingPosition: {right:number;top:number} | null;
54
+ model: { provider: string; model: string } | null;
55
+ /** True for a Git working tree; false for global-only settings in ordinary directories. */
56
+ isRepository: boolean;
57
+ /** Canonical Git root, or null outside a repository. */
58
+ root: string | null;
59
+ }
60
+ export interface GitCommitApi {
61
+ commit(request: GitCommitRequest): Promise<GitCommitResult>;
62
+ status(request: GitCommitRequest): Promise<
63
+ | { isRepository: false; root: null }
64
+ | { isRepository: true; root: string; repo: string; branch: string; head: string;
65
+ additions: number; deletions: number; binary: number;
66
+ untracked: number; unknownUntrackedLines: number; stagedFiles: number }
67
+ >;
68
+ changeToken(request: GitCommitRequest): Promise<{ token: number | null }>;
69
+ settings(request?: GitCommitRequest): Promise<GitCommitSettings>;
70
+ saveSettings(request: GitCommitRequest & { patch: Partial<Pick<GitCommitSettings, 'globalInstruction' | 'repositoryInstruction' | 'inheritGlobal' | 'model' | 'autoHideNonGit' | 'showTokenDetails' | 'countdownColor' | 'floatingPosition'>> }): Promise<GitCommitSettings>;
71
+ models(): Promise<{ models: Array<{provider:string;providerName:string;model:string;name:string}>; defaultModel: {provider:string;model:string}|null }>;
72
+ }
73
+ declare module '@deepseek-ai/cordis' {
74
+ interface Context { gitCommit: GitCommitApi; }
75
+ }
76
+ export const name: string;
77
+ export const inject: string[];
78
+ export function apply(ctx: import('@deepseek-ai/cordis').Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * DSH Host plugin. Register a feature endpoint on Connection's already-authenticated
3
+ * /api transport, NOT a new webServer route. Connection's own host context does
4
+ * not inject webServer and ctx.connection.rpc.handle() can trigger Cordis's
5
+ * "cannot get property webServer without inject" on that shadow context.
6
+ *
7
+ * Browser/desktop clients use the public connection.rpc.call('/api', 'git-commit', ...)
8
+ * protocol. Other host plugins use the in-process ctx.gitCommit service.
9
+ */
10
+ import { GitCommitService } from './core/service.js';
11
+
12
+ export const name = 'dsh-git-commit';
13
+ export const inject = ['connection', 'llm'];
14
+ export const ROUTE = '/api/git-commit';
15
+ const ENDPOINT = 'git-commit';
16
+ const COMMANDS = new Set(['status', 'changeToken', 'settings', 'saveSettings', 'models', 'commit']);
17
+ const MAX_REQUEST_CHARS = 262_144;
18
+ const HEADERS = { 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' };
19
+
20
+ function record(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ function response(rpcId, result) {
25
+ return Response.json({ type: 'server-response', rpcId, result }, { headers: HEADERS });
26
+ }
27
+
28
+ function failure(code, error) {
29
+ return { ok: false, error: { code, message: error instanceof Error ? error.message : String(error), details: {} } };
30
+ }
31
+
32
+ /**
33
+ * The DSH Connection bridge has already authenticated the caller and enforces
34
+ * an owner principal for exact Fetch routes. We only validate application input
35
+ * and return the standard Connection RPC response envelope here.
36
+ */
37
+ export function createHandler(service) {
38
+ return async function serve(request) {
39
+ const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
40
+ if (request.method !== 'POST') return new Response('Method not allowed', { status: 405, headers: HEADERS });
41
+ if (contentType !== 'application/json') return new Response('JSON required', { status: 415, headers: HEADERS });
42
+ let input;
43
+ try {
44
+ const text = await request.text();
45
+ if (text.length > MAX_REQUEST_CHARS) return new Response('Request too large', { status: 413, headers: HEADERS });
46
+ input = JSON.parse(text);
47
+ } catch {
48
+ return new Response('Invalid JSON', { status: 400, headers: HEADERS });
49
+ }
50
+ if (!record(input) || input.type !== 'client-request' || input.method !== ENDPOINT
51
+ || typeof input.rpcId !== 'string' || input.rpcId.length === 0 || input.rpcId.length > 256
52
+ || !record(input.payload) || typeof input.payload.method !== 'string') {
53
+ return new Response('Invalid Connection request', { status: 400, headers: HEADERS });
54
+ }
55
+ const { method, payload } = input.payload;
56
+ if (!COMMANDS.has(method)) return response(input.rpcId, failure('NOT_FOUND', 'Unknown method'));
57
+ const args = record(payload) ? payload : {};
58
+ try {
59
+ const value = await service[method](method === 'commit' ? { ...args, signal: request.signal } : args);
60
+ return response(input.rpcId, { ok: true, value });
61
+ } catch (error) {
62
+ return response(input.rpcId, failure('GIT_COMMIT_ERROR', error));
63
+ }
64
+ };
65
+ }
66
+
67
+ export function apply(ctx) {
68
+ const service = new GitCommitService(ctx);
69
+ // Cordis-native, injected by other Host plugins via inject: ['gitCommit'].
70
+ const release = ctx.reflect.provide('gitCommit', service);
71
+ ctx.effect(() => () => { service.close(); return release(); }, 'dsh-git-commit: service cleanup');
72
+ // Connection handles authentication, Origin trust, and DSH Web/Desktop transport.
73
+ // Use the same exact Fetch-route seam as DSH's /api/file and /api/present.
74
+ ctx.effect(() => ctx.connection.fetch.register({
75
+ path: ROUTE,
76
+ methods: ['POST'],
77
+ requestBody: 'buffered',
78
+ fetch: createHandler(service),
79
+ }), 'dsh-git-commit: authenticated API route');
80
+ }
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@ibobbyts/dsh-git-commit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Floating Git stats and AI commits with native DSH LLM, authenticated RPC, settings and shared service",
6
+ "license": "MIT",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": "./lib/client.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "platform": "web",
22
+ "inject": [
23
+ "@deepseek-ai/dsh-client-connection",
24
+ "@deepseek-ai/dsh-client-ui-renderer",
25
+ "@deepseek-ai/dsh-client-ui-layout",
26
+ "@deepseek-ai/dsh-client-ui-session",
27
+ "@deepseek-ai/dsh-client-ui-sidebar-right"
28
+ ],
29
+ "immediately": true
30
+ }
31
+ },
32
+ "files": [
33
+ "lib",
34
+ "cordis.patch.yml",
35
+ "README.md",
36
+ "LICENSE",
37
+ "THIRD_PARTY.md"
38
+ ],
39
+ "scripts": {
40
+ "test": "node --test test/*.test.js",
41
+ "check": "node --check lib/index.js && node --check lib/client.js && node --test test/*.test.js",
42
+ "prepublishOnly": "npm run check"
43
+ },
44
+ "dependencies": {
45
+ "@visulima/redact": "5.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@deepseek-ai/cordis": "^4.0.1",
49
+ "@deepseek-ai/dsh-llm": ">=0.1.1-rc.2"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.22.1"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/iBobbyTS/dsh-git-commit.git"
57
+ },
58
+ "keywords": [
59
+ "deepseek-harness",
60
+ "dsh",
61
+ "git",
62
+ "commit",
63
+ "ai",
64
+ "plugin"
65
+ ],
66
+ "types": "lib/index.d.ts",
67
+ "homepage": "https://github.com/iBobbyTS/dsh-git-commit#readme",
68
+ "bugs": {
69
+ "url": "https://github.com/iBobbyTS/dsh-git-commit/issues"
70
+ },
71
+ "publishConfig": {
72
+ "access": "public",
73
+ "registry": "https://registry.npmjs.org"
74
+ }
75
+ }