@trydecember/cli 0.2.2 → 0.2.4

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