getownit 0.1.3 → 0.1.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.
@@ -3,6 +3,7 @@ import { ApiError, createApi } from '../api.js';
3
3
  import { readConfig, serverUrl, updateConfig } from '../config.js';
4
4
  import { describeKey, modelKey } from '../providers.js';
5
5
  import { readSecret } from '../secret.js';
6
+ import { serverModels } from './model.js';
6
7
  import { paint } from '../ui.js';
7
8
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
9
  /** POSTs a form to github.com's OAuth endpoints. OWNIT_GITHUB_URL overrides the host in tests. */
@@ -114,5 +115,8 @@ export async function whoamiCommand() {
114
115
  out.write(`GitHub: ${config.token ? (config.login ?? 'logged in') : 'not logged in (run "ownit login")'}\n`);
115
116
  const model = modelKey(config);
116
117
  out.write(`API key: ${model ? `set (${describeKey(model)})` : 'not set (run "ownit key set")'}\n`);
118
+ const current = model && (await serverModels())?.find((m) => m.id === model.provider);
119
+ if (current)
120
+ out.write(`Model: ${current.model}\n`);
117
121
  return 0;
118
122
  }
@@ -0,0 +1,38 @@
1
+ import { createApi } from '../api.js';
2
+ import { readConfig } from '../config.js';
3
+ import { describeKey, modelKey } from '../providers.js';
4
+ import { paint } from '../ui.js';
5
+ /** The model each provider runs on the server, or null when the server doesn't say (older servers). */
6
+ export async function serverModels() {
7
+ try {
8
+ const { providers } = await createApi().request('GET', '/models', undefined, { timeoutMs: 5_000 });
9
+ return providers;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ /**
16
+ * `ownit model`: which provider and model your debriefs use. The key is saved
17
+ * for your user, so this is the same in every repo on this computer.
18
+ */
19
+ export async function modelCommand() {
20
+ const out = process.stderr;
21
+ const c = paint(out);
22
+ const key = modelKey(readConfig());
23
+ const models = await serverModels();
24
+ const current = key && models?.find((m) => m.id === key.provider);
25
+ if (!key)
26
+ out.write(`Provider: ${c.yellow('no key set')} (run "ownit key set")\n`);
27
+ else {
28
+ out.write(`Provider: ${describeKey(key)}\n`);
29
+ out.write(`Model: ${current ? c.bold(current.model) : c.dim("this server doesn't report its models")}\n`);
30
+ }
31
+ for (const other of models ?? []) {
32
+ if (other.id === key?.provider)
33
+ continue;
34
+ out.write(c.dim(`With ${other.name}: ${other.model} (ownit key set ${other.id})\n`));
35
+ }
36
+ out.write(c.dim('Your key is saved for your user, so this applies in every repo on this computer.\n'));
37
+ return 0;
38
+ }
package/dist/debrief.js CHANGED
@@ -19,6 +19,32 @@ async function withSpinner(out, text, work) {
19
19
  spin.stop();
20
20
  }
21
21
  }
22
+ /** A provider error worth retrying: the model did not answer in time or failed on its side. */
23
+ const isUnavailable = (error) => error instanceof ApiError && error.isProviderError && error.body.reason === 'unavailable';
24
+ /**
25
+ * Runs a model request. When the model doesn't answer, it tries once more by
26
+ * itself, then asks before each further try, so a slow moment never throws
27
+ * away a debrief in progress.
28
+ */
29
+ async function withRetry(prompter, request) {
30
+ const c = paint(prompter.out);
31
+ for (let attempt = 1;; attempt++) {
32
+ try {
33
+ return await request();
34
+ }
35
+ catch (error) {
36
+ if (!isUnavailable(error))
37
+ throw error;
38
+ if (attempt === 1) {
39
+ prompter.out.write(c.dim('The model did not answer in time. Trying again…\n'));
40
+ continue;
41
+ }
42
+ const again = await prompter.ask(`${c.yellow(error.message)} Try again? [Y/n] `);
43
+ if (again === null || /^n/i.test(again.trim()))
44
+ throw error;
45
+ }
46
+ }
47
+ }
22
48
  function printQuestion(out, question, total, followUp) {
23
49
  const c = paint(out);
24
50
  const label = `Question ${question.idx + 1}/${total}`;
@@ -55,12 +81,12 @@ async function askUntilPassed(debrief, first) {
55
81
  return later(out);
56
82
  let result;
57
83
  try {
58
- result = await withSpinner(out, 'Checking your answer…', () => api.request('POST', '/api/interview/answer', {
84
+ result = await withRetry(prompter, () => withSpinner(out, 'Checking your answer…', () => api.request('POST', '/api/interview/answer', {
59
85
  session: debrief.session,
60
86
  idx: question.idx,
61
87
  answer: answer.slice(0, MAX_ANSWER_CHARS),
62
88
  ...debrief.payload,
63
- }));
89
+ })));
64
90
  }
65
91
  catch (error) {
66
92
  return failure(out, error);
@@ -92,14 +118,14 @@ export async function runDebrief(api, prompter, target) {
92
118
  };
93
119
  let start;
94
120
  try {
95
- start = await withSpinner(out, 'Nemotron is reading your push…', () => api.request('POST', '/api/interview/start', {
121
+ start = await withRetry(prompter, () => withSpinner(out, 'Nemotron is reading your push…', () => api.request('POST', '/api/interview/start', {
96
122
  repo: target.repo,
97
123
  base_sha: target.baseSha,
98
124
  head_sha: target.headSha,
99
125
  changed_lines: target.diff.changedLines,
100
126
  push_id: target.pushId,
101
127
  ...payload,
102
- }));
128
+ })));
103
129
  }
104
130
  catch (error) {
105
131
  return failure(out, error);
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { initCommand, uninstallCommand } from './commands/init.js';
6
6
  import { interviewCommand } from './commands/interview.js';
7
7
  import { keyCommand } from './commands/key.js';
8
8
  import { loginCommand, logoutCommand, whoamiCommand } from './commands/login.js';
9
+ import { modelCommand } from './commands/model.js';
9
10
  import { settingsCommand } from './commands/settings.js';
10
11
  import { closestMatch, errorMessage } from './ui.js';
11
12
  /** Every command, in the order `ownit help` lists them. */
@@ -54,6 +55,15 @@ Never type the key after the command: it would end up in your shell history.
54
55
  NVIDIA_API_KEY or NEBIUS_API_KEY in the environment work too.`,
55
56
  run: keyCommand,
56
57
  },
58
+ model: {
59
+ help: [['ownit model', 'Show the provider and model your debriefs use']],
60
+ details: `Shows which provider your saved key is for and which Nemotron model this server runs
61
+ for it, plus what the other provider would use. The key is saved for your user
62
+ (~/.ownit/config.json), so it applies in every repo on this computer. Switch with
63
+ "ownit key set nebius" or "ownit key set nvidia". NVIDIA_API_KEY or NEBIUS_API_KEY in the
64
+ environment override it for that terminal.`,
65
+ run: modelCommand,
66
+ },
57
67
  badge: {
58
68
  help: [['ownit badge', "Print this repo's badge Markdown for your README"]],
59
69
  details: `Paste the output into your README. To copy it straight away:
@@ -85,7 +95,7 @@ Private repos never show questions or answers.`,
85
95
  hook: { run: (args) => hookCommand(args) },
86
96
  version: { run: () => print(`${version()}\n`) },
87
97
  };
88
- const ALIASES = { interviews: 'interview', setting: 'settings', page: 'settings', status: 'whoami', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version' };
98
+ const ALIASES = { plan: 'model', currentplan: 'model', models: 'model', interviews: 'interview', setting: 'settings', page: 'settings', status: 'whoami', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version' };
89
99
  const commandName = (name) => (Object.hasOwn(ALIASES, name) ? ALIASES[name] : name);
90
100
  function findCommand(name) {
91
101
  const key = commandName(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getownit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Debrief the code you just pushed, in your terminal, with NVIDIA Nemotron.",
5
5
  "license": "MIT",
6
6
  "repository": {