2026-03-10 22:28:37 -06:00
|
|
|
import {
|
|
|
|
|
AuthStorage,
|
2026-03-11 11:21:12 -06:00
|
|
|
DefaultResourceLoader,
|
2026-03-10 22:28:37 -06:00
|
|
|
ModelRegistry,
|
|
|
|
|
SettingsManager,
|
|
|
|
|
SessionManager,
|
|
|
|
|
createAgentSession,
|
|
|
|
|
InteractiveMode,
|
2026-03-11 11:21:12 -06:00
|
|
|
runPrintMode,
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
runRpcMode,
|
2026-03-12 21:55:17 -06:00
|
|
|
} from '@gsd/pi-coding-agent'
|
2026-03-11 17:07:54 -06:00
|
|
|
import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs'
|
2026-03-11 10:52:45 -06:00
|
|
|
import { join } from 'node:path'
|
2026-03-10 22:28:37 -06:00
|
|
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js'
|
2026-03-15 00:58:18 -04:00
|
|
|
import { initResources, buildResourceLoader, getNewerManagedResourceVersion } from './resource-loader.js'
|
2026-03-11 10:52:45 -06:00
|
|
|
import { ensureManagedTools } from './tool-bootstrap.js'
|
2026-03-12 10:02:00 -06:00
|
|
|
import { loadStoredEnvKeys } from './wizard.js'
|
2026-03-12 20:44:01 -07:00
|
|
|
import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js'
|
2026-03-12 10:02:00 -06:00
|
|
|
import { shouldRunOnboarding, runOnboarding } from './onboarding.js'
|
2026-03-13 14:28:43 -03:00
|
|
|
import { checkForUpdates } from './update-check.js'
|
2026-03-10 22:28:37 -06:00
|
|
|
|
2026-03-11 11:21:12 -06:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Minimal CLI arg parser — detects print/subagent mode flags
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
interface CliFlags {
|
|
|
|
|
mode?: 'text' | 'json' | 'rpc'
|
|
|
|
|
print?: boolean
|
2026-03-13 07:51:29 +05:30
|
|
|
continue?: boolean
|
2026-03-11 11:21:12 -06:00
|
|
|
noSession?: boolean
|
|
|
|
|
model?: string
|
2026-03-14 11:43:56 -03:00
|
|
|
listModels?: string | true
|
2026-03-11 11:21:12 -06:00
|
|
|
extensions: string[]
|
|
|
|
|
appendSystemPrompt?: string
|
|
|
|
|
tools?: string[]
|
|
|
|
|
messages: string[]
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 00:58:18 -04:00
|
|
|
function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
|
|
|
|
|
const currentVersion = process.env.GSD_VERSION || '0.0.0'
|
|
|
|
|
const managedVersion = getNewerManagedResourceVersion(currentAgentDir, currentVersion)
|
|
|
|
|
if (!managedVersion) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const yellow = '\x1b[33m'
|
|
|
|
|
const dim = '\x1b[2m'
|
|
|
|
|
const reset = '\x1b[0m'
|
|
|
|
|
const bold = '\x1b[1m'
|
|
|
|
|
|
|
|
|
|
process.stderr.write(
|
|
|
|
|
`[gsd] ${yellow}Version mismatch detected${reset}\n` +
|
|
|
|
|
`[gsd] Synced resources are from ${bold}v${managedVersion}${reset}, but this \`gsd\` binary is ${dim}v${currentVersion}${reset}.\n` +
|
|
|
|
|
`[gsd] Run ${bold}npm install -g gsd-pi@latest${reset} or ${bold}gsd update${reset}, then try again.\n`,
|
|
|
|
|
)
|
|
|
|
|
process.exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 11:21:12 -06:00
|
|
|
function parseCliArgs(argv: string[]): CliFlags {
|
|
|
|
|
const flags: CliFlags = { extensions: [], messages: [] }
|
|
|
|
|
const args = argv.slice(2) // skip node + script
|
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
|
|
const arg = args[i]
|
|
|
|
|
if (arg === '--mode' && i + 1 < args.length) {
|
|
|
|
|
const m = args[++i]
|
|
|
|
|
if (m === 'text' || m === 'json' || m === 'rpc') flags.mode = m
|
|
|
|
|
} else if (arg === '--print' || arg === '-p') {
|
|
|
|
|
flags.print = true
|
2026-03-13 07:51:29 +05:30
|
|
|
} else if (arg === '--continue' || arg === '-c') {
|
|
|
|
|
flags.continue = true
|
2026-03-11 11:21:12 -06:00
|
|
|
} else if (arg === '--no-session') {
|
|
|
|
|
flags.noSession = true
|
|
|
|
|
} else if (arg === '--model' && i + 1 < args.length) {
|
|
|
|
|
flags.model = args[++i]
|
|
|
|
|
} else if (arg === '--extension' && i + 1 < args.length) {
|
|
|
|
|
flags.extensions.push(args[++i])
|
|
|
|
|
} else if (arg === '--append-system-prompt' && i + 1 < args.length) {
|
|
|
|
|
flags.appendSystemPrompt = args[++i]
|
|
|
|
|
} else if (arg === '--tools' && i + 1 < args.length) {
|
|
|
|
|
flags.tools = args[++i].split(',')
|
2026-03-14 11:43:56 -03:00
|
|
|
} else if (arg === '--list-models') {
|
|
|
|
|
flags.listModels = (i + 1 < args.length && !args[i + 1].startsWith('-')) ? args[++i] : true
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
} else if (arg === '--version' || arg === '-v') {
|
|
|
|
|
process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n')
|
|
|
|
|
process.exit(0)
|
|
|
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
|
|
|
process.stdout.write(`GSD v${process.env.GSD_VERSION || '0.0.0'} — Get Shit Done\n\n`)
|
|
|
|
|
process.stdout.write('Usage: gsd [options] [message...]\n\n')
|
|
|
|
|
process.stdout.write('Options:\n')
|
|
|
|
|
process.stdout.write(' --mode <text|json|rpc> Output mode (default: interactive)\n')
|
|
|
|
|
process.stdout.write(' --print, -p Single-shot print mode\n')
|
2026-03-13 07:51:29 +05:30
|
|
|
process.stdout.write(' --continue, -c Resume the most recent session\n')
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n')
|
|
|
|
|
process.stdout.write(' --no-session Disable session persistence\n')
|
|
|
|
|
process.stdout.write(' --extension <path> Load additional extension\n')
|
|
|
|
|
process.stdout.write(' --tools <a,b,c> Restrict available tools\n')
|
2026-03-14 11:43:56 -03:00
|
|
|
process.stdout.write(' --list-models [search] List available models and exit\n')
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
process.stdout.write(' --version, -v Print version and exit\n')
|
|
|
|
|
process.stdout.write(' --help, -h Print this help and exit\n')
|
2026-03-12 10:02:00 -06:00
|
|
|
process.stdout.write('\nSubcommands:\n')
|
|
|
|
|
process.stdout.write(' config Re-run the setup wizard\n')
|
2026-03-13 18:47:33 -03:00
|
|
|
process.stdout.write(' update Update GSD to the latest version\n')
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
process.exit(0)
|
2026-03-11 11:21:12 -06:00
|
|
|
} else if (!arg.startsWith('--') && !arg.startsWith('-')) {
|
|
|
|
|
flags.messages.push(arg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return flags
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cliFlags = parseCliArgs(process.argv)
|
|
|
|
|
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined
|
|
|
|
|
|
2026-03-12 10:02:00 -06:00
|
|
|
// `gsd config` — replay the setup wizard and exit
|
|
|
|
|
if (cliFlags.messages[0] === 'config') {
|
|
|
|
|
const authStorage = AuthStorage.create(authFilePath)
|
|
|
|
|
await runOnboarding(authStorage)
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 18:47:33 -03:00
|
|
|
// `gsd update` — update to the latest version via npm
|
|
|
|
|
if (cliFlags.messages[0] === 'update') {
|
|
|
|
|
const { runUpdate } = await import('./update-cmd.js')
|
|
|
|
|
await runUpdate()
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 10:52:45 -06:00
|
|
|
// Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
|
|
|
|
|
// because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
|
|
|
|
|
// Provision local managed binaries first so Pi sees them without probing PATH.
|
|
|
|
|
ensureManagedTools(join(agentDir, 'bin'))
|
|
|
|
|
|
2026-03-10 22:28:37 -06:00
|
|
|
const authStorage = AuthStorage.create(authFilePath)
|
|
|
|
|
loadStoredEnvKeys(authStorage)
|
2026-03-12 11:06:31 -06:00
|
|
|
migratePiCredentials(authStorage)
|
2026-03-11 11:21:12 -06:00
|
|
|
|
2026-03-12 10:02:00 -06:00
|
|
|
// Run onboarding wizard on first launch (no LLM provider configured)
|
|
|
|
|
if (!isPrintMode && shouldRunOnboarding(authStorage)) {
|
|
|
|
|
await runOnboarding(authStorage)
|
2026-03-11 11:21:12 -06:00
|
|
|
}
|
2026-03-10 22:28:37 -06:00
|
|
|
|
2026-03-13 14:28:43 -03:00
|
|
|
// Non-blocking update check — runs at most once per 24h, fire-and-forget
|
|
|
|
|
if (!isPrintMode) {
|
|
|
|
|
checkForUpdates().catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 22:28:37 -06:00
|
|
|
const modelRegistry = new ModelRegistry(authStorage)
|
|
|
|
|
const settingsManager = SettingsManager.create(agentDir)
|
|
|
|
|
|
2026-03-14 11:43:56 -03:00
|
|
|
// --list-models: print available models and exit (no TTY needed)
|
|
|
|
|
if (cliFlags.listModels !== undefined) {
|
|
|
|
|
const models = modelRegistry.getAvailable()
|
|
|
|
|
if (models.length === 0) {
|
|
|
|
|
console.log('No models available. Set API keys in environment variables.')
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchPattern = typeof cliFlags.listModels === 'string' ? cliFlags.listModels : undefined
|
|
|
|
|
let filtered = models
|
|
|
|
|
if (searchPattern) {
|
|
|
|
|
const q = searchPattern.toLowerCase()
|
|
|
|
|
filtered = models.filter((m) => `${m.provider} ${m.id} ${m.name}`.toLowerCase().includes(q))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by name descending (newest first), then provider, then id
|
|
|
|
|
filtered.sort((a, b) => {
|
|
|
|
|
const nameCmp = b.name.localeCompare(a.name)
|
|
|
|
|
if (nameCmp !== 0) return nameCmp
|
|
|
|
|
const provCmp = a.provider.localeCompare(b.provider)
|
|
|
|
|
if (provCmp !== 0) return provCmp
|
|
|
|
|
return a.id.localeCompare(b.id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const fmt = (n: number) => n >= 1_000_000 ? `${n / 1_000_000}M` : n >= 1_000 ? `${n / 1_000}K` : `${n}`
|
|
|
|
|
const rows = filtered.map((m) => [
|
|
|
|
|
m.provider,
|
|
|
|
|
m.id,
|
|
|
|
|
m.name,
|
|
|
|
|
fmt(m.contextWindow),
|
|
|
|
|
fmt(m.maxTokens),
|
|
|
|
|
m.reasoning ? 'yes' : 'no',
|
|
|
|
|
])
|
|
|
|
|
const hdrs = ['provider', 'model', 'name', 'context', 'max-out', 'thinking']
|
|
|
|
|
const widths = hdrs.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)))
|
|
|
|
|
const pad = (s: string, w: number) => s.padEnd(w)
|
|
|
|
|
console.log(hdrs.map((h, i) => pad(h, widths[i])).join(' '))
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
console.log(row.map((c, i) => pad(c, widths[i])).join(' '))
|
|
|
|
|
}
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:27:31 +05:30
|
|
|
// Validate configured model on startup — catches stale settings from prior installs
|
2026-03-11 01:51:48 -06:00
|
|
|
// (e.g. grok-2 which no longer exists) and fresh installs with no settings.
|
2026-03-11 18:27:31 +05:30
|
|
|
// Only resets the default when the configured model no longer exists in the registry;
|
|
|
|
|
// never overwrites a valid user choice.
|
2026-03-11 01:37:14 -06:00
|
|
|
const configuredProvider = settingsManager.getDefaultProvider()
|
|
|
|
|
const configuredModel = settingsManager.getDefaultModel()
|
2026-03-11 01:51:48 -06:00
|
|
|
const allModels = modelRegistry.getAll()
|
2026-03-12 20:44:01 -07:00
|
|
|
const availableModels = modelRegistry.getAvailable()
|
2026-03-11 01:37:14 -06:00
|
|
|
const configuredExists = configuredProvider && configuredModel &&
|
2026-03-11 01:51:48 -06:00
|
|
|
allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel)
|
2026-03-12 20:44:01 -07:00
|
|
|
const configuredAvailable = configuredProvider && configuredModel &&
|
|
|
|
|
availableModels.some((m) => m.provider === configuredProvider && m.id === configuredModel)
|
2026-03-11 01:37:14 -06:00
|
|
|
|
2026-03-12 20:44:01 -07:00
|
|
|
if (!configuredModel || !configuredExists || !configuredAvailable) {
|
|
|
|
|
const piDefault = getPiDefaultModelAndProvider()
|
2026-03-11 01:51:48 -06:00
|
|
|
const preferred =
|
2026-03-12 20:44:01 -07:00
|
|
|
(piDefault
|
|
|
|
|
? availableModels.find((m) => m.provider === piDefault.provider && m.id === piDefault.model)
|
|
|
|
|
: undefined) ||
|
|
|
|
|
availableModels.find((m) => m.provider === 'openai' && m.id === 'gpt-5.4') ||
|
|
|
|
|
availableModels.find((m) => m.provider === 'openai') ||
|
|
|
|
|
availableModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
|
|
|
|
|
availableModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
|
|
|
|
|
availableModels.find((m) => m.provider === 'anthropic') ||
|
|
|
|
|
availableModels[0]
|
2026-03-11 01:51:48 -06:00
|
|
|
if (preferred) {
|
2026-03-10 23:54:33 -06:00
|
|
|
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 20:44:01 -07:00
|
|
|
if (settingsManager.getDefaultThinkingLevel() !== 'off' && (!configuredExists || !configuredAvailable)) {
|
2026-03-11 01:37:14 -06:00
|
|
|
settingsManager.setDefaultThinkingLevel('off')
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 22:28:37 -06:00
|
|
|
// GSD always uses quiet startup — the gsd extension renders its own branded header
|
|
|
|
|
if (!settingsManager.getQuietStartup()) {
|
|
|
|
|
settingsManager.setQuietStartup(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collapse changelog by default — avoid wall of text on updates
|
|
|
|
|
if (!settingsManager.getCollapseChangelog()) {
|
|
|
|
|
settingsManager.setCollapseChangelog(true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 11:21:12 -06:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Print / subagent mode — single-shot execution, no TTY required
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
if (isPrintMode) {
|
|
|
|
|
const sessionManager = cliFlags.noSession
|
|
|
|
|
? SessionManager.inMemory()
|
|
|
|
|
: SessionManager.create(process.cwd())
|
|
|
|
|
|
|
|
|
|
// Read --append-system-prompt file content (subagent writes agent system prompts to temp files)
|
|
|
|
|
let appendSystemPrompt: string | undefined
|
|
|
|
|
if (cliFlags.appendSystemPrompt) {
|
|
|
|
|
try {
|
|
|
|
|
appendSystemPrompt = readFileSync(cliFlags.appendSystemPrompt, 'utf-8')
|
|
|
|
|
} catch {
|
|
|
|
|
// If it's not a file path, treat it as literal text
|
|
|
|
|
appendSystemPrompt = cliFlags.appendSystemPrompt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 00:58:18 -04:00
|
|
|
exitIfManagedResourcesAreNewer(agentDir)
|
2026-03-11 11:21:12 -06:00
|
|
|
initResources(agentDir)
|
|
|
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
|
|
|
agentDir,
|
|
|
|
|
additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined,
|
|
|
|
|
appendSystemPrompt,
|
|
|
|
|
})
|
|
|
|
|
await resourceLoader.reload()
|
|
|
|
|
|
|
|
|
|
const { session, extensionsResult } = await createAgentSession({
|
|
|
|
|
authStorage,
|
|
|
|
|
modelRegistry,
|
|
|
|
|
settingsManager,
|
|
|
|
|
sessionManager,
|
|
|
|
|
resourceLoader,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (extensionsResult.errors.length > 0) {
|
|
|
|
|
for (const err of extensionsResult.errors) {
|
|
|
|
|
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply --model override if specified
|
|
|
|
|
if (cliFlags.model) {
|
|
|
|
|
const available = modelRegistry.getAvailable()
|
|
|
|
|
const match =
|
|
|
|
|
available.find((m) => m.id === cliFlags.model) ||
|
|
|
|
|
available.find((m) => `${m.provider}/${m.id}` === cliFlags.model)
|
|
|
|
|
if (match) {
|
|
|
|
|
session.setModel(match)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mode = cliFlags.mode || 'text'
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
|
|
|
|
|
if (mode === 'rpc') {
|
|
|
|
|
await runRpcMode(session)
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 11:21:12 -06:00
|
|
|
await runPrintMode(session, {
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
mode,
|
2026-03-11 11:21:12 -06:00
|
|
|
messages: cliFlags.messages,
|
|
|
|
|
})
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Interactive mode — normal TTY session
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-11 21:39:02 +05:30
|
|
|
// Per-directory session storage — same encoding as the upstream SDK so that
|
|
|
|
|
// /resume only shows sessions from the current working directory.
|
|
|
|
|
const cwd = process.cwd()
|
|
|
|
|
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`
|
|
|
|
|
const projectSessionsDir = join(sessionsDir, safePath)
|
2026-03-11 17:07:54 -06:00
|
|
|
|
|
|
|
|
// Migrate legacy flat sessions: before per-directory scoping, all .jsonl session
|
|
|
|
|
// files lived directly in ~/.gsd/sessions/. Move them into the correct per-cwd
|
|
|
|
|
// subdirectory so /resume can find them.
|
|
|
|
|
if (existsSync(sessionsDir)) {
|
|
|
|
|
try {
|
|
|
|
|
const entries = readdirSync(sessionsDir)
|
|
|
|
|
const flatJsonl = entries.filter(f => f.endsWith('.jsonl'))
|
|
|
|
|
if (flatJsonl.length > 0) {
|
|
|
|
|
const { mkdirSync } = await import('node:fs')
|
|
|
|
|
mkdirSync(projectSessionsDir, { recursive: true })
|
|
|
|
|
for (const file of flatJsonl) {
|
|
|
|
|
const src = join(sessionsDir, file)
|
|
|
|
|
const dst = join(projectSessionsDir, file)
|
|
|
|
|
if (!existsSync(dst)) {
|
|
|
|
|
renameSync(src, dst)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Non-fatal — don't block startup if migration fails
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 07:51:29 +05:30
|
|
|
const sessionManager = cliFlags.continue
|
|
|
|
|
? SessionManager.continueRecent(cwd, projectSessionsDir)
|
|
|
|
|
: SessionManager.create(cwd, projectSessionsDir)
|
2026-03-10 22:28:37 -06:00
|
|
|
|
2026-03-15 00:58:18 -04:00
|
|
|
exitIfManagedResourcesAreNewer(agentDir)
|
2026-03-10 22:28:37 -06:00
|
|
|
initResources(agentDir)
|
2026-03-12 11:06:31 -06:00
|
|
|
const resourceLoader = buildResourceLoader(agentDir)
|
2026-03-10 22:28:37 -06:00
|
|
|
await resourceLoader.reload()
|
|
|
|
|
|
|
|
|
|
const { session, extensionsResult } = await createAgentSession({
|
|
|
|
|
authStorage,
|
|
|
|
|
modelRegistry,
|
|
|
|
|
settingsManager,
|
|
|
|
|
sessionManager,
|
|
|
|
|
resourceLoader,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (extensionsResult.errors.length > 0) {
|
|
|
|
|
for (const err of extensionsResult.errors) {
|
|
|
|
|
process.stderr.write(`[gsd] Extension load error: ${err.error}\n`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 07:57:55 -05:00
|
|
|
// Restore scoped models from settings on startup.
|
|
|
|
|
// The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
|
|
|
|
|
// but doesn't apply them to the session at startup — so Ctrl+P cycles all models instead of
|
|
|
|
|
// just the saved selection until the user re-runs /scoped-models.
|
|
|
|
|
const enabledModelPatterns = settingsManager.getEnabledModels()
|
|
|
|
|
if (enabledModelPatterns && enabledModelPatterns.length > 0) {
|
|
|
|
|
const availableModels = modelRegistry.getAvailable()
|
|
|
|
|
const scopedModels: Array<{ model: (typeof availableModels)[number] }> = []
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
|
|
|
|
for (const pattern of enabledModelPatterns) {
|
|
|
|
|
// Patterns are "provider/modelId" exact strings saved by /scoped-models
|
|
|
|
|
const slashIdx = pattern.indexOf('/')
|
|
|
|
|
if (slashIdx !== -1) {
|
|
|
|
|
const provider = pattern.substring(0, slashIdx)
|
|
|
|
|
const modelId = pattern.substring(slashIdx + 1)
|
|
|
|
|
const model = availableModels.find((m) => m.provider === provider && m.id === modelId)
|
|
|
|
|
if (model) {
|
|
|
|
|
const key = `${model.provider}/${model.id}`
|
|
|
|
|
if (!seen.has(key)) {
|
|
|
|
|
seen.add(key)
|
|
|
|
|
scopedModels.push({ model })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: match by model id alone
|
|
|
|
|
const model = availableModels.find((m) => m.id === pattern)
|
|
|
|
|
if (model) {
|
|
|
|
|
const key = `${model.provider}/${model.id}`
|
|
|
|
|
if (!seen.has(key)) {
|
|
|
|
|
seen.add(key)
|
|
|
|
|
scopedModels.push({ model })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only apply if we resolved some models and it's a genuine subset
|
|
|
|
|
if (scopedModels.length > 0 && scopedModels.length < availableModels.length) {
|
|
|
|
|
session.setScopedModels(scopedModels)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: address 11 community-reported bugs across CLI, auto-mode, and extensions
CLI routing (#81, #107):
- Import and route --mode rpc to runRpcMode() instead of silently falling through to runPrintMode
- Add TTY guard before interactive mode — exit with helpful message when stdin is not a TTY
- Add --version and --help flags
Auto-mode infinite loop (#96):
- Move summarizing/complete-slice dispatch before reassessment check (D1) — ensures mergeSliceToMain always runs
- Add per-unit dispatch counter to detect alternating loops like A→B→A→B (D3)
Windows shell escaping (#106, #98):
- Platform-aware escapeShellArg() in mcporter extension — double quotes on Windows, single quotes on Unix
CRASH: parseSummary (#91):
- Add asStringArray() helper to safely coerce YAML bare scalars (e.g. "none") to string arrays
- Applied to all 7 frontmatter fields that expect string[]
Google Search model (#99):
- Replace hardcoded gemini-3-flash-preview with env var GEMINI_SEARCH_MODEL (default: gemini-2.5-flash)
Worktree branch collision (#84):
- Check git worktree list before checkout to detect branches already in use by another worktree
Migration UX (#90, #93):
- Improve error messages to distinguish migration from new project setup, suggest /gsd:new-project
Keyboard shortcuts (#100, #104):
- Document terminal protocol requirement in shortcut descriptions — Ctrl+Alt combos need Kitty/modifyOtherKeys
Closes #81, #84, #91, #96, #99, #106, #107
Addresses #90, #93, #95, #98, #100, #104
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:48:15 -06:00
|
|
|
if (!process.stdin.isTTY) {
|
|
|
|
|
process.stderr.write('[gsd] Error: Interactive mode requires a terminal (TTY).\n')
|
|
|
|
|
process.stderr.write('[gsd] Non-interactive alternatives:\n')
|
|
|
|
|
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n')
|
|
|
|
|
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n')
|
|
|
|
|
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n')
|
|
|
|
|
process.exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 22:28:37 -06:00
|
|
|
const interactiveMode = new InteractiveMode(session)
|
|
|
|
|
await interactiveMode.run()
|