@ivotoby/postgram-cli 1.38.2 → 1.39.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,267 @@
1
+ // cli/src/tracking/here.ts: pgm here — hook and manual workspace/session reports.
2
+ import { appendFile, mkdir, rename, stat } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { readStdinText, resolvePgmConfig, shortId } from '../shared.js';
6
+ import { createTrackingClient } from './api.js';
7
+ import { loadTrackingConfig, usageExit } from './config.js';
8
+ import { readBootId, readMarker, resolveLocalWorkspace } from './local.js';
9
+ import { AGENT_KINDS } from './types.js';
10
+ const HOOK_EVENTS = {
11
+ SessionStart: 'start',
12
+ UserPromptSubmit: 'activity',
13
+ Stop: 'stop',
14
+ SessionEnd: 'end'
15
+ };
16
+ const SESSION_EVENTS = ['start', 'activity', 'stop', 'end'];
17
+ const LOG_ROTATE_BYTES = 1_048_576;
18
+ export function parseHookInput(raw) {
19
+ const trimmed = raw.trim();
20
+ if (trimmed === '') {
21
+ return { kind: 'invalid' };
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(trimmed);
26
+ }
27
+ catch {
28
+ return { kind: 'invalid' };
29
+ }
30
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
31
+ return { kind: 'invalid' };
32
+ }
33
+ const obj = parsed;
34
+ const hookEvent = obj.hook_event_name;
35
+ if (typeof hookEvent !== 'string') {
36
+ return { kind: 'invalid' };
37
+ }
38
+ const event = HOOK_EVENTS[hookEvent];
39
+ if (event === undefined) {
40
+ return { kind: 'ignore' };
41
+ }
42
+ const sessionId = typeof obj.session_id === 'string' && obj.session_id !== ''
43
+ ? obj.session_id
44
+ : null;
45
+ const cwd = typeof obj.cwd === 'string' ? obj.cwd : undefined;
46
+ const transcriptRaw = obj.transcript_path;
47
+ const transcriptPath = typeof transcriptRaw === 'string' || transcriptRaw === null
48
+ ? transcriptRaw
49
+ : undefined;
50
+ const reasonRaw = hookEvent === 'SessionStart'
51
+ ? obj.source
52
+ : hookEvent === 'SessionEnd'
53
+ ? obj.reason
54
+ : undefined;
55
+ const reason = typeof reasonRaw === 'string' && reasonRaw !== '' ? reasonRaw : undefined;
56
+ return {
57
+ kind: 'report',
58
+ event,
59
+ sessionId,
60
+ ...(cwd !== undefined ? { cwd } : {}),
61
+ ...(transcriptPath !== undefined ? { transcriptPath } : {}),
62
+ ...(reason !== undefined ? { reason } : {}),
63
+ hookEvent
64
+ };
65
+ }
66
+ function asAgentKind(value) {
67
+ if (value !== undefined && AGENT_KINDS.includes(value)) {
68
+ return value;
69
+ }
70
+ return null;
71
+ }
72
+ function asSessionEvent(value) {
73
+ if (value !== undefined &&
74
+ SESSION_EVENTS.includes(value)) {
75
+ return value;
76
+ }
77
+ return null;
78
+ }
79
+ function featureLine(feature) {
80
+ return `Postgram: this worktree belongs to feature ${shortId(feature.id)} "${feature.title}" (stage: ${feature.stage ?? 'building'}).`;
81
+ }
82
+ export async function runHere(options, deps) {
83
+ let agent;
84
+ let sessionId;
85
+ let event;
86
+ let cwd;
87
+ let transcriptPath;
88
+ let reason;
89
+ let hookEvent = null;
90
+ if (options.hook) {
91
+ const parsed = parseHookInput(await deps.stdin());
92
+ if (parsed.kind === 'invalid') {
93
+ await deps.log('invalid hook input');
94
+ return { stdout: '', exitCode: 0 };
95
+ }
96
+ if (parsed.kind === 'ignore') {
97
+ return { stdout: '', exitCode: 0 };
98
+ }
99
+ agent = asAgentKind(options.agent) ?? 'claude-code';
100
+ sessionId = parsed.sessionId ?? options.sessionId ?? null;
101
+ event = parsed.event;
102
+ cwd = parsed.cwd ?? options.cwd ?? deps.cwd;
103
+ transcriptPath = parsed.transcriptPath ?? options.transcript ?? null;
104
+ reason = parsed.reason ?? options.reason;
105
+ hookEvent = parsed.hookEvent;
106
+ }
107
+ else {
108
+ if (options.agent !== undefined && asAgentKind(options.agent) === null) {
109
+ process.stderr.write(`pgm here: invalid --agent '${options.agent}'\n`);
110
+ return { stdout: '', exitCode: 2 };
111
+ }
112
+ if (options.event !== undefined && asSessionEvent(options.event) === null) {
113
+ process.stderr.write(`pgm here: invalid --event '${options.event}'\n`);
114
+ return { stdout: '', exitCode: 2 };
115
+ }
116
+ agent = asAgentKind(options.agent) ?? 'claude-code';
117
+ sessionId = options.sessionId ?? null;
118
+ event = asSessionEvent(options.event);
119
+ cwd = options.cwd ?? deps.cwd;
120
+ transcriptPath = options.transcript ?? null;
121
+ reason = options.reason;
122
+ }
123
+ if (deps.config === null) {
124
+ await deps.log('missing config');
125
+ return { stdout: '', exitCode: 0 };
126
+ }
127
+ const config = deps.config;
128
+ if (deps.client === null) {
129
+ await deps.log('missing credentials');
130
+ return { stdout: '', exitCode: 0 };
131
+ }
132
+ const client = deps.client;
133
+ const start = deps.now();
134
+ const local = await deps.resolveWorkspace(cwd, 800);
135
+ if (local.kind === 'timeout') {
136
+ await deps.log('git timeout');
137
+ return { stdout: '', exitCode: 0 };
138
+ }
139
+ const marker = await deps.readMarker(local.path);
140
+ const bootId = await deps.bootId();
141
+ const session = sessionId !== null || event !== null
142
+ ? {
143
+ agent_kind: agent,
144
+ agent_session_id: sessionId,
145
+ event: event ?? 'activity',
146
+ ...(reason !== undefined ? { reason } : {}),
147
+ cwd,
148
+ transcript_path: transcriptPath
149
+ }
150
+ : undefined;
151
+ const body = {
152
+ source: 'hook',
153
+ machine: { alias: config.alias, hostname: deps.hostname, boot_id: bootId },
154
+ workspace: local.workspace,
155
+ ...(marker !== null ? { feature_id: marker } : {}),
156
+ ...(session !== undefined ? { session } : {})
157
+ };
158
+ const timeoutMs = Math.max(100, config.hereTimeoutMs - (deps.now() - start) - 100);
159
+ let feature;
160
+ try {
161
+ const response = await client.report(body, { timeoutMs });
162
+ feature = response.feature;
163
+ }
164
+ catch (error) {
165
+ await deps.log(error instanceof Error ? error.message : 'report failed');
166
+ return { stdout: '', exitCode: 0 };
167
+ }
168
+ if (options.hook) {
169
+ if (event === 'start' &&
170
+ hookEvent === 'SessionStart' &&
171
+ options.quiet === false &&
172
+ feature !== null) {
173
+ return { stdout: `${featureLine(feature)}\n`, exitCode: 0 };
174
+ }
175
+ return { stdout: '', exitCode: 0 };
176
+ }
177
+ const line = feature !== null
178
+ ? `registered ${config.alias}:${local.path} → feature ${shortId(feature.id)} (${feature.stage ?? 'building'})`
179
+ : `registered ${config.alias}:${local.path} (unassigned)`;
180
+ return { stdout: `${line}\n`, exitCode: 0 };
181
+ }
182
+ export async function appendHereLog(homeDir, message, now) {
183
+ const dir = path.join(homeDir, '.pgm');
184
+ await mkdir(dir, { recursive: true });
185
+ const file = path.join(dir, 'here.log');
186
+ const rotated = path.join(dir, 'here.log.1');
187
+ try {
188
+ const existing = await stat(file);
189
+ if (existing.size >= LOG_ROTATE_BYTES) {
190
+ await rename(file, rotated);
191
+ }
192
+ }
193
+ catch {
194
+ // no existing log file
195
+ }
196
+ const line = `${(now ?? new Date()).toISOString()} ${message.replace(/\r?\n/g, ' ')}\n`;
197
+ await appendFile(file, line, 'utf8');
198
+ }
199
+ export function registerHereCommand(program) {
200
+ const command = usageExit(program
201
+ .command('here')
202
+ .description('Report this workspace (and session) to Postgram')
203
+ .option('--agent <kind>', 'agent kind (claude-code, pi, talon, other)')
204
+ .option('--session-id <id>', 'agent session id')
205
+ .option('--event <event>', 'session event (start, activity, stop, end)')
206
+ .option('--reason <text>', 'reason text')
207
+ .option('--cwd <dir>', 'working directory to report')
208
+ .option('--transcript <path>', 'transcript path')
209
+ .option('--hook', 'read the Claude Code hook JSON from stdin')
210
+ .option('--quiet', 'suppress the SessionStart feature line')).action(async () => {
211
+ const opts = command.opts();
212
+ let config = null;
213
+ try {
214
+ config = await loadTrackingConfig();
215
+ }
216
+ catch {
217
+ config = null;
218
+ }
219
+ let client = null;
220
+ try {
221
+ client = createTrackingClient(await resolvePgmConfig());
222
+ }
223
+ catch {
224
+ client = null;
225
+ }
226
+ const homeDir = os.homedir();
227
+ const deps = {
228
+ config,
229
+ client,
230
+ stdin: readStdinText,
231
+ cwd: process.cwd(),
232
+ hostname: os.hostname(),
233
+ homeDir,
234
+ now: Date.now,
235
+ bootId: readBootId,
236
+ resolveWorkspace: (cwd, budgetMs) => resolveLocalWorkspace(cwd, { budgetMs }),
237
+ readMarker,
238
+ log: (line) => appendHereLog(homeDir, line)
239
+ };
240
+ try {
241
+ const result = await runHere({
242
+ hook: opts.hook === true,
243
+ quiet: opts.quiet === true,
244
+ ...(opts.agent !== undefined ? { agent: opts.agent } : {}),
245
+ ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
246
+ ...(opts.event !== undefined ? { event: opts.event } : {}),
247
+ ...(opts.reason !== undefined ? { reason: opts.reason } : {}),
248
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
249
+ ...(opts.transcript !== undefined ? { transcript: opts.transcript } : {})
250
+ }, deps);
251
+ if (result.stdout !== '') {
252
+ process.stdout.write(result.stdout);
253
+ }
254
+ if (result.exitCode !== 0) {
255
+ process.exitCode = result.exitCode;
256
+ }
257
+ }
258
+ catch (error) {
259
+ try {
260
+ await appendHereLog(homeDir, error instanceof Error ? error.message : 'here failed');
261
+ }
262
+ catch {
263
+ // the hook must still exit 0
264
+ }
265
+ }
266
+ });
267
+ }
@@ -0,0 +1,375 @@
1
+ // cli/src/tracking/hooks.ts: pgm hooks install / uninstall.
2
+ import { realpathSync } from 'node:fs';
3
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { handleCliFailure, isJsonMode, printHuman, printJson } from '../shared.js';
9
+ import { UsageError, shq, usageExit } from './config.js';
10
+ export const HOOK_EVENTS = [
11
+ 'SessionStart',
12
+ 'UserPromptSubmit',
13
+ 'Stop',
14
+ 'SessionEnd'
15
+ ];
16
+ export function hookCommand(nodePath, scriptPath) {
17
+ return `${shq(nodePath)} ${shq(scriptPath)} here --hook`;
18
+ }
19
+ export function isPostgramHook(command) {
20
+ return command.includes('pgm') && /\bhere --hook\b/.test(command);
21
+ }
22
+ function isRecord(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+ function isOurHook(hook) {
26
+ return (isRecord(hook) &&
27
+ typeof hook.command === 'string' &&
28
+ isPostgramHook(hook.command));
29
+ }
30
+ function withoutPostgramHooks(entries) {
31
+ const kept = [];
32
+ if (!Array.isArray(entries)) {
33
+ return kept;
34
+ }
35
+ for (const entry of entries) {
36
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) {
37
+ kept.push(entry);
38
+ continue;
39
+ }
40
+ const hooks = entry.hooks.filter((hook) => !isOurHook(hook));
41
+ if (hooks.length === 0) {
42
+ continue;
43
+ }
44
+ kept.push({ ...entry, hooks });
45
+ }
46
+ return kept;
47
+ }
48
+ export function installHooks(settings, command, keepTranscripts) {
49
+ const next = structuredClone(settings);
50
+ const hooks = isRecord(next.hooks) ? next.hooks : {};
51
+ for (const event of HOOK_EVENTS) {
52
+ const kept = withoutPostgramHooks(hooks[event]);
53
+ kept.push({ hooks: [{ type: 'command', command, timeout: 5 }] });
54
+ hooks[event] = kept;
55
+ }
56
+ next.hooks = hooks;
57
+ if (typeof keepTranscripts === 'number') {
58
+ const current = next.cleanupPeriodDays;
59
+ if (!(typeof current === 'number' && current >= keepTranscripts)) {
60
+ next.cleanupPeriodDays = keepTranscripts;
61
+ }
62
+ }
63
+ return next;
64
+ }
65
+ export function uninstallHooks(settings) {
66
+ const next = structuredClone(settings);
67
+ const hooks = isRecord(next.hooks) ? next.hooks : {};
68
+ for (const event of HOOK_EVENTS) {
69
+ hooks[event] = withoutPostgramHooks(hooks[event]);
70
+ }
71
+ next.hooks = hooks;
72
+ return next;
73
+ }
74
+ export function unifiedDiff(before, after, label) {
75
+ if (before === after) {
76
+ return '';
77
+ }
78
+ const a = before.split('\n');
79
+ const b = after.split('\n');
80
+ const n = a.length;
81
+ const m = b.length;
82
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
83
+ for (let i = n - 1; i >= 0; i--) {
84
+ const row = lcs[i];
85
+ const below = lcs[i + 1];
86
+ if (row === undefined || below === undefined) {
87
+ continue;
88
+ }
89
+ for (let j = m - 1; j >= 0; j--) {
90
+ const ai = a[i];
91
+ const bj = b[j];
92
+ if (ai !== undefined && bj !== undefined && ai === bj) {
93
+ row[j] = (below[j + 1] ?? 0) + 1;
94
+ }
95
+ else {
96
+ row[j] = Math.max(row[j + 1] ?? 0, below[j] ?? 0);
97
+ }
98
+ }
99
+ }
100
+ const lines = [`--- ${label}`, `+++ ${label}`];
101
+ let i = 0;
102
+ let j = 0;
103
+ while (i < n && j < m) {
104
+ const ai = a[i];
105
+ const bj = b[j];
106
+ if (ai !== undefined && bj !== undefined && ai === bj) {
107
+ i += 1;
108
+ j += 1;
109
+ continue;
110
+ }
111
+ const down = lcs[i + 1]?.[j] ?? 0;
112
+ const right = lcs[i]?.[j + 1] ?? 0;
113
+ if (down >= right) {
114
+ if (ai !== undefined) {
115
+ lines.push(`-${ai}`);
116
+ }
117
+ i += 1;
118
+ }
119
+ else {
120
+ if (bj !== undefined) {
121
+ lines.push(`+${bj}`);
122
+ }
123
+ j += 1;
124
+ }
125
+ }
126
+ while (i < n) {
127
+ const ai = a[i];
128
+ if (ai !== undefined) {
129
+ lines.push(`-${ai}`);
130
+ }
131
+ i += 1;
132
+ }
133
+ while (j < m) {
134
+ const bj = b[j];
135
+ if (bj !== undefined) {
136
+ lines.push(`+${bj}`);
137
+ }
138
+ j += 1;
139
+ }
140
+ return lines.join('\n');
141
+ }
142
+ export function renderPiExtension(template, nodePath, scriptPath) {
143
+ return template
144
+ .replaceAll("'__PGM_NODE__'", JSON.stringify(nodePath))
145
+ .replaceAll("'__PGM_SCRIPT__'", JSON.stringify(scriptPath));
146
+ }
147
+ export function settingsPath(input) {
148
+ if (input.flag !== undefined) {
149
+ return input.flag;
150
+ }
151
+ const configDir = input.env.CLAUDE_CONFIG_DIR;
152
+ if (configDir !== undefined && configDir !== '') {
153
+ return path.join(configDir, 'settings.json');
154
+ }
155
+ return path.join(input.homeDir, '.claude', 'settings.json');
156
+ }
157
+ export function piExtensionPath(env, homeDir) {
158
+ const piDir = env.PI_CODING_AGENT_DIR;
159
+ const base = piDir !== undefined && piDir !== ''
160
+ ? piDir
161
+ : path.join(homeDir, '.pi', 'agent');
162
+ return path.join(base, 'extensions', 'postgram.ts');
163
+ }
164
+ function isEnoent(error) {
165
+ return (error instanceof Error &&
166
+ 'code' in error &&
167
+ error.code === 'ENOENT');
168
+ }
169
+ export async function runHooksInstall(options, deps) {
170
+ let keep = null;
171
+ if (options.keepTranscripts !== undefined) {
172
+ const value = Number(options.keepTranscripts);
173
+ if (!Number.isInteger(value) || value <= 0) {
174
+ throw new UsageError('--keep-transcripts must be a positive integer');
175
+ }
176
+ keep = value;
177
+ }
178
+ if (options.pi) {
179
+ const target = piExtensionPath(deps.env, deps.homeDir);
180
+ const template = await readFile(deps.piTemplatePath, 'utf8');
181
+ await mkdir(path.dirname(target), { recursive: true });
182
+ await writeFile(target, renderPiExtension(template, deps.nodePath, deps.scriptPath), 'utf8');
183
+ return [`installed pi extension ${target}`];
184
+ }
185
+ const target = settingsPath({
186
+ ...(options.settings !== undefined
187
+ ? { flag: options.settings }
188
+ : {}),
189
+ env: deps.env,
190
+ homeDir: deps.homeDir
191
+ });
192
+ let settings = {};
193
+ try {
194
+ const text = await readFile(target, 'utf8');
195
+ const parsed = JSON.parse(text);
196
+ if (!isRecord(parsed)) {
197
+ throw new UsageError(`settings file ${target} must contain a JSON object`);
198
+ }
199
+ settings = parsed;
200
+ }
201
+ catch (error) {
202
+ if (error instanceof UsageError) {
203
+ throw error;
204
+ }
205
+ if (!isEnoent(error)) {
206
+ throw new UsageError(`cannot read settings ${target}: ${error instanceof Error ? error.message : 'read failed'}`);
207
+ }
208
+ settings = {};
209
+ }
210
+ const next = installHooks(settings, hookCommand(deps.nodePath, deps.scriptPath), keep ?? (options.yes ? null : 365));
211
+ const diff = unifiedDiff(JSON.stringify(settings, null, 2), JSON.stringify(next, null, 2), target);
212
+ if (diff !== '') {
213
+ process.stderr.write(`${diff}\n`);
214
+ }
215
+ if (!options.yes) {
216
+ const confirmed = await deps.confirm(`Write ${target}?`);
217
+ if (!confirmed) {
218
+ throw new UsageError('aborted');
219
+ }
220
+ }
221
+ await mkdir(path.dirname(target), { recursive: true });
222
+ await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
223
+ if (!(await deps.hasCredentials())) {
224
+ process.stderr.write('Warning: ~/.pgmrc has no api_url or api_key; hooks may not inherit PGM_API_*.\n');
225
+ }
226
+ return [`installed hooks in ${target}`];
227
+ }
228
+ export async function runHooksUninstall(options, deps) {
229
+ if (options.pi) {
230
+ const target = piExtensionPath(deps.env, deps.homeDir);
231
+ try {
232
+ await rm(target);
233
+ }
234
+ catch (error) {
235
+ if (!isEnoent(error)) {
236
+ throw error;
237
+ }
238
+ }
239
+ return [`removed pi extension ${target}`];
240
+ }
241
+ const target = settingsPath({
242
+ ...(options.settings !== undefined
243
+ ? { flag: options.settings }
244
+ : {}),
245
+ env: deps.env,
246
+ homeDir: deps.homeDir
247
+ });
248
+ try {
249
+ const text = await readFile(target, 'utf8');
250
+ const parsed = JSON.parse(text);
251
+ if (!isRecord(parsed)) {
252
+ throw new UsageError(`settings file ${target} must contain a JSON object`);
253
+ }
254
+ await writeFile(target, `${JSON.stringify(uninstallHooks(parsed), null, 2)}\n`, 'utf8');
255
+ }
256
+ catch (error) {
257
+ if (error instanceof UsageError) {
258
+ throw error;
259
+ }
260
+ if (!isEnoent(error)) {
261
+ throw error;
262
+ }
263
+ }
264
+ return [`removed hooks in ${target}`];
265
+ }
266
+ async function hasCredentials(homeDir) {
267
+ try {
268
+ const text = await readFile(path.join(homeDir, '.pgmrc'), 'utf8');
269
+ const parsed = JSON.parse(text);
270
+ if (!isRecord(parsed)) {
271
+ return false;
272
+ }
273
+ return (typeof parsed.api_url === 'string' &&
274
+ parsed.api_url !== '' &&
275
+ typeof parsed.api_key === 'string' &&
276
+ parsed.api_key !== '');
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
282
+ function confirmStdin(question) {
283
+ process.stderr.write(`${question} [y/N] `);
284
+ return new Promise((resolve) => {
285
+ const rl = readline.createInterface({
286
+ input: process.stdin,
287
+ terminal: false
288
+ });
289
+ let settled = false;
290
+ const done = (value) => {
291
+ if (settled) {
292
+ return;
293
+ }
294
+ settled = true;
295
+ rl.close();
296
+ resolve(value);
297
+ };
298
+ rl.on('line', (line) => {
299
+ const answer = line.trim().toLowerCase();
300
+ done(answer === 'y' || answer === 'yes');
301
+ });
302
+ rl.on('close', () => done(false));
303
+ });
304
+ }
305
+ function realDeps() {
306
+ const homeDir = os.homedir();
307
+ return {
308
+ homeDir,
309
+ env: process.env,
310
+ nodePath: process.execPath,
311
+ scriptPath: realpathSync(process.argv[1] ?? ''),
312
+ piTemplatePath: fileURLToPath(new URL('../../integrations/pi/postgram.ts', import.meta.url)),
313
+ confirm: confirmStdin,
314
+ hasCredentials: () => hasCredentials(homeDir)
315
+ };
316
+ }
317
+ async function runHooksCommand(command, run) {
318
+ const json = isJsonMode(command);
319
+ try {
320
+ const lines = await run();
321
+ if (json) {
322
+ printJson(lines);
323
+ }
324
+ else {
325
+ printHuman(lines);
326
+ }
327
+ }
328
+ catch (error) {
329
+ if (error instanceof UsageError) {
330
+ process.stderr.write(`${error.message}\n`);
331
+ process.exitCode = 2;
332
+ return;
333
+ }
334
+ await handleCliFailure(error, json);
335
+ }
336
+ }
337
+ export function registerHooksCommands(program) {
338
+ const hooks = usageExit(program
339
+ .command('hooks')
340
+ .description('Install and remove Postgram agent hooks'));
341
+ const install = hooks
342
+ .command('install')
343
+ .description('Install the Claude Code settings hooks (or the pi extension with --pi)')
344
+ .option('--settings <path>', 'path to the Claude Code settings file')
345
+ .option('--yes', 'write without confirming')
346
+ .option('--keep-transcripts <days>', 'set cleanupPeriodDays to this many days')
347
+ .option('--pi', 'install only the pi extension');
348
+ usageExit(install);
349
+ install.action(async (options, command) => {
350
+ await runHooksCommand(command, () => runHooksInstall({
351
+ yes: options.yes === true,
352
+ pi: options.pi === true,
353
+ ...(options.settings !== undefined
354
+ ? { settings: options.settings }
355
+ : {}),
356
+ ...(options.keepTranscripts !== undefined
357
+ ? { keepTranscripts: options.keepTranscripts }
358
+ : {})
359
+ }, realDeps()));
360
+ });
361
+ const uninstall = hooks
362
+ .command('uninstall')
363
+ .description('Remove the Postgram hooks (or the pi extension with --pi)')
364
+ .option('--settings <path>', 'path to the Claude Code settings file')
365
+ .option('--pi', 'remove the pi extension');
366
+ usageExit(uninstall);
367
+ uninstall.action(async (options, command) => {
368
+ await runHooksCommand(command, () => runHooksUninstall({
369
+ pi: options.pi === true,
370
+ ...(options.settings !== undefined
371
+ ? { settings: options.settings }
372
+ : {})
373
+ }, realDeps()));
374
+ });
375
+ }