ultrahope 0.0.1
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 +62 -0
- package/dist/index.js +225 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ultrahope
|
|
2
|
+
|
|
3
|
+
LLM-powered development workflow assistant CLI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ultrahope
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Login
|
|
14
|
+
|
|
15
|
+
Authenticate with your ultrahope account using device flow:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ultrahope login
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This will display a URL and code. Open the URL in your browser, sign in, and enter the code to authorize the CLI.
|
|
22
|
+
|
|
23
|
+
### Translate
|
|
24
|
+
|
|
25
|
+
Translate input to various formats. Pipe content to the command:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Generate a commit message from git diff
|
|
29
|
+
git diff --staged | ultrahope translate --target vcs-commit-message
|
|
30
|
+
|
|
31
|
+
# Generate PR title and body from diff
|
|
32
|
+
git diff main | ultrahope translate --target pr-title-body
|
|
33
|
+
|
|
34
|
+
# Analyze PR intent
|
|
35
|
+
git diff main | ultrahope translate --target pr-intent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Targets
|
|
39
|
+
|
|
40
|
+
- `vcs-commit-message` - Generate a commit message
|
|
41
|
+
- `pr-title-body` - Generate PR title and body
|
|
42
|
+
- `pr-intent` - Analyze the intent of changes
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
### Environment Variables
|
|
47
|
+
|
|
48
|
+
- `ULTRAHOPE_API_URL` - API endpoint (default: `https://ultrahope.dev`)
|
|
49
|
+
|
|
50
|
+
### Credentials
|
|
51
|
+
|
|
52
|
+
Credentials are stored in `~/.config/ultrahope/credentials.json`.
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Build
|
|
58
|
+
pnpm run build
|
|
59
|
+
|
|
60
|
+
# Link for local testing
|
|
61
|
+
pnpm link --global
|
|
62
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/lib/api-client.ts
|
|
2
|
+
var API_BASE_URL = process.env.ULTRAHOPE_API_URL ?? "https://ultrahope.dev";
|
|
3
|
+
var InsufficientBalanceError = class extends Error {
|
|
4
|
+
constructor(balance) {
|
|
5
|
+
super("Token balance exhausted");
|
|
6
|
+
this.balance = balance;
|
|
7
|
+
this.name = "InsufficientBalanceError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function createApiClient(token) {
|
|
11
|
+
const headers = {
|
|
12
|
+
"Content-Type": "application/json"
|
|
13
|
+
};
|
|
14
|
+
if (token) {
|
|
15
|
+
headers.Authorization = `Bearer ${token}`;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
async translate(req) {
|
|
19
|
+
const res = await fetch(`${API_BASE_URL}/api/v1/translate`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers,
|
|
22
|
+
body: JSON.stringify(req)
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
if (res.status === 402) {
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
throw new InsufficientBalanceError(data.balance ?? 0);
|
|
28
|
+
}
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
},
|
|
34
|
+
async requestDeviceCode() {
|
|
35
|
+
const res = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body: JSON.stringify({ client_id: "ultrahope-cli" })
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
43
|
+
}
|
|
44
|
+
return res.json();
|
|
45
|
+
},
|
|
46
|
+
async pollDeviceToken(deviceCode) {
|
|
47
|
+
const res = await fetch(`${API_BASE_URL}/api/auth/device/token`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers,
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
52
|
+
device_code: deviceCode,
|
|
53
|
+
client_id: "ultrahope-cli"
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok && res.status !== 400) {
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
59
|
+
}
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/lib/auth.ts
|
|
66
|
+
import * as fs from "fs";
|
|
67
|
+
import * as os from "os";
|
|
68
|
+
import * as path from "path";
|
|
69
|
+
function getCredentialsPath() {
|
|
70
|
+
const configDir = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
71
|
+
return path.join(configDir, "ultrahope", "credentials.json");
|
|
72
|
+
}
|
|
73
|
+
async function getToken() {
|
|
74
|
+
const credPath = getCredentialsPath();
|
|
75
|
+
try {
|
|
76
|
+
const content = await fs.promises.readFile(credPath, "utf-8");
|
|
77
|
+
const creds = JSON.parse(content);
|
|
78
|
+
return creds.access_token ?? null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function saveToken(token) {
|
|
84
|
+
const credPath = getCredentialsPath();
|
|
85
|
+
const dir = path.dirname(credPath);
|
|
86
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
87
|
+
await fs.promises.writeFile(
|
|
88
|
+
credPath,
|
|
89
|
+
JSON.stringify({ access_token: token }, null, 2),
|
|
90
|
+
{ mode: 384 }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/commands/login.ts
|
|
95
|
+
async function login(_args) {
|
|
96
|
+
const api = createApiClient();
|
|
97
|
+
console.log("Requesting device code...");
|
|
98
|
+
const deviceCode = await api.requestDeviceCode();
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(`Open this URL in your browser: ${deviceCode.verification_uri}`);
|
|
101
|
+
console.log(`Enter code: ${deviceCode.user_code}`);
|
|
102
|
+
console.log();
|
|
103
|
+
console.log("Waiting for authorization...");
|
|
104
|
+
const token = await pollForToken(
|
|
105
|
+
api,
|
|
106
|
+
deviceCode.device_code,
|
|
107
|
+
deviceCode.interval,
|
|
108
|
+
deviceCode.expires_in
|
|
109
|
+
);
|
|
110
|
+
await saveToken(token);
|
|
111
|
+
console.log("Successfully authenticated!");
|
|
112
|
+
}
|
|
113
|
+
async function pollForToken(api, deviceCode, interval, expiresIn) {
|
|
114
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
await sleep(interval * 1e3);
|
|
117
|
+
const result = await api.pollDeviceToken(deviceCode);
|
|
118
|
+
if (result.access_token) {
|
|
119
|
+
return result.access_token;
|
|
120
|
+
}
|
|
121
|
+
if (result.error && result.error !== "authorization_pending") {
|
|
122
|
+
throw new Error(`Authentication failed: ${result.error}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error("Authentication timed out");
|
|
126
|
+
}
|
|
127
|
+
function sleep(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/lib/stdin.ts
|
|
132
|
+
async function stdin() {
|
|
133
|
+
const chunks = [];
|
|
134
|
+
for await (const chunk of process.stdin) {
|
|
135
|
+
chunks.push(chunk);
|
|
136
|
+
}
|
|
137
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/commands/translate.ts
|
|
141
|
+
var VALID_TARGETS = [
|
|
142
|
+
"vcs-commit-message",
|
|
143
|
+
"pr-title-body",
|
|
144
|
+
"pr-intent"
|
|
145
|
+
];
|
|
146
|
+
async function translate(args2) {
|
|
147
|
+
const target = parseTarget(args2);
|
|
148
|
+
const input = await stdin();
|
|
149
|
+
if (!input.trim()) {
|
|
150
|
+
console.error(
|
|
151
|
+
"Error: No input provided. Pipe content to ultrahope translate."
|
|
152
|
+
);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const token = await getToken();
|
|
156
|
+
if (!token) {
|
|
157
|
+
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const api = createApiClient(token);
|
|
161
|
+
try {
|
|
162
|
+
const result = await api.translate({ input, target });
|
|
163
|
+
console.log(result.output);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof InsufficientBalanceError) {
|
|
166
|
+
console.error(
|
|
167
|
+
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
168
|
+
);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function parseTarget(args2) {
|
|
175
|
+
const idx = args2.findIndex((a) => a === "--target" || a === "-t");
|
|
176
|
+
if (idx === -1 || !args2[idx + 1]) {
|
|
177
|
+
console.error("Error: Missing --target option");
|
|
178
|
+
console.error(
|
|
179
|
+
"Usage: ultrahope translate --target <vcs-commit-message|pr-title-body|pr-intent>"
|
|
180
|
+
);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const value = args2[idx + 1];
|
|
184
|
+
if (!VALID_TARGETS.includes(value)) {
|
|
185
|
+
console.error(`Error: Invalid target "${value}"`);
|
|
186
|
+
console.error(`Valid targets: ${VALID_TARGETS.join(", ")}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/index.ts
|
|
193
|
+
var [command, ...args] = process.argv.slice(2);
|
|
194
|
+
async function main() {
|
|
195
|
+
switch (command) {
|
|
196
|
+
case "translate":
|
|
197
|
+
await translate(args);
|
|
198
|
+
break;
|
|
199
|
+
case "login":
|
|
200
|
+
await login(args);
|
|
201
|
+
break;
|
|
202
|
+
case "--help":
|
|
203
|
+
case "-h":
|
|
204
|
+
case void 0:
|
|
205
|
+
printHelp();
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
console.error(`Unknown command: ${command}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function printHelp() {
|
|
213
|
+
console.log(`Usage: ultrahope <command>
|
|
214
|
+
|
|
215
|
+
Commands:
|
|
216
|
+
translate Translate input to various formats
|
|
217
|
+
login Authenticate with device flow
|
|
218
|
+
|
|
219
|
+
Options:
|
|
220
|
+
--help, -h Show this help message`);
|
|
221
|
+
}
|
|
222
|
+
main().catch((err) => {
|
|
223
|
+
console.error(err);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ultrahope",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "LLM-powered development workflow assistant",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/toyamarinyon/ultrahope.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"cli",
|
|
13
|
+
"llm",
|
|
14
|
+
"ai",
|
|
15
|
+
"git",
|
|
16
|
+
"commit-message",
|
|
17
|
+
"pull-request"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"ultrahope": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "pnpm run build"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.15.29",
|
|
32
|
+
"tsup": "8.5.0",
|
|
33
|
+
"typescript": "^5.8.3"
|
|
34
|
+
}
|
|
35
|
+
}
|