@trydecember/cli 0.2.1 → 0.2.3
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/dist/december.js +61357 -22231
- package/package.json +7 -5
- package/src/auth.ts +204 -0
- package/src/index.ts +86 -8
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"module": "src/index.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=18.0.0"
|
|
9
9
|
},
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "bun build ./src/index.ts --target=node --outfile=./dist/december.js"
|
|
15
15
|
},
|
|
16
|
-
"
|
|
16
|
+
"devDependencies": {
|
|
17
17
|
"@december/agent": "workspace:*",
|
|
18
|
+
"@december/providers": "workspace:*",
|
|
18
19
|
"@december/tools": "workspace:*",
|
|
19
20
|
"@december/tui": "workspace:*",
|
|
20
21
|
"fast-glob": "3.3.3",
|
|
21
22
|
"ink": "7.1.0",
|
|
22
|
-
"react": "19.2.7"
|
|
23
|
-
},
|
|
24
|
-
"devDependencies": {
|
|
23
|
+
"react": "19.2.7",
|
|
25
24
|
"@types/bun": "latest",
|
|
26
25
|
"react-devtools-core": "7.0.1"
|
|
27
26
|
},
|
|
28
27
|
"peerDependencies": {
|
|
29
28
|
"typescript": "^5"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"open": "11.0.0"
|
|
30
32
|
}
|
|
31
33
|
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import open from 'open'
|
|
3
|
+
|
|
4
|
+
export async function loginViaBrowser(
|
|
5
|
+
baseUrl: string = 'http://localhost:3000/cli/login'
|
|
6
|
+
): Promise<{ token: string; email: string | null }> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const server = http.createServer((req, res) => {
|
|
9
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`)
|
|
10
|
+
|
|
11
|
+
if (url.pathname === '/callback') {
|
|
12
|
+
const token = url.searchParams.get('token')
|
|
13
|
+
const email = url.searchParams.get('email')
|
|
14
|
+
|
|
15
|
+
if (token) {
|
|
16
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
17
|
+
res.end(`
|
|
18
|
+
<html>
|
|
19
|
+
<head>
|
|
20
|
+
<title>Authorized</title>
|
|
21
|
+
<style>
|
|
22
|
+
body {
|
|
23
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
align-items: center;
|
|
28
|
+
height: 100vh;
|
|
29
|
+
margin: 0;
|
|
30
|
+
background-color: #141414;
|
|
31
|
+
color: #fff;
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
.container {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: center;
|
|
38
|
+
max-width: 380px;
|
|
39
|
+
width: 100%;
|
|
40
|
+
}
|
|
41
|
+
svg {
|
|
42
|
+
width: 42px;
|
|
43
|
+
height: 42px;
|
|
44
|
+
margin-bottom: 24px;
|
|
45
|
+
}
|
|
46
|
+
h1 {
|
|
47
|
+
font-size: 22px;
|
|
48
|
+
font-weight: 400;
|
|
49
|
+
margin: 0 0 4px 0;
|
|
50
|
+
letter-spacing: -0.025em;
|
|
51
|
+
}
|
|
52
|
+
p {
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
color: #A3A3A3;
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<div class="container">
|
|
61
|
+
<svg viewBox="5 4 14 16" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
62
|
+
<line x1="12" y1="12" x2="12" y2="5.3" />
|
|
63
|
+
<line x1="12" y1="7.5" x2="13.9" y2="6.4" />
|
|
64
|
+
<line x1="12" y1="7.5" x2="10.1" y2="6.4" />
|
|
65
|
+
<line x1="12" y1="12" x2="17.8" y2="8.65" />
|
|
66
|
+
<line x1="15.9" y1="9.75" x2="17.8" y2="10.85" />
|
|
67
|
+
<line x1="15.9" y1="9.75" x2="15.9" y2="7.55" />
|
|
68
|
+
<line x1="12" y1="12" x2="17.8" y2="15.35" />
|
|
69
|
+
<line x1="15.9" y1="14.25" x2="15.9" y2="16.45" />
|
|
70
|
+
<line x1="15.9" y1="14.25" x2="17.8" y2="13.15" />
|
|
71
|
+
<line x1="12" y1="12" x2="12" y2="18.7" />
|
|
72
|
+
<line x1="12" y1="16.5" x2="10.1" y2="17.6" />
|
|
73
|
+
<line x1="12" y1="16.5" x2="13.9" y2="17.6" />
|
|
74
|
+
<line x1="12" y1="12" x2="6.2" y2="15.35" />
|
|
75
|
+
<line x1="8.1" y1="14.25" x2="6.2" y2="13.15" />
|
|
76
|
+
<line x1="8.1" y1="14.25" x2="8.1" y2="16.45" />
|
|
77
|
+
<line x1="12" y1="12" x2="6.2" y2="8.65" />
|
|
78
|
+
<line x1="8.1" y1="9.75" x2="8.1" y2="7.55" />
|
|
79
|
+
<line x1="8.1" y1="9.75" x2="6.2" y2="10.85" />
|
|
80
|
+
</svg>
|
|
81
|
+
<h1>Authorized</h1>
|
|
82
|
+
<p>You can safely close this tab and return to the terminal.</p>
|
|
83
|
+
</div>
|
|
84
|
+
<script>
|
|
85
|
+
setTimeout(() => window.close(), 3000);
|
|
86
|
+
</script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
89
|
+
`)
|
|
90
|
+
|
|
91
|
+
server.close(() => {
|
|
92
|
+
resolve({ token, email })
|
|
93
|
+
})
|
|
94
|
+
} else {
|
|
95
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' })
|
|
96
|
+
res.end('Missing token in callback.')
|
|
97
|
+
server.close(() => {
|
|
98
|
+
reject(new Error('Missing token in callback'))
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
103
|
+
res.end('Not found')
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
server.listen(0, '127.0.0.1', () => {
|
|
108
|
+
const address = server.address()
|
|
109
|
+
if (address && typeof address === 'object') {
|
|
110
|
+
const port = address.port
|
|
111
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`
|
|
112
|
+
const loginUrl = `${baseUrl}?redirect_uri=${encodeURIComponent(redirectUri)}`
|
|
113
|
+
|
|
114
|
+
open(loginUrl).catch((err) => {
|
|
115
|
+
server.close()
|
|
116
|
+
reject(err)
|
|
117
|
+
})
|
|
118
|
+
} else {
|
|
119
|
+
server.close()
|
|
120
|
+
reject(new Error('Failed to start local server'))
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Timeout after 5 minutes
|
|
125
|
+
setTimeout(
|
|
126
|
+
() => {
|
|
127
|
+
server.close()
|
|
128
|
+
reject(new Error('Login timed out'))
|
|
129
|
+
},
|
|
130
|
+
5 * 60 * 1000
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function loginViaDeviceCode(
|
|
136
|
+
baseUrl: string = 'http://localhost:4000',
|
|
137
|
+
onCodeGenerated: (userCode: string, verificationUri: string) => void
|
|
138
|
+
): Promise<{ token: string; email: string | null }> {
|
|
139
|
+
return new Promise(async (resolve, reject) => {
|
|
140
|
+
try {
|
|
141
|
+
// 1. Generate Code
|
|
142
|
+
const genRes = await fetch(`${baseUrl}/api/v1/auth/device/code`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!genRes.ok) {
|
|
148
|
+
throw new Error('Failed to generate device code')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const genData = await genRes.json()
|
|
152
|
+
if (!genData.success) {
|
|
153
|
+
throw new Error(genData.message || 'Failed to generate device code')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { deviceCode, userCode, verificationUri, expiresIn, interval } = genData.data
|
|
157
|
+
|
|
158
|
+
onCodeGenerated(userCode, verificationUri)
|
|
159
|
+
|
|
160
|
+
// 2. Poll for token
|
|
161
|
+
const startTime = Date.now()
|
|
162
|
+
const pollInterval = interval * 1000 || 5000
|
|
163
|
+
|
|
164
|
+
const poll = async () => {
|
|
165
|
+
if (Date.now() - startTime > expiresIn * 1000) {
|
|
166
|
+
reject(new Error('Device code expired'))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const tokenRes = await fetch(`${baseUrl}/api/v1/auth/device/token`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ deviceCode }),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const tokenData = await tokenRes.json()
|
|
178
|
+
|
|
179
|
+
if (tokenRes.ok && tokenData.success) {
|
|
180
|
+
resolve({
|
|
181
|
+
token: tokenData.data.token,
|
|
182
|
+
email: tokenData.data.email,
|
|
183
|
+
})
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If not ok, check if it's authorization_pending
|
|
188
|
+
if (tokenData.message === 'authorization_pending') {
|
|
189
|
+
setTimeout(poll, pollInterval)
|
|
190
|
+
} else {
|
|
191
|
+
reject(new Error(tokenData.message || 'Polling failed'))
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
// Network errors during polling, just keep trying
|
|
195
|
+
setTimeout(poll, pollInterval)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setTimeout(poll, pollInterval)
|
|
200
|
+
} catch (err) {
|
|
201
|
+
reject(err)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,35 +4,88 @@ import React from 'react'
|
|
|
4
4
|
import { App } from '@december/tui'
|
|
5
5
|
import {
|
|
6
6
|
Agent,
|
|
7
|
-
|
|
7
|
+
AgentHarness,
|
|
8
|
+
FileSessionRepository,
|
|
8
9
|
ProviderConfig,
|
|
9
10
|
getProviderConfig,
|
|
10
|
-
|
|
11
|
+
loadConfig,
|
|
11
12
|
} from '@december/agent'
|
|
13
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
14
|
+
import {
|
|
15
|
+
OpenAIProvider,
|
|
16
|
+
AnthropicProvider,
|
|
17
|
+
GeminiProvider,
|
|
18
|
+
OpenRouterProvider,
|
|
19
|
+
} from '@december/providers'
|
|
12
20
|
import {
|
|
13
21
|
BashTool,
|
|
14
22
|
ReadFileTool,
|
|
15
23
|
WriteFileTool,
|
|
16
24
|
LsTool,
|
|
17
25
|
EditFileTool,
|
|
26
|
+
EditDiffTool,
|
|
18
27
|
FindFilesTool,
|
|
19
28
|
GrepSearchTool,
|
|
20
29
|
SubagentTool,
|
|
21
30
|
} from '@december/tools'
|
|
22
31
|
import { localOperations } from './local-operations'
|
|
32
|
+
import { loginViaBrowser, loginViaDeviceCode } from './auth'
|
|
23
33
|
|
|
24
34
|
async function main() {
|
|
25
35
|
let providerConfig = await getProviderConfig()
|
|
26
36
|
|
|
27
37
|
// If not authenticated, we pass a dummy provider so the Agent can boot.
|
|
28
38
|
// The TUI will intercept prompts and force them to /login
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
let llm: any
|
|
40
|
+
if (providerConfig) {
|
|
41
|
+
switch (providerConfig.provider) {
|
|
42
|
+
case 'openai':
|
|
43
|
+
llm = new OpenAIProvider(undefined, providerConfig.apiKey)
|
|
44
|
+
break
|
|
45
|
+
case 'anthropic':
|
|
46
|
+
llm = new AnthropicProvider(providerConfig.apiKey)
|
|
47
|
+
break
|
|
48
|
+
case 'google':
|
|
49
|
+
case 'gemini':
|
|
50
|
+
llm = new GeminiProvider(providerConfig.apiKey)
|
|
51
|
+
break
|
|
52
|
+
case 'openrouter':
|
|
53
|
+
llm = new OpenRouterProvider(providerConfig.apiKey)
|
|
54
|
+
break
|
|
55
|
+
case 'deepseek':
|
|
56
|
+
llm = new OpenAIProvider('https://api.deepseek.com', providerConfig.apiKey)
|
|
57
|
+
break
|
|
58
|
+
case 'groq':
|
|
59
|
+
llm = new OpenAIProvider('https://api.groq.com/openai/v1', providerConfig.apiKey)
|
|
60
|
+
break
|
|
61
|
+
case 'huggingface':
|
|
62
|
+
llm = new OpenAIProvider(
|
|
63
|
+
'https://api-inference.huggingface.co/v1',
|
|
64
|
+
providerConfig.apiKey
|
|
65
|
+
)
|
|
66
|
+
break
|
|
67
|
+
case 'kimi':
|
|
68
|
+
case 'moonshoot':
|
|
69
|
+
llm = new OpenAIProvider('https://api.moonshot.cn/v1', providerConfig.apiKey)
|
|
70
|
+
break
|
|
71
|
+
case 'mistral':
|
|
72
|
+
llm = new OpenAIProvider('https://api.mistral.ai/v1', providerConfig.apiKey)
|
|
73
|
+
break
|
|
74
|
+
case 'xai':
|
|
75
|
+
llm = new OpenAIProvider('https://api.x.ai/v1', providerConfig.apiKey)
|
|
76
|
+
break
|
|
77
|
+
case 'zai':
|
|
78
|
+
llm = new OpenAIProvider('https://api.zai.ai/v1', providerConfig.apiKey)
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
llm = new OpenAIProvider(undefined, 'dummy-key')
|
|
83
|
+
}
|
|
84
|
+
|
|
32
85
|
const isAuthenticated = !!providerConfig
|
|
33
86
|
|
|
34
|
-
const
|
|
35
|
-
|
|
87
|
+
const harness = new AgentHarness({
|
|
88
|
+
baseSystemPrompt:
|
|
36
89
|
'You are December, an autonomous software engineer. You have access to tools. When executing code, please use JSON schemas for tool inputs.',
|
|
37
90
|
llm: llm,
|
|
38
91
|
tools: [
|
|
@@ -41,16 +94,41 @@ async function main() {
|
|
|
41
94
|
WriteFileTool,
|
|
42
95
|
LsTool,
|
|
43
96
|
EditFileTool,
|
|
97
|
+
EditDiffTool,
|
|
44
98
|
FindFilesTool,
|
|
45
99
|
GrepSearchTool,
|
|
46
100
|
SubagentTool,
|
|
47
101
|
],
|
|
48
102
|
operations: localOperations,
|
|
103
|
+
modelOptions: providerConfig?.model ? { model: providerConfig.model } : undefined,
|
|
104
|
+
sessionRepository: new FileSessionRepository(),
|
|
105
|
+
workspaceDir: process.cwd(),
|
|
106
|
+
hooks: {
|
|
107
|
+
beforeToolCall: async (toolCall) => {
|
|
108
|
+
// Future integration: Hook into the TUI to request user approval for destructive bash commands
|
|
109
|
+
},
|
|
110
|
+
},
|
|
49
111
|
})
|
|
50
112
|
|
|
113
|
+
const agent = harness.getAgent()
|
|
114
|
+
|
|
51
115
|
await agent.loadContext()
|
|
52
116
|
|
|
53
|
-
|
|
117
|
+
const config = await loadConfig()
|
|
118
|
+
const userEmail = config.decemberToken ? config.email : undefined
|
|
119
|
+
|
|
120
|
+
render(
|
|
121
|
+
React.createElement(App, {
|
|
122
|
+
agent,
|
|
123
|
+
isAuthenticated,
|
|
124
|
+
cliVersion: pkg.version,
|
|
125
|
+
userEmail,
|
|
126
|
+
onLogin: loginViaBrowser,
|
|
127
|
+
onLoginHeadless: (onCode: (code: string, uri: string) => void) =>
|
|
128
|
+
loginViaDeviceCode('http://localhost:4000', onCode), // Replace with prod URL later
|
|
129
|
+
}),
|
|
130
|
+
{ exitOnCtrlC: false }
|
|
131
|
+
)
|
|
54
132
|
}
|
|
55
133
|
|
|
56
134
|
main().catch(console.error)
|