getownit 0.1.0 → 0.1.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/dist/api.js +2 -1
- package/dist/commands/hook.js +63 -48
- package/dist/commands/init.js +28 -19
- package/dist/commands/interview.js +2 -2
- package/dist/commands/key.js +43 -32
- package/dist/commands/login.js +51 -37
- package/dist/config.js +0 -36
- package/dist/debrief.js +81 -55
- package/dist/index.js +53 -48
- package/dist/providers.js +36 -0
- package/dist/ui.js +4 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
package/dist/commands/hook.js
CHANGED
|
@@ -6,7 +6,7 @@ import { RESUME_HINT, runDebrief } from '../debrief.js';
|
|
|
6
6
|
import { buildDiff } from '../diff.js';
|
|
7
7
|
import { githubRepo } from '../git.js';
|
|
8
8
|
import { computePush, parseRefs } from '../pushRange.js';
|
|
9
|
-
import { createPrompter, paint, sentence } from '../ui.js';
|
|
9
|
+
import { createPrompter, errorMessage, paint, plural, sentence } from '../ui.js';
|
|
10
10
|
const PROMPT_TIMEOUT_MS = 120_000;
|
|
11
11
|
const QUICK_TIMEOUT_MS = 8_000;
|
|
12
12
|
// Coding agents and CI run git without a person at the keyboard.
|
|
@@ -52,6 +52,60 @@ async function record(api, repo, push, status) {
|
|
|
52
52
|
const res = await api.request('POST', '/api/pushes', { repo, base_sha: push.base, head_sha: push.head, commits: push.commits.map((c) => ({ sha: c.sha })), status }, { timeoutMs: QUICK_TIMEOUT_MS });
|
|
53
53
|
return res.status;
|
|
54
54
|
}
|
|
55
|
+
async function recordPending(api, repo, push, out) {
|
|
56
|
+
await record(api, repo, push, 'pending');
|
|
57
|
+
out.write(`Ownit: push recorded as pending. ${RESUME_HINT}\n`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What the server already recorded for these commits, so a verified push is not
|
|
62
|
+
* asked again. Only an unreachable server is an error; anything else means unknown.
|
|
63
|
+
*/
|
|
64
|
+
async function knownStatus(api, repo, push) {
|
|
65
|
+
const known = await api
|
|
66
|
+
.request('POST', '/api/pushes/lookup', { repo, head_sha: push.head, shas: push.commits.map((x) => x.sha) }, { timeoutMs: QUICK_TIMEOUT_MS })
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
if (error instanceof ApiError && error.code === 'network')
|
|
69
|
+
throw error;
|
|
70
|
+
return { status: null };
|
|
71
|
+
});
|
|
72
|
+
return known.status;
|
|
73
|
+
}
|
|
74
|
+
/** Asks whether to debrief now, then debriefs, records a skip, or leaves the push pending. */
|
|
75
|
+
async function askAndDebrief(api, terminal, repo, push, out) {
|
|
76
|
+
const c = paint(out);
|
|
77
|
+
const prompter = createPrompter(terminal, out);
|
|
78
|
+
try {
|
|
79
|
+
const n = push.commits.length;
|
|
80
|
+
const answer = await prompter.ask(`${c.bold('Ownit')} ${c.dim(`(${n} ${plural(n, 'commit')} to ${repo})`)} Debrief now? [Y/n/s=skip] `, PROMPT_TIMEOUT_MS);
|
|
81
|
+
const choice = interpretChoice(answer);
|
|
82
|
+
if (choice === 'skip') {
|
|
83
|
+
await record(api, repo, push, 'skipped');
|
|
84
|
+
out.write(c.dim('Ownit: skipped. Push continues.\n'));
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (choice === 'later')
|
|
88
|
+
return await recordPending(api, repo, push, out);
|
|
89
|
+
await runDebrief(api, prompter, {
|
|
90
|
+
repo,
|
|
91
|
+
baseSha: push.base,
|
|
92
|
+
headSha: push.head,
|
|
93
|
+
commits: push.commits,
|
|
94
|
+
diff: buildDiff(push.ranges),
|
|
95
|
+
});
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
prompter.close();
|
|
100
|
+
if (terminal !== process.stdin)
|
|
101
|
+
terminal.destroy();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function continueReason(error) {
|
|
105
|
+
return error instanceof ApiError && error.code === 'network'
|
|
106
|
+
? `${sentence(error.message)} The push continues and will show as pending.`
|
|
107
|
+
: `${sentence(errorMessage(error))} The push continues.`;
|
|
108
|
+
}
|
|
55
109
|
/** `ownit hook pre-push <remote> <url>`. Always exits 0: the debrief never blocks a push. */
|
|
56
110
|
export async function hookCommand(argv, env = process.env) {
|
|
57
111
|
const [event, remote = '', url = ''] = argv;
|
|
@@ -73,58 +127,19 @@ export async function hookCommand(argv, env = process.env) {
|
|
|
73
127
|
return 0;
|
|
74
128
|
}
|
|
75
129
|
const api = createApi(config, env);
|
|
76
|
-
const known = await api
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
throw error;
|
|
81
|
-
return { status: null };
|
|
82
|
-
});
|
|
83
|
-
if (known.status === 'verified' || known.status === 'skipped') {
|
|
84
|
-
out.write(c.dim(`Ownit: this push is already ${known.status}.\n`));
|
|
130
|
+
const known = await knownStatus(api, repo, push);
|
|
131
|
+
// Skipped pushes are asked again: they stay unverified until debriefed.
|
|
132
|
+
if (known === 'verified') {
|
|
133
|
+
out.write(c.dim(`Ownit: this push is already ${known}.\n`));
|
|
85
134
|
return 0;
|
|
86
135
|
}
|
|
87
136
|
const terminal = openTerminal(env);
|
|
88
|
-
if (!terminal)
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
const prompter = createPrompter(terminal, out);
|
|
94
|
-
try {
|
|
95
|
-
const n = push.commits.length;
|
|
96
|
-
const answer = await prompter.ask(`${c.bold('Ownit')} ${c.dim(`(${n} ${n === 1 ? 'commit' : 'commits'} to ${repo})`)} Debrief now? [Y/n/s=skip] `, PROMPT_TIMEOUT_MS);
|
|
97
|
-
const choice = interpretChoice(answer);
|
|
98
|
-
if (choice === 'skip') {
|
|
99
|
-
await record(api, repo, push, 'skipped');
|
|
100
|
-
out.write(c.dim('Ownit: skipped. Push continues.\n'));
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
|
-
if (choice === 'later') {
|
|
104
|
-
await record(api, repo, push, 'pending');
|
|
105
|
-
out.write(`Ownit: push recorded as pending. ${RESUME_HINT}\n`);
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
await runDebrief(api, prompter, {
|
|
109
|
-
repo,
|
|
110
|
-
baseSha: push.base,
|
|
111
|
-
headSha: push.head,
|
|
112
|
-
commits: push.commits,
|
|
113
|
-
diff: buildDiff(push.ranges),
|
|
114
|
-
});
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
prompter.close();
|
|
119
|
-
if (terminal !== process.stdin)
|
|
120
|
-
terminal.destroy();
|
|
121
|
-
}
|
|
137
|
+
if (!terminal)
|
|
138
|
+
return await recordPending(api, repo, push, out);
|
|
139
|
+
return await askAndDebrief(api, terminal, repo, push, out);
|
|
122
140
|
}
|
|
123
141
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
? `${sentence(error.message)} The push continues and will show as pending.`
|
|
126
|
-
: `${sentence(error instanceof Error ? error.message : String(error))} The push continues.`;
|
|
127
|
-
out.write(`${c.yellow('Ownit:')} ${reason} ${RESUME_HINT}\n`);
|
|
142
|
+
out.write(`${c.yellow('Ownit:')} ${continueReason(error)} ${RESUME_HINT}\n`);
|
|
128
143
|
return 0;
|
|
129
144
|
}
|
|
130
145
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -2,31 +2,26 @@ import { parseArgs } from 'node:util';
|
|
|
2
2
|
import { createApi } from '../api.js';
|
|
3
3
|
import { git, githubRepo } from '../git.js';
|
|
4
4
|
import { hookLocation, installHook, MANAGED_SNIPPET, uninstallHook } from '../hooks.js';
|
|
5
|
-
import { paint, sentence } from '../ui.js';
|
|
5
|
+
import { errorMessage, paint, sentence } from '../ui.js';
|
|
6
6
|
import { badgeMarkdown } from './badge.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const out = process.stderr;
|
|
7
|
+
/** Installs the pre-push hook where git looks for it, or explains how to add it to a hook manager. */
|
|
8
|
+
function installHookHere(out) {
|
|
10
9
|
const c = paint(out);
|
|
11
|
-
if (values.repo)
|
|
12
|
-
git(['config', 'ownit.repo', values.repo]);
|
|
13
|
-
const repo = githubRepo();
|
|
14
|
-
if (!repo) {
|
|
15
|
-
out.write('Ownit: no GitHub remote found. Add one, or run "ownit init --repo owner/name".\n');
|
|
16
|
-
return 1;
|
|
17
|
-
}
|
|
18
10
|
const location = hookLocation();
|
|
19
11
|
if (location.kind === 'manual') {
|
|
20
12
|
out.write(`Ownit: ${location.reason}, so add these lines to the end of .husky/pre-push (create the file if needed):\n\n`);
|
|
21
13
|
out.write(`${MANAGED_SNIPPET}\n\n`);
|
|
14
|
+
return;
|
|
22
15
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
const result = installHook(location.dir);
|
|
17
|
+
out.write(`${c.green('✓')} ${result.updated ? 'Updated' : 'Installed'} the pre-push hook in ${location.dir}`);
|
|
18
|
+
out.write(location.via === 'core.hooksPath' ? ' (from core.hooksPath).\n' : '.\n');
|
|
19
|
+
if (result.chained)
|
|
20
|
+
out.write(` Your existing hook is now pre-push.local and still runs first.\n`);
|
|
21
|
+
}
|
|
22
|
+
/** Registers the repo with the Ownit server and shows what to do next. */
|
|
23
|
+
async function registerRepo(repo, out) {
|
|
24
|
+
const c = paint(out);
|
|
30
25
|
try {
|
|
31
26
|
const api = createApi();
|
|
32
27
|
const registered = await api.request('POST', '/api/repos', { repo }, { timeoutMs: 15_000 });
|
|
@@ -42,12 +37,26 @@ export async function initCommand(argv) {
|
|
|
42
37
|
out.write(`\nAdd the badge to your README (run "ownit badge" to see this again):\n ${badgeMarkdown(origin, registered.repo)}\n`);
|
|
43
38
|
}
|
|
44
39
|
catch (error) {
|
|
45
|
-
out.write(`${c.yellow('Ownit:')} the hook is installed, but registering ${repo} failed: ${sentence(
|
|
40
|
+
out.write(`${c.yellow('Ownit:')} the hook is installed, but registering ${repo} failed: ${sentence(errorMessage(error))}\n`);
|
|
46
41
|
out.write('Run "ownit init" again once that is fixed.\n');
|
|
47
42
|
return 1;
|
|
48
43
|
}
|
|
49
44
|
return 0;
|
|
50
45
|
}
|
|
46
|
+
/** `ownit init [--repo owner/name]` */
|
|
47
|
+
export async function initCommand(argv) {
|
|
48
|
+
const { values } = parseArgs({ args: argv, options: { repo: { type: 'string' } } });
|
|
49
|
+
const out = process.stderr;
|
|
50
|
+
if (values.repo)
|
|
51
|
+
git(['config', 'ownit.repo', values.repo]);
|
|
52
|
+
const repo = githubRepo();
|
|
53
|
+
if (!repo) {
|
|
54
|
+
out.write('Ownit: no GitHub remote found. Add one, or run "ownit init --repo owner/name".\n');
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
installHookHere(out);
|
|
58
|
+
return registerRepo(repo, out);
|
|
59
|
+
}
|
|
51
60
|
export async function uninstallCommand() {
|
|
52
61
|
const out = process.stderr;
|
|
53
62
|
const location = hookLocation();
|
|
@@ -4,7 +4,7 @@ import { buildDiff } from '../diff.js';
|
|
|
4
4
|
import { runDebrief } from '../debrief.js';
|
|
5
5
|
import { githubRepo, listCommits, objectExists, resolveCommit } from '../git.js';
|
|
6
6
|
import { rangeForRecordedPush } from '../pushRange.js';
|
|
7
|
-
import { createPrompter, paint } from '../ui.js';
|
|
7
|
+
import { createPrompter, paint, plural } from '../ui.js';
|
|
8
8
|
export async function interviewCommand(argv) {
|
|
9
9
|
const { values } = parseArgs({
|
|
10
10
|
args: argv,
|
|
@@ -55,7 +55,7 @@ async function debriefOpenPushes(api, prompter, repo) {
|
|
|
55
55
|
out.write(`${c.green('✓')} Nothing to debrief for ${repo}.\n`);
|
|
56
56
|
return 0;
|
|
57
57
|
}
|
|
58
|
-
out.write(`${pushes.length}
|
|
58
|
+
out.write(`${pushes.length} ${plural(pushes.length, 'push', 'pushes')} waiting for a debrief in ${repo}.\n`);
|
|
59
59
|
let code = 0;
|
|
60
60
|
for (const push of pushes) {
|
|
61
61
|
const missing = [push.head_sha, ...push.commits].find((sha) => !objectExists(sha));
|
package/dist/commands/key.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readConfig, updateConfig } from '../config.js';
|
|
2
|
+
import { describeKey, isProvider, modelKey, PROVIDER_IDS, PROVIDERS, providerByPrefix } from '../providers.js';
|
|
2
3
|
import { readSecret } from '../secret.js';
|
|
3
4
|
import { createPrompter } from '../ui.js';
|
|
4
5
|
const CHOICES = PROVIDER_IDS.join('|');
|
|
@@ -30,39 +31,49 @@ async function chooseProvider(arg) {
|
|
|
30
31
|
out.write(`Ownit: "${answer}" is not one of the choices; nothing changed.\n`);
|
|
31
32
|
return picked;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
async function setKey(providerArg) {
|
|
34
35
|
const out = process.stderr;
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
out.write('Ownit: no key entered; nothing changed.\n');
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
const looksLike = providerByPrefix(key);
|
|
48
|
-
if (looksLike && looksLike !== provider) {
|
|
49
|
-
out.write(`Ownit: that key looks like it is for ${PROVIDERS[looksLike].name}. Run "ownit key set ${looksLike}" instead; nothing changed.\n`);
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
updateConfig({ provider, apiKey: key, nebiusKey: undefined });
|
|
53
|
-
out.write(`Saved your ${name} key to ~/.ownit/config.json. It is sent only with debrief requests and never stored by Ownit.\n`);
|
|
54
|
-
return 0;
|
|
36
|
+
const provider = await chooseProvider(providerArg);
|
|
37
|
+
if (!provider)
|
|
38
|
+
return 1;
|
|
39
|
+
const { name } = PROVIDERS[provider];
|
|
40
|
+
// Read from a prompt or stdin, never argv, so the key stays out of shell history.
|
|
41
|
+
const key = (await readSecret(`${name} API key (input hidden): `)).trim();
|
|
42
|
+
if (!key) {
|
|
43
|
+
out.write('Ownit: no key entered; nothing changed.\n');
|
|
44
|
+
return 1;
|
|
55
45
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
out.write(
|
|
59
|
-
return
|
|
46
|
+
const looksLike = providerByPrefix(key);
|
|
47
|
+
if (looksLike && looksLike !== provider) {
|
|
48
|
+
out.write(`Ownit: that key looks like it is for ${PROVIDERS[looksLike].name}. Run "ownit key set ${looksLike}" instead; nothing changed.\n`);
|
|
49
|
+
return 1;
|
|
60
50
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
updateConfig({ provider, apiKey: key, nebiusKey: undefined });
|
|
52
|
+
out.write(`Saved your ${name} key to ~/.ownit/config.json. It is sent only with debrief requests and never stored by Ownit.\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
function clearKey() {
|
|
56
|
+
updateConfig({ provider: undefined, apiKey: undefined, nebiusKey: undefined });
|
|
57
|
+
process.stderr.write('Removed your API key.\n');
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
function showKey() {
|
|
61
|
+
const model = modelKey(readConfig());
|
|
62
|
+
process.stderr.write(model ? `API key set (${describeKey(model)}).\n` : 'No API key set. Run "ownit key set" with a Nebius or NVIDIA key.\n');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
/** `ownit key set [provider] | clear | status` */
|
|
66
|
+
export async function keyCommand(argv) {
|
|
67
|
+
const [action = 'status', providerArg] = argv;
|
|
68
|
+
switch (action) {
|
|
69
|
+
case 'set':
|
|
70
|
+
return setKey(providerArg);
|
|
71
|
+
case 'clear':
|
|
72
|
+
return clearKey();
|
|
73
|
+
case 'status':
|
|
74
|
+
return showKey();
|
|
75
|
+
default:
|
|
76
|
+
process.stderr.write(`Usage: ownit key set [${CHOICES}] | clear | status\n`);
|
|
77
|
+
return 1;
|
|
65
78
|
}
|
|
66
|
-
out.write(`Usage: ownit key set [${CHOICES}] | clear | status\n`);
|
|
67
|
-
return 1;
|
|
68
79
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { ApiError, createApi } from '../api.js';
|
|
3
|
-
import {
|
|
3
|
+
import { readConfig, serverUrl, updateConfig } from '../config.js';
|
|
4
|
+
import { describeKey, modelKey } from '../providers.js';
|
|
4
5
|
import { readSecret } from '../secret.js';
|
|
5
6
|
import { paint } from '../ui.js';
|
|
6
7
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -24,33 +25,10 @@ async function githubOAuth(path, form) {
|
|
|
24
25
|
return (await response.json());
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* hit). The server then verifies the identity, drops the GitHub token, and
|
|
30
|
-
* issues an Ownit session token.
|
|
28
|
+
* Polls GitHub until the user authorizes the device code. Returns the GitHub
|
|
29
|
+
* token, or the reason the login ended without one.
|
|
31
30
|
*/
|
|
32
|
-
|
|
33
|
-
const { values } = parseArgs({ args: argv, options: { server: { type: 'string' }, 'with-token': { type: 'boolean' } } });
|
|
34
|
-
const out = process.stderr;
|
|
35
|
-
const c = paint(out);
|
|
36
|
-
if (values.server)
|
|
37
|
-
updateConfig({ server: values.server.replace(/\/+$/, '') });
|
|
38
|
-
if (values['with-token']) {
|
|
39
|
-
// For self-hosted or local servers: read a session token from stdin.
|
|
40
|
-
const token = (await readSecret('Ownit session token: ')).trim();
|
|
41
|
-
if (!/^ownit_[\w-]{20,}$/.test(token)) {
|
|
42
|
-
out.write('Ownit: that does not look like an Ownit session token.\n');
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
updateConfig({ token, login: undefined });
|
|
46
|
-
out.write(`${c.green('✓')} Saved the session token.\n`);
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
const api = createApi({ ...readConfig(), token: undefined });
|
|
50
|
-
const { github_client_id: clientId } = await api.request('GET', '/auth/config');
|
|
51
|
-
const start = await githubOAuth('/login/device/code', { client_id: clientId });
|
|
52
|
-
out.write(`\nOpen ${c.bold(start.verification_uri)} and enter the code ${c.bold(c.cyan(start.user_code))}\n`);
|
|
53
|
-
out.write(c.dim('Waiting for you to authorize Ownit on GitHub…\n'));
|
|
31
|
+
async function pollForAccessToken(clientId, start) {
|
|
54
32
|
let interval = Math.max(start.interval, 0) * 1000;
|
|
55
33
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
56
34
|
while (Date.now() < deadline) {
|
|
@@ -60,24 +38,60 @@ export async function loginCommand(argv) {
|
|
|
60
38
|
device_code: start.device_code,
|
|
61
39
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
62
40
|
});
|
|
63
|
-
if (reply.access_token)
|
|
64
|
-
|
|
65
|
-
const session = await api.request('POST', '/auth/github', { access_token: reply.access_token });
|
|
66
|
-
updateConfig({ token: session.token, login: session.login });
|
|
67
|
-
out.write(`${c.green('✓')} Logged in as ${c.bold(session.login)} on ${serverUrl(readConfig())}.\n`);
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
41
|
+
if (reply.access_token)
|
|
42
|
+
return { token: reply.access_token };
|
|
70
43
|
if (reply.error === 'authorization_pending')
|
|
71
44
|
continue;
|
|
72
45
|
if (reply.error === 'slow_down') {
|
|
73
46
|
interval = Math.max(interval + 5000, (reply.interval ?? 0) * 1000);
|
|
74
47
|
continue;
|
|
75
48
|
}
|
|
76
|
-
|
|
49
|
+
return { failure: `login ${reply.error === 'access_denied' ? 'was cancelled' : `failed (${reply.error ?? 'unknown error'})`}.` };
|
|
50
|
+
}
|
|
51
|
+
return { failure: 'the login code expired.' };
|
|
52
|
+
}
|
|
53
|
+
/** For self-hosted or local servers: reads a session token from stdin. */
|
|
54
|
+
async function loginWithToken() {
|
|
55
|
+
const out = process.stderr;
|
|
56
|
+
const token = (await readSecret('Ownit session token: ')).trim();
|
|
57
|
+
if (!/^ownit_[\w-]{20,}$/.test(token)) {
|
|
58
|
+
out.write('Ownit: that does not look like an Ownit session token.\n');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
updateConfig({ token, login: undefined });
|
|
62
|
+
out.write(`${paint(out).green('✓')} Saved the session token.\n`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The GitHub device flow runs here, from the user's own IP (GitHub rate-limits
|
|
67
|
+
* its login endpoints per IP, which shared server IPs hit). The server then
|
|
68
|
+
* verifies the identity, drops the GitHub token, and issues an Ownit session token.
|
|
69
|
+
*/
|
|
70
|
+
async function deviceFlowLogin() {
|
|
71
|
+
const out = process.stderr;
|
|
72
|
+
const c = paint(out);
|
|
73
|
+
const api = createApi({ ...readConfig(), token: undefined });
|
|
74
|
+
const { github_client_id: clientId } = await api.request('GET', '/auth/config');
|
|
75
|
+
const start = await githubOAuth('/login/device/code', { client_id: clientId });
|
|
76
|
+
out.write(`\nOpen ${c.bold(start.verification_uri)} and enter the code ${c.bold(c.cyan(start.user_code))}\n`);
|
|
77
|
+
out.write(c.dim('Waiting for you to authorize Ownit on GitHub…\n'));
|
|
78
|
+
const result = await pollForAccessToken(clientId, start);
|
|
79
|
+
if ('failure' in result) {
|
|
80
|
+
out.write(`Ownit: ${result.failure} Run "ownit login" to try again.\n`);
|
|
77
81
|
return 1;
|
|
78
82
|
}
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
// The GitHub token goes to the Ownit server once, to prove who you are, and is not kept anywhere.
|
|
84
|
+
const session = await api.request('POST', '/auth/github', { access_token: result.token });
|
|
85
|
+
updateConfig({ token: session.token, login: session.login });
|
|
86
|
+
out.write(`${c.green('✓')} Logged in as ${c.bold(session.login)} on ${serverUrl(readConfig())}.\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
/** `ownit login [--server URL] [--with-token]` */
|
|
90
|
+
export async function loginCommand(argv) {
|
|
91
|
+
const { values } = parseArgs({ args: argv, options: { server: { type: 'string' }, 'with-token': { type: 'boolean' } } });
|
|
92
|
+
if (values.server)
|
|
93
|
+
updateConfig({ server: values.server.replace(/\/+$/, '') });
|
|
94
|
+
return values['with-token'] ? loginWithToken() : deviceFlowLogin();
|
|
81
95
|
}
|
|
82
96
|
export async function logoutCommand() {
|
|
83
97
|
const config = readConfig();
|
package/dist/config.js
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
/**
|
|
5
|
-
* Model providers the worker accepts, in the order `ownit key set` lists them.
|
|
6
|
-
* Add a provider here and in the worker's PROVIDERS.
|
|
7
|
-
*/
|
|
8
|
-
export const PROVIDERS = {
|
|
9
|
-
nebius: { name: 'Nebius', envVar: 'NEBIUS_API_KEY', keyUrl: 'https://tokenfactory.nebius.com/', keyPrefix: undefined },
|
|
10
|
-
nvidia: { name: 'NVIDIA', envVar: 'NVIDIA_API_KEY', keyUrl: 'https://build.nvidia.com/', keyPrefix: 'nvapi-' },
|
|
11
|
-
};
|
|
12
|
-
export const PROVIDER_IDS = Object.keys(PROVIDERS);
|
|
13
|
-
export function isProvider(value) {
|
|
14
|
-
return Object.hasOwn(PROVIDERS, value);
|
|
15
|
-
}
|
|
16
4
|
// The hosted Ownit server. Self-hosters point the CLI elsewhere with `ownit login --server`.
|
|
17
5
|
export const DEFAULT_SERVER = 'https://ownit.ownit-worker.workers.dev';
|
|
18
6
|
export function configDir(env = process.env) {
|
|
@@ -53,30 +41,6 @@ export function updateConfig(patch, env = process.env) {
|
|
|
53
41
|
writeConfig(next, env);
|
|
54
42
|
return next;
|
|
55
43
|
}
|
|
56
|
-
/** The key to send with debriefs and its provider. A provider's env var wins over the saved key. */
|
|
57
|
-
export function modelKey(config, env = process.env) {
|
|
58
|
-
for (const provider of PROVIDER_IDS) {
|
|
59
|
-
const key = env[PROVIDERS[provider].envVar];
|
|
60
|
-
if (key)
|
|
61
|
-
return { provider, key };
|
|
62
|
-
}
|
|
63
|
-
if (config.apiKey)
|
|
64
|
-
return { provider: config.provider ?? 'nebius', key: config.apiKey };
|
|
65
|
-
if (config.nebiusKey)
|
|
66
|
-
return { provider: 'nebius', key: config.nebiusKey };
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
/** The provider whose keys have this key's prefix, if any. */
|
|
70
|
-
export function providerByPrefix(key) {
|
|
71
|
-
return PROVIDER_IDS.find((id) => {
|
|
72
|
-
const prefix = PROVIDERS[id].keyPrefix;
|
|
73
|
-
return prefix !== undefined && key.startsWith(prefix);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
/** e.g. "NVIDIA, …1a2b" */
|
|
77
|
-
export function describeKey({ provider, key }) {
|
|
78
|
-
return `${PROVIDERS[provider]?.name ?? provider}, …${key.slice(-4)}`;
|
|
79
|
-
}
|
|
80
44
|
export function serverUrl(config, env = process.env) {
|
|
81
45
|
return (env.OWNIT_SERVER || config.server || DEFAULT_SERVER).replace(/\/+$/, '');
|
|
82
46
|
}
|
package/dist/debrief.js
CHANGED
|
@@ -1,85 +1,111 @@
|
|
|
1
1
|
import { ApiError } from './api.js';
|
|
2
|
-
import { paint, sentence, spinner, wrap } from './ui.js';
|
|
2
|
+
import { errorMessage, paint, plural, sentence, spinner, wrap } from './ui.js';
|
|
3
3
|
const MAX_ANSWER_CHARS = 2000;
|
|
4
4
|
export const RESUME_HINT = 'Run "ownit interview" when you\'re ready.';
|
|
5
5
|
const KIND_LABEL = { why: 'why', how: 'how', next: "what's next" };
|
|
6
6
|
function isSkip(answer) {
|
|
7
7
|
return /^(s|skip|:skip)$/i.test(answer.trim());
|
|
8
8
|
}
|
|
9
|
+
/** Shows a spinner while `work` runs. */
|
|
10
|
+
async function withSpinner(out, text, work) {
|
|
11
|
+
const spin = spinner(out, text);
|
|
12
|
+
try {
|
|
13
|
+
return await work();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
spin.stop();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function printQuestion(out, question, total, followUp) {
|
|
20
|
+
const c = paint(out);
|
|
21
|
+
const label = `Question ${question.idx + 1}/${total}`;
|
|
22
|
+
out.write(`\n${c.bold(c.cyan(label))} ${c.dim(`(${KIND_LABEL[question.kind]}${followUp ? ', follow-up' : ''})`)}\n`);
|
|
23
|
+
out.write(`${wrap(question.text)}\n`);
|
|
24
|
+
}
|
|
25
|
+
/** The next non-blank answer, or null when the input ends. */
|
|
26
|
+
async function readAnswer(prompter) {
|
|
27
|
+
const c = paint(prompter.out);
|
|
28
|
+
let answer;
|
|
29
|
+
do
|
|
30
|
+
answer = await prompter.ask(c.bold('> '));
|
|
31
|
+
while (answer !== null && !answer.trim());
|
|
32
|
+
return answer;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Asks a question, and follow-ups after a miss, until an answer passes (null)
|
|
36
|
+
* or the debrief ends early (its outcome).
|
|
37
|
+
*/
|
|
38
|
+
async function askUntilPassed(debrief, first) {
|
|
39
|
+
const { api, prompter } = debrief;
|
|
40
|
+
const out = prompter.out;
|
|
41
|
+
const c = paint(out);
|
|
42
|
+
let question = first;
|
|
43
|
+
let followUp = false;
|
|
44
|
+
for (;;) {
|
|
45
|
+
printQuestion(out, question, debrief.total, followUp);
|
|
46
|
+
const answer = await readAnswer(prompter);
|
|
47
|
+
if (answer === null) {
|
|
48
|
+
out.write(`\n${c.yellow('Debrief paused.')} ${RESUME_HINT}\n`);
|
|
49
|
+
return { status: 'pending', reason: 'debrief paused' };
|
|
50
|
+
}
|
|
51
|
+
if (isSkip(answer))
|
|
52
|
+
return skip(api, out, debrief.session);
|
|
53
|
+
let result;
|
|
54
|
+
try {
|
|
55
|
+
result = await withSpinner(out, 'Checking your answer…', () => api.request('POST', '/api/interview/answer', {
|
|
56
|
+
session: debrief.session,
|
|
57
|
+
idx: question.idx,
|
|
58
|
+
answer: answer.slice(0, MAX_ANSWER_CHARS),
|
|
59
|
+
...debrief.payload,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return failure(out, error);
|
|
64
|
+
}
|
|
65
|
+
debrief.session = result.session;
|
|
66
|
+
if (result.pass) {
|
|
67
|
+
out.write(`${c.green('✓ Passed.')}\n${c.dim(wrap(result.feedback))}\n`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
out.write(`${c.yellow('Not quite yet.')}\n${wrap(result.feedback)}\n`);
|
|
71
|
+
if (!result.followup)
|
|
72
|
+
return { status: 'pending', reason: 'no follow-up question' };
|
|
73
|
+
out.write(c.dim("Here's another angle on the same topic:\n"));
|
|
74
|
+
question = result.followup;
|
|
75
|
+
followUp = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
9
78
|
/** Runs one debrief: asks each question, grades answers, and offers follow-ups until every question passes. */
|
|
10
79
|
export async function runDebrief(api, prompter, target) {
|
|
11
80
|
const out = prompter.out;
|
|
12
81
|
const c = paint(out);
|
|
13
|
-
const commitWord = target.commits.length
|
|
14
|
-
out.write(`\n${c.bold('Ownit debrief')} ${c.dim(`· ${target.repo} · ${target.commits.length} ${commitWord} · ${target.diff.changedLines} changed ${target.diff.changedLines
|
|
82
|
+
const commitWord = plural(target.commits.length, 'commit');
|
|
83
|
+
out.write(`\n${c.bold('Ownit debrief')} ${c.dim(`· ${target.repo} · ${target.commits.length} ${commitWord} · ${target.diff.changedLines} changed ${plural(target.diff.changedLines, 'line')}`)}\n`);
|
|
15
84
|
const payload = {
|
|
16
85
|
commits: target.commits,
|
|
17
86
|
diff: target.diff.diff,
|
|
18
87
|
diff_truncated: target.diff.truncated,
|
|
19
88
|
};
|
|
20
89
|
let start;
|
|
21
|
-
const spin = spinner(out, 'Nemotron is reading your push…');
|
|
22
90
|
try {
|
|
23
|
-
start = await api.request('POST', '/api/interview/start', {
|
|
91
|
+
start = await withSpinner(out, 'Nemotron is reading your push…', () => api.request('POST', '/api/interview/start', {
|
|
24
92
|
repo: target.repo,
|
|
25
93
|
base_sha: target.baseSha,
|
|
26
94
|
head_sha: target.headSha,
|
|
27
95
|
changed_lines: target.diff.changedLines,
|
|
28
96
|
push_id: target.pushId,
|
|
29
97
|
...payload,
|
|
30
|
-
});
|
|
98
|
+
}));
|
|
31
99
|
}
|
|
32
100
|
catch (error) {
|
|
33
|
-
spin.stop();
|
|
34
101
|
return failure(out, error);
|
|
35
102
|
}
|
|
36
|
-
spin.stop();
|
|
37
|
-
let session = start.session;
|
|
38
103
|
out.write(c.dim(`Answer in your own words, up to 5 sentences. Rough is fine. Type "s" to skip.\n`));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
out.write(`\n${c.bold(c.cyan(label))} ${c.dim(`(${KIND_LABEL[question.kind]}${followUp ? ', follow-up' : ''})`)}\n`);
|
|
45
|
-
out.write(`${wrap(question.text)}\n`);
|
|
46
|
-
let answer;
|
|
47
|
-
do
|
|
48
|
-
answer = await prompter.ask(c.bold('> '));
|
|
49
|
-
while (answer !== null && !answer.trim());
|
|
50
|
-
if (answer === null) {
|
|
51
|
-
out.write(`\n${c.yellow('Debrief paused.')} ${RESUME_HINT}\n`);
|
|
52
|
-
return { status: 'pending', reason: 'debrief paused' };
|
|
53
|
-
}
|
|
54
|
-
if (isSkip(answer))
|
|
55
|
-
return skip(api, out, session);
|
|
56
|
-
const grading = spinner(out, 'Checking your answer…');
|
|
57
|
-
let result;
|
|
58
|
-
try {
|
|
59
|
-
result = await api.request('POST', '/api/interview/answer', {
|
|
60
|
-
session,
|
|
61
|
-
idx: question.idx,
|
|
62
|
-
answer: answer.slice(0, MAX_ANSWER_CHARS),
|
|
63
|
-
...payload,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
grading.stop();
|
|
68
|
-
return failure(out, error);
|
|
69
|
-
}
|
|
70
|
-
grading.stop();
|
|
71
|
-
session = result.session;
|
|
72
|
-
if (result.pass) {
|
|
73
|
-
out.write(`${c.green('✓ Passed.')}\n${c.dim(wrap(result.feedback))}\n`);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
out.write(`${c.yellow('Not quite yet.')}\n${wrap(result.feedback)}\n`);
|
|
77
|
-
if (!result.followup)
|
|
78
|
-
return { status: 'pending', reason: 'no follow-up question' };
|
|
79
|
-
out.write(c.dim("Here's another angle on the same topic:\n"));
|
|
80
|
-
question = result.followup;
|
|
81
|
-
followUp = true;
|
|
82
|
-
}
|
|
104
|
+
const debrief = { api, prompter, total: start.total, payload, session: start.session };
|
|
105
|
+
for (const question of start.questions) {
|
|
106
|
+
const ended = await askUntilPassed(debrief, question);
|
|
107
|
+
if (ended)
|
|
108
|
+
return ended;
|
|
83
109
|
}
|
|
84
110
|
out.write(`\n${c.green(c.bold('✓ Push verified.'))} ${c.dim(`${target.commits.length} ${commitWord} owned. Nice work.`)}\n`);
|
|
85
111
|
return { status: 'verified' };
|
|
@@ -99,7 +125,7 @@ function failure(out, error) {
|
|
|
99
125
|
const c = paint(out);
|
|
100
126
|
if (error instanceof ApiError) {
|
|
101
127
|
if (error.isProviderError) {
|
|
102
|
-
out.write(`${c.yellow('Ownit:')} ${error.message}
|
|
128
|
+
out.write(`${c.yellow('Ownit:')} ${error.message} These commits stay unverified until you retry. ${RESUME_HINT}\n`);
|
|
103
129
|
return { status: 'provider_error', reason: error.message };
|
|
104
130
|
}
|
|
105
131
|
if (error.code === 'already_resolved') {
|
|
@@ -110,7 +136,7 @@ function failure(out, error) {
|
|
|
110
136
|
out.write(`${c.yellow('Ownit:')} ${sentence(error.message)} ${RESUME_HINT}\n`);
|
|
111
137
|
return { status: 'pending', reason: error.message };
|
|
112
138
|
}
|
|
113
|
-
const message =
|
|
139
|
+
const message = errorMessage(error);
|
|
114
140
|
out.write(`${c.yellow('Ownit:')} ${sentence(message)} ${RESUME_HINT}\n`);
|
|
115
141
|
return { status: 'pending', reason: message };
|
|
116
142
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,72 +1,77 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { badgeCommand } from './commands/badge.js';
|
|
3
4
|
import { hookCommand } from './commands/hook.js';
|
|
4
5
|
import { initCommand, uninstallCommand } from './commands/init.js';
|
|
5
6
|
import { interviewCommand } from './commands/interview.js';
|
|
6
|
-
import { badgeCommand } from './commands/badge.js';
|
|
7
7
|
import { keyCommand } from './commands/key.js';
|
|
8
8
|
import { loginCommand, logoutCommand, whoamiCommand } from './commands/login.js';
|
|
9
|
-
|
|
9
|
+
import { errorMessage } from './ui.js';
|
|
10
|
+
/** Every command, in the order `ownit help` lists them. */
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
login: { help: [['ownit login [--server URL]', 'Sign in with GitHub (device flow)']], run: loginCommand },
|
|
13
|
+
logout: { help: [['ownit logout', 'Sign out']], run: logoutCommand },
|
|
14
|
+
whoami: { help: [['ownit whoami', 'Show server, login, and key status']], run: whoamiCommand },
|
|
15
|
+
init: { help: [['ownit init [--repo owner/name]', 'Install the pre-push hook in this repo']], run: initCommand },
|
|
16
|
+
interview: {
|
|
17
|
+
help: [
|
|
18
|
+
['ownit interview', 'Debrief pushes that are still pending'],
|
|
19
|
+
['ownit interview --range A..B', 'Debrief a commit range'],
|
|
20
|
+
],
|
|
21
|
+
run: interviewCommand,
|
|
22
|
+
},
|
|
23
|
+
key: {
|
|
24
|
+
help: [
|
|
25
|
+
['ownit key set [nebius|nvidia]', 'Save your model API key (stored locally)'],
|
|
26
|
+
['ownit key clear|status', 'Remove or show it'],
|
|
27
|
+
],
|
|
28
|
+
run: keyCommand,
|
|
29
|
+
},
|
|
30
|
+
badge: { help: [['ownit badge', "Print this repo's badge Markdown for your README"]], run: badgeCommand },
|
|
31
|
+
uninstall: { help: [['ownit uninstall', 'Remove the pre-push hook']], run: uninstallCommand },
|
|
32
|
+
help: { help: [['ownit help', 'Show this help']], run: () => print(helpText()) },
|
|
33
|
+
// Not listed: the installed git hook calls `ownit hook pre-push`.
|
|
34
|
+
hook: { run: (args) => hookCommand(args) },
|
|
35
|
+
version: { run: () => print(`${version()}\n`) },
|
|
36
|
+
};
|
|
37
|
+
const ALIASES = { status: 'whoami', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version' };
|
|
38
|
+
function findCommand(name) {
|
|
39
|
+
const key = Object.hasOwn(ALIASES, name) ? ALIASES[name] : name;
|
|
40
|
+
return Object.hasOwn(COMMANDS, key) ? COMMANDS[key] : undefined;
|
|
41
|
+
}
|
|
42
|
+
function helpText() {
|
|
43
|
+
const usage = Object.values(COMMANDS)
|
|
44
|
+
.flatMap((command) => command.help ?? [])
|
|
45
|
+
.map(([line, description]) => ` ${line.padEnd(33)}${description}`)
|
|
46
|
+
.join('\n');
|
|
47
|
+
return `ownit: debrief the code you just pushed.
|
|
10
48
|
|
|
11
49
|
Usage:
|
|
12
|
-
|
|
13
|
-
ownit logout Sign out
|
|
14
|
-
ownit whoami Show server, login, and key status
|
|
15
|
-
ownit init [--repo owner/name] Install the pre-push hook in this repo
|
|
16
|
-
ownit interview Debrief pushes that are still pending
|
|
17
|
-
ownit interview --range A..B Debrief a commit range
|
|
18
|
-
ownit key set [nebius|nvidia] Save your model API key (stored locally)
|
|
19
|
-
ownit key clear|status Remove or show it
|
|
20
|
-
ownit badge Print this repo's badge Markdown for your README
|
|
21
|
-
ownit uninstall Remove the pre-push hook
|
|
22
|
-
ownit help Show this help
|
|
50
|
+
${usage}
|
|
23
51
|
|
|
24
52
|
Set OWNIT_SKIP=1 to push once without a debrief prompt (the push is pending).
|
|
25
53
|
`;
|
|
54
|
+
}
|
|
26
55
|
function version() {
|
|
27
56
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
28
57
|
return pkg.version;
|
|
29
58
|
}
|
|
59
|
+
function print(text) {
|
|
60
|
+
process.stdout.write(text);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
30
63
|
async function main(argv) {
|
|
31
|
-
const [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return logoutCommand();
|
|
37
|
-
case 'whoami':
|
|
38
|
-
case 'status':
|
|
39
|
-
return whoamiCommand();
|
|
40
|
-
case 'init':
|
|
41
|
-
return initCommand(rest);
|
|
42
|
-
case 'uninstall':
|
|
43
|
-
return uninstallCommand();
|
|
44
|
-
case 'hook':
|
|
45
|
-
return hookCommand(rest);
|
|
46
|
-
case 'interview':
|
|
47
|
-
return interviewCommand(rest);
|
|
48
|
-
case 'key':
|
|
49
|
-
return keyCommand(rest);
|
|
50
|
-
case 'badge':
|
|
51
|
-
return badgeCommand();
|
|
52
|
-
case '--version':
|
|
53
|
-
case '-v':
|
|
54
|
-
process.stdout.write(`${version()}\n`);
|
|
55
|
-
return 0;
|
|
56
|
-
case undefined:
|
|
57
|
-
case 'help':
|
|
58
|
-
case '--help':
|
|
59
|
-
case '-h':
|
|
60
|
-
process.stdout.write(HELP);
|
|
61
|
-
return 0;
|
|
62
|
-
default:
|
|
63
|
-
process.stderr.write(`Unknown command "${command}".\n\n${HELP}`);
|
|
64
|
-
return 1;
|
|
64
|
+
const [name = 'help', ...args] = argv;
|
|
65
|
+
const command = findCommand(name);
|
|
66
|
+
if (!command) {
|
|
67
|
+
process.stderr.write(`Unknown command "${name}".\n\n${helpText()}`);
|
|
68
|
+
return 1;
|
|
65
69
|
}
|
|
70
|
+
return command.run(args);
|
|
66
71
|
}
|
|
67
72
|
main(process.argv.slice(2)).then((code) => {
|
|
68
73
|
process.exitCode = code;
|
|
69
74
|
}, (error) => {
|
|
70
|
-
process.stderr.write(`Ownit: ${
|
|
75
|
+
process.stderr.write(`Ownit: ${errorMessage(error)}\n`);
|
|
71
76
|
process.exitCode = 1;
|
|
72
77
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model providers the worker accepts, in the order `ownit key set` lists them.
|
|
3
|
+
* Add a provider here and in the worker's PROVIDERS (worker/src/model.ts).
|
|
4
|
+
*/
|
|
5
|
+
export const PROVIDERS = {
|
|
6
|
+
nebius: { name: 'Nebius', envVar: 'NEBIUS_API_KEY', keyUrl: 'https://tokenfactory.nebius.com/', keyPrefix: undefined },
|
|
7
|
+
nvidia: { name: 'NVIDIA', envVar: 'NVIDIA_API_KEY', keyUrl: 'https://build.nvidia.com/', keyPrefix: 'nvapi-' },
|
|
8
|
+
};
|
|
9
|
+
export const PROVIDER_IDS = Object.keys(PROVIDERS);
|
|
10
|
+
export function isProvider(value) {
|
|
11
|
+
return Object.hasOwn(PROVIDERS, value);
|
|
12
|
+
}
|
|
13
|
+
/** The key to send with debriefs and its provider. A provider's env var wins over the saved key. */
|
|
14
|
+
export function modelKey(config, env = process.env) {
|
|
15
|
+
for (const provider of PROVIDER_IDS) {
|
|
16
|
+
const key = env[PROVIDERS[provider].envVar];
|
|
17
|
+
if (key)
|
|
18
|
+
return { provider, key };
|
|
19
|
+
}
|
|
20
|
+
if (config.apiKey)
|
|
21
|
+
return { provider: config.provider ?? 'nebius', key: config.apiKey };
|
|
22
|
+
if (config.nebiusKey)
|
|
23
|
+
return { provider: 'nebius', key: config.nebiusKey };
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The provider whose keys have this key's prefix, if any. */
|
|
27
|
+
export function providerByPrefix(key) {
|
|
28
|
+
return PROVIDER_IDS.find((id) => {
|
|
29
|
+
const prefix = PROVIDERS[id].keyPrefix;
|
|
30
|
+
return prefix !== undefined && key.startsWith(prefix);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** e.g. "NVIDIA, …1a2b" */
|
|
34
|
+
export function describeKey({ provider, key }) {
|
|
35
|
+
return `${PROVIDERS[provider]?.name ?? provider}, …${key.slice(-4)}`;
|
|
36
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -5,6 +5,10 @@ export function paint(out) {
|
|
|
5
5
|
const wrap = (code) => (text) => (on ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
6
6
|
return { bold: wrap('1'), dim: wrap('2'), green: wrap('32'), red: wrap('31'), yellow: wrap('33'), cyan: wrap('36') };
|
|
7
7
|
}
|
|
8
|
+
/** "1 commit", "2 commits". Pass the plural form for irregular words. */
|
|
9
|
+
export const plural = (n, singular, pluralForm = `${singular}s`) => (n === 1 ? singular : pluralForm);
|
|
10
|
+
/** The message of whatever was thrown. */
|
|
11
|
+
export const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
|
8
12
|
/** Ends a message with a period unless it already ends a sentence (server messages usually do). */
|
|
9
13
|
export function sentence(text) {
|
|
10
14
|
return /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
|