@qodly/gentrace 0.1.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,162 @@
1
+ import { loadConfig, resolveApiKey, saveConfig } from '../lib/config.js';
2
+ import { debugLog, parseVerboseFlags } from '../lib/debug.js';
3
+ import { which } from '../lib/exec.js';
4
+ import { getGitAiVersion } from '../lib/git-ai.js';
5
+ import { promptSecret } from '../lib/prompt.js';
6
+ function tokenAllowsTelemetryIngest(iams) {
7
+ return iams.includes('*') || iams.includes('telemetry:ingest');
8
+ }
9
+ function formatIamList(iams) {
10
+ return [...iams].sort().join(', ') || '(none)';
11
+ }
12
+ export async function doctor(argv = []) {
13
+ const rest = parseVerboseFlags(argv);
14
+ if (rest.length > 0) {
15
+ console.error('Usage: gentrace doctor [--verbose|--debug|-v]');
16
+ process.exit(1);
17
+ }
18
+ let ok = true;
19
+ // 1. git
20
+ const gitPath = await which('git');
21
+ if (gitPath) {
22
+ console.log(`✓ git found at ${gitPath}`);
23
+ }
24
+ else {
25
+ console.error('✗ git not found on PATH');
26
+ ok = false;
27
+ }
28
+ // 2. git-ai
29
+ const gitAiVersion = await getGitAiVersion();
30
+ if (gitAiVersion) {
31
+ console.log(`✓ git-ai ${gitAiVersion}`);
32
+ }
33
+ else {
34
+ console.error('✗ git-ai not found — run: gentrace install-git-ai');
35
+ ok = false;
36
+ }
37
+ // 3. API key
38
+ const cfg = loadConfig();
39
+ let apiKey = resolveApiKey(cfg);
40
+ if (apiKey) {
41
+ console.log('✓ API key configured');
42
+ }
43
+ else {
44
+ console.error('✗ API key not configured');
45
+ try {
46
+ apiKey = await promptSecret('Enter your Gentrace API key: ');
47
+ if (apiKey.trim()) {
48
+ saveConfig({ apiKey: apiKey.trim() });
49
+ console.log('✓ API key saved');
50
+ }
51
+ else {
52
+ ok = false;
53
+ }
54
+ }
55
+ catch {
56
+ console.error(' Set GENTRACE_API_KEY or run: gentrace config set apiKey <token>');
57
+ ok = false;
58
+ }
59
+ }
60
+ // 4. API liveness (no auth)
61
+ if (apiKey) {
62
+ const healthUrl = `${cfg.apiUrl}/health`;
63
+ try {
64
+ debugLog('GET', healthUrl);
65
+ const res = await fetch(healthUrl, {
66
+ signal: AbortSignal.timeout(5_000),
67
+ });
68
+ const text = await res.text();
69
+ debugLog('health response', res.status, text.slice(0, 2000));
70
+ if (res.ok) {
71
+ console.log(`✓ API reachable at ${cfg.apiUrl}`);
72
+ }
73
+ else {
74
+ console.error(`✗ API returned ${res.status} at ${healthUrl}`);
75
+ ok = false;
76
+ }
77
+ }
78
+ catch (err) {
79
+ debugLog('health fetch error', err);
80
+ console.error(`✗ API unreachable at ${cfg.apiUrl}`);
81
+ ok = false;
82
+ }
83
+ }
84
+ // 5. Token validity + IAMs (telemetry ingest for gentrace publish)
85
+ if (apiKey) {
86
+ const permUrl = `${cfg.apiUrl}/v1/auth/permissions`;
87
+ try {
88
+ debugLog('GET', permUrl);
89
+ const res = await fetch(permUrl, {
90
+ headers: { 'X-API-Key': apiKey },
91
+ signal: AbortSignal.timeout(8_000),
92
+ });
93
+ const text = await res.text();
94
+ debugLog('permissions response', res.status, text.slice(0, 2000));
95
+ if (res.ok) {
96
+ try {
97
+ const body = JSON.parse(text);
98
+ const iams = body.data?.iams;
99
+ if (!Array.isArray(iams)) {
100
+ console.error('✗ API token check: missing data.iams in response');
101
+ ok = false;
102
+ }
103
+ else if (tokenAllowsTelemetryIngest(iams)) {
104
+ console.log(`✓ API token active; gentrace publish allowed (telemetry:ingest or full access). Effective IAMs: ${formatIamList(iams)}`);
105
+ }
106
+ else {
107
+ console.error('✗ API token is active but cannot ingest telemetry — enable IAM "telemetry:ingest" (or "*") on this access token in the dashboard.');
108
+ console.error(` Effective IAMs on this token: ${formatIamList(iams)}`);
109
+ ok = false;
110
+ }
111
+ }
112
+ catch {
113
+ console.error('✗ API token check: response was not valid JSON');
114
+ ok = false;
115
+ }
116
+ }
117
+ else {
118
+ let detail = text.trim();
119
+ let reason = '';
120
+ try {
121
+ const j = JSON.parse(text);
122
+ reason = j.reason ?? '';
123
+ detail = j.message || j.reason || j.error || detail;
124
+ }
125
+ catch {
126
+ /* keep raw */
127
+ }
128
+ if (res.status === 401) {
129
+ console.error('✗ API token rejected (401 Unauthorized) — missing or unknown credentials.');
130
+ }
131
+ else if (res.status === 403) {
132
+ if (reason === 'token_revoked') {
133
+ console.error('✗ API token has been revoked.');
134
+ }
135
+ else if (reason === 'token_disabled') {
136
+ console.error('✗ API token is disabled.');
137
+ }
138
+ else if (reason === 'token_expired') {
139
+ console.error('✗ API token has expired.');
140
+ }
141
+ else {
142
+ console.error('✗ API token cannot call GET /v1/auth/permissions (403). Use a key that includes at least one of: telemetry:ingest, auth:read, auth:me.');
143
+ if (detail)
144
+ console.error(` ${detail}`);
145
+ }
146
+ }
147
+ else {
148
+ console.error(`✗ API token check failed (${res.status}) at ${permUrl}`);
149
+ if (detail)
150
+ console.error(` ${detail}`);
151
+ }
152
+ ok = false;
153
+ }
154
+ }
155
+ catch (err) {
156
+ debugLog('permissions fetch error', err);
157
+ console.error(`✗ API token check failed to reach ${permUrl}: ${err instanceof Error ? err.message : err}`);
158
+ ok = false;
159
+ }
160
+ }
161
+ process.exit(ok ? 0 : 1);
162
+ }
@@ -0,0 +1,2 @@
1
+ export declare function hooksInstall(cwd?: string): Promise<void>;
2
+ export declare function hooksUninstall(cwd?: string): Promise<void>;
@@ -0,0 +1,128 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { loadConfig, resolveApiKey, saveConfig } from '../lib/config.js';
4
+ import { which } from '../lib/exec.js';
5
+ import { getRepoRoot } from '../lib/git.js';
6
+ import { promptSecret } from '../lib/prompt.js';
7
+ const HOOK_MARKER = '# gentrace-cli post-commit hook';
8
+ function isGentraceHookLine(line) {
9
+ return (line.startsWith('GIT_DIR=') ||
10
+ line.startsWith('GENTRACE_BIN=') ||
11
+ line.startsWith('LOG=') ||
12
+ (line.includes('[gentrace]') && line.includes('Telemetry:')) ||
13
+ line.startsWith('nohup ') ||
14
+ line.startsWith('gentrace ') ||
15
+ line.startsWith('gentrace publish') ||
16
+ (line.startsWith('"') && line.includes(' publish ') && line.includes('2>&1')));
17
+ }
18
+ /** Remove the gentrace block (from marker through its lines); used for uninstall and hook upgrades. */
19
+ function stripGentraceHookFromContent(content) {
20
+ const lines = content.split('\n');
21
+ const filtered = [];
22
+ let skipping = false;
23
+ for (const line of lines) {
24
+ if (line.includes(HOOK_MARKER)) {
25
+ skipping = true;
26
+ continue;
27
+ }
28
+ if (skipping && line === '') {
29
+ skipping = false;
30
+ continue;
31
+ }
32
+ if (skipping && isGentraceHookLine(line)) {
33
+ continue;
34
+ }
35
+ skipping = false;
36
+ filtered.push(line);
37
+ }
38
+ return filtered.join('\n').trim();
39
+ }
40
+ function hookScript() {
41
+ return `#!/bin/sh
42
+ ${HOOK_MARKER}
43
+ GIT_DIR=$(git rev-parse --git-dir)
44
+ GENTRACE_BIN="\${GENTRACE_BIN:-gentrace}"
45
+ LOG="$GIT_DIR/gentrace-publish.log"
46
+ echo "[gentrace] Telemetry: publishing in background (log: $LOG)"
47
+ nohup "$GENTRACE_BIN" publish --daemon >>"$LOG" 2>&1 &\n`;
48
+ }
49
+ function hasAsyncTelemetryHook(body) {
50
+ return body.includes('publish --daemon') && body.includes('nohup');
51
+ }
52
+ export async function hooksInstall(cwd) {
53
+ const repoRoot = await getRepoRoot(cwd || process.cwd());
54
+ const hooksDir = join(repoRoot, '.git', 'hooks');
55
+ const hookPath = join(hooksDir, 'post-commit');
56
+ // Verify gentrace is on PATH
57
+ const gentraceBin = await which('gentrace');
58
+ if (!gentraceBin) {
59
+ console.log('Warning: "gentrace" not found on PATH. The hook will fail unless the CLI is available at commit time.');
60
+ console.log(' Run `bun link` in the gentrace-cli package or add it to your PATH.\n');
61
+ }
62
+ // Ensure API key is set
63
+ const cfg = loadConfig();
64
+ let apiKey = resolveApiKey(cfg);
65
+ if (!apiKey) {
66
+ try {
67
+ apiKey = await promptSecret('Enter your Gentrace API key: ');
68
+ if (apiKey.trim()) {
69
+ saveConfig({ apiKey: apiKey.trim() });
70
+ console.log('API key saved.\n');
71
+ }
72
+ }
73
+ catch {
74
+ console.log('No API key set. Set GENTRACE_API_KEY env or run: gentrace config set apiKey <token>\n');
75
+ }
76
+ }
77
+ if (!existsSync(hooksDir)) {
78
+ mkdirSync(hooksDir, { recursive: true });
79
+ }
80
+ if (existsSync(hookPath)) {
81
+ const existing = readFileSync(hookPath, 'utf-8');
82
+ if (existing.includes(HOOK_MARKER)) {
83
+ if (hasAsyncTelemetryHook(existing)) {
84
+ console.log('Gentrace post-commit hook is already installed.');
85
+ return;
86
+ }
87
+ const stripped = stripGentraceHookFromContent(existing);
88
+ const merged = `${stripped.trimEnd()}\n\n${hookScript()}`.trimEnd();
89
+ writeFileSync(hookPath, `${merged}\n`);
90
+ chmodSync(hookPath, 0o755);
91
+ console.log(`Replaced older gentrace hook with async telemetry at ${hookPath}`);
92
+ return;
93
+ }
94
+ // Append to existing hook
95
+ const appended = `${existing.trimEnd()}\n\n${hookScript()}`;
96
+ writeFileSync(hookPath, appended);
97
+ chmodSync(hookPath, 0o755);
98
+ console.log(`Appended gentrace hook to existing ${hookPath}`);
99
+ }
100
+ else {
101
+ writeFileSync(hookPath, hookScript());
102
+ chmodSync(hookPath, 0o755);
103
+ console.log(`Installed gentrace post-commit hook at ${hookPath}`);
104
+ }
105
+ }
106
+ export async function hooksUninstall(cwd) {
107
+ const repoRoot = await getRepoRoot(cwd || process.cwd());
108
+ const hookPath = join(repoRoot, '.git', 'hooks', 'post-commit');
109
+ if (!existsSync(hookPath)) {
110
+ console.log('No post-commit hook found.');
111
+ return;
112
+ }
113
+ const content = readFileSync(hookPath, 'utf-8');
114
+ if (!content.includes(HOOK_MARKER)) {
115
+ console.log('No gentrace hook found in post-commit.');
116
+ return;
117
+ }
118
+ const remaining = stripGentraceHookFromContent(content);
119
+ if (!remaining || remaining === '#!/bin/sh') {
120
+ unlinkSync(hookPath);
121
+ console.log('Removed post-commit hook.');
122
+ }
123
+ else {
124
+ writeFileSync(hookPath, `${remaining}\n`);
125
+ chmodSync(hookPath, 0o755);
126
+ console.log('Removed gentrace section from post-commit hook.');
127
+ }
128
+ }
@@ -0,0 +1 @@
1
+ export declare function installGitAi(): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { isGitAiInstalled } from '../lib/git-ai.js';
3
+ import { promptConfirm } from '../lib/prompt.js';
4
+ export async function installGitAi() {
5
+ if (await isGitAiInstalled()) {
6
+ const { getGitAiVersion } = await import('../lib/git-ai.js');
7
+ const version = await getGitAiVersion();
8
+ console.log(`git-ai is already installed${version ? ` (${version})` : ''}.`);
9
+ return;
10
+ }
11
+ if (process.platform === 'win32') {
12
+ console.log('On Windows (non-WSL), run:');
13
+ console.log(' powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://usegitai.com/install.ps1 | iex"');
14
+ console.log('\nOn WSL, the Unix installer below works.');
15
+ }
16
+ const ok = await promptConfirm('Install git-ai via curl -sSL https://usegitai.com/install.sh | bash ?');
17
+ if (!ok) {
18
+ console.log('Aborted.');
19
+ process.exit(1);
20
+ }
21
+ console.log('Installing git-ai…');
22
+ await new Promise((resolve, reject) => {
23
+ const child = spawn('bash', ['-c', 'curl -sSL https://usegitai.com/install.sh | bash'], {
24
+ stdio: 'inherit',
25
+ });
26
+ child.on('error', reject);
27
+ child.on('close', (code) => {
28
+ if (code === 0)
29
+ resolve();
30
+ else
31
+ reject(new Error(`Installer exited with code ${code}`));
32
+ });
33
+ });
34
+ if (await isGitAiInstalled()) {
35
+ console.log('git-ai installed successfully.');
36
+ }
37
+ else {
38
+ console.error('git-ai binary not found after install. You may need to reload your shell or add ~/.git-ai/bin to PATH.');
39
+ process.exit(1);
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export declare function publish(argv?: string[]): Promise<void>;
@@ -0,0 +1,224 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { loadConfig, resolveApiKey, saveConfig } from '../lib/config.js';
3
+ import { debugLog, isDebug, parseVerboseFlags, redactApiKey } from '../lib/debug.js';
4
+ import { getCommitInfo, getOriginUrl, getRepoRoot } from '../lib/git.js';
5
+ import { getDiff, getGitAiVersion, getPrompt, getShow, isGitAiInstalled, waitForGitAiStats, } from '../lib/git-ai.js';
6
+ import { resolvePublishAiContext } from '../lib/git-ai-ingest-context.js';
7
+ import { promptSecret } from '../lib/prompt.js';
8
+ import { matchesSkipList } from '../lib/skip.js';
9
+ import { postTelemetryWithRetries } from '../lib/telemetry-post.js';
10
+ const CLI_VERSION = '2.0.0';
11
+ const MAX_DEBUG_BODY = 200_000;
12
+ const DAEMON_FLAGS = new Set(['--daemon', '--background']);
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ export async function publish(argv = []) {
17
+ const rest = parseVerboseFlags(argv);
18
+ let daemon = false;
19
+ const positional = [];
20
+ for (const a of rest) {
21
+ if (DAEMON_FLAGS.has(a))
22
+ daemon = true;
23
+ else
24
+ positional.push(a);
25
+ }
26
+ if (positional.length > 1) {
27
+ console.error('Usage: gentrace publish [--verbose|--debug|-v] [--daemon] [cwd]');
28
+ process.exit(1);
29
+ }
30
+ const cwd = positional[0];
31
+ const repoRoot = await getRepoRoot(cwd || process.cwd());
32
+ // Resolve repository identity: prefer origin URL, fallback to path
33
+ const originUrl = await getOriginUrl(repoRoot);
34
+ const repositoryId = originUrl || repoRoot;
35
+ const cfg = loadConfig();
36
+ // Skip list check
37
+ if (matchesSkipList(repositoryId, cfg.skipRepositories)) {
38
+ console.log(`Skipping ${repositoryId} (matches skip list)`);
39
+ return;
40
+ }
41
+ // Ensure git-ai is available
42
+ if (!(await isGitAiInstalled())) {
43
+ console.error('git-ai is not installed. Run: gentrace install-git-ai');
44
+ process.exit(1);
45
+ }
46
+ // Ensure API key
47
+ let apiKey = resolveApiKey(cfg);
48
+ if (!apiKey) {
49
+ try {
50
+ apiKey = await promptSecret('Enter your Gentrace API key: ');
51
+ if (apiKey.trim()) {
52
+ saveConfig({ apiKey: apiKey.trim() });
53
+ }
54
+ }
55
+ catch {
56
+ // non-interactive — bail
57
+ }
58
+ if (!apiKey?.trim()) {
59
+ console.error('No API key. Set GENTRACE_API_KEY or run: gentrace config set apiKey <token>');
60
+ process.exit(1);
61
+ }
62
+ }
63
+ const delayFromEnv = Number(process.env.GENTRACE_PUBLISH_DELAY_MS);
64
+ const delayMs = Number.isFinite(delayFromEnv) ? Math.max(0, delayFromEnv) : daemon ? 2500 : 0;
65
+ if (delayMs > 0) {
66
+ console.log(`[gentrace] waiting ${delayMs}ms before collecting telemetry…`);
67
+ await sleep(delayMs);
68
+ }
69
+ // Resolve HEAD first so stats/diff target the same commit.
70
+ const commitInfo = await getCommitInfo(repoRoot);
71
+ // post-commit can run before git-ai finishes attributing; poll until stats are populated.
72
+ const stats = await waitForGitAiStats(repoRoot, 'HEAD', commitInfo.additions);
73
+ const [diff, authorshipLog, gitAiVersion] = await Promise.all([
74
+ getDiff(repoRoot),
75
+ getShow(repoRoot),
76
+ getGitAiVersion(),
77
+ ]);
78
+ // Optionally collect prompts
79
+ let prompts;
80
+ if (cfg.includePrompts && authorshipLog) {
81
+ prompts = await collectPrompts(repoRoot, authorshipLog);
82
+ }
83
+ // Derive AI tool / model / IDE from git-ai stats (and diff sessions as fallback)
84
+ const { aiProvider, aiModel, ide } = resolvePublishAiContext(stats, diff);
85
+ // Deterministic event ID: stable across retries
86
+ const repoHash = createHash('sha256').update(repositoryId).digest('hex').slice(0, 12);
87
+ const eventId = `gitai:${repoHash}:${commitInfo.sha}`;
88
+ const event = {
89
+ id: eventId,
90
+ type: 'commit_attribution',
91
+ timestamp: commitInfo.committedAt || new Date().toISOString(),
92
+ sessionId: `git-ai:${commitInfo.sha}`,
93
+ repositoryId,
94
+ scmProvider: 'git',
95
+ branch: commitInfo.branch,
96
+ commitSha: commitInfo.sha,
97
+ filePath: '__repository__',
98
+ aiProvider,
99
+ aiModel,
100
+ ide,
101
+ developerId: commitInfo.authorEmail || commitInfo.authorName,
102
+ developerEmail: commitInfo.authorEmail,
103
+ eventSource: 'command',
104
+ tokensGenerated: stats?.ai_additions,
105
+ metadata: {
106
+ source: 'git-ai',
107
+ cliVersion: CLI_VERSION,
108
+ gitAiVersion,
109
+ stats,
110
+ diff,
111
+ authorshipLog,
112
+ ...(prompts ? { prompts } : {}),
113
+ git: {
114
+ sha: commitInfo.sha,
115
+ parentShas: commitInfo.parentShas,
116
+ subject: commitInfo.subject,
117
+ body: commitInfo.body,
118
+ authorName: commitInfo.authorName,
119
+ authorEmail: commitInfo.authorEmail,
120
+ committerName: commitInfo.committerName,
121
+ committerEmail: commitInfo.committerEmail,
122
+ authoredAt: commitInfo.authoredAt,
123
+ committedAt: commitInfo.committedAt,
124
+ additions: commitInfo.additions,
125
+ deletions: commitInfo.deletions,
126
+ changedFiles: commitInfo.changedFiles,
127
+ },
128
+ attribution: {
129
+ aiLines: Number(stats?.ai_additions ?? 0),
130
+ humanLines: Number(stats?.human_additions ?? 0),
131
+ totalAddedLines: Number(stats?.ai_additions ?? 0) + Number(stats?.human_additions ?? 0),
132
+ totalRemovedLines: Number(stats?.git_diff_deleted_lines ?? 0),
133
+ byProvider: stats?.tool_model_breakdown ?? {},
134
+ },
135
+ },
136
+ };
137
+ const batch = {
138
+ events: [event],
139
+ sentAt: new Date().toISOString(),
140
+ sdkVersion: `gentrace-cli/${CLI_VERSION}`,
141
+ };
142
+ // Send to API
143
+ const url = `${cfg.apiUrl}/v1/telemetry`;
144
+ const bodyJson = JSON.stringify(batch);
145
+ if (isDebug()) {
146
+ debugLog('repository root', repoRoot);
147
+ debugLog('repositoryId', repositoryId);
148
+ debugLog('event id', eventId);
149
+ debugLog('POST', url);
150
+ debugLog('headers', {
151
+ 'Content-Type': 'application/json',
152
+ 'X-API-Key': redactApiKey(apiKey),
153
+ });
154
+ if (bodyJson.length > MAX_DEBUG_BODY) {
155
+ debugLog(`request body (${bodyJson.length} bytes, truncated)`);
156
+ debugLog(bodyJson.slice(0, MAX_DEBUG_BODY));
157
+ debugLog('…');
158
+ }
159
+ else {
160
+ debugLog('request body', bodyJson);
161
+ }
162
+ }
163
+ const res = await postTelemetryWithRetries(url, apiKey, bodyJson);
164
+ const responseText = res.responseText;
165
+ if (isDebug()) {
166
+ debugLog('response status', res.status);
167
+ const preview = responseText.length > MAX_DEBUG_BODY
168
+ ? `${responseText.slice(0, MAX_DEBUG_BODY)}…`
169
+ : responseText;
170
+ debugLog('response body', preview || '(empty)');
171
+ }
172
+ if (res.ok) {
173
+ let accepted = 1;
174
+ if (responseText.trim()) {
175
+ try {
176
+ accepted = JSON.parse(responseText).accepted ?? 1;
177
+ }
178
+ catch {
179
+ debugLog('response JSON parse failed; treating accepted as 1');
180
+ }
181
+ }
182
+ console.log(`Published commit ${commitInfo.sha.slice(0, 8)} (accepted: ${accepted})`);
183
+ }
184
+ else if (res.status === 409) {
185
+ console.log(`Commit ${commitInfo.sha.slice(0, 8)} already recorded.`);
186
+ }
187
+ else if (res.status === 401 || res.status === 403) {
188
+ let message = '';
189
+ try {
190
+ message = responseText
191
+ ? (JSON.parse(responseText).message ?? '')
192
+ : '';
193
+ }
194
+ catch {
195
+ message = responseText;
196
+ }
197
+ console.error(`Auth error (${res.status}): ${message || 'check your API key'}`);
198
+ console.error('Create a token with telemetry:ingest at your Gentrace dashboard.');
199
+ process.exit(1);
200
+ }
201
+ else {
202
+ const detail = res.status === 0
203
+ ? responseText.trim() || 'could not reach API after retries'
204
+ : responseText.trim() || '(empty body; try gentrace publish --verbose)';
205
+ console.error(`API error ${res.status}: ${detail}`);
206
+ process.exit(1);
207
+ }
208
+ }
209
+ /** Extract prompt IDs from authorship log text and fetch each. */
210
+ async function collectPrompts(cwd, authorshipLog) {
211
+ const idPattern = /^\s+(\S+)\s+[\d,-]+$/gm;
212
+ const ids = new Set();
213
+ for (const match of authorshipLog.matchAll(idPattern)) {
214
+ if (match[1])
215
+ ids.add(match[1]);
216
+ }
217
+ const result = {};
218
+ for (const id of ids) {
219
+ const prompt = await getPrompt(cwd, id);
220
+ if (prompt)
221
+ result[id] = prompt;
222
+ }
223
+ return result;
224
+ }
@@ -0,0 +1,3 @@
1
+ export declare function skipAdd(pattern: string): void;
2
+ export declare function skipRemove(pattern: string): void;
3
+ export declare function skipList(): void;
@@ -0,0 +1,30 @@
1
+ import { loadConfig, saveConfig } from '../lib/config.js';
2
+ export function skipAdd(pattern) {
3
+ const cfg = loadConfig();
4
+ if (cfg.skipRepositories.includes(pattern)) {
5
+ console.log(`Already in skip list: ${pattern}`);
6
+ return;
7
+ }
8
+ saveConfig({ skipRepositories: [...cfg.skipRepositories, pattern] });
9
+ console.log(`Added to skip list: ${pattern}`);
10
+ }
11
+ export function skipRemove(pattern) {
12
+ const cfg = loadConfig();
13
+ const next = cfg.skipRepositories.filter((p) => p !== pattern);
14
+ if (next.length === cfg.skipRepositories.length) {
15
+ console.log(`Not in skip list: ${pattern}`);
16
+ return;
17
+ }
18
+ saveConfig({ skipRepositories: next });
19
+ console.log(`Removed from skip list: ${pattern}`);
20
+ }
21
+ export function skipList() {
22
+ const cfg = loadConfig();
23
+ if (cfg.skipRepositories.length === 0) {
24
+ console.log('No repositories in skip list.');
25
+ return;
26
+ }
27
+ for (const p of cfg.skipRepositories) {
28
+ console.log(` ${p}`);
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ export { publish } from './commands/publish.js';
2
+ export { loadConfig, saveConfig } from './lib/config.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { publish } from './commands/publish.js';
2
+ export { loadConfig, saveConfig } from './lib/config.js';
@@ -0,0 +1,9 @@
1
+ export interface GentraceCliConfig {
2
+ apiUrl: string;
3
+ apiKey: string;
4
+ includePrompts: boolean;
5
+ skipRepositories: string[];
6
+ }
7
+ export declare function loadConfig(): GentraceCliConfig;
8
+ export declare function saveConfig(patch: Partial<GentraceCliConfig>): GentraceCliConfig;
9
+ export declare function resolveApiKey(cfg: GentraceCliConfig): string;
@@ -0,0 +1,50 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const DEFAULTS = {
5
+ apiUrl: 'https://gentrace.4d-ps.com/api',
6
+ apiKey: '',
7
+ includePrompts: false,
8
+ skipRepositories: [],
9
+ };
10
+ function configDir() {
11
+ const xdg = process.env.XDG_CONFIG_HOME;
12
+ return join(xdg || join(homedir(), '.config'), 'gentrace');
13
+ }
14
+ function configPath() {
15
+ return join(configDir(), 'config.json');
16
+ }
17
+ export function loadConfig() {
18
+ const envUrl = process.env.GENTRACE_API_URL;
19
+ const envKey = process.env.GENTRACE_API_KEY;
20
+ let persisted = {};
21
+ const p = configPath();
22
+ if (existsSync(p)) {
23
+ try {
24
+ persisted = JSON.parse(readFileSync(p, 'utf-8'));
25
+ }
26
+ catch {
27
+ // corrupt file — fall through to defaults
28
+ }
29
+ }
30
+ return {
31
+ apiUrl: envUrl || persisted.apiUrl || DEFAULTS.apiUrl,
32
+ apiKey: envKey || persisted.apiKey || DEFAULTS.apiKey,
33
+ includePrompts: persisted.includePrompts ?? DEFAULTS.includePrompts,
34
+ skipRepositories: persisted.skipRepositories ?? DEFAULTS.skipRepositories,
35
+ };
36
+ }
37
+ export function saveConfig(patch) {
38
+ const current = loadConfig();
39
+ const merged = { ...current, ...patch };
40
+ const dir = configDir();
41
+ if (!existsSync(dir)) {
42
+ mkdirSync(dir, { recursive: true });
43
+ }
44
+ const p = configPath();
45
+ writeFileSync(p, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
46
+ return merged;
47
+ }
48
+ export function resolveApiKey(cfg) {
49
+ return process.env.GENTRACE_API_KEY || cfg.apiKey;
50
+ }
@@ -0,0 +1,8 @@
1
+ /** CLI / GENTRACE_DEBUG verbose logging (stderr). */
2
+ export declare const VERBOSE_FLAGS: Set<string>;
3
+ export declare function isDebug(): boolean;
4
+ export declare function enableDebug(): void;
5
+ /** Remove --verbose / --debug / -v from argv; enables debug if any were present. */
6
+ export declare function parseVerboseFlags(argv: string[]): string[];
7
+ export declare function debugLog(...parts: unknown[]): void;
8
+ export declare function redactApiKey(key: string): string;