@giovannijecha/jecode 0.1.8 → 0.2.0

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.
Files changed (47) hide show
  1. package/README.md +41 -27
  2. package/dist/account-lock.js +112 -0
  3. package/dist/accounts.js +100 -0
  4. package/dist/cli-info.js +2 -2
  5. package/dist/commands.js +27 -102
  6. package/dist/controller.js +3 -0
  7. package/dist/credential-commands.js +40 -14
  8. package/dist/credential-safety.js +2 -1
  9. package/dist/external-browser.js +53 -0
  10. package/dist/oauth-http.js +114 -0
  11. package/dist/openai-account-command.js +124 -0
  12. package/dist/openai-account.js +91 -0
  13. package/dist/openai-oauth-callback.js +186 -0
  14. package/dist/openai-oauth-tokens.js +65 -0
  15. package/dist/openai-oauth.js +203 -0
  16. package/dist/permission-command.js +107 -0
  17. package/dist/permissions.js +113 -0
  18. package/dist/provider-commands.js +22 -62
  19. package/dist/provider-label.js +10 -0
  20. package/dist/providers/anthropic.js +1 -1
  21. package/dist/providers/index.js +2 -1
  22. package/dist/providers/ollama.js +1 -1
  23. package/dist/providers/openai-codex.js +114 -0
  24. package/dist/providers/openai-stream.js +13 -9
  25. package/dist/providers/openai-wire.js +4 -4
  26. package/dist/providers/openai.js +2 -2
  27. package/dist/providers/sse.js +1 -1
  28. package/dist/settings-command.js +13 -6
  29. package/dist/tools/shell.js +1 -1
  30. package/dist/transcript.js +0 -3
  31. package/dist/tui/app-workflows.js +17 -17
  32. package/dist/tui/app.js +6 -11
  33. package/dist/tui/approve.js +6 -34
  34. package/dist/tui/blocks.js +1 -3
  35. package/dist/tui/complete.js +4 -4
  36. package/dist/tui/components/menu.js +4 -3
  37. package/dist/tui/components/misc.js +0 -6
  38. package/dist/tui/components/status.js +6 -7
  39. package/dist/tui/feedback.js +11 -9
  40. package/dist/tui/help.js +29 -0
  41. package/dist/tui/modal.js +22 -11
  42. package/dist/tui/overlay.js +15 -4
  43. package/dist/tui/picker.js +1 -1
  44. package/dist/tui/session-view.js +12 -6
  45. package/dist/version.js +14 -0
  46. package/docs/assets/brand/jeco-256.png +0 -0
  47. package/package.json +5 -1
@@ -4,28 +4,15 @@
4
4
  // worth more than a line of prose and a key to guess at. It is a menu — the
5
5
  // generic one — with an attention title and three answers written out.
6
6
  // Nothing is approved by a key nobody meant to press.
7
+ import { scopeFor } from "../permissions.js";
8
+ export { scopeFor };
7
9
  /**
8
- * The narrow permission represented by "always".
10
+ * The three answers, in the order a person actually wants them.
9
11
  *
10
- * File-changing tools share a grant for one displayed path. Shell grants are
11
- * tied to the exact command line. Unknown dangerous tools fall back to their
12
- * exact, stable input rather than silently inheriting a tool-wide grant.
12
+ * "Always" is scoped to the tool and to this session: it is a way to stop being
13
+ * asked about `write_file` twenty times in a row, not a setting that outlives
14
+ * the window it was granted in.
13
15
  */
14
- export function scopeFor(call) {
15
- const path = typeof call.input.path === "string" ? call.input.path : undefined;
16
- if ((call.name === "write_file" || call.name === "edit_file") && path !== undefined) {
17
- return { key: `file\0${path}`, label: `changes to ${path}`, summary: `file changes · ${path}` };
18
- }
19
- const command = typeof call.input.command === "string" ? call.input.command : undefined;
20
- if (call.name === "run_command" && command !== undefined) {
21
- return { key: `command\0${command}`, label: "this exact command", summary: `command · ${command}` };
22
- }
23
- return {
24
- key: `${call.name}\0${stable(call.input)}`,
25
- label: "this exact call",
26
- summary: `${call.name} · ${target(call.input)}`,
27
- };
28
- }
29
16
  export function promptFor(call, target, pal) {
30
17
  const scope = scopeFor(call);
31
18
  const options = [
@@ -56,21 +43,6 @@ function scopeNoun(scope) {
56
43
  return "this command";
57
44
  return "this call";
58
45
  }
59
- function stable(value) {
60
- if (Array.isArray(value))
61
- return `[${value.map(stable).join(",")}]`;
62
- if (value !== null && typeof value === "object") {
63
- return `{${Object.entries(value)
64
- .sort(([a], [b]) => a.localeCompare(b))
65
- .map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)
66
- .join(",")}}`;
67
- }
68
- return JSON.stringify(value);
69
- }
70
- function target(input) {
71
- const value = input.path ?? input.command;
72
- return typeof value === "string" ? value : stable(input);
73
- }
74
46
  const ANSWERS = ["once", "always", "no"];
75
47
  /** What picking row `index` meant. Anything unrecognised refuses. */
76
48
  export function answerAt(index) {
@@ -1,6 +1,6 @@
1
1
  // Semantic transcript blocks routed to small, owned production components.
2
2
  import { renderAnswer, renderReasoning, renderUser } from "./components/messages.js";
3
- import { renderList, renderNotice } from "./components/misc.js";
3
+ import { renderNotice } from "./components/misc.js";
4
4
  import { renderTool } from "./components/tool.js";
5
5
  export function render(block, width, pal, context = {}) {
6
6
  switch (block.kind) {
@@ -19,8 +19,6 @@ export function render(block, width, pal, context = {}) {
19
19
  });
20
20
  case "notice":
21
21
  return renderNotice(block, width, pal);
22
- case "list":
23
- return renderList(block, width, pal);
24
22
  }
25
23
  }
26
24
  export function renderAll(blocks, width, pal, context = {}) {
@@ -1,9 +1,9 @@
1
1
  // Completing a slash command.
2
2
  //
3
- // A command the user cannot remember is a command that does not exist, and
4
- // `/help` only helps someone who already knows to ask. So the menu appears
5
- // while the line is being typed. Selection is state, not an edit: arrows move
6
- // through the list without rewriting what the user typed.
3
+ // A command the user cannot remember is a command that does not exist. `/help`
4
+ // owns keyboard controls; discovery belongs here, while the line is being
5
+ // typed. Selection is state, not an edit: arrows move through the list without
6
+ // rewriting what the user typed.
7
7
  import { COMMANDS } from "../commands.js";
8
8
  /**
9
9
  * The commands a half-typed line still matches.
@@ -16,10 +16,11 @@ export function menuWindow(length, selected, visible) {
16
16
  return { first, last: Math.min(count, first + room) };
17
17
  }
18
18
  function renderEntry(entry, labelWidth, width, pal) {
19
- // Colour terminals use one quiet selection band. In monochrome the arrow is
20
- // structural rather than decorative, so selection remains unambiguous.
19
+ // Colour terminals use one quiet selection band and keep every label on the
20
+ // composer's content edge. Monochrome has no band, so it alone reserves a
21
+ // fixed arrow column to keep selection visible without shifting peer rows.
21
22
  const monochrome = !hasColor();
22
- const selectedMark = entry.selected && monochrome ? "→ " : " ";
23
+ const selectedMark = monochrome ? (entry.selected ? "→ " : " ") : "";
23
24
  const fg = entry.selected ? pal.ink.bright : pal.ink.fg;
24
25
  const primary = [
25
26
  { text: selectedMark, fg: entry.selected ? pal.accent : fg },
@@ -14,9 +14,3 @@ export function renderNotice(block, width, pal) {
14
14
  ], [], undefined, 1)),
15
15
  ];
16
16
  }
17
- export function renderList(block, width, pal) {
18
- return [
19
- "",
20
- ...block.items.map((item) => row(width, [{ text: item.text, fg: item.dim ? pal.ink.muted : pal.ink.fg }], [], undefined, 1)),
21
- ];
22
- }
@@ -16,15 +16,14 @@ export function renderStatus(info, pal) {
16
16
  return info.readiness === undefined ? [] : feedbackSegments(info.readiness, pal);
17
17
  }
18
18
  function feedbackSegments(feedback, pal) {
19
- const mark = feedback.tone === "error" ? "×" : feedback.tone === "warn" ? "!" : "·";
20
- const markColor = {
21
- info: pal.accent,
22
- warn: pal.ink.attention,
23
- error: pal.ink.removed,
24
- };
19
+ if (feedback.tone === "info") {
20
+ return [{ text: feedback.text, fg: pal.ink.muted }];
21
+ }
22
+ const mark = feedback.tone === "error" ? "×" : "!";
23
+ const markColor = feedback.tone === "error" ? pal.ink.removed : pal.ink.attention;
25
24
  const textColor = feedback.tone === "error" ? pal.ink.removed : pal.ink.muted;
26
25
  return [
27
- { text: `${mark} `, fg: markColor[feedback.tone], bold: true },
26
+ { text: `${mark} `, fg: markColor, bold: true },
28
27
  { text: feedback.text, fg: textColor },
29
28
  ];
30
29
  }
@@ -1,5 +1,6 @@
1
1
  // Operational feedback belongs in the footer, not in the conversation.
2
- const INFO_MS = 2_800;
2
+ import { providerLabel } from "../provider-label.js";
3
+ const INFO_MS = 2_200;
3
4
  const WARN_MS = 4_200;
4
5
  const ERROR_MS = 6_000;
5
6
  /** Own replacement and expiry without leaking timers into the TUI shell. */
@@ -41,8 +42,6 @@ export function feedbackController(changed) {
41
42
  }
42
43
  /** Turn a command notice into one replaceable message in the footer status channel. */
43
44
  export function commandFeedback(block) {
44
- if (block.kind !== "notice")
45
- return undefined;
46
45
  return {
47
46
  text: block.text,
48
47
  tone: block.tone,
@@ -52,23 +51,26 @@ export function commandFeedback(block) {
52
51
  /** Explain why a model turn cannot start, without exposing configuration internals. */
53
52
  export function turnBlocker(session) {
54
53
  const blocked = session.provider.blocked();
55
- if (blocked !== undefined && !blocked.startsWith(`${session.provider.keyVar} `)) {
54
+ const auth = session.provider.auth;
55
+ const expected = auth.kind === "api-key"
56
+ ? blocked?.startsWith(`${auth.keyVar} `) === true
57
+ : blocked !== undefined;
58
+ if (blocked !== undefined && !expected) {
56
59
  return { text: blocked, tone: "error" };
57
60
  }
58
61
  if (blocked !== undefined) {
59
62
  return {
60
- text: `${providerName(session.provider.id)} needs an API key · /settings`,
63
+ text: auth.kind === "oauth"
64
+ ? `${providerLabel(session.provider.id)} needs ${auth.label} sign-in · /settings`
65
+ : `${providerLabel(session.provider.id)} needs an API key · /settings`,
61
66
  tone: "warn",
62
67
  };
63
68
  }
64
69
  if (session.model === "") {
65
70
  return {
66
- text: `${providerName(session.provider.id)} needs a model · /models`,
71
+ text: `${providerLabel(session.provider.id)} needs a model · /models`,
67
72
  tone: "warn",
68
73
  };
69
74
  }
70
75
  return undefined;
71
76
  }
72
- function providerName(id) {
73
- return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
74
- }
@@ -0,0 +1,29 @@
1
+ // The compact, non-persistent keyboard reference shown inside the dock.
2
+ import { row } from "../ui/render.js";
3
+ import { textWidth } from "../ui/width.js";
4
+ const CONTROL_WIDTH = 18;
5
+ const CONTROLS = [
6
+ { key: "up / down", description: "move through menus or history" },
7
+ { key: "enter / tab", description: "select or send · complete" },
8
+ { key: "alt+enter", description: "insert a new line" },
9
+ { key: "esc", description: "close UI or interrupt work" },
10
+ { key: "ctrl+c", description: "interrupt work or exit" },
11
+ { key: "ctrl+o", description: "toggle reasoning or tool details" },
12
+ { key: "wheel / pgup/dn", description: "scroll the transcript" },
13
+ { key: "ctrl+l", description: "redraw the screen" },
14
+ ];
15
+ export function panel(width, pal, maxRows = CONTROLS.length + 1) {
16
+ const heading = row(width, [
17
+ { text: "help ", fg: pal.accent, bold: true },
18
+ { text: "keyboard controls", fg: pal.ink.fg },
19
+ ], [{ text: "esc close", fg: pal.ink.muted }]);
20
+ const controls = CONTROLS.map((control) => {
21
+ const gap = Math.max(2, CONTROL_WIDTH - textWidth(control.key));
22
+ return row(width, [
23
+ { text: control.key, fg: pal.ink.bright, bold: true },
24
+ { text: " ".repeat(gap) },
25
+ { text: control.description, fg: pal.ink.muted },
26
+ ]);
27
+ });
28
+ return [heading, ...controls].slice(0, Math.max(1, maxRows));
29
+ }
package/dist/tui/modal.js CHANGED
@@ -1,17 +1,23 @@
1
1
  // What can take the dock over, and how it draws.
2
2
  //
3
- // Two kinds, and the union exists so that everything between the command that
3
+ // Three kinds, and the union exists so that everything between the command that
4
4
  // opens one and the frame that draws it — the view, the shell, the key handler
5
- // — speaks about "the thing that is open" rather than about a picker and a
6
- // field separately. A third kind would be added here and nowhere else.
5
+ // — speaks about "the thing that is open" rather than about each interaction
6
+ // separately.
7
7
  import * as picker from "./picker.js";
8
8
  import * as field from "./field.js";
9
+ import * as help from "./help.js";
9
10
  export function panel(modal, width, pal, maxRows) {
10
- return modal.kind === "pick"
11
- ? picker.panel(modal.picker, width, pal, maxRows)
12
- : maxRows === undefined
13
- ? field.panel(modal.field, width, pal)
14
- : field.panel(modal.field, width, pal).slice(0, maxRows);
11
+ switch (modal.kind) {
12
+ case "pick":
13
+ return picker.panel(modal.picker, width, pal, maxRows);
14
+ case "type":
15
+ return maxRows === undefined
16
+ ? field.panel(modal.field, width, pal)
17
+ : field.panel(modal.field, width, pal).slice(0, maxRows);
18
+ case "help":
19
+ return help.panel(width, pal, maxRows);
20
+ }
15
21
  }
16
22
  /**
17
23
  * Where the caret goes while this is open, if it goes anywhere.
@@ -20,7 +26,12 @@ export function panel(modal, width, pal, maxRows) {
20
26
  * query prompt and place the terminal caret at its real editing position.
21
27
  */
22
28
  export function caret(modal, width, maxRows) {
23
- return modal.kind === "type"
24
- ? field.caret(modal.field, width)
25
- : picker.caret(modal.picker, width, maxRows);
29
+ switch (modal.kind) {
30
+ case "pick":
31
+ return picker.caret(modal.picker, width, maxRows);
32
+ case "type":
33
+ return field.caret(modal.field, width);
34
+ case "help":
35
+ return undefined;
36
+ }
26
37
  }
@@ -1,16 +1,23 @@
1
- // The modal interaction layer: one picker or one single-line field.
1
+ // The modal interaction layer: a picker, a single-line field, or read-only help.
2
2
  import * as picker from "./picker.js";
3
3
  import { oneLine } from "./field.js";
4
4
  import { applyKey } from "./input.js";
5
5
  export function shown(open) {
6
6
  if (open === undefined)
7
7
  return undefined;
8
- return "picker" in open ? { kind: "pick", picker: open.picker } : { kind: "type", field: open.field };
8
+ if ("picker" in open)
9
+ return { kind: "pick", picker: open.picker };
10
+ if ("field" in open)
11
+ return { kind: "type", field: open.field };
12
+ return { kind: "help" };
9
13
  }
10
14
  export function cancel(open) {
11
15
  if (open === undefined)
12
16
  return undefined;
13
- open.settle(undefined);
17
+ if ("help" in open)
18
+ open.settle();
19
+ else
20
+ open.settle(undefined);
14
21
  return undefined;
15
22
  }
16
23
  export function handle(open, key) {
@@ -26,7 +33,11 @@ export function handle(open, key) {
26
33
  cancel(open);
27
34
  return {};
28
35
  }
29
- return "picker" in open ? handlePicker(open, key) : handleField(open, key);
36
+ if ("picker" in open)
37
+ return handlePicker(open, key);
38
+ if ("field" in open)
39
+ return handleField(open, key);
40
+ return { open };
30
41
  }
31
42
  function handlePicker(open, key) {
32
43
  switch (key.name) {
@@ -64,7 +64,7 @@ function layout(picker, width, pal, maxRows) {
64
64
  const options = colors === undefined
65
65
  ? []
66
66
  : shown.length === 0
67
- ? [row(width, [{ text: " no matches", fg: colors.ink.muted }])]
67
+ ? [row(width, [{ text: "no matches", fg: colors.ink.muted }])]
68
68
  : renderMenuRows(shown.map(({ option, index }) => ({
69
69
  label: option.label,
70
70
  hint: option.hint,
@@ -1,10 +1,10 @@
1
1
  // Small projections of the live session used by the shell and footer.
2
2
  import { credentialSource } from "../credentials.js";
3
3
  import { providerFailure } from "../provider-errors.js";
4
- export function controllerOptions(session) {
4
+ export function controllerOptions(session, tools = session.tools) {
5
5
  return {
6
6
  provider: session.provider,
7
- tools: session.tools,
7
+ tools,
8
8
  model: session.model,
9
9
  system: session.system,
10
10
  maxTokens: session.config.maxTokens,
@@ -26,10 +26,16 @@ export function turnFailure(session, error, aborted) {
26
26
  return { kind: "notice", text: "[interrupted]", tone: "warn" };
27
27
  let text = providerFailure(session.provider, error);
28
28
  if (/\b401\b/.test(text)) {
29
- const source = credentialSource(session.provider.keyVar);
30
- text += source === "environment"
31
- ? ` · update ${session.provider.keyVar} in the environment and restart`
32
- : " · check credentials with /settings";
29
+ const auth = session.provider.auth;
30
+ if (auth.kind === "oauth") {
31
+ text += ` · reconnect ${auth.label} in /settings`;
32
+ }
33
+ else {
34
+ const source = credentialSource(auth.keyVar);
35
+ text += source === "environment"
36
+ ? ` · update ${auth.keyVar} in the environment and restart`
37
+ : " · check credentials with /settings";
38
+ }
33
39
  }
34
40
  return { kind: "notice", text, tone: "error" };
35
41
  }
@@ -0,0 +1,14 @@
1
+ // The package version is runtime identity, not configuration.
2
+ import { readFileSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ let cached;
5
+ export function applicationVersion() {
6
+ if (cached !== undefined)
7
+ return cached;
8
+ const manifest = JSON.parse(readFileSync(path.resolve(import.meta.dirname, "..", "package.json"), "utf8"));
9
+ if (typeof manifest.version !== "string" || manifest.version === "") {
10
+ throw new Error("package version is missing");
11
+ }
12
+ cached = manifest.version;
13
+ return cached;
14
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,12 +16,16 @@
16
16
  "terminal",
17
17
  "tui",
18
18
  "ollama",
19
+ "openai",
20
+ "chatgpt",
21
+ "oauth",
19
22
  "typescript"
20
23
  ],
21
24
  "type": "module",
22
25
  "files": [
23
26
  "bin/",
24
27
  "dist/",
28
+ "docs/assets/brand/jeco-256.png",
25
29
  "LICENSE",
26
30
  "README.md"
27
31
  ],