tissues 0.6.0 → 0.6.2
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 +78 -1
- package/package.json +4 -2
- package/src/cli.js +12 -1
- package/src/commands/ai.js +149 -173
- package/src/commands/config.js +143 -69
- package/src/commands/create.js +16 -9
- package/src/commands/create.test.js +381 -0
- package/src/commands/enhancements.js +282 -0
- package/src/commands/flush.test.js +299 -0
- package/src/commands/list.js +3 -2
- package/src/commands/providers.js +347 -0
- package/src/commands/providers.test.js +28 -0
- package/src/commands/storage.js +167 -0
- package/src/commands/sync.js +225 -0
- package/src/daemon/sync.js +189 -0
- package/src/lib/ai/adapters/claude-cli.js +55 -0
- package/src/lib/ai/adapters/cli-adapters.test.js +133 -0
- package/src/lib/ai/adapters/codex-cli.js +77 -0
- package/src/lib/ai/adapters/command.js +23 -13
- package/src/lib/ai/adapters/gemini-cli.js +55 -0
- package/src/lib/ai/adapters/openclaw.js +91 -0
- package/src/lib/ai/agent-actions.js +271 -0
- package/src/lib/ai/agent.js +323 -0
- package/src/lib/ai/body-template.js +15 -0
- package/src/lib/ai/discovery.js +89 -0
- package/src/lib/ai/discovery.test.js +74 -0
- package/src/lib/ai/enhance.js +48 -11
- package/src/lib/ai/enhancement-adapter.js +109 -0
- package/src/lib/ai/enhancement-adapter.test.js +188 -0
- package/src/lib/ai/index.js +2 -2
- package/src/lib/ai/pipeline.js +20 -2
- package/src/lib/ai/pipeline.test.js +257 -0
- package/src/lib/ai/prompt.test.js +30 -0
- package/src/lib/ai/router.js +118 -7
- package/src/lib/ai/router.test.js +481 -0
- package/src/lib/ai/steps.js +23 -3
- package/src/lib/ai/steps.test.js +335 -0
- package/src/lib/attribution.test.js +64 -0
- package/src/lib/cache.js +408 -0
- package/src/lib/db.js +42 -0
- package/src/lib/dedup.js +44 -48
- package/src/lib/dedup.test.js +227 -0
- package/src/lib/defaults.js +37 -1
- package/src/lib/defaults.test.js +217 -0
- package/src/lib/drafts-perf.test.js +203 -0
- package/src/lib/drafts.test.js +300 -0
- package/src/lib/enhancements.js +436 -0
- package/src/lib/enhancements.test.js +294 -0
- package/src/lib/gh.js +76 -10
- package/src/lib/safety.test.js +217 -0
- package/src/lib/storage.js +298 -0
- package/src/lib/templates.test.js +207 -0
package/README.md
CHANGED
|
@@ -122,9 +122,64 @@ Manage issue templates interactively. View, edit, or create templates. Editing a
|
|
|
122
122
|
tissues templates
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### `tissues ai`
|
|
126
|
+
|
|
127
|
+
AI-powered issue management. The agent translates natural language into JSON actions with built-in guardrails — it only handles tissues/GitHub operations and declines off-topic requests.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Single-shot: natural language → action
|
|
131
|
+
tissues ai list my open issues
|
|
132
|
+
tissues ai what repos do I have access to
|
|
133
|
+
tissues ai create a bug report about login failing on Safari
|
|
134
|
+
|
|
135
|
+
# Preview without executing
|
|
136
|
+
tissues ai --dry-run add a feature request for dark mode
|
|
137
|
+
|
|
138
|
+
# Multi-turn conversation
|
|
139
|
+
tissues ai --chat
|
|
140
|
+
tissues ai --chat "set up labels for our project"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In chat mode, the agent can list issues, switch repos, manage config, create issues, and more — all from natural language. Test the agent with `npm run test:agent -- --mock`.
|
|
144
|
+
|
|
145
|
+
> See [AI Agent docs](docs/features/ai.md#ai-agent-tissues-ai) for prompt architecture, guardrails, and testing details.
|
|
146
|
+
|
|
147
|
+
### `tissues sync`
|
|
148
|
+
|
|
149
|
+
Sync issues, labels, and repos to the local cache. The cache makes dedup checks instant and eliminates redundant network calls.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# One-shot sync
|
|
153
|
+
tissues sync
|
|
154
|
+
tissues sync --repo owner/other-repo
|
|
155
|
+
tissues sync --force # full re-sync
|
|
156
|
+
|
|
157
|
+
# Background daemon
|
|
158
|
+
tissues sync start # fork daemon process
|
|
159
|
+
tissues sync status # check daemon health
|
|
160
|
+
tissues sync stop # stop daemon
|
|
161
|
+
tissues sync restart
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The daemon uses adaptive intervals (2 min when active, up to 60 min when idle) and incremental `since`-based fetching for issues.
|
|
165
|
+
|
|
166
|
+
### `tissues storage`
|
|
167
|
+
|
|
168
|
+
Inspect and manage all cached/stored data.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
tissues storage # overview dashboard
|
|
172
|
+
tissues storage issues # per-repo issue breakdown
|
|
173
|
+
tissues storage clear issues # clear cached issues
|
|
174
|
+
tissues storage clear all # clear everything
|
|
175
|
+
tissues storage clear all --include-safety # also reset circuit breaker
|
|
176
|
+
tissues storage export # JSON dump for debugging
|
|
177
|
+
tissues storage --json # machine-readable overview
|
|
178
|
+
```
|
|
179
|
+
|
|
125
180
|
### `tissues list`
|
|
126
181
|
|
|
127
|
-
Browse open issues for the active repo.
|
|
182
|
+
Browse open issues for the active repo (served from local cache when fresh).
|
|
128
183
|
|
|
129
184
|
```bash
|
|
130
185
|
tissues list
|
|
@@ -277,6 +332,22 @@ Configuration is loaded and merged from three sources in ascending priority orde
|
|
|
277
332
|
}
|
|
278
333
|
```
|
|
279
334
|
|
|
335
|
+
Background sync configuration (opt-in):
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"sync": {
|
|
340
|
+
"enabled": true,
|
|
341
|
+
"repos": [],
|
|
342
|
+
"interval": 300,
|
|
343
|
+
"adaptiveInterval": true,
|
|
344
|
+
"issueLimit": 500,
|
|
345
|
+
"includeClosedDays": 30,
|
|
346
|
+
"autoStart": false
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
280
351
|
AI keys go in user-level config only (`~/.config/tissues/config.json`) — never commit them:
|
|
281
352
|
|
|
282
353
|
```json
|
|
@@ -414,6 +485,12 @@ tissues stores local state in a SQLite database at:
|
|
|
414
485
|
- `rate_events` — timestamped creation events (for rate limiting)
|
|
415
486
|
- `circuit_breaker` — circuit state per repo+agent (status, failure count, cooldown)
|
|
416
487
|
- `idempotency_keys` — deterministic keys for agent-driven idempotency
|
|
488
|
+
- `cached_issues` — locally cached GitHub issues for instant dedup and offline context
|
|
489
|
+
- `cached_labels` — locally cached repo labels
|
|
490
|
+
- `cached_repos` — locally cached repo list
|
|
491
|
+
- `sync_meta` — sync cursors and timestamps for incremental fetching
|
|
492
|
+
|
|
493
|
+
The cache tables are populated by `tissues sync` (manual or background daemon) and used transparently by `create`, `list`, `ai`, and dedup checks. Run `tissues storage` to inspect cache state.
|
|
417
494
|
|
|
418
495
|
The database file can be committed to share dedup history across a team, or added to `.gitignore` to keep it local. It is created automatically on first use.
|
|
419
496
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tissues",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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": {
|
|
@@ -12,8 +12,10 @@
|
|
|
12
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
|
+
"prepublishOnly": "node scripts/audit.js",
|
|
15
16
|
"dev:install": "node scripts/dev-install.js",
|
|
16
|
-
"dev:uninstall": "node scripts/dev-install.js --uninstall"
|
|
17
|
+
"dev:uninstall": "node scripts/dev-install.js --uninstall",
|
|
18
|
+
"test:agent": "node scripts/test-agent.js"
|
|
17
19
|
},
|
|
18
20
|
"keywords": [
|
|
19
21
|
"git",
|
package/src/cli.js
CHANGED
|
@@ -7,8 +7,12 @@ import { createCommand, draftCommand } from './commands/create.js'
|
|
|
7
7
|
import { listCommand } from './commands/list.js'
|
|
8
8
|
import { statusCommand } from './commands/status.js'
|
|
9
9
|
import { templatesCommand } from './commands/templates.js'
|
|
10
|
+
import { enhancementsCommand } from './commands/enhancements.js'
|
|
10
11
|
import { draftsCommand } from './commands/drafts.js'
|
|
11
12
|
import { aiCommand } from './commands/ai.js'
|
|
13
|
+
import { syncCommand, recordActivity } from './commands/sync.js'
|
|
14
|
+
import { storageCommand } from './commands/storage.js'
|
|
15
|
+
import { providersCommand } from './commands/providers.js'
|
|
12
16
|
import { store } from './lib/config.js'
|
|
13
17
|
import { requireGh } from './lib/gh.js'
|
|
14
18
|
import { dim } from './lib/color.js'
|
|
@@ -21,7 +25,7 @@ function authDescription() {
|
|
|
21
25
|
|
|
22
26
|
const SKIP_REPO_BANNER = new Set([
|
|
23
27
|
'auth', 'login', 'status', 'switch', 'logout',
|
|
24
|
-
'config', 'drafts', 'publish', 'templates', 'ai',
|
|
28
|
+
'config', 'drafts', 'publish', 'templates', 'enhancements', 'ai', 'sync', 'storage', 'providers',
|
|
25
29
|
])
|
|
26
30
|
|
|
27
31
|
program
|
|
@@ -41,6 +45,9 @@ program
|
|
|
41
45
|
// Ensure gh is installed before any command
|
|
42
46
|
requireGh()
|
|
43
47
|
|
|
48
|
+
// Record activity timestamp for adaptive sync interval
|
|
49
|
+
try { recordActivity() } catch { /* non-fatal */ }
|
|
50
|
+
|
|
44
51
|
// Show active repo context on relevant commands only
|
|
45
52
|
const parentName = actionCommand.parent?.name?.()
|
|
46
53
|
if (!SKIP_REPO_BANNER.has(name) && !SKIP_REPO_BANNER.has(parentName)) {
|
|
@@ -67,5 +74,9 @@ program.addCommand(draftCommand)
|
|
|
67
74
|
program.addCommand(listCommand)
|
|
68
75
|
program.addCommand(statusCommand)
|
|
69
76
|
program.addCommand(templatesCommand)
|
|
77
|
+
program.addCommand(enhancementsCommand)
|
|
70
78
|
program.addCommand(draftsCommand)
|
|
71
79
|
program.addCommand(aiCommand)
|
|
80
|
+
program.addCommand(syncCommand)
|
|
81
|
+
program.addCommand(storageCommand)
|
|
82
|
+
program.addCommand(providersCommand)
|
package/src/commands/ai.js
CHANGED
|
@@ -1,64 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI command — general-purpose CLI operator with local context.
|
|
3
|
+
*
|
|
4
|
+
* Single-shot mode: backward-compatible NL → create_issue translation.
|
|
5
|
+
* Chat mode: multi-turn conversation for any tissues operation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import { Command } from 'commander'
|
|
2
|
-
import { confirm } from '@inquirer/prompts'
|
|
9
|
+
import { input, confirm } from '@inquirer/prompts'
|
|
3
10
|
import { store } from '../lib/config.js'
|
|
4
11
|
import { theme } from '../lib/theme.js'
|
|
5
12
|
import { loadConfig, findRepoRoot } from '../lib/defaults.js'
|
|
6
|
-
import { requireAuth, getAuthenticatedUser
|
|
7
|
-
import {
|
|
13
|
+
import { requireAuth, getAuthenticatedUser } from '../lib/gh.js'
|
|
14
|
+
import { ensureFresh } from '../lib/cache.js'
|
|
8
15
|
import { checkDuplicate } from '../lib/dedup.js'
|
|
9
16
|
import { resolveRoute } from '../lib/ai/router.js'
|
|
10
17
|
import { checkAvailable } from '../lib/ai/index.js'
|
|
18
|
+
import { runAgentSingleShot, runAgentLoop, parseAgentResponse } from '../lib/ai/agent.js'
|
|
19
|
+
import { ACTION_SCHEMAS, executeAction } from '../lib/ai/agent-actions.js'
|
|
11
20
|
import { runCreate } from './create.js'
|
|
12
21
|
import { red, yellow, dim, bold, cyan } from '../lib/color.js'
|
|
13
22
|
import ora from 'ora'
|
|
14
23
|
|
|
15
24
|
// ---------------------------------------------------------------------------
|
|
16
|
-
//
|
|
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
|
|
25
|
+
// Gather context from cache (zero network calls if warm)
|
|
62
26
|
// ---------------------------------------------------------------------------
|
|
63
27
|
|
|
64
28
|
function gatherContext(opts) {
|
|
@@ -69,150 +33,163 @@ function gatherContext(opts) {
|
|
|
69
33
|
const context = { activeRepo, config, repoRoot }
|
|
70
34
|
|
|
71
35
|
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
36
|
|
|
79
37
|
return context
|
|
80
38
|
}
|
|
81
39
|
|
|
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
40
|
// ---------------------------------------------------------------------------
|
|
98
41
|
// Command
|
|
99
42
|
// ---------------------------------------------------------------------------
|
|
100
43
|
|
|
101
44
|
export const aiCommand = new Command('ai')
|
|
102
|
-
.description('
|
|
103
|
-
.argument('[prompt...]', 'Describe
|
|
104
|
-
.option('--
|
|
105
|
-
.option('--
|
|
45
|
+
.description('AI-powered issue management (single-shot or chat)')
|
|
46
|
+
.argument('[prompt...]', 'Describe what you want to do')
|
|
47
|
+
.option('--chat', 'Start a multi-turn conversation')
|
|
48
|
+
.option('--dry-run', 'Preview actions without executing')
|
|
49
|
+
.option('--yes, -y', 'Skip confirmation, run action immediately')
|
|
106
50
|
.option('--repo <repo>', 'Override active repo')
|
|
51
|
+
.option('--enhancements <names>', 'Comma-separated enhancement names to run')
|
|
107
52
|
.option('--provider <name>', 'AI provider override')
|
|
108
53
|
.option('--model <name>', 'AI model override')
|
|
109
54
|
.action(async (promptWords, opts) => {
|
|
110
|
-
|
|
55
|
+
const prompt = promptWords?.length ? promptWords.join(' ') : null
|
|
56
|
+
const isChat = opts.chat || false
|
|
57
|
+
|
|
58
|
+
if (!prompt && !isChat) {
|
|
111
59
|
aiCommand.help()
|
|
112
60
|
return
|
|
113
61
|
}
|
|
114
|
-
const prompt = promptWords.join(' ')
|
|
115
62
|
|
|
116
63
|
requireAuth()
|
|
117
64
|
|
|
118
|
-
// Gather context (no spinner — fast local calls)
|
|
119
65
|
const context = gatherContext(opts)
|
|
120
66
|
const { config } = context
|
|
121
|
-
const repo = opts.repo || context.activeRepo
|
|
122
|
-
|
|
123
67
|
const aiContext = { provider: opts.provider, model: opts.model }
|
|
124
68
|
|
|
125
|
-
//
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
69
|
+
// Check AI is available
|
|
70
|
+
if (!checkAvailable(config, aiContext)) {
|
|
71
|
+
console.warn(yellow('AI not configured.'))
|
|
72
|
+
console.warn(dim('Run `tissues config` to set up an AI provider.\n'))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
131
75
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}
|
|
76
|
+
// -----------------------------------------------------------------------
|
|
77
|
+
// Chat mode
|
|
78
|
+
// -----------------------------------------------------------------------
|
|
79
|
+
if (isChat) {
|
|
80
|
+
console.log(bold('\ntissues ai') + dim(' — chat mode (type "exit" to quit)\n'))
|
|
144
81
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
82
|
+
// If a prompt was given with --chat, process it first
|
|
83
|
+
if (prompt) {
|
|
84
|
+
await handleSingleTurn(prompt, config, context, opts)
|
|
164
85
|
}
|
|
165
|
-
}
|
|
166
86
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
87
|
+
await runAgentLoop(config, context, {
|
|
88
|
+
provider: opts.provider,
|
|
89
|
+
model: opts.model,
|
|
90
|
+
onInput: async () => {
|
|
91
|
+
try {
|
|
92
|
+
const userInput = await input({ message: '>', theme })
|
|
93
|
+
return userInput
|
|
94
|
+
} catch {
|
|
95
|
+
return null // Ctrl+C
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
onOutput: (text) => {
|
|
99
|
+
console.log(text)
|
|
100
|
+
console.log()
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
return
|
|
172
104
|
}
|
|
173
105
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
// Single-shot mode (backward compatible)
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
await handleSingleTurn(prompt, config, context, opts)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Single-turn handler
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async function handleSingleTurn(prompt, config, context, opts) {
|
|
117
|
+
const repo = opts.repo || context.activeRepo
|
|
180
118
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let raw
|
|
119
|
+
// Early dedup check for create-like prompts
|
|
120
|
+
if (repo) {
|
|
184
121
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
122
|
+
const dedupResult = await checkDuplicate(repo, { title: prompt, agent: 'ai' })
|
|
123
|
+
if (dedupResult.action === 'block') {
|
|
124
|
+
const blockReasons = dedupResult.results.filter(r => r.action === 'block')
|
|
125
|
+
for (const r of blockReasons) {
|
|
126
|
+
console.error(r.reason)
|
|
127
|
+
if (r.existingIssue?.number) {
|
|
128
|
+
console.error(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` — ${r.existingIssue.title}` : ''}`))
|
|
129
|
+
if (r.existingIssue.url) console.error(dim(` ${r.existingIssue.url}`))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.error(red('Duplicate detected — issue not created.'))
|
|
133
|
+
process.exit(1)
|
|
192
134
|
}
|
|
193
|
-
return fallbackToCreate(prompt, opts)
|
|
194
|
-
}
|
|
195
135
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
136
|
+
if (dedupResult.action === 'warn') {
|
|
137
|
+
console.warn(yellow('Similar issue(s) found:'))
|
|
138
|
+
for (const r of dedupResult.results.filter(r => r.action === 'warn')) {
|
|
139
|
+
console.warn(r.reason)
|
|
140
|
+
if (r.existingIssue?.number) {
|
|
141
|
+
console.warn(dim(` #${r.existingIssue.number}${r.existingIssue.title ? ` — ${r.existingIssue.title}` : ''}`))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
console.warn()
|
|
145
|
+
const proceed = await confirm({ message: 'Continue anyway?', default: false, theme })
|
|
146
|
+
if (!proceed) {
|
|
147
|
+
console.log(dim('Aborted.'))
|
|
148
|
+
process.exit(0)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
199
151
|
} catch {
|
|
200
|
-
|
|
152
|
+
// Non-fatal dedup check
|
|
201
153
|
}
|
|
154
|
+
}
|
|
202
155
|
|
|
203
|
-
|
|
204
|
-
|
|
156
|
+
// Ask AI
|
|
157
|
+
const spinner = ora('Thinking...').start()
|
|
158
|
+
let agentResult
|
|
159
|
+
try {
|
|
160
|
+
agentResult = await runAgentSingleShot(prompt, config, context, {
|
|
161
|
+
provider: opts.provider,
|
|
162
|
+
model: opts.model,
|
|
163
|
+
})
|
|
164
|
+
spinner.stop()
|
|
165
|
+
} catch (err) {
|
|
166
|
+
spinner.fail('AI failed')
|
|
167
|
+
if (/\b401\b/.test(err.message)) {
|
|
168
|
+
console.warn(red(' API key is invalid or expired.'))
|
|
169
|
+
console.warn(dim(' Run ') + cyan('tissues config') + dim(' to update your API key.'))
|
|
170
|
+
} else {
|
|
171
|
+
console.warn(red(` ${err.message}`))
|
|
172
|
+
}
|
|
173
|
+
console.warn(dim(' Tip: use --chat for interactive mode, or try rephrasing.\n'))
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { action, params } = agentResult
|
|
178
|
+
|
|
179
|
+
// Handle create_issue action (backward compatible preview + confirm flow)
|
|
180
|
+
if (action === 'create_issue') {
|
|
181
|
+
const planRepo = params.repo || context.activeRepo
|
|
205
182
|
console.log()
|
|
206
183
|
console.log(bold(' tissues create'))
|
|
207
|
-
console.log(` ${dim('--title')} ${
|
|
184
|
+
console.log(` ${dim('--title')} ${params.title}`)
|
|
208
185
|
if (planRepo) console.log(` ${dim('--repo')} ${planRepo}`)
|
|
209
|
-
if (
|
|
210
|
-
if (
|
|
211
|
-
if (
|
|
186
|
+
if (params.template) console.log(` ${dim('--template')} ${params.template}`)
|
|
187
|
+
if (params.labels) console.log(` ${dim('--labels')} ${params.labels}`)
|
|
188
|
+
if (params.body) {
|
|
212
189
|
console.log(` ${dim('--body')}`)
|
|
213
|
-
const preview =
|
|
214
|
-
?
|
|
215
|
-
:
|
|
190
|
+
const preview = params.body.length > 400
|
|
191
|
+
? params.body.slice(0, 400) + dim('\n ...(truncated)')
|
|
192
|
+
: params.body
|
|
216
193
|
for (const line of preview.split('\n')) {
|
|
217
194
|
console.log(` ${dim(line)}`)
|
|
218
195
|
}
|
|
@@ -232,35 +209,34 @@ export const aiCommand = new Command('ai')
|
|
|
232
209
|
}
|
|
233
210
|
}
|
|
234
211
|
|
|
235
|
-
// Hand off to the same runCreate that `tissues create` uses
|
|
236
212
|
try {
|
|
237
213
|
const result = await runCreate({
|
|
238
214
|
repo: planRepo,
|
|
239
|
-
title:
|
|
240
|
-
body:
|
|
241
|
-
labels:
|
|
242
|
-
template:
|
|
215
|
+
title: params.title,
|
|
216
|
+
body: params.body || undefined,
|
|
217
|
+
labels: params.labels || undefined,
|
|
218
|
+
template: params.template || undefined,
|
|
219
|
+
enhancements: opts.enhancements || undefined,
|
|
243
220
|
})
|
|
244
221
|
if (result === null) process.exit(0)
|
|
245
222
|
} catch (err) {
|
|
246
223
|
process.exit(err.isAbort ? 0 : 1)
|
|
247
224
|
}
|
|
248
|
-
|
|
225
|
+
return
|
|
226
|
+
}
|
|
249
227
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
228
|
+
// Handle answer action
|
|
229
|
+
if (action === 'answer') {
|
|
230
|
+
console.log(params.text || params.content || '')
|
|
231
|
+
return
|
|
232
|
+
}
|
|
253
233
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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)
|
|
234
|
+
// Handle other read-only actions
|
|
235
|
+
const result = await executeAction(action, params || {}, { ...context, config })
|
|
236
|
+
if (result.display) {
|
|
237
|
+
console.log(result.display)
|
|
238
|
+
} else if (result.result) {
|
|
239
|
+
console.log(typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2))
|
|
265
240
|
}
|
|
266
241
|
}
|
|
242
|
+
|