tissues 0.5.2 โ 0.6.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/README.md +94 -40
- package/package.json +3 -4
- package/src/cli.js +24 -22
- package/src/commands/ai.js +266 -0
- package/src/commands/auth.js +4 -4
- package/src/commands/config.js +961 -12
- package/src/commands/create.js +516 -157
- package/src/commands/drafts.js +288 -0
- package/src/commands/list.js +7 -5
- package/src/commands/status.js +81 -19
- package/src/commands/templates.js +157 -0
- package/src/lib/ai/adapters/anthropic.js +52 -0
- package/src/lib/ai/adapters/base.js +45 -0
- package/src/lib/ai/adapters/command.js +58 -0
- package/src/lib/ai/adapters/gemini.js +56 -0
- package/src/lib/ai/adapters/ollama.js +60 -0
- package/src/lib/ai/adapters/openai-compat.js +51 -0
- package/src/lib/ai/adapters/openai.js +44 -0
- package/src/lib/ai/body-template.js +60 -0
- package/src/lib/ai/enhance.js +70 -0
- package/src/lib/ai/index.js +122 -0
- package/src/lib/ai/pipeline.js +79 -0
- package/src/lib/ai/prompt.js +39 -0
- package/src/lib/ai/router.js +128 -0
- package/src/lib/ai/steps.js +472 -0
- package/src/lib/attribution.js +18 -179
- package/src/lib/clipboard.js +147 -0
- package/src/lib/color.js +9 -0
- package/src/lib/dedup.js +33 -4
- package/src/lib/defaults.js +38 -2
- package/src/lib/drafts.js +439 -0
- package/src/lib/gh.js +86 -11
- package/src/lib/repo-picker.js +2 -0
- package/src/lib/safety.js +1 -1
- package/src/lib/templates.js +8 -12
- package/src/lib/theme.js +9 -0
- package/src/commands/use.js +0 -19
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# tissues
|
|
1
|
+
# tissues ๐งป
|
|
2
2
|
|
|
3
|
-
AI-
|
|
3
|
+
AI Enhanced GitHub Issues: create consistent, high-quality GitHub Issues cut for agents to make reliable updates to your codebase through rich context add-ons for git history, related issues, project rules, coding standards, risk and complexity scores, auto-labeling, and templates.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Agent Safety Mechanics: automatic data recovery, configurable rate limits, loop detection with circuit breakers, auditable signatures.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/tissues)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -32,13 +32,13 @@ Requires Node.js >= 18.
|
|
|
32
32
|
# Authenticate (auto-detects gh CLI token)
|
|
33
33
|
tissues auth
|
|
34
34
|
|
|
35
|
-
# Set your active repo
|
|
36
|
-
tissues
|
|
35
|
+
# Set your active repo and configure AI (interactive wizard)
|
|
36
|
+
tissues config
|
|
37
37
|
|
|
38
38
|
# Create an issue interactively (title as positional argument โ no quotes needed)
|
|
39
39
|
tissues create fix the login bug
|
|
40
40
|
|
|
41
|
-
# Check safety
|
|
41
|
+
# Check auth, safety, and config status
|
|
42
42
|
tissues status
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -54,16 +54,6 @@ Authenticate with GitHub. Auto-detects your `gh` CLI token if you're already log
|
|
|
54
54
|
tissues auth
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
### `tissues use`
|
|
58
|
-
|
|
59
|
-
Set the active repository context. Optional โ all commands also accept `--repo` directly. Once set, subsequent commands use this repo automatically unless overridden.
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
tissues use calebogden/tissues # set by name
|
|
63
|
-
tissues use # prompts: pick a repo from your GitHub account
|
|
64
|
-
tissues use --repo calebogden/tissues # same, via flag
|
|
65
|
-
```
|
|
66
|
-
|
|
67
57
|
### `tissues create`
|
|
68
58
|
|
|
69
59
|
Create a new GitHub issue. Runs dedup checks and safety gates before creating anything.
|
|
@@ -77,13 +67,15 @@ tissues create [title...] [options]
|
|
|
77
67
|
| Flag | Description |
|
|
78
68
|
|---|---|
|
|
79
69
|
| `--repo <owner/name>` | Override active repo |
|
|
80
|
-
| `--template <name>` | Template to use: `
|
|
81
|
-
| `--title <title>` | Issue title (skips interactive prompt) |
|
|
70
|
+
| `--template <name>` | Template to use: `default`, `bug`, `feature`, or custom |
|
|
71
|
+
| `--title <title>` | Issue title (skips interactive prompt and triage step โ title used as-is) |
|
|
82
72
|
| `--body <text>` | Issue body / description โ the actual content describing the issue (skips interactive prompt) |
|
|
83
73
|
| `--instructions <text>` | AI enhancement instructions: guides how the AI writes the issue body (e.g. "keep it under 200 words", "use formal tone"). Does not appear in the created issue. Optional โ skips interactive prompt when provided. |
|
|
84
74
|
| `--no-enhance` | Skip AI enhancement, use rendered template as-is |
|
|
75
|
+
| `--pipeline` | Force multi-step AI pipeline even if config disabled |
|
|
76
|
+
| `--no-pipeline` | Force single-shot AI enhancement even if pipeline enabled |
|
|
85
77
|
| `--batch <file>` | Create multiple issues from a JSON file. Each item supports: `title`, `body`, `template`, `labels`, `agent`, `session` |
|
|
86
|
-
| `--labels <labels>` | Comma-separated labels to apply |
|
|
78
|
+
| `--labels <labels>` | Comma-separated labels to apply. If a label doesn't exist on the repo, tissues will prompt to create it automatically. |
|
|
87
79
|
| `--agent <name>` | Agent identifier for attribution (default: `human`) |
|
|
88
80
|
| `--session <id>` | Session ID for attribution and idempotency |
|
|
89
81
|
| `--force` | Skip dedup warnings (still blocks on exact matches) |
|
|
@@ -113,6 +105,23 @@ tissues create \
|
|
|
113
105
|
--dry-run
|
|
114
106
|
```
|
|
115
107
|
|
|
108
|
+
### `tissues drafts`
|
|
109
|
+
|
|
110
|
+
Manage issue drafts. tissues saves your issue to `.tissues/drafts/` before any safety or dedup checks โ the drafts command lets you view, flush, retry failed items, or clear the queue.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
tissues drafts # interactive management menu
|
|
114
|
+
tissues drafts flush # flush pending issues (non-interactive)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `tissues templates`
|
|
118
|
+
|
|
119
|
+
Manage issue templates interactively. View, edit, or create templates. Editing a built-in template creates a user copy that overrides the default.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
tissues templates
|
|
123
|
+
```
|
|
124
|
+
|
|
116
125
|
### `tissues list`
|
|
117
126
|
|
|
118
127
|
Browse open issues for the active repo.
|
|
@@ -124,7 +133,7 @@ tissues list --repo owner/other-repo
|
|
|
124
133
|
|
|
125
134
|
### `tissues status`
|
|
126
135
|
|
|
127
|
-
Show
|
|
136
|
+
Show auth, AI config, safety status, and draft count for the active repo.
|
|
128
137
|
|
|
129
138
|
```bash
|
|
130
139
|
tissues status
|
|
@@ -132,17 +141,51 @@ tissues status --agent claude-opus-4-6
|
|
|
132
141
|
tissues status --reset # force-reset circuit breaker to closed
|
|
133
142
|
```
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Data Recovery
|
|
147
|
+
|
|
148
|
+
tissues never silently discards your input. Before any safety check or network call, your issue is written to `.tissues/drafts/`. If a rate limit, circuit breaker, or network failure blocks submission, your work is saved and `tissues drafts flush` sends it when you're ready.
|
|
136
149
|
|
|
150
|
+
```bash
|
|
151
|
+
# See what's waiting
|
|
152
|
+
tissues status # shows "Drafts: N pending"
|
|
153
|
+
|
|
154
|
+
# Manage or flush
|
|
155
|
+
tissues drafts # interactive: view, flush, retry, clear
|
|
156
|
+
tissues drafts flush # send all pending (non-interactive)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
โ See [Non-Destructive Data Policy](docs/data-policy.md) and [Your Data in tissues](docs/user-data.md) for full details.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## AI Enhancement
|
|
164
|
+
|
|
165
|
+
tissues enhances your issues using an 8-step AI pipeline. You type freeform text into "What's the issue" and the pipeline derives everything: the **triage** step extracts a concise title and structured description, followed by duplicate check, context extraction, scope analysis, complexity scoring, risk assessment, label suggestion, and body formatting. Each step feeds the next, producing structured issues with scores, auto-labels, and rich sections.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Interactive setup
|
|
169
|
+
tissues config
|
|
170
|
+
|
|
171
|
+
# Or set a key directly
|
|
172
|
+
tissues config set ai.keys.anthropic sk-ant-your-key
|
|
137
173
|
```
|
|
138
|
-
Circuit Breaker: closed โ
|
|
139
|
-
Rate Limit: 3/10 per hour (7 remaining)
|
|
140
|
-
Burst: 1/5 in last 5 min
|
|
141
|
-
Global: 3/30 per hour (27 remaining)
|
|
142
174
|
|
|
143
|
-
Last issue created: 12 minutes ago
|
|
144
|
-
Fingerprints stored: 47
|
|
145
175
|
```
|
|
176
|
+
โ Input analyzed: Fix OAuth 401 error on Safari
|
|
177
|
+
โ Duplicate check (none)
|
|
178
|
+
โ Context gathered
|
|
179
|
+
โ Scope analyzed (3 files)
|
|
180
|
+
โ Complexity: 4/10
|
|
181
|
+
โ Risk: 2/10
|
|
182
|
+
โ Labels: bug, P2-medium
|
|
183
|
+
โ Body formatted
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The pipeline is resilient โ failed steps are skipped, and the format step always runs with whatever data is available. Use `--no-pipeline` for single-shot enhancement, or `--no-enhance` to skip AI entirely.
|
|
187
|
+
|
|
188
|
+
โ See [AI Enhancement docs](docs/features/ai.md) for pipeline configuration, providers, routing rules, model selection, and token budgets.
|
|
146
189
|
|
|
147
190
|
---
|
|
148
191
|
|
|
@@ -234,20 +277,34 @@ Configuration is loaded and merged from three sources in ascending priority orde
|
|
|
234
277
|
}
|
|
235
278
|
```
|
|
236
279
|
|
|
237
|
-
|
|
280
|
+
AI keys go in user-level config only (`~/.config/tissues/config.json`) โ never commit them:
|
|
238
281
|
|
|
239
|
-
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"ai": {
|
|
285
|
+
"provider": "anthropic",
|
|
286
|
+
"keys": {
|
|
287
|
+
"anthropic": "sk-ant-...",
|
|
288
|
+
"openai": "sk-..."
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
240
293
|
|
|
241
|
-
|
|
242
|
-
# Set a value
|
|
243
|
-
tissues config set safety.maxPerHour 5
|
|
244
|
-
tissues config set attribution.defaultAgent my-agent
|
|
294
|
+
### `tissues config`
|
|
245
295
|
|
|
246
|
-
|
|
247
|
-
tissues config get safety.maxPerHour
|
|
296
|
+
Interactive config wizard or direct KVP access. Values are stored in `~/.config/tissues/config.json`.
|
|
248
297
|
|
|
249
|
-
|
|
250
|
-
|
|
298
|
+
```bash
|
|
299
|
+
# Interactive wizard (main menu: repo, AI provider, keys, budgets, templates, safety, routing, backup)
|
|
300
|
+
tissues config
|
|
301
|
+
|
|
302
|
+
# Direct KVP access
|
|
303
|
+
tissues config ai.provider # get
|
|
304
|
+
tissues config ai.provider anthropic # set
|
|
305
|
+
tissues config set safety.maxPerHour 5 # set (subcommand form)
|
|
306
|
+
tissues config get safety.maxPerHour # get (subcommand form)
|
|
307
|
+
tissues config list # show all resolved config
|
|
251
308
|
```
|
|
252
309
|
|
|
253
310
|
### Hooks
|
|
@@ -271,9 +328,6 @@ TISSUES_ISSUE_URL # https://github.com/owner/repo/issues/142
|
|
|
271
328
|
| `default` | Generic summary + details + additional context |
|
|
272
329
|
| `bug` | Steps to reproduce, expected vs actual behavior, environment |
|
|
273
330
|
| `feature` | Motivation, proposed solution, alternatives |
|
|
274
|
-
| `security` | Severity, affected components, suggested fix |
|
|
275
|
-
| `performance` | Current metric, target metric, affected area |
|
|
276
|
-
| `refactor` | Motivation, scope, risk assessment |
|
|
277
331
|
|
|
278
332
|
### Custom Templates
|
|
279
333
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tissues",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "AI-enhanced GitHub issue creation with built-in safety guardrails. Wraps gh CLI with circuit breakers, rate limiting, dedup, and templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "node bin/tissues.js",
|
|
11
11
|
"watch": "node --watch bin/tissues.js --help",
|
|
12
|
-
"test": "node --test src/**/*.test.js",
|
|
12
|
+
"test": "node --experimental-test-module-mocks --test 'src/**/*.test.js'",
|
|
13
13
|
"audit": "node scripts/audit.js",
|
|
14
14
|
"build": "node scripts/build.js",
|
|
15
15
|
"dev:install": "node scripts/dev-install.js",
|
|
@@ -42,12 +42,11 @@
|
|
|
42
42
|
"LICENSE"
|
|
43
43
|
],
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
45
|
+
"node": ">=20.12"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@inquirer/prompts": "^7.0.0",
|
|
49
49
|
"better-sqlite3": "^11.0.0",
|
|
50
|
-
"chalk": "^5.3.0",
|
|
51
50
|
"commander": "^12.0.0",
|
|
52
51
|
"conf": "^13.0.0",
|
|
53
52
|
"ora": "^8.0.0"
|
package/src/cli.js
CHANGED
|
@@ -3,33 +3,35 @@ import { createRequire } from 'module'
|
|
|
3
3
|
const { version } = createRequire(import.meta.url)('../package.json')
|
|
4
4
|
import { authCommand } from './commands/auth.js'
|
|
5
5
|
import { configCommand } from './commands/config.js'
|
|
6
|
-
import {
|
|
7
|
-
import { createCommand } from './commands/create.js'
|
|
6
|
+
import { createCommand, draftCommand } from './commands/create.js'
|
|
8
7
|
import { listCommand } from './commands/list.js'
|
|
9
8
|
import { statusCommand } from './commands/status.js'
|
|
9
|
+
import { templatesCommand } from './commands/templates.js'
|
|
10
|
+
import { draftsCommand } from './commands/drafts.js'
|
|
11
|
+
import { aiCommand } from './commands/ai.js'
|
|
10
12
|
import { store } from './lib/config.js'
|
|
11
|
-
import { requireGh
|
|
12
|
-
import
|
|
13
|
+
import { requireGh } from './lib/gh.js'
|
|
14
|
+
import { dim } from './lib/color.js'
|
|
13
15
|
|
|
14
16
|
export const program = new Command()
|
|
15
17
|
|
|
16
18
|
function authDescription() {
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const status = getAuthStatus()
|
|
20
|
-
const active = status.accounts?.find(a => a.active)
|
|
21
|
-
if (!active) return base
|
|
22
|
-
const check = chalk.green('โ')
|
|
23
|
-
return `${base} ${check} ${chalk.dim(active.login)}`
|
|
24
|
-
} catch {
|
|
25
|
-
return base
|
|
26
|
-
}
|
|
19
|
+
return 'Authenticate with GitHub (via gh CLI)'
|
|
27
20
|
}
|
|
28
21
|
|
|
22
|
+
const SKIP_REPO_BANNER = new Set([
|
|
23
|
+
'auth', 'login', 'status', 'switch', 'logout',
|
|
24
|
+
'config', 'drafts', 'publish', 'templates', 'ai',
|
|
25
|
+
])
|
|
26
|
+
|
|
29
27
|
program
|
|
30
28
|
.name('tissues')
|
|
31
29
|
.description('AI-enhanced GitHub issue creation from the command line.')
|
|
32
30
|
.version(version)
|
|
31
|
+
.configureHelp({
|
|
32
|
+
subcommandTerm: (cmd) => cmd.name(),
|
|
33
|
+
})
|
|
34
|
+
.helpCommand('help', 'Show this help')
|
|
33
35
|
.hook('preAction', (_thisCommand, actionCommand) => {
|
|
34
36
|
const name = actionCommand.name()
|
|
35
37
|
|
|
@@ -39,11 +41,12 @@ program
|
|
|
39
41
|
// Ensure gh is installed before any command
|
|
40
42
|
requireGh()
|
|
41
43
|
|
|
42
|
-
// Show active repo context on
|
|
43
|
-
|
|
44
|
+
// Show active repo context on relevant commands only
|
|
45
|
+
const parentName = actionCommand.parent?.name?.()
|
|
46
|
+
if (!SKIP_REPO_BANNER.has(name) && !SKIP_REPO_BANNER.has(parentName)) {
|
|
44
47
|
const activeRepo = store.get('activeRepo')
|
|
45
48
|
if (activeRepo) {
|
|
46
|
-
console.log(
|
|
49
|
+
console.log(dim(`Working in: ${activeRepo}\n`))
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
})
|
|
@@ -57,13 +60,12 @@ Pkg: https://www.npmjs.com/package/tissues
|
|
|
57
60
|
`,
|
|
58
61
|
)
|
|
59
62
|
|
|
60
|
-
program.hook('preAction', () => {
|
|
61
|
-
// Lazily resolve auth description only when a command actually runs
|
|
62
|
-
authCommand.description(authDescription())
|
|
63
|
-
})
|
|
64
63
|
program.addCommand(authCommand)
|
|
65
64
|
program.addCommand(configCommand)
|
|
66
|
-
program.addCommand(useCommand)
|
|
67
65
|
program.addCommand(createCommand)
|
|
66
|
+
program.addCommand(draftCommand)
|
|
68
67
|
program.addCommand(listCommand)
|
|
69
68
|
program.addCommand(statusCommand)
|
|
69
|
+
program.addCommand(templatesCommand)
|
|
70
|
+
program.addCommand(draftsCommand)
|
|
71
|
+
program.addCommand(aiCommand)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { confirm } from '@inquirer/prompts'
|
|
3
|
+
import { store } from '../lib/config.js'
|
|
4
|
+
import { theme } from '../lib/theme.js'
|
|
5
|
+
import { loadConfig, findRepoRoot } from '../lib/defaults.js'
|
|
6
|
+
import { requireAuth, getAuthenticatedUser, listLabels, listRepos } from '../lib/gh.js'
|
|
7
|
+
import { listTemplates } from '../lib/templates.js'
|
|
8
|
+
import { checkDuplicate } from '../lib/dedup.js'
|
|
9
|
+
import { resolveRoute } from '../lib/ai/router.js'
|
|
10
|
+
import { checkAvailable } from '../lib/ai/index.js'
|
|
11
|
+
import { runCreate } from './create.js'
|
|
12
|
+
import { red, yellow, dim, bold, cyan } from '../lib/color.js'
|
|
13
|
+
import ora from 'ora'
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// System prompt โ tells the AI how to build a `tissues create` invocation
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function buildSystemPrompt(context) {
|
|
20
|
+
const lines = [
|
|
21
|
+
'You translate natural language into a `tissues create` command.',
|
|
22
|
+
'Return ONLY a JSON object with the fields to pass to the create command.',
|
|
23
|
+
'',
|
|
24
|
+
'Available fields:',
|
|
25
|
+
' "title" (required) โ concise issue title',
|
|
26
|
+
' "body" (optional) โ issue body in markdown',
|
|
27
|
+
' "labels" (optional) โ comma-separated label names',
|
|
28
|
+
' "template" (optional) โ template key to use',
|
|
29
|
+
' "repo" (optional) โ "owner/name", omit to use the active repo',
|
|
30
|
+
'',
|
|
31
|
+
'Rules:',
|
|
32
|
+
'- Return ONLY valid JSON. No markdown fences, no explanation.',
|
|
33
|
+
'- Write a clear, actionable title.',
|
|
34
|
+
'- If the user gives enough detail, write a well-structured markdown body.',
|
|
35
|
+
'- Only include fields the user mentions or that you can clearly infer.',
|
|
36
|
+
'- Pick labels from the available labels when appropriate.',
|
|
37
|
+
'- Pick a template that fits the issue type (bug, feature, default).',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
if (context.activeRepo) {
|
|
41
|
+
lines.push(`\nActive repo: ${context.activeRepo}`)
|
|
42
|
+
}
|
|
43
|
+
if (context.user) {
|
|
44
|
+
lines.push(`Authenticated user: ${context.user}`)
|
|
45
|
+
}
|
|
46
|
+
if (context.repos?.length) {
|
|
47
|
+
lines.push(`Available repos: ${context.repos.join(', ')}`)
|
|
48
|
+
}
|
|
49
|
+
if (context.templates?.length) {
|
|
50
|
+
const keys = [...new Set(context.templates.map((t) => t.key))]
|
|
51
|
+
lines.push(`Available templates: ${keys.join(', ')}`)
|
|
52
|
+
}
|
|
53
|
+
if (context.labels?.length) {
|
|
54
|
+
lines.push(`Labels on active repo: ${context.labels.join(', ')}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines.join('\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Gather context for the AI prompt
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function gatherContext(opts) {
|
|
65
|
+
const activeRepo = opts.repo || store.get('activeRepo')
|
|
66
|
+
const repoRoot = findRepoRoot()
|
|
67
|
+
const config = loadConfig(repoRoot)
|
|
68
|
+
|
|
69
|
+
const context = { activeRepo, config, repoRoot }
|
|
70
|
+
|
|
71
|
+
try { context.user = getAuthenticatedUser() } catch { /* non-fatal */ }
|
|
72
|
+
try { context.templates = listTemplates(repoRoot) } catch { /* non-fatal */ }
|
|
73
|
+
try { context.repos = listRepos({ limit: 30 }) } catch { /* non-fatal */ }
|
|
74
|
+
|
|
75
|
+
if (activeRepo) {
|
|
76
|
+
try { context.labels = listLabels(activeRepo) } catch { /* non-fatal */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return context
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Parse AI response into runCreate opts
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function parseCreateOpts(raw) {
|
|
87
|
+
let cleaned = raw.trim()
|
|
88
|
+
if (cleaned.startsWith('```')) {
|
|
89
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = JSON.parse(cleaned)
|
|
93
|
+
if (!parsed.title) throw new Error('AI response missing "title"')
|
|
94
|
+
return parsed
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Command
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export const aiCommand = new Command('ai')
|
|
102
|
+
.description('Create an issue from a natural language prompt')
|
|
103
|
+
.argument('[prompt...]', 'Describe the issue you want to create')
|
|
104
|
+
.option('--dry-run', 'Preview the create command without executing')
|
|
105
|
+
.option('--yes, -y', 'Skip confirmation, run create immediately')
|
|
106
|
+
.option('--repo <repo>', 'Override active repo')
|
|
107
|
+
.option('--provider <name>', 'AI provider override')
|
|
108
|
+
.option('--model <name>', 'AI model override')
|
|
109
|
+
.action(async (promptWords, opts) => {
|
|
110
|
+
if (!promptWords || promptWords.length === 0) {
|
|
111
|
+
aiCommand.help()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const prompt = promptWords.join(' ')
|
|
115
|
+
|
|
116
|
+
requireAuth()
|
|
117
|
+
|
|
118
|
+
// Gather context (no spinner โ fast local calls)
|
|
119
|
+
const context = gatherContext(opts)
|
|
120
|
+
const { config } = context
|
|
121
|
+
const repo = opts.repo || context.activeRepo
|
|
122
|
+
|
|
123
|
+
const aiContext = { provider: opts.provider, model: opts.model }
|
|
124
|
+
|
|
125
|
+
// Early dedup check โ before any LLM call
|
|
126
|
+
if (repo) {
|
|
127
|
+
const dedupSpinner = ora('Checking for duplicates...').start()
|
|
128
|
+
try {
|
|
129
|
+
const dedupResult = await checkDuplicate(repo, { title: prompt, agent: 'ai' })
|
|
130
|
+
dedupSpinner.stop()
|
|
131
|
+
|
|
132
|
+
if (dedupResult.action === 'block') {
|
|
133
|
+
const blockReasons = dedupResult.results.filter((r) => r.action === 'block')
|
|
134
|
+
for (const r of blockReasons) {
|
|
135
|
+
console.error(r.reason)
|
|
136
|
+
if (r.existingIssue?.number) {
|
|
137
|
+
console.error(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` โ ${r.existingIssue.title}` : ''}`))
|
|
138
|
+
if (r.existingIssue.url) console.error(dim(` ${r.existingIssue.url}`))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.error(red('Duplicate detected โ issue not created.'))
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (dedupResult.action === 'warn') {
|
|
146
|
+
console.warn(yellow('Similar issue(s) found:'))
|
|
147
|
+
for (const r of dedupResult.results.filter((r) => r.action === 'warn')) {
|
|
148
|
+
console.warn(r.reason)
|
|
149
|
+
if (r.existingIssue?.number) {
|
|
150
|
+
console.warn(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` โ ${r.existingIssue.title}` : ''}`))
|
|
151
|
+
if (r.existingIssue.url) console.warn(dim(` ${r.existingIssue.url}`))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.warn()
|
|
155
|
+
const proceed = await confirm({ message: 'Continue anyway?', default: false, theme })
|
|
156
|
+
if (!proceed) {
|
|
157
|
+
console.log(dim('Aborted.'))
|
|
158
|
+
process.exit(0)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
dedupSpinner.stop()
|
|
163
|
+
// Non-fatal โ proceed without dedup
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If AI isn't configured at all, go straight to create with the prompt as title
|
|
168
|
+
if (!checkAvailable(config, aiContext)) {
|
|
169
|
+
console.warn(yellow('AI not configured โ using prompt as issue title.'))
|
|
170
|
+
console.warn(dim('Run `tissues config` to set up an AI provider.\n'))
|
|
171
|
+
return fallbackToCreate(prompt, opts)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Ask AI to build the create options
|
|
175
|
+
const systemPrompt = buildSystemPrompt(context)
|
|
176
|
+
const messages = [
|
|
177
|
+
{ role: 'system', content: systemPrompt },
|
|
178
|
+
{ role: 'user', content: prompt },
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
const { adapter, model } = resolveRoute(config, aiContext)
|
|
182
|
+
const spinner = ora('Building create command...').start()
|
|
183
|
+
let raw
|
|
184
|
+
try {
|
|
185
|
+
raw = await adapter.complete(messages, { model, maxTokens: 2048 })
|
|
186
|
+
spinner.stop()
|
|
187
|
+
} catch (err) {
|
|
188
|
+
spinner.fail('AI failed')
|
|
189
|
+
if (/\b401\b/.test(err.message)) {
|
|
190
|
+
console.warn(red(' API key is invalid or expired.'))
|
|
191
|
+
console.warn(dim(' Run ') + cyan('tissues config') + dim(' to update your API key.'))
|
|
192
|
+
}
|
|
193
|
+
return fallbackToCreate(prompt, opts)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let plan
|
|
197
|
+
try {
|
|
198
|
+
plan = parseCreateOpts(raw)
|
|
199
|
+
} catch {
|
|
200
|
+
return fallbackToCreate(prompt, opts)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Show what create will receive
|
|
204
|
+
const planRepo = plan.repo || context.activeRepo
|
|
205
|
+
console.log()
|
|
206
|
+
console.log(bold(' tissues create'))
|
|
207
|
+
console.log(` ${dim('--title')} ${plan.title}`)
|
|
208
|
+
if (planRepo) console.log(` ${dim('--repo')} ${planRepo}`)
|
|
209
|
+
if (plan.template) console.log(` ${dim('--template')} ${plan.template}`)
|
|
210
|
+
if (plan.labels) console.log(` ${dim('--labels')} ${plan.labels}`)
|
|
211
|
+
if (plan.body) {
|
|
212
|
+
console.log(` ${dim('--body')}`)
|
|
213
|
+
const preview = plan.body.length > 400
|
|
214
|
+
? plan.body.slice(0, 400) + dim('\n ...(truncated)')
|
|
215
|
+
: plan.body
|
|
216
|
+
for (const line of preview.split('\n')) {
|
|
217
|
+
console.log(` ${dim(line)}`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
console.log()
|
|
221
|
+
|
|
222
|
+
if (opts.dryRun) {
|
|
223
|
+
console.log(dim(' --dry-run: stopping here.\n'))
|
|
224
|
+
process.exit(0)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!opts.yes) {
|
|
228
|
+
const ok = await confirm({ message: 'Run this create?', default: true, theme })
|
|
229
|
+
if (!ok) {
|
|
230
|
+
console.log(dim('Aborted.'))
|
|
231
|
+
process.exit(0)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Hand off to the same runCreate that `tissues create` uses
|
|
236
|
+
try {
|
|
237
|
+
const result = await runCreate({
|
|
238
|
+
repo: planRepo,
|
|
239
|
+
title: plan.title,
|
|
240
|
+
body: plan.body || undefined,
|
|
241
|
+
labels: plan.labels || undefined,
|
|
242
|
+
template: plan.template || undefined,
|
|
243
|
+
})
|
|
244
|
+
if (result === null) process.exit(0)
|
|
245
|
+
} catch (err) {
|
|
246
|
+
process.exit(err.isAbort ? 0 : 1)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Fallback โ AI unavailable, run create with the prompt as the title
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
async function fallbackToCreate(prompt, opts) {
|
|
255
|
+
try {
|
|
256
|
+
const result = await runCreate({
|
|
257
|
+
repo: opts.repo || undefined,
|
|
258
|
+
_titleDefault: prompt,
|
|
259
|
+
dryRun: opts.dryRun || false,
|
|
260
|
+
enhance: true, // let create's own AI enhancement try (it handles failure gracefully)
|
|
261
|
+
})
|
|
262
|
+
if (result === null) process.exit(0)
|
|
263
|
+
} catch (err) {
|
|
264
|
+
process.exit(err.isAbort ? 0 : 1)
|
|
265
|
+
}
|
|
266
|
+
}
|
package/src/commands/auth.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
authSwitch,
|
|
8
8
|
authLogout,
|
|
9
9
|
} from '../lib/gh.js'
|
|
10
|
-
import
|
|
10
|
+
import { yellow, bold, dim, cyan, green } from '../lib/color.js'
|
|
11
11
|
|
|
12
12
|
export const authCommand = new Command('auth')
|
|
13
13
|
.description('Authenticate with GitHub (via gh CLI)')
|
|
@@ -35,10 +35,10 @@ authCommand.addCommand(
|
|
|
35
35
|
const { ok, missing } = checkScopes()
|
|
36
36
|
if (!ok) {
|
|
37
37
|
console.log()
|
|
38
|
-
console.log(
|
|
39
|
-
console.log(
|
|
38
|
+
console.log(yellow('โ Missing required scope: ') + bold(missing.join(', ')))
|
|
39
|
+
console.log(dim(' Fix with: ') + cyan(`gh auth refresh -s ${missing.join(',')}`))
|
|
40
40
|
} else {
|
|
41
|
-
console.log(
|
|
41
|
+
console.log(green('\nโ Token has required scopes for issue creation'))
|
|
42
42
|
}
|
|
43
43
|
}),
|
|
44
44
|
)
|