@ondrej-svec/hog 1.3.0 → 1.4.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.
package/dist/cli.js CHANGED
@@ -9,6 +9,191 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/config.ts
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ import { z } from "zod";
17
+ function migrateConfig(raw) {
18
+ const version = typeof raw["version"] === "number" ? raw["version"] : 1;
19
+ if (version < 2) {
20
+ raw = {
21
+ ...raw,
22
+ version: 2,
23
+ repos: LEGACY_REPOS,
24
+ board: {
25
+ refreshInterval: 60,
26
+ backlogLimit: 20,
27
+ assignee: "unknown"
28
+ }
29
+ };
30
+ }
31
+ const currentVersion = typeof raw["version"] === "number" ? raw["version"] : 2;
32
+ if (currentVersion < 3) {
33
+ raw = {
34
+ ...raw,
35
+ version: 3,
36
+ ticktick: { enabled: existsSync(AUTH_FILE) }
37
+ };
38
+ }
39
+ return HOG_CONFIG_SCHEMA.parse(raw);
40
+ }
41
+ function loadFullConfig() {
42
+ const raw = loadRawConfig();
43
+ if (Object.keys(raw).length === 0) {
44
+ const config2 = migrateConfig({});
45
+ saveFullConfig(config2);
46
+ return config2;
47
+ }
48
+ const version = typeof raw["version"] === "number" ? raw["version"] : 1;
49
+ if (version < 3) {
50
+ const migrated = migrateConfig(raw);
51
+ saveFullConfig(migrated);
52
+ return migrated;
53
+ }
54
+ return HOG_CONFIG_SCHEMA.parse(raw);
55
+ }
56
+ function saveFullConfig(config2) {
57
+ ensureDir();
58
+ writeFileSync(CONFIG_FILE, `${JSON.stringify(config2, null, 2)}
59
+ `, { mode: 384 });
60
+ }
61
+ function loadRawConfig() {
62
+ if (!existsSync(CONFIG_FILE)) return {};
63
+ try {
64
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+ function resolveProfile(config2, profileName) {
70
+ const name = profileName ?? config2.defaultProfile;
71
+ if (!name) {
72
+ return { resolved: config2, activeProfile: null };
73
+ }
74
+ const profile = config2.profiles[name];
75
+ if (!profile) {
76
+ console.error(
77
+ `Profile "${name}" not found. Available: ${Object.keys(config2.profiles).join(", ") || "(none)"}`
78
+ );
79
+ process.exit(1);
80
+ }
81
+ return {
82
+ resolved: { ...config2, repos: profile.repos, board: profile.board, ticktick: profile.ticktick },
83
+ activeProfile: name
84
+ };
85
+ }
86
+ function findRepo(config2, shortNameOrFull) {
87
+ return config2.repos.find((r) => r.shortName === shortNameOrFull || r.name === shortNameOrFull);
88
+ }
89
+ function validateRepoName(name) {
90
+ return REPO_NAME_PATTERN.test(name);
91
+ }
92
+ function ensureDir() {
93
+ mkdirSync(CONFIG_DIR, { recursive: true });
94
+ }
95
+ function getAuth() {
96
+ if (!existsSync(AUTH_FILE)) return null;
97
+ try {
98
+ return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function saveAuth(data) {
104
+ ensureDir();
105
+ writeFileSync(AUTH_FILE, `${JSON.stringify(data, null, 2)}
106
+ `, {
107
+ mode: 384
108
+ });
109
+ }
110
+ function getLlmAuth() {
111
+ const auth = getAuth();
112
+ if (auth?.openrouterApiKey) return { provider: "openrouter", apiKey: auth.openrouterApiKey };
113
+ return null;
114
+ }
115
+ function saveLlmAuth(openrouterApiKey) {
116
+ const existing = getAuth();
117
+ const updated = existing ? { ...existing, openrouterApiKey } : { accessToken: "", clientId: "", clientSecret: "", openrouterApiKey };
118
+ saveAuth(updated);
119
+ }
120
+ function clearLlmAuth() {
121
+ const existing = getAuth();
122
+ if (!existing) return;
123
+ const { openrouterApiKey: _, ...rest } = existing;
124
+ saveAuth(rest);
125
+ }
126
+ function getConfig() {
127
+ if (!existsSync(CONFIG_FILE)) return {};
128
+ try {
129
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
130
+ } catch {
131
+ return {};
132
+ }
133
+ }
134
+ function saveConfig(data) {
135
+ ensureDir();
136
+ const existing = getConfig();
137
+ writeFileSync(CONFIG_FILE, `${JSON.stringify({ ...existing, ...data }, null, 2)}
138
+ `);
139
+ }
140
+ function requireAuth() {
141
+ const auth = getAuth();
142
+ if (!auth) {
143
+ console.error("Not authenticated. Run `hog init` first.");
144
+ process.exit(1);
145
+ }
146
+ return auth;
147
+ }
148
+ var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA, LEGACY_REPOS;
149
+ var init_config = __esm({
150
+ "src/config.ts"() {
151
+ "use strict";
152
+ CONFIG_DIR = join(homedir(), ".config", "hog");
153
+ AUTH_FILE = join(CONFIG_DIR, "auth.json");
154
+ CONFIG_FILE = join(CONFIG_DIR, "config.json");
155
+ COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
156
+ z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
157
+ z.object({ type: z.literal("closeIssue") }),
158
+ z.object({ type: z.literal("addLabel"), label: z.string() })
159
+ ]);
160
+ REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
161
+ REPO_CONFIG_SCHEMA = z.object({
162
+ name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
163
+ shortName: z.string().min(1),
164
+ projectNumber: z.number().int().positive(),
165
+ statusFieldId: z.string().min(1),
166
+ completionAction: COMPLETION_ACTION_SCHEMA,
167
+ statusGroups: z.array(z.string()).optional()
168
+ });
169
+ BOARD_CONFIG_SCHEMA = z.object({
170
+ refreshInterval: z.number().int().min(10).default(60),
171
+ backlogLimit: z.number().int().min(1).default(20),
172
+ assignee: z.string().min(1),
173
+ focusDuration: z.number().int().min(60).default(1500)
174
+ });
175
+ TICKTICK_CONFIG_SCHEMA = z.object({
176
+ enabled: z.boolean().default(true)
177
+ });
178
+ PROFILE_SCHEMA = z.object({
179
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
180
+ board: BOARD_CONFIG_SCHEMA,
181
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true })
182
+ });
183
+ HOG_CONFIG_SCHEMA = z.object({
184
+ version: z.number().int().default(3),
185
+ defaultProjectId: z.string().optional(),
186
+ defaultProjectName: z.string().optional(),
187
+ repos: z.array(REPO_CONFIG_SCHEMA).default([]),
188
+ board: BOARD_CONFIG_SCHEMA,
189
+ ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),
190
+ profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
191
+ defaultProfile: z.string().optional()
192
+ });
193
+ LEGACY_REPOS = [];
194
+ }
195
+ });
196
+
12
197
  // src/ai.ts
13
198
  async function parseHeuristic(input2, today = /* @__PURE__ */ new Date()) {
14
199
  let remaining = input2;
@@ -46,7 +231,7 @@ function detectProvider() {
46
231
  if (orKey) return { provider: "openrouter", apiKey: orKey };
47
232
  const antKey = process.env["ANTHROPIC_API_KEY"];
48
233
  if (antKey) return { provider: "anthropic", apiKey: antKey };
49
- return null;
234
+ return getLlmAuth();
50
235
  }
51
236
  async function callLLM(userText, validLabels, today, providerConfig) {
52
237
  const { provider, apiKey } = providerConfig;
@@ -166,6 +351,7 @@ function hasLlmApiKey() {
166
351
  var init_ai = __esm({
167
352
  "src/ai.ts"() {
168
353
  "use strict";
354
+ init_config();
169
355
  }
170
356
  });
171
357
 
@@ -233,168 +419,6 @@ var init_api = __esm({
233
419
  }
234
420
  });
235
421
 
236
- // src/config.ts
237
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
238
- import { homedir } from "os";
239
- import { join } from "path";
240
- import { z } from "zod";
241
- function migrateConfig(raw) {
242
- const version = typeof raw["version"] === "number" ? raw["version"] : 1;
243
- if (version < 2) {
244
- raw = {
245
- ...raw,
246
- version: 2,
247
- repos: LEGACY_REPOS,
248
- board: {
249
- refreshInterval: 60,
250
- backlogLimit: 20,
251
- assignee: "unknown"
252
- }
253
- };
254
- }
255
- const currentVersion = typeof raw["version"] === "number" ? raw["version"] : 2;
256
- if (currentVersion < 3) {
257
- raw = {
258
- ...raw,
259
- version: 3,
260
- ticktick: { enabled: existsSync(AUTH_FILE) }
261
- };
262
- }
263
- return HOG_CONFIG_SCHEMA.parse(raw);
264
- }
265
- function loadFullConfig() {
266
- const raw = loadRawConfig();
267
- if (Object.keys(raw).length === 0) {
268
- const config2 = migrateConfig({});
269
- saveFullConfig(config2);
270
- return config2;
271
- }
272
- const version = typeof raw["version"] === "number" ? raw["version"] : 1;
273
- if (version < 3) {
274
- const migrated = migrateConfig(raw);
275
- saveFullConfig(migrated);
276
- return migrated;
277
- }
278
- return HOG_CONFIG_SCHEMA.parse(raw);
279
- }
280
- function saveFullConfig(config2) {
281
- ensureDir();
282
- writeFileSync(CONFIG_FILE, `${JSON.stringify(config2, null, 2)}
283
- `, { mode: 384 });
284
- }
285
- function loadRawConfig() {
286
- if (!existsSync(CONFIG_FILE)) return {};
287
- try {
288
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
289
- } catch {
290
- return {};
291
- }
292
- }
293
- function resolveProfile(config2, profileName) {
294
- const name = profileName ?? config2.defaultProfile;
295
- if (!name) {
296
- return { resolved: config2, activeProfile: null };
297
- }
298
- const profile = config2.profiles[name];
299
- if (!profile) {
300
- console.error(
301
- `Profile "${name}" not found. Available: ${Object.keys(config2.profiles).join(", ") || "(none)"}`
302
- );
303
- process.exit(1);
304
- }
305
- return {
306
- resolved: { ...config2, repos: profile.repos, board: profile.board, ticktick: profile.ticktick },
307
- activeProfile: name
308
- };
309
- }
310
- function findRepo(config2, shortNameOrFull) {
311
- return config2.repos.find((r) => r.shortName === shortNameOrFull || r.name === shortNameOrFull);
312
- }
313
- function validateRepoName(name) {
314
- return REPO_NAME_PATTERN.test(name);
315
- }
316
- function ensureDir() {
317
- mkdirSync(CONFIG_DIR, { recursive: true });
318
- }
319
- function getAuth() {
320
- if (!existsSync(AUTH_FILE)) return null;
321
- try {
322
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
323
- } catch {
324
- return null;
325
- }
326
- }
327
- function getConfig() {
328
- if (!existsSync(CONFIG_FILE)) return {};
329
- try {
330
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
331
- } catch {
332
- return {};
333
- }
334
- }
335
- function saveConfig(data) {
336
- ensureDir();
337
- const existing = getConfig();
338
- writeFileSync(CONFIG_FILE, `${JSON.stringify({ ...existing, ...data }, null, 2)}
339
- `);
340
- }
341
- function requireAuth() {
342
- const auth = getAuth();
343
- if (!auth) {
344
- console.error("Not authenticated. Run `hog init` first.");
345
- process.exit(1);
346
- }
347
- return auth;
348
- }
349
- var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA, LEGACY_REPOS;
350
- var init_config = __esm({
351
- "src/config.ts"() {
352
- "use strict";
353
- CONFIG_DIR = join(homedir(), ".config", "hog");
354
- AUTH_FILE = join(CONFIG_DIR, "auth.json");
355
- CONFIG_FILE = join(CONFIG_DIR, "config.json");
356
- COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
357
- z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
358
- z.object({ type: z.literal("closeIssue") }),
359
- z.object({ type: z.literal("addLabel"), label: z.string() })
360
- ]);
361
- REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
362
- REPO_CONFIG_SCHEMA = z.object({
363
- name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
364
- shortName: z.string().min(1),
365
- projectNumber: z.number().int().positive(),
366
- statusFieldId: z.string().min(1),
367
- completionAction: COMPLETION_ACTION_SCHEMA,
368
- statusGroups: z.array(z.string()).optional()
369
- });
370
- BOARD_CONFIG_SCHEMA = z.object({
371
- refreshInterval: z.number().int().min(10).default(60),
372
- backlogLimit: z.number().int().min(1).default(20),
373
- assignee: z.string().min(1),
374
- focusDuration: z.number().int().min(60).default(1500)
375
- });
376
- TICKTICK_CONFIG_SCHEMA = z.object({
377
- enabled: z.boolean().default(true)
378
- });
379
- PROFILE_SCHEMA = z.object({
380
- repos: z.array(REPO_CONFIG_SCHEMA).default([]),
381
- board: BOARD_CONFIG_SCHEMA,
382
- ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true })
383
- });
384
- HOG_CONFIG_SCHEMA = z.object({
385
- version: z.number().int().default(3),
386
- defaultProjectId: z.string().optional(),
387
- defaultProjectName: z.string().optional(),
388
- repos: z.array(REPO_CONFIG_SCHEMA).default([]),
389
- board: BOARD_CONFIG_SCHEMA,
390
- ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),
391
- profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
392
- defaultProfile: z.string().optional()
393
- });
394
- LEGACY_REPOS = [];
395
- }
396
- });
397
-
398
422
  // src/types.ts
399
423
  var init_types = __esm({
400
424
  "src/types.ts"() {
@@ -5088,6 +5112,28 @@ Configuring ${repoName}...`);
5088
5112
  message: " Focus timer duration (seconds):",
5089
5113
  default: "1500"
5090
5114
  });
5115
+ console.log("\nAI-enhanced issue creation (optional):");
5116
+ console.log(
5117
+ ' Press I on the board to create issues with natural language (e.g. "fix login bug #backend @alice due friday").'
5118
+ );
5119
+ console.log(" Without a key the heuristic parser still works \u2014 labels, assignee, and due dates");
5120
+ console.log(" are extracted from #, @, and due tokens. An OpenRouter key enables richer title");
5121
+ console.log(" cleanup and inference for ambiguous input.");
5122
+ const setupLlm = await confirm({
5123
+ message: " Set up an OpenRouter API key now?",
5124
+ default: false
5125
+ });
5126
+ if (setupLlm) {
5127
+ console.log(" Get a free key at https://openrouter.ai/keys");
5128
+ const llmKey = await input({
5129
+ message: " OpenRouter API key:",
5130
+ validate: (v) => v.trim().startsWith("sk-or-") ? true : 'Key must start with "sk-or-"'
5131
+ });
5132
+ saveLlmAuth(llmKey.trim());
5133
+ console.log(" OpenRouter key saved to ~/.config/hog/auth.json");
5134
+ } else {
5135
+ console.log(" Skipped. You can add it later: hog config ai:set-key");
5136
+ }
5091
5137
  const existingConfig = configExists ? loadFullConfig() : void 0;
5092
5138
  const config2 = {
5093
5139
  version: 3,
@@ -5510,7 +5556,7 @@ function resolveProjectId(projectId) {
5510
5556
  process.exit(1);
5511
5557
  }
5512
5558
  var program = new Command();
5513
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.3.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5559
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.4.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5514
5560
  const opts = thisCommand.opts();
5515
5561
  if (opts.json) setFormat("json");
5516
5562
  if (opts.human) setFormat("human");
@@ -5789,6 +5835,61 @@ config.command("ticktick:disable").description("Disable TickTick integration in
5789
5835
  printSuccess("TickTick integration disabled. Board will no longer show TickTick tasks.");
5790
5836
  }
5791
5837
  });
5838
+ config.command("ai:set-key <key>").description("Store an OpenRouter API key for AI-enhanced issue creation (I key on board)").action((key) => {
5839
+ if (!key.startsWith("sk-or-")) {
5840
+ console.error('Error: key must start with "sk-or-". Get one at https://openrouter.ai/keys');
5841
+ process.exit(1);
5842
+ }
5843
+ saveLlmAuth(key);
5844
+ if (useJson()) {
5845
+ jsonOut({ ok: true, message: "OpenRouter key saved" });
5846
+ } else {
5847
+ printSuccess("OpenRouter key saved to ~/.config/hog/auth.json");
5848
+ console.log(" Press I on the board to create issues with natural language.");
5849
+ }
5850
+ });
5851
+ config.command("ai:clear-key").description("Remove the stored OpenRouter API key").action(() => {
5852
+ const existing = getLlmAuth();
5853
+ if (!existing) {
5854
+ if (useJson()) {
5855
+ jsonOut({ ok: true, message: "No key was stored" });
5856
+ } else {
5857
+ console.log("No OpenRouter key stored.");
5858
+ }
5859
+ return;
5860
+ }
5861
+ clearLlmAuth();
5862
+ if (useJson()) {
5863
+ jsonOut({ ok: true, message: "OpenRouter key removed" });
5864
+ } else {
5865
+ printSuccess("OpenRouter key removed from ~/.config/hog/auth.json");
5866
+ }
5867
+ });
5868
+ config.command("ai:status").description("Show whether AI-enhanced issue creation is available and which source provides it").action(() => {
5869
+ const envOr = process.env["OPENROUTER_API_KEY"];
5870
+ const envAnt = process.env["ANTHROPIC_API_KEY"];
5871
+ const stored = getLlmAuth();
5872
+ if (useJson()) {
5873
+ jsonOut({
5874
+ ok: true,
5875
+ data: {
5876
+ active: !!(envOr ?? envAnt ?? stored),
5877
+ source: envOr ? "env:OPENROUTER_API_KEY" : envAnt ? "env:ANTHROPIC_API_KEY" : stored ? "config:auth.json" : null,
5878
+ provider: envOr ? "openrouter" : envAnt ? "anthropic" : stored ? "openrouter" : null
5879
+ }
5880
+ });
5881
+ } else if (envOr) {
5882
+ console.log("AI: active (source: OPENROUTER_API_KEY env var, provider: openrouter)");
5883
+ } else if (envAnt) {
5884
+ console.log("AI: active (source: ANTHROPIC_API_KEY env var, provider: anthropic)");
5885
+ } else if (stored) {
5886
+ console.log("AI: active (source: ~/.config/hog/auth.json, provider: openrouter)");
5887
+ } else {
5888
+ console.log("AI: off \u2014 heuristic-only mode");
5889
+ console.log(" Enable with: hog config ai:set-key <sk-or-...>");
5890
+ console.log(" Or set env: export OPENROUTER_API_KEY=sk-or-...");
5891
+ }
5892
+ });
5792
5893
  config.command("profile:create <name>").description("Create a board profile (copies current top-level config)").action((name) => {
5793
5894
  const cfg = loadFullConfig();
5794
5895
  if (cfg.profiles[name]) {