@meistrari/tela-cli 0.4.0 → 0.5.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 +23 -6
- package/package.json +1 -1
- package/src/agent-api.ts +76 -0
- package/src/claude.ts +101 -0
- package/src/commands/metadata.ts +23 -6
- package/src/commands/run.ts +116 -18
- package/src/commands/status.ts +14 -1
- package/src/config.ts +40 -0
- package/src/diagnostics.ts +122 -9
- package/src/forgejo.ts +44 -0
- package/src/git.ts +252 -45
- package/src/ink/screens/setup/setup-screen.tsx +37 -0
- package/src/ink/screens/setup/steps/review-step.tsx +1 -0
- package/src/ink/screens/setup/steps.ts +11 -0
- package/src/ink/screens/setup/types.ts +2 -0
- package/src/program.ts +95 -33
- package/src/types.ts +22 -0
package/README.md
CHANGED
|
@@ -14,6 +14,18 @@ This installs the package globally and adds a `tela` alias to your `~/.zshrc`. A
|
|
|
14
14
|
source ~/.zshrc
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Default command
|
|
18
|
+
|
|
19
|
+
### `tela`
|
|
20
|
+
|
|
21
|
+
List workspace repositories that are available to sync locally.
|
|
22
|
+
|
|
23
|
+
When to use: Run this with no subcommand when you want to see which workspace repositories are available before using `tela sync <repo>`.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
- `tela` — Render the available repositories in a human-readable table
|
|
27
|
+
- `tela --json` — Emit the available repositories as JSON
|
|
28
|
+
|
|
17
29
|
## Commands
|
|
18
30
|
|
|
19
31
|
### `setup`
|
|
@@ -29,7 +41,7 @@ Examples:
|
|
|
29
41
|
|
|
30
42
|
Clone or update a Tela repository, or refresh the current checkout
|
|
31
43
|
|
|
32
|
-
When to use: Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root.
|
|
44
|
+
When to use: Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root. SSH-backed sync automatically accepts first-seen host keys during clone and update flows.
|
|
33
45
|
|
|
34
46
|
Examples:
|
|
35
47
|
- `tela sync agent-api` — Clone or update a specific repository under the clone root
|
|
@@ -38,14 +50,14 @@ Examples:
|
|
|
38
50
|
|
|
39
51
|
### `run`
|
|
40
52
|
|
|
41
|
-
Run the local agent repository with Claude
|
|
53
|
+
Run the local agent repository with project-scoped Claude
|
|
42
54
|
|
|
43
|
-
When to use: Use this from inside an agent repository when you want Claude to follow the local contract files, show its progress live, and produce the expected result artifact.
|
|
55
|
+
When to use: Use this from inside an agent repository when you want Claude to follow the repo-local contract files, ignore user/global Claude config by default, show its progress live, and produce the expected result artifact.
|
|
44
56
|
|
|
45
57
|
Examples:
|
|
46
|
-
- `tela run` — Run the current agent repository with the live viewer
|
|
47
|
-
- `tela run --plain` — Force scrollback-first text output
|
|
48
|
-
- `tela run --json` — Emit the final execution and artifact envelope as JSON
|
|
58
|
+
- `tela run` — Run the current agent repository with project-scoped Claude and the live viewer
|
|
59
|
+
- `tela run --plain` — Force project-scoped execution with scrollback-first text output
|
|
60
|
+
- `tela run --json` — Emit the final project-scoped execution and artifact envelope as JSON
|
|
49
61
|
|
|
50
62
|
### `push`
|
|
51
63
|
|
|
@@ -96,6 +108,11 @@ Examples:
|
|
|
96
108
|
- Human output is structured for terminal use with grouped summaries and next actions.
|
|
97
109
|
- `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.
|
|
98
110
|
|
|
111
|
+
## `tela run` scope
|
|
112
|
+
|
|
113
|
+
- `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.
|
|
114
|
+
- Remaining external inputs are explicit: Claude's built-in system prompt, managed enterprise settings, auth/provider credentials, minimal shell runtime environment, and the default model when `agent.config.json` does not set one.
|
|
115
|
+
|
|
99
116
|
## First run
|
|
100
117
|
|
|
101
118
|
1. `tela setup`
|
package/package.json
CHANGED
package/src/agent-api.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { buildAgentApiHeaders, type FetchLike } from './auth.ts'
|
|
2
2
|
import type {
|
|
3
|
+
AgentRepoSummary,
|
|
4
|
+
AvailableRepositorySummary,
|
|
3
5
|
AgentFeedbackItem,
|
|
4
6
|
AgentFeedbackListResponse,
|
|
5
7
|
ForgejoUserCreateResponse,
|
|
6
8
|
ForgejoUserSshKeyInput,
|
|
7
9
|
OrganizationListResponse,
|
|
10
|
+
OrganizationSummary,
|
|
11
|
+
RepositoryListItemResponse,
|
|
12
|
+
RepositoryListResponse,
|
|
8
13
|
RepositoryLookupResponse,
|
|
9
14
|
TelaCliConfig,
|
|
10
15
|
TelaCliForgejoOrganization,
|
|
@@ -45,6 +50,39 @@ function isForgejoOrganization(value: unknown): value is TelaCliForgejoOrganizat
|
|
|
45
50
|
return typeof candidate.id === 'string' && typeof candidate.name === 'string'
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
function isOrganizationSummary(value: unknown): value is OrganizationSummary {
|
|
54
|
+
if (!value || typeof value !== 'object') {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const candidate = value as Partial<OrganizationSummary>
|
|
59
|
+
return typeof candidate.id === 'string'
|
|
60
|
+
&& typeof candidate.workspaceId === 'string'
|
|
61
|
+
&& typeof candidate.name === 'string'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isAgentRepoSummary(value: unknown): value is AgentRepoSummary {
|
|
65
|
+
if (!value || typeof value !== 'object') {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const candidate = value as Partial<AgentRepoSummary>
|
|
70
|
+
return typeof candidate.id === 'string'
|
|
71
|
+
&& typeof candidate.workspaceId === 'string'
|
|
72
|
+
&& typeof candidate.organizationId === 'string'
|
|
73
|
+
&& typeof candidate.repository === 'string'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isRepositoryListItemResponse(value: unknown): value is RepositoryListItemResponse {
|
|
77
|
+
if (!value || typeof value !== 'object') {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const candidate = value as Partial<RepositoryListItemResponse>
|
|
82
|
+
return isOrganizationSummary(candidate.organization)
|
|
83
|
+
&& isAgentRepoSummary(candidate.agentRepo)
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
export class AgentApiError extends Error {
|
|
49
87
|
constructor(
|
|
50
88
|
message: string,
|
|
@@ -173,6 +211,44 @@ export async function listOrganizations(
|
|
|
173
211
|
return payload.organizations
|
|
174
212
|
}
|
|
175
213
|
|
|
214
|
+
export async function listAvailableRepositories(
|
|
215
|
+
config: TelaCliConfig,
|
|
216
|
+
fetchFn: FetchLike = fetch,
|
|
217
|
+
): Promise<AvailableRepositorySummary[]> {
|
|
218
|
+
const headers = await buildAgentApiHeaders(config, { fetchFn })
|
|
219
|
+
const response = await fetchFn(
|
|
220
|
+
joinUrl(config.agentApiUrl, '/v3/agents/repositories'),
|
|
221
|
+
{
|
|
222
|
+
method: 'GET',
|
|
223
|
+
headers,
|
|
224
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const responseText = await response.text()
|
|
229
|
+
const payload = parseJsonBody(responseText) as RepositoryListResponse | null
|
|
230
|
+
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new AgentApiError(
|
|
233
|
+
payload?.error ?? `Agent API repository listing failed with status ${response.status}`,
|
|
234
|
+
response.status,
|
|
235
|
+
responseText,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!payload
|
|
240
|
+
|| payload.success !== true
|
|
241
|
+
|| !Array.isArray(payload.repositories)
|
|
242
|
+
|| !payload.repositories.every(isRepositoryListItemResponse)) {
|
|
243
|
+
throw new AgentApiError('Agent API repository listing returned an invalid payload', response.status, responseText)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return payload.repositories.map(repository => ({
|
|
247
|
+
...repository,
|
|
248
|
+
syncStatus: 'available' as const,
|
|
249
|
+
}))
|
|
250
|
+
}
|
|
251
|
+
|
|
176
252
|
export async function provisionForgejoUser(
|
|
177
253
|
input: {
|
|
178
254
|
organizationName: string
|
package/src/claude.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { commandExists, runCommand } from './process.ts'
|
|
2
|
+
|
|
3
|
+
export interface ClaudeCliVersion {
|
|
4
|
+
major: number
|
|
5
|
+
minor: number
|
|
6
|
+
patch: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClaudeCliInspection {
|
|
10
|
+
available: boolean
|
|
11
|
+
version?: string
|
|
12
|
+
parsedVersion?: ClaudeCliVersion
|
|
13
|
+
supportsProjectScopedRun: boolean
|
|
14
|
+
problem?: 'missing' | 'unparseable' | 'unsupported'
|
|
15
|
+
detail?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN: ClaudeCliVersion = {
|
|
19
|
+
major: 2,
|
|
20
|
+
minor: 1,
|
|
21
|
+
patch: 108,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatClaudeCliVersion(version: ClaudeCliVersion): string {
|
|
25
|
+
return `${version.major}.${version.minor}.${version.patch}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function compareClaudeCliVersions(left: ClaudeCliVersion, right: ClaudeCliVersion): number {
|
|
29
|
+
if (left.major !== right.major) {
|
|
30
|
+
return left.major - right.major
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (left.minor !== right.minor) {
|
|
34
|
+
return left.minor - right.minor
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return left.patch - right.patch
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseClaudeCliVersion(raw: string): ClaudeCliVersion | null {
|
|
41
|
+
const match = raw.match(/\b(\d+)\.(\d+)\.(\d+)\b/)
|
|
42
|
+
if (!match) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
major: Number(match[1]),
|
|
48
|
+
minor: Number(match[2]),
|
|
49
|
+
patch: Number(match[3]),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function inspectClaudeCli(options: {
|
|
54
|
+
commandExistsFn?: typeof commandExists
|
|
55
|
+
runCommandFn?: typeof runCommand
|
|
56
|
+
} = {}): Promise<ClaudeCliInspection> {
|
|
57
|
+
const commandExistsFn = options.commandExistsFn ?? commandExists
|
|
58
|
+
const runCommandFn = options.runCommandFn ?? runCommand
|
|
59
|
+
|
|
60
|
+
if (!await commandExistsFn('claude')) {
|
|
61
|
+
return {
|
|
62
|
+
available: false,
|
|
63
|
+
supportsProjectScopedRun: false,
|
|
64
|
+
problem: 'missing',
|
|
65
|
+
detail: '`claude` is not available on PATH.',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await runCommandFn(['claude', '--version'], { allowFailure: true })
|
|
70
|
+
const output = `${result.stdout}\n${result.stderr}`.trim()
|
|
71
|
+
const parsedVersion = parseClaudeCliVersion(output)
|
|
72
|
+
|
|
73
|
+
if (!parsedVersion) {
|
|
74
|
+
return {
|
|
75
|
+
available: true,
|
|
76
|
+
supportsProjectScopedRun: false,
|
|
77
|
+
problem: 'unparseable',
|
|
78
|
+
detail: output || 'Claude CLI version output was empty.',
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const version = formatClaudeCliVersion(parsedVersion)
|
|
83
|
+
if (compareClaudeCliVersions(parsedVersion, MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN) < 0) {
|
|
84
|
+
return {
|
|
85
|
+
available: true,
|
|
86
|
+
version,
|
|
87
|
+
parsedVersion,
|
|
88
|
+
supportsProjectScopedRun: false,
|
|
89
|
+
problem: 'unsupported',
|
|
90
|
+
detail: `Claude CLI ${version} is older than the required ${formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)}.`,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
available: true,
|
|
96
|
+
version,
|
|
97
|
+
parsedVersion,
|
|
98
|
+
supportsProjectScopedRun: true,
|
|
99
|
+
detail: `Claude CLI ${version} supports project-scoped runs.`,
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/commands/metadata.ts
CHANGED
|
@@ -32,7 +32,7 @@ const COMMANDS: CommandMetadata[] = [
|
|
|
32
32
|
name: 'sync',
|
|
33
33
|
summary: 'Clone or update a Tela repository, or refresh the current checkout',
|
|
34
34
|
argumentHint: '[repo]',
|
|
35
|
-
whenToUse: 'Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root.',
|
|
35
|
+
whenToUse: 'Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root. SSH-backed sync automatically accepts first-seen host keys during clone and update flows.',
|
|
36
36
|
examples: [
|
|
37
37
|
{
|
|
38
38
|
command: 'tela sync agent-api',
|
|
@@ -52,20 +52,20 @@ const COMMANDS: CommandMetadata[] = [
|
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
name: 'run',
|
|
55
|
-
summary: 'Run the local agent repository with Claude',
|
|
56
|
-
whenToUse: 'Use this from inside an agent repository when you want Claude to follow the local contract files, show its progress live, and produce the expected result artifact.',
|
|
55
|
+
summary: 'Run the local agent repository with project-scoped Claude',
|
|
56
|
+
whenToUse: 'Use this from inside an agent repository when you want Claude to follow the repo-local contract files, ignore user/global Claude config by default, show its progress live, and produce the expected result artifact.',
|
|
57
57
|
examples: [
|
|
58
58
|
{
|
|
59
59
|
command: 'tela run',
|
|
60
|
-
description: 'Run the current agent repository with the live viewer',
|
|
60
|
+
description: 'Run the current agent repository with project-scoped Claude and the live viewer',
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
command: 'tela run --plain',
|
|
64
|
-
description: 'Force scrollback-first text output',
|
|
64
|
+
description: 'Force project-scoped execution with scrollback-first text output',
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
command: 'tela run --json',
|
|
68
|
-
description: 'Emit the final execution and artifact envelope as JSON',
|
|
68
|
+
description: 'Emit the final project-scoped execution and artifact envelope as JSON',
|
|
69
69
|
},
|
|
70
70
|
],
|
|
71
71
|
supportsJson: true,
|
|
@@ -176,6 +176,18 @@ export function renderReadmeMarkdown(): string {
|
|
|
176
176
|
'source ~/.zshrc',
|
|
177
177
|
'```',
|
|
178
178
|
'',
|
|
179
|
+
'## Default command',
|
|
180
|
+
'',
|
|
181
|
+
'### `tela`',
|
|
182
|
+
'',
|
|
183
|
+
'List workspace repositories that are available to sync locally.',
|
|
184
|
+
'',
|
|
185
|
+
'When to use: Run this with no subcommand when you want to see which workspace repositories are available before using `tela sync <repo>`.',
|
|
186
|
+
'',
|
|
187
|
+
'Examples:',
|
|
188
|
+
'- `tela` — Render the available repositories in a human-readable table',
|
|
189
|
+
'- `tela --json` — Emit the available repositories as JSON',
|
|
190
|
+
'',
|
|
179
191
|
'## Commands',
|
|
180
192
|
'',
|
|
181
193
|
]
|
|
@@ -205,6 +217,11 @@ export function renderReadmeMarkdown(): string {
|
|
|
205
217
|
lines.push('- Human output is structured for terminal use with grouped summaries and next actions.')
|
|
206
218
|
lines.push('- `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.')
|
|
207
219
|
lines.push('')
|
|
220
|
+
lines.push('## `tela run` scope')
|
|
221
|
+
lines.push('')
|
|
222
|
+
lines.push('- `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.')
|
|
223
|
+
lines.push('- Remaining external inputs are explicit: Claude\'s built-in system prompt, managed enterprise settings, auth/provider credentials, minimal shell runtime environment, and the default model when `agent.config.json` does not set one.')
|
|
224
|
+
lines.push('')
|
|
208
225
|
lines.push('## First run')
|
|
209
226
|
lines.push('')
|
|
210
227
|
lines.push('1. `tela setup`')
|
package/src/commands/run.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
1
2
|
import { readFile } from 'node:fs/promises'
|
|
2
3
|
import { basename, join } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
formatClaudeCliVersion,
|
|
6
|
+
inspectClaudeCli,
|
|
7
|
+
MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
|
|
8
|
+
} from '../claude.ts'
|
|
3
9
|
import { getRepositoryRoot } from '../git.ts'
|
|
4
10
|
import { createUserFacingError } from '../errors.ts'
|
|
5
11
|
import {
|
|
6
12
|
commandExists,
|
|
13
|
+
runCommand,
|
|
7
14
|
runInteractiveCommand,
|
|
8
15
|
runStreamingCommand,
|
|
9
16
|
spawnStreamingCommand,
|
|
@@ -37,6 +44,7 @@ export interface PreparedLocalAgentRun {
|
|
|
37
44
|
config: AgentRunConfig
|
|
38
45
|
resultKind: 'json' | 'markdown'
|
|
39
46
|
resultPath: string
|
|
47
|
+
mcpConfigPath?: string
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export interface RunStepSummary {
|
|
@@ -110,6 +118,8 @@ export interface RunLocalAgentCommandOptions {
|
|
|
110
118
|
contract?: PreparedLocalAgentRun
|
|
111
119
|
surface?: RunOutputSurface
|
|
112
120
|
commandExists?: typeof commandExists
|
|
121
|
+
inspectClaudeCli?: typeof inspectClaudeCli
|
|
122
|
+
runCommand?: typeof runCommand
|
|
113
123
|
runInteractiveCommand?: typeof runInteractiveCommand
|
|
114
124
|
runStreamingCommand?: typeof runStreamingCommand
|
|
115
125
|
spawnStreamingCommand?: typeof spawnStreamingCommand
|
|
@@ -1130,6 +1140,88 @@ function parseAgentConfig(path: string, content: string): AgentRunConfig {
|
|
|
1130
1140
|
}
|
|
1131
1141
|
}
|
|
1132
1142
|
|
|
1143
|
+
const CLAUDE_RUN_ENV_DENYLIST = new Set([
|
|
1144
|
+
'CLAUDE_CONFIG_DIR',
|
|
1145
|
+
'CLAUDE_ENV_FILE',
|
|
1146
|
+
'ANTHROPIC_MODEL',
|
|
1147
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
1148
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
1149
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
1150
|
+
'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
|
|
1151
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
1152
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
|
|
1153
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
|
|
1154
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
|
1155
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
1156
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
|
1157
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
|
1158
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
|
1159
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
1160
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
1161
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
1162
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
|
1163
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
1164
|
+
])
|
|
1165
|
+
|
|
1166
|
+
function buildClaudeRunEnvironment(
|
|
1167
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
1168
|
+
): Record<string, string | undefined> {
|
|
1169
|
+
return Object.fromEntries(
|
|
1170
|
+
Object.entries(baseEnv).filter(([key]) => !CLAUDE_RUN_ENV_DENYLIST.has(key)),
|
|
1171
|
+
)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function buildUnsupportedClaudeVersionError(detail: string | undefined): ReturnType<typeof createUserFacingError> {
|
|
1175
|
+
const requiredVersion = formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)
|
|
1176
|
+
|
|
1177
|
+
return createUserFacingError({
|
|
1178
|
+
code: 'run_claude_version_unsupported',
|
|
1179
|
+
summary: `\`tela run\` requires Claude CLI ${requiredVersion} or newer for project-scoped runs.`,
|
|
1180
|
+
details: detail,
|
|
1181
|
+
fixes: [
|
|
1182
|
+
'Update Claude CLI before using `tela run`.',
|
|
1183
|
+
'Run `claude --version` to confirm the installed version.',
|
|
1184
|
+
],
|
|
1185
|
+
})
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function buildClaudeBaseCommand(prepared: PreparedLocalAgentRun): string[] {
|
|
1189
|
+
const command = [
|
|
1190
|
+
'claude',
|
|
1191
|
+
'--setting-sources',
|
|
1192
|
+
'project',
|
|
1193
|
+
]
|
|
1194
|
+
|
|
1195
|
+
if (prepared.mcpConfigPath) {
|
|
1196
|
+
command.push('--strict-mcp-config', '--mcp-config', prepared.mcpConfigPath)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (prepared.config.model) {
|
|
1200
|
+
command.push('--model', prepared.config.model)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return command
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function buildClaudeCommand(
|
|
1207
|
+
prepared: PreparedLocalAgentRun,
|
|
1208
|
+
mode: RunMode,
|
|
1209
|
+
): string[] {
|
|
1210
|
+
const command = buildClaudeBaseCommand(prepared)
|
|
1211
|
+
|
|
1212
|
+
if (mode === 'one-shot') {
|
|
1213
|
+
command.push(
|
|
1214
|
+
'-p',
|
|
1215
|
+
'--output-format',
|
|
1216
|
+
'stream-json',
|
|
1217
|
+
'--verbose',
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
command.push(INITIAL_PROMPT)
|
|
1222
|
+
return command
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1133
1225
|
async function resolveRepositoryRoot(cwd: string): Promise<string> {
|
|
1134
1226
|
try {
|
|
1135
1227
|
return await getRepositoryRoot(cwd)
|
|
@@ -1176,12 +1268,19 @@ async function validateRequiredFiles(repoRoot: string): Promise<void> {
|
|
|
1176
1268
|
export async function prepareLocalAgentRun(options: {
|
|
1177
1269
|
cwd?: string
|
|
1178
1270
|
commandExists?: typeof commandExists
|
|
1271
|
+
inspectClaudeCli?: typeof inspectClaudeCli
|
|
1272
|
+
runCommand?: typeof runCommand
|
|
1179
1273
|
} = {}): Promise<PreparedLocalAgentRun> {
|
|
1180
1274
|
const cwd = options.cwd ?? process.cwd()
|
|
1181
|
-
const commandExistsFn = options.commandExists ?? commandExists
|
|
1182
1275
|
const repoRoot = await resolveRepositoryRoot(cwd)
|
|
1276
|
+
const inspectClaudeCliFn = options.inspectClaudeCli ?? inspectClaudeCli
|
|
1277
|
+
|
|
1278
|
+
const claudeInspection = await inspectClaudeCliFn({
|
|
1279
|
+
commandExistsFn: options.commandExists,
|
|
1280
|
+
runCommandFn: options.runCommand,
|
|
1281
|
+
})
|
|
1183
1282
|
|
|
1184
|
-
if (!
|
|
1283
|
+
if (!claudeInspection.available) {
|
|
1185
1284
|
throw createUserFacingError({
|
|
1186
1285
|
code: 'run_claude_missing',
|
|
1187
1286
|
summary: 'Claude CLI is not available on PATH.',
|
|
@@ -1189,6 +1288,10 @@ export async function prepareLocalAgentRun(options: {
|
|
|
1189
1288
|
})
|
|
1190
1289
|
}
|
|
1191
1290
|
|
|
1291
|
+
if (!claudeInspection.supportsProjectScopedRun) {
|
|
1292
|
+
throw buildUnsupportedClaudeVersionError(claudeInspection.detail)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1192
1295
|
await validateRequiredFiles(repoRoot)
|
|
1193
1296
|
|
|
1194
1297
|
const [outputFormatContent, agentConfigContent] = await Promise.all([
|
|
@@ -1209,6 +1312,9 @@ export async function prepareLocalAgentRun(options: {
|
|
|
1209
1312
|
config,
|
|
1210
1313
|
resultKind,
|
|
1211
1314
|
resultPath: join(repoRoot, 'output', resultKind === 'json' ? 'result.json' : 'result.md'),
|
|
1315
|
+
mcpConfigPath: existsSync(join(repoRoot, '.mcp.json'))
|
|
1316
|
+
? join(repoRoot, '.mcp.json')
|
|
1317
|
+
: undefined,
|
|
1212
1318
|
}
|
|
1213
1319
|
}
|
|
1214
1320
|
|
|
@@ -1375,13 +1481,10 @@ async function runInteractiveAgent(
|
|
|
1375
1481
|
writeLine(options.writeStdout, 'Launching Claude in interactive chat mode. Tela will resume after Claude exits.')
|
|
1376
1482
|
}
|
|
1377
1483
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
]
|
|
1383
|
-
|
|
1384
|
-
await runInteractiveCommandFn(command, { cwd: prepared.repoRoot })
|
|
1484
|
+
await runInteractiveCommandFn(buildClaudeCommand(prepared, 'chat'), {
|
|
1485
|
+
cwd: prepared.repoRoot,
|
|
1486
|
+
env: buildClaudeRunEnvironment(),
|
|
1487
|
+
})
|
|
1385
1488
|
}
|
|
1386
1489
|
|
|
1387
1490
|
async function runOneShotAgent(
|
|
@@ -1408,18 +1511,11 @@ async function runOneShotAgent(
|
|
|
1408
1511
|
}
|
|
1409
1512
|
|
|
1410
1513
|
let cancelled = false
|
|
1411
|
-
const command =
|
|
1412
|
-
'claude',
|
|
1413
|
-
'-p',
|
|
1414
|
-
'--output-format',
|
|
1415
|
-
'stream-json',
|
|
1416
|
-
'--verbose',
|
|
1417
|
-
...(prepared.config.model ? ['--model', prepared.config.model] : []),
|
|
1418
|
-
INITIAL_PROMPT,
|
|
1419
|
-
]
|
|
1514
|
+
const command = buildClaudeCommand(prepared, 'one-shot')
|
|
1420
1515
|
|
|
1421
1516
|
const handle = spawnStreamingCommandFn(command, {
|
|
1422
1517
|
cwd: prepared.repoRoot,
|
|
1518
|
+
env: buildClaudeRunEnvironment(),
|
|
1423
1519
|
allowFailure: true,
|
|
1424
1520
|
onStdoutLine: async (line) => {
|
|
1425
1521
|
const trimmed = line.trim()
|
|
@@ -1509,6 +1605,8 @@ export async function runLocalAgentCommand(
|
|
|
1509
1605
|
const prepared = options.contract ?? await prepareLocalAgentRun({
|
|
1510
1606
|
cwd: options.cwd ?? process.cwd(),
|
|
1511
1607
|
commandExists: options.commandExists,
|
|
1608
|
+
inspectClaudeCli: options.inspectClaudeCli,
|
|
1609
|
+
runCommand: options.runCommand,
|
|
1512
1610
|
})
|
|
1513
1611
|
const mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot'
|
|
1514
1612
|
const tracker = new RunStateTracker(prepared, mode)
|
package/src/commands/status.ts
CHANGED
|
@@ -31,6 +31,9 @@ export function renderStatusReport(report: StatusReport, options: RenderOptions
|
|
|
31
31
|
{ label: 'Workspace', value: report.config.workspace ?? '<signed out>' },
|
|
32
32
|
{ label: 'Organization', value: report.config.forgejoOrganization ?? '<not selected>' },
|
|
33
33
|
{ label: 'Forgejo user', value: report.config.forgejoUserRegistered == null ? '<unknown>' : (report.config.forgejoUserRegistered ? 'registered' : 'pending') },
|
|
34
|
+
{ label: 'Git SSH key', value: report.config.gitSshKeyTitle ?? '<missing>' },
|
|
35
|
+
{ label: 'Git SSH public key', value: report.config.gitSshPublicKeyPath ?? '<missing>' },
|
|
36
|
+
{ label: 'Git SSH private key', value: report.config.gitSshPrivateKeyPath ?? '<missing>' },
|
|
34
37
|
{ label: 'Access token', value: report.config.accessToken ?? '<missing>' },
|
|
35
38
|
{ label: 'Session expiry', value: report.config.sessionExpiresAt ? new Date(report.config.sessionExpiresAt).toISOString() : '<missing>' },
|
|
36
39
|
{ label: 'Refresh token', value: report.config.hasRefreshToken ? 'present' : 'missing' },
|
|
@@ -49,7 +52,17 @@ export function renderStatusReport(report: StatusReport, options: RenderOptions
|
|
|
49
52
|
|
|
50
53
|
const toolRows = [
|
|
51
54
|
['git', report.tools.git ? 'available' : 'missing', report.tools.git ? 'ready' : 'install git'],
|
|
52
|
-
[
|
|
55
|
+
[
|
|
56
|
+
'claude',
|
|
57
|
+
!report.tools.claude
|
|
58
|
+
? 'missing'
|
|
59
|
+
: (report.tools.claudeProjectScopedRunSupported === false ? 'unsupported' : 'available'),
|
|
60
|
+
!report.tools.claude
|
|
61
|
+
? 'required for `tela run`, `tela feedback`, and Claude commit messages'
|
|
62
|
+
: (report.tools.claudeProjectScopedRunSupported === false
|
|
63
|
+
? (report.tools.claudeDetail ?? `version ${report.tools.claudeVersion ?? '<unknown>'}`)
|
|
64
|
+
: (report.tools.claudeVersion ? `version ${report.tools.claudeVersion}` : 'ready')),
|
|
65
|
+
],
|
|
53
66
|
[
|
|
54
67
|
'agent-api-origin',
|
|
55
68
|
report.tools.agentApiReachable === null
|
package/src/config.ts
CHANGED
|
@@ -5,9 +5,13 @@ import { listOrganizations, provisionForgejoUser } from './agent-api.ts'
|
|
|
5
5
|
import type { FetchLike } from './auth.ts'
|
|
6
6
|
import { createUserFacingError } from './errors.ts'
|
|
7
7
|
import {
|
|
8
|
+
createGitSshKey,
|
|
8
9
|
ensureForgejoUserAccess,
|
|
10
|
+
inferSshPrivateKeyPath,
|
|
9
11
|
listDiscoveredSshPublicKeys,
|
|
10
12
|
loadSshPublicKey,
|
|
13
|
+
resolveSshPrivateKeyPath,
|
|
14
|
+
validateSshPrivateKeyPath,
|
|
11
15
|
validateSshPublicKeyPath,
|
|
12
16
|
type SelectedSshPublicKey,
|
|
13
17
|
} from './forgejo.ts'
|
|
@@ -15,6 +19,7 @@ import type {
|
|
|
15
19
|
TelaCliAuthState,
|
|
16
20
|
TelaCliConfig,
|
|
17
21
|
TelaCliForgejoOrganization,
|
|
22
|
+
TelaCliGitSshKey,
|
|
18
23
|
TelaEnvironment,
|
|
19
24
|
} from './types.ts'
|
|
20
25
|
|
|
@@ -213,6 +218,18 @@ function normalizeForgejoOrganization(
|
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
function normalizeGitSshKey(gitSshKey: TelaCliConfig['gitSshKey']): TelaCliGitSshKey | null {
|
|
222
|
+
if (!gitSshKey) {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
title: ensureNonEmpty(String(gitSshKey.title ?? ''), 'Git SSH key title'),
|
|
228
|
+
publicKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.publicKeyPath ?? ''), 'Git SSH public key path')),
|
|
229
|
+
privateKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.privateKeyPath ?? ''), 'Git SSH private key path')),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
|
|
217
234
|
const environment = ensureEnvironment(config.environment)
|
|
218
235
|
const auth = normalizeAuth(config.auth)
|
|
@@ -229,6 +246,7 @@ function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
|
|
|
229
246
|
auth,
|
|
230
247
|
forgejoOrganization: auth ? normalizeForgejoOrganization(config.forgejoOrganization) : null,
|
|
231
248
|
forgejoUserRegistered: Boolean(config.forgejoUserRegistered),
|
|
249
|
+
gitSshKey: normalizeGitSshKey(config.gitSshKey),
|
|
232
250
|
}
|
|
233
251
|
}
|
|
234
252
|
|
|
@@ -257,6 +275,7 @@ function parseJsonConfig(input: string): TelaCliConfig {
|
|
|
257
275
|
auth,
|
|
258
276
|
forgejoOrganization: parsed.forgejoOrganization as TelaCliConfig['forgejoOrganization'] ?? null,
|
|
259
277
|
forgejoUserRegistered: Boolean(parsed.forgejoUserRegistered ?? false),
|
|
278
|
+
gitSshKey: parsed.gitSshKey as TelaCliConfig['gitSshKey'] ?? null,
|
|
260
279
|
})
|
|
261
280
|
}
|
|
262
281
|
|
|
@@ -317,6 +336,25 @@ async function promptForSshPublicKey(prompts: PromptModule): Promise<SelectedSsh
|
|
|
317
336
|
return loadSshPublicKey(promptValue(customPath))
|
|
318
337
|
}
|
|
319
338
|
|
|
339
|
+
async function promptForSshPrivateKeyPath(
|
|
340
|
+
prompts: PromptModule,
|
|
341
|
+
sshKey: SelectedSshPublicKey,
|
|
342
|
+
): Promise<string> {
|
|
343
|
+
const privateKeyPath = await prompts.text({
|
|
344
|
+
message: 'SSH private key path',
|
|
345
|
+
defaultValue: inferSshPrivateKeyPath(sshKey.path),
|
|
346
|
+
placeholder: sshKey.path.endsWith('.pub')
|
|
347
|
+
? sshKey.path.slice(0, -4)
|
|
348
|
+
: '~/.ssh/id_ed25519',
|
|
349
|
+
validate: validateSshPrivateKeyPath,
|
|
350
|
+
})
|
|
351
|
+
if (prompts.isCancel(privateKeyPath)) {
|
|
352
|
+
cancelSetup(prompts)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return resolveSshPrivateKeyPath(promptValue(privateKeyPath))
|
|
356
|
+
}
|
|
357
|
+
|
|
320
358
|
function getHomeDir(): string {
|
|
321
359
|
return process.env.HOME || homedir()
|
|
322
360
|
}
|
|
@@ -700,6 +738,7 @@ export async function setupConfigInteractive(options: {
|
|
|
700
738
|
}
|
|
701
739
|
|
|
702
740
|
const sshKey = await promptForSshPublicKey(prompts)
|
|
741
|
+
const privateKeyPath = await promptForSshPrivateKeyPath(prompts, sshKey)
|
|
703
742
|
|
|
704
743
|
const resolvedSetup = {
|
|
705
744
|
...resolveEnvironmentConfigForPreset(environment, customValues),
|
|
@@ -718,6 +757,7 @@ export async function setupConfigInteractive(options: {
|
|
|
718
757
|
auth,
|
|
719
758
|
forgejoOrganization: null,
|
|
720
759
|
forgejoUserRegistered: false,
|
|
760
|
+
gitSshKey: createGitSshKey(sshKey, privateKeyPath),
|
|
721
761
|
})
|
|
722
762
|
const organization = await resolveForgejoOrganizationInteractive({
|
|
723
763
|
prompts,
|