@trydecember/cli 0.2.3 → 0.2.5
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 +15 -0
- package/dist/december.js +106540 -91021
- package/package.json +8 -1
- package/src/{auth.ts → auth/browser.ts} +1 -71
- package/src/auth/device.ts +70 -0
- package/src/auth/index.ts +2 -0
- package/src/index.ts +26 -18
- package/src/local-operations.ts +11 -2
package/package.json
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
"module": "src/index.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.5",
|
|
7
|
+
"description": "Turn ideas into reality.",
|
|
8
|
+
"author": "Chaitanya Sonawane",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"december",
|
|
12
|
+
"terminal agent"
|
|
13
|
+
],
|
|
7
14
|
"engines": {
|
|
8
15
|
"node": ">=18.0.0"
|
|
9
16
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
+
|
|
2
3
|
import open from 'open'
|
|
3
4
|
|
|
4
5
|
export async function loginViaBrowser(
|
|
@@ -131,74 +132,3 @@ export async function loginViaBrowser(
|
|
|
131
132
|
)
|
|
132
133
|
})
|
|
133
134
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export async function loginViaDeviceCode(
|
|
2
|
+
baseUrl: string = 'http://localhost:4000',
|
|
3
|
+
onCodeGenerated: (userCode: string, verificationUri: string) => void
|
|
4
|
+
): Promise<{ token: string; email: string | null }> {
|
|
5
|
+
return new Promise(async (resolve, reject) => {
|
|
6
|
+
try {
|
|
7
|
+
// 1. Generate Code
|
|
8
|
+
const genRes = await fetch(`${baseUrl}/api/v1/auth/device/code`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
if (!genRes.ok) {
|
|
14
|
+
throw new Error('Failed to generate device code')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const genData = await genRes.json()
|
|
18
|
+
if (!genData.success) {
|
|
19
|
+
throw new Error(genData.message || 'Failed to generate device code')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { deviceCode, userCode, verificationUri, expiresIn, interval } = genData.data
|
|
23
|
+
|
|
24
|
+
onCodeGenerated(userCode, verificationUri)
|
|
25
|
+
|
|
26
|
+
// 2. Poll for token
|
|
27
|
+
const startTime = Date.now()
|
|
28
|
+
const pollInterval = interval * 1000 || 5000
|
|
29
|
+
|
|
30
|
+
const poll = async () => {
|
|
31
|
+
if (Date.now() - startTime > expiresIn * 1000) {
|
|
32
|
+
reject(new Error('Device code expired'))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const tokenRes = await fetch(`${baseUrl}/api/v1/auth/device/token`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ deviceCode }),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const tokenData = await tokenRes.json()
|
|
44
|
+
|
|
45
|
+
if (tokenRes.ok && tokenData.success) {
|
|
46
|
+
resolve({
|
|
47
|
+
token: tokenData.data.token,
|
|
48
|
+
email: tokenData.data.email,
|
|
49
|
+
})
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If not ok, check if it's authorization_pending
|
|
54
|
+
if (tokenData.message === 'authorization_pending') {
|
|
55
|
+
setTimeout(poll, pollInterval)
|
|
56
|
+
} else {
|
|
57
|
+
reject(new Error(tokenData.message || 'Polling failed'))
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Network errors during polling, just keep trying
|
|
61
|
+
setTimeout(poll, pollInterval)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setTimeout(poll, pollInterval)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
reject(err)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { App } from '@december/tui'
|
|
5
|
-
import {
|
|
6
|
-
Agent,
|
|
7
|
-
AgentHarness,
|
|
8
|
-
FileSessionRepository,
|
|
9
|
-
ProviderConfig,
|
|
10
|
-
getProviderConfig,
|
|
11
|
-
loadConfig,
|
|
12
|
-
} from '@december/agent'
|
|
13
|
-
import pkg from '../package.json' with { type: 'json' }
|
|
2
|
+
import { AgentHarness, FileSessionRepository, getProviderConfig, loadConfig } from '@december/agent'
|
|
14
3
|
import {
|
|
15
4
|
OpenAIProvider,
|
|
16
5
|
AnthropicProvider,
|
|
@@ -28,11 +17,25 @@ import {
|
|
|
28
17
|
GrepSearchTool,
|
|
29
18
|
SubagentTool,
|
|
30
19
|
} from '@december/tools'
|
|
31
|
-
import {
|
|
20
|
+
import { App } from '@december/tui'
|
|
21
|
+
import { render } from 'ink'
|
|
22
|
+
import React from 'react'
|
|
23
|
+
|
|
24
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
25
|
+
|
|
32
26
|
import { loginViaBrowser, loginViaDeviceCode } from './auth'
|
|
27
|
+
import { localOperations } from './local-operations'
|
|
28
|
+
|
|
29
|
+
const originalConsoleError = console.error
|
|
33
30
|
|
|
34
31
|
async function main() {
|
|
35
|
-
|
|
32
|
+
// Suppress noisy SDK console logs that corrupt the Ink TUI layout
|
|
33
|
+
console.warn = () => {}
|
|
34
|
+
console.error = () => {}
|
|
35
|
+
console.log = () => {}
|
|
36
|
+
console.info = () => {}
|
|
37
|
+
|
|
38
|
+
const providerConfig = await getProviderConfig()
|
|
36
39
|
|
|
37
40
|
// If not authenticated, we pass a dummy provider so the Agent can boot.
|
|
38
41
|
// The TUI will intercept prompts and force them to /login
|
|
@@ -84,9 +87,12 @@ async function main() {
|
|
|
84
87
|
|
|
85
88
|
const isAuthenticated = !!providerConfig
|
|
86
89
|
|
|
90
|
+
const sessionRepository = new FileSessionRepository()
|
|
91
|
+
const sessionId = `session-${Date.now()}`
|
|
92
|
+
|
|
87
93
|
const harness = new AgentHarness({
|
|
88
94
|
baseSystemPrompt:
|
|
89
|
-
'You are December, an autonomous software engineer. You have access to tools. When executing code, please use JSON schemas for tool inputs.',
|
|
95
|
+
'You are December, an autonomous software engineer. You have access to tools. When executing code, please use JSON schemas for tool inputs. Before using a tool, you MUST enclose your thought process inside <thought>...</thought> tags. At the end of your work, provide a summary of what you did, highlighting important keywords.',
|
|
90
96
|
llm: llm,
|
|
91
97
|
tools: [
|
|
92
98
|
BashTool,
|
|
@@ -100,8 +106,9 @@ async function main() {
|
|
|
100
106
|
SubagentTool,
|
|
101
107
|
],
|
|
102
108
|
operations: localOperations,
|
|
103
|
-
modelOptions:
|
|
104
|
-
sessionRepository
|
|
109
|
+
modelOptions: { model: providerConfig?.model || 'gemini-3.5-flash' },
|
|
110
|
+
sessionRepository,
|
|
111
|
+
sessionId,
|
|
105
112
|
workspaceDir: process.cwd(),
|
|
106
113
|
hooks: {
|
|
107
114
|
beforeToolCall: async (toolCall) => {
|
|
@@ -123,6 +130,7 @@ async function main() {
|
|
|
123
130
|
isAuthenticated,
|
|
124
131
|
cliVersion: pkg.version,
|
|
125
132
|
userEmail,
|
|
133
|
+
sessionRepository,
|
|
126
134
|
onLogin: loginViaBrowser,
|
|
127
135
|
onLoginHeadless: (onCode: (code: string, uri: string) => void) =>
|
|
128
136
|
loginViaDeviceCode('http://localhost:4000', onCode), // Replace with prod URL later
|
|
@@ -131,4 +139,4 @@ async function main() {
|
|
|
131
139
|
)
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
main().catch(
|
|
142
|
+
main().catch(originalConsoleError)
|
package/src/local-operations.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AgentOperations } from '@december/agent'
|
|
2
1
|
import { spawn, exec } from 'node:child_process'
|
|
3
2
|
import fs from 'node:fs/promises'
|
|
4
3
|
import path from 'node:path'
|
|
5
|
-
import fg from 'fast-glob'
|
|
6
4
|
import { promisify } from 'node:util'
|
|
7
5
|
|
|
6
|
+
import { AgentOperations, taskManager } from '@december/agent'
|
|
7
|
+
import fg from 'fast-glob'
|
|
8
|
+
|
|
8
9
|
const execAsync = promisify(exec)
|
|
9
10
|
|
|
10
11
|
export const localOperations: AgentOperations = {
|
|
@@ -20,29 +21,37 @@ export const localOperations: AgentOperations = {
|
|
|
20
21
|
stdio: 'pipe',
|
|
21
22
|
})
|
|
22
23
|
|
|
24
|
+
const task = taskManager.addTask(command, child)
|
|
25
|
+
|
|
23
26
|
child.stdout?.on('data', (data) => {
|
|
24
27
|
const chunk = data.toString()
|
|
25
28
|
stdout += chunk
|
|
29
|
+
taskManager.appendOutput(task.id, chunk)
|
|
26
30
|
onData(chunk)
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
child.stderr?.on('data', (data) => {
|
|
30
34
|
const chunk = data.toString()
|
|
31
35
|
stderr += chunk
|
|
36
|
+
taskManager.appendOutput(task.id, chunk)
|
|
32
37
|
onData(chunk)
|
|
33
38
|
})
|
|
34
39
|
|
|
35
40
|
let isFinished = false
|
|
36
41
|
|
|
37
42
|
child.on('close', (code) => {
|
|
43
|
+
taskManager.markCompleted(task.id, code)
|
|
38
44
|
if (isFinished) return
|
|
39
45
|
isFinished = true
|
|
46
|
+
taskManager.removeTask(task.id)
|
|
40
47
|
resolve({ exitCode: code, output: stdout + '\n' + stderr })
|
|
41
48
|
})
|
|
42
49
|
|
|
43
50
|
child.on('error', (err) => {
|
|
51
|
+
taskManager.markCompleted(task.id, 1)
|
|
44
52
|
if (isFinished) return
|
|
45
53
|
isFinished = true
|
|
54
|
+
taskManager.removeTask(task.id)
|
|
46
55
|
resolve({ exitCode: 1, output: `Failed to start command: ${err.message}` })
|
|
47
56
|
})
|
|
48
57
|
|