{"version":3,"file":"issue-reporter.mjs","names":[],"sources":["../../../src/services/issue-reporter.ts"],"sourcesContent":["/**\n * Issue Reporter Service — file GitHub issues from chat.\n *\n * Persistent opt-in per user. When enabled, the agent can proactively\n * suggest filing issues when it detects bugs, errors, or UX problems.\n *\n * Issues are filed via `gh issue create` against the openclawnch repo.\n * Requires `gh` CLI installed and authenticated.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { execSync } from 'node:child_process';\n\n// ── Config ──────────────────────────────────────────────────────────────\n\nconst REPO = 'clawnchdev/openclawnch';\nconst MAX_BODY_CHARS = 4000;\n\ninterface IssueReporterConfig {\n  /** Whether the user has opted in to issue reporting. */\n  enabled: boolean;\n  /** ISO timestamp of when opt-in was granted. */\n  optedInAt?: string;\n  /** Number of issues filed. */\n  issueCount: number;\n}\n\nconst DEFAULT_CONFIG: IssueReporterConfig = {\n  enabled: false,\n  issueCount: 0,\n};\n\n// ── State ───────────────────────────────────────────────────────────────\n\nconst configCache = new Map<string, IssueReporterConfig>();\n\nfunction getStateDir(): string {\n  return process.env.OPENCLAWNCH_STATE_DIR\n    ? join(process.env.OPENCLAWNCH_STATE_DIR, 'issue-reporter')\n    : join(process.env.HOME ?? '/tmp', '.openclawnch', 'issue-reporter');\n}\n\nfunction ensureDir(): void {\n  const dir = getStateDir();\n  if (!existsSync(dir)) {\n    mkdirSync(dir, { recursive: true });\n  }\n}\n\nfunction sanitizeUserId(userId: string): string {\n  return userId.replace(/[^a-zA-Z0-9_\\-. ]/g, '_').slice(0, 64);\n}\n\nfunction configPath(userId: string): string {\n  return join(getStateDir(), `${sanitizeUserId(userId)}.json`);\n}\n\n// ── Public API ──────────────────────────────────────────────────────────\n\nexport function getReporterConfig(userId: string): IssueReporterConfig {\n  const cached = configCache.get(userId);\n  if (cached) return cached;\n\n  const path = configPath(userId);\n  if (existsSync(path)) {\n    try {\n      const raw = JSON.parse(readFileSync(path, 'utf8')) as IssueReporterConfig;\n      configCache.set(userId, raw);\n      return raw;\n    } catch {\n      // Corrupted file — return default\n    }\n  }\n  return { ...DEFAULT_CONFIG };\n}\n\nexport function isReportingEnabled(userId: string): boolean {\n  return getReporterConfig(userId).enabled;\n}\n\nexport function enableReporting(userId: string): void {\n  ensureDir();\n  const config: IssueReporterConfig = {\n    ...getReporterConfig(userId),\n    enabled: true,\n    optedInAt: new Date().toISOString(),\n  };\n  writeFileSync(configPath(userId), JSON.stringify(config, null, 2));\n  configCache.set(userId, config);\n}\n\nexport function disableReporting(userId: string): void {\n  ensureDir();\n  const config: IssueReporterConfig = {\n    ...getReporterConfig(userId),\n    enabled: false,\n  };\n  writeFileSync(configPath(userId), JSON.stringify(config, null, 2));\n  configCache.set(userId, config);\n}\n\n/**\n * File a GitHub issue. Returns the issue URL on success.\n *\n * Labels are auto-applied:\n *   - `from-agent` on every issue\n *   - `bug` / `enhancement` / `question` based on the category param\n */\nexport function fileIssue(opts: {\n  title: string;\n  body: string;\n  category: 'bug' | 'feature' | 'ux' | 'question';\n  userId: string;\n}): { url: string } | { error: string } {\n  const config = getReporterConfig(opts.userId);\n  if (!config.enabled) {\n    return { error: 'Issue reporting is not enabled. Use /report_opt_in to enable.' };\n  }\n\n  // Verify gh CLI is available\n  try {\n    execSync('gh auth status', { stdio: 'pipe', timeout: 5000 });\n  } catch {\n    return { error: 'GitHub CLI (gh) is not authenticated. Run `gh auth login` first.' };\n  }\n\n  const labelMap: Record<string, string> = {\n    bug: 'bug',\n    feature: 'enhancement',\n    ux: 'ux',\n    question: 'question',\n  };\n  const label = labelMap[opts.category] ?? 'bug';\n\n  // Truncate body to prevent massive issues\n  const body = opts.body.length > MAX_BODY_CHARS\n    ? opts.body.slice(0, MAX_BODY_CHARS) + '\\n\\n---\\n*[Truncated — full context exceeded limit]*'\n    : opts.body;\n\n  // Build the issue body with metadata footer\n  const fullBody = [\n    body,\n    '',\n    '---',\n    `*Filed by OpenClawnch agent on behalf of user. Category: ${opts.category}.*`,\n  ].join('\\n');\n\n  try {\n    const result = execSync(\n      `gh issue create --repo ${REPO} --title ${shellEscape(opts.title)} --body ${shellEscape(fullBody)} --label from-agent --label ${shellEscape(label)}`,\n      { encoding: 'utf8', timeout: 15000, stdio: 'pipe' },\n    ).trim();\n\n    // gh issue create prints the URL on success\n    const url = result.split('\\n').pop() ?? result;\n\n    // Update issue count\n    ensureDir();\n    const updated: IssueReporterConfig = {\n      ...config,\n      issueCount: config.issueCount + 1,\n    };\n    writeFileSync(configPath(opts.userId), JSON.stringify(updated, null, 2));\n    configCache.set(opts.userId, updated);\n\n    return { url };\n  } catch (err) {\n    const msg = err instanceof Error ? err.message : String(err);\n    // Don't leak full stderr — extract just the useful part\n    if (msg.includes('label')) {\n      // Labels may not exist on the repo yet — retry without labels\n      try {\n        const result = execSync(\n          `gh issue create --repo ${REPO} --title ${shellEscape(opts.title)} --body ${shellEscape(fullBody)}`,\n          { encoding: 'utf8', timeout: 15000, stdio: 'pipe' },\n        ).trim();\n        const url = result.split('\\n').pop() ?? result;\n        return { url };\n      } catch (retryErr) {\n        return { error: `Failed to create issue: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}` };\n      }\n    }\n    return { error: `Failed to create issue: ${msg}` };\n  }\n}\n\n/** Reset for testing. */\nexport function resetReporter(): void {\n  configCache.clear();\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nfunction shellEscape(s: string): string {\n  // Use $'...' syntax to handle newlines and special chars\n  return \"$'\" + s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\\n/g, '\\\\n') + \"'\";\n}\n"],"mappings":"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"}