oakode 0.0.0-stage → 0.5.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.
- package/CHANGELOG.md +86 -0
- package/LICENSE +9 -0
- package/README.md +243 -2
- package/SECURITY.md +13 -0
- package/dist/account.js +34 -0
- package/dist/agent.js +570 -0
- package/dist/agents/approval.js +116 -0
- package/dist/agents/budget.js +293 -0
- package/dist/agents/child.js +102 -0
- package/dist/agents/host.js +289 -0
- package/dist/agents/orchestrator.js +286 -0
- package/dist/agents/plan.js +143 -0
- package/dist/agents/types.js +20 -0
- package/dist/agents/verify.js +94 -0
- package/dist/app.js +619 -0
- package/dist/auth.js +456 -0
- package/dist/brief.js +97 -0
- package/dist/commands.js +587 -0
- package/dist/compact.js +231 -0
- package/dist/config.js +82 -0
- package/dist/diff.js +98 -0
- package/dist/doctor.js +106 -0
- package/dist/errors.js +15 -0
- package/dist/fileio.js +688 -0
- package/dist/input.js +244 -0
- package/dist/jobs.js +262 -0
- package/dist/main.js +560 -0
- package/dist/mask.js +63 -0
- package/dist/mcp.js +721 -0
- package/dist/models.js +106 -0
- package/dist/modes.js +114 -0
- package/dist/oicki.js +475 -0
- package/dist/output.js +101 -0
- package/dist/permissions.js +389 -0
- package/dist/permissions_ui.js +134 -0
- package/dist/print.js +76 -0
- package/dist/proc.js +63 -0
- package/dist/runners.js +264 -0
- package/dist/serial.js +9 -0
- package/dist/session.js +452 -0
- package/dist/shell.js +40 -0
- package/dist/skills.js +322 -0
- package/dist/syntax.js +80 -0
- package/dist/tools.js +480 -0
- package/dist/tui/editor.js +279 -0
- package/dist/tui/highlight.js +78 -0
- package/dist/tui/markdown.js +254 -0
- package/dist/tui/screen.js +146 -0
- package/dist/tui/tui.js +505 -0
- package/dist/tui/view.js +337 -0
- package/dist/ui.js +105 -0
- package/dist/update.js +64 -0
- package/package.json +57 -4
package/dist/models.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Presentation helpers for the OICKI model catalog (369 entries in production, so lookup must be filterable). */
|
|
2
|
+
import { configFile, configProblem, loadConfig } from "./config.js";
|
|
3
|
+
import { dim, fmtTokens, green, red, yellow } from "./ui.js";
|
|
4
|
+
/**
|
|
5
|
+
* The models OAKODE recommends: affordable coding models whose tool calls, multi-step turns and prompt caching were
|
|
6
|
+
* checked through OICKI (eval/probe.ts, eval/cache-probe.ts, 2026-10-30), plus two stronger ones. The rest of the
|
|
7
|
+
* catalog stays reachable by search (`/model <text>`, `/models all`).
|
|
8
|
+
*/
|
|
9
|
+
export const CURATED = [
|
|
10
|
+
{ id: "auto", note: "OICKI's pick (default)" },
|
|
11
|
+
{ id: "deepseek/deepseek-v4.1-flash", note: "fast, cheap, caches" },
|
|
12
|
+
{ id: "xiaomi/mimo-v2.6-flash", note: "cheapest once cached; text only", noImages: true }, // images did not reach it (2026-10-30)
|
|
13
|
+
{ id: "minimax/minimax-m3.1-flash", note: "cheap, caches" },
|
|
14
|
+
{ id: "z-ai/glm-5.3-flash", note: "cheap" },
|
|
15
|
+
{ id: "openai/gpt-6-luna", note: "cheap, caches" },
|
|
16
|
+
{ id: "inclusionai/ling-3.1-flash", note: "low price, slower; no images" },
|
|
17
|
+
{ id: "anthropic/claude-sonnet-5.5", note: "stronger; about 20x the cost" },
|
|
18
|
+
{ id: "openai/gpt-6.1-sol", note: "stronger; about 20x the cost" },
|
|
19
|
+
];
|
|
20
|
+
// ---------------------------------------------------------------- model policy
|
|
21
|
+
// OAKODE supports the curated models. `"experimentalModels": true` in config.json opts in to the rest of the catalog,
|
|
22
|
+
// unsupported. It only changes what OAKODE offers and accepts: OICKI still decides what a credential may use.
|
|
23
|
+
export const isSupported = (id) => CURATED.some((c) => c.id === id);
|
|
24
|
+
export const experimentalModels = () => loadConfig().experimentalModels === true;
|
|
25
|
+
/** May a new choice of this model be made (picker, /model, -m, a saved default)? */
|
|
26
|
+
export const allowedModel = (id) => isSupported(id) || experimentalModels();
|
|
27
|
+
/** How to opt in, for every message that refuses a model. */
|
|
28
|
+
export const experimentalHint = () => configProblem() ?? `To use models outside the supported list anyway, add "experimentalModels": true to ${configFile()} (unsupported; OICKI's access rules still apply).`;
|
|
29
|
+
export const notSupported = (id) => `"${id}" is not one of OAKODE's supported models (/models or \`oakode models\` lists them). ${experimentalHint()}`;
|
|
30
|
+
/** What can be browsed and chosen: the supported models, plus (experimental) the rest of the catalog after them. */
|
|
31
|
+
export function selectableModels(catalog) {
|
|
32
|
+
const supported = curatedModels(catalog);
|
|
33
|
+
return experimentalModels() ? [...supported, ...catalog.filter((m) => !isSupported(m.id))] : supported;
|
|
34
|
+
}
|
|
35
|
+
/** For a resumed session whose model may no longer be chosen: it keeps working, nothing is switched. */
|
|
36
|
+
export const compatibilityNotice = (id) => `This session uses "${id}", which is not one of OAKODE's supported models. It continues as before; /model switches to a supported one. ${experimentalHint()}`;
|
|
37
|
+
export const ignoredDefaultNotice = (id) => `Your saved default model "${id}" is not one of OAKODE's supported models, so this session uses auto (the saved default is left as it is). ${experimentalHint()}`;
|
|
38
|
+
/** The model for a new session: the saved default if it may still be chosen, else `auto`. The config is not changed. */
|
|
39
|
+
export function defaultModel() {
|
|
40
|
+
const saved = loadConfig().model;
|
|
41
|
+
return !saved || allowedModel(saved) ? { id: saved ?? "auto" } : { id: "auto", ignored: saved };
|
|
42
|
+
}
|
|
43
|
+
/** The curated models that are in this catalog, in the curated order. */
|
|
44
|
+
export const curatedModels = (models) => CURATED.map((c) => models.find((m) => m.id === c.id)).filter((m) => !!m);
|
|
45
|
+
export const curatedNote = (id) => CURATED.find((c) => c.id === id)?.note;
|
|
46
|
+
/** Corrects what the catalog claims where OAKODE's own checks found otherwise (a model that does not receive images). */
|
|
47
|
+
function withKnownLimits(m) {
|
|
48
|
+
if (!CURATED.find((c) => c.id === m.id)?.noImages)
|
|
49
|
+
return m;
|
|
50
|
+
return { ...m, oicki: { ...m.oicki, capabilities: { ...m.oicki.capabilities, input_modalities: m.oicki.capabilities.input_modalities.filter((x) => x !== "image") } } };
|
|
51
|
+
}
|
|
52
|
+
/** Why requests to this model may be refused for the current credential, or undefined if they won't be. */
|
|
53
|
+
export function accessNote(m) {
|
|
54
|
+
const a = m.oicki.access;
|
|
55
|
+
if (!a.permission_required || a.permission_granted)
|
|
56
|
+
return undefined;
|
|
57
|
+
// A "standard" model can still turn expensive for large prompts (expensive_from_prompt_tokens > 0).
|
|
58
|
+
return a.expensive_from_prompt_tokens
|
|
59
|
+
? `needs ${a.permission_required} for prompts over ${fmtTokens(a.expensive_from_prompt_tokens)} tokens`
|
|
60
|
+
: `needs ${a.permission_required}`;
|
|
61
|
+
}
|
|
62
|
+
export function describeModel(m, current) {
|
|
63
|
+
const o = m.oicki;
|
|
64
|
+
const price = o.pricing ? `$${o.pricing.input_usd_per_million}/${o.pricing.output_usd_per_million} per M tok${o.pricing.tiers?.length ? "+" : ""}` : "price n/a";
|
|
65
|
+
const note = accessNote(m);
|
|
66
|
+
const flags = [
|
|
67
|
+
note ? yellow(note) : o.access.permission_required ? "expensive (allowed)" : "",
|
|
68
|
+
o.capabilities.tools ? "" : red("no tools"),
|
|
69
|
+
o.resolves_to ? `→ ${o.resolves_to}` : "",
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
const pick = curatedNote(m.id);
|
|
72
|
+
return `${m.id === current ? green("*") : " "} ${m.id} ${pick ? `${pick} · ` : `${yellow("experimental")} · `}${dim(`ctx ${o.context_window ? fmtTokens(o.context_window) : "?"} · ${price}`)} ${flags.join(" ")}`;
|
|
73
|
+
}
|
|
74
|
+
/** Exact id, else case-insensitive substring of id or name. */
|
|
75
|
+
export function matchModels(models, query) {
|
|
76
|
+
const q = query.trim().toLowerCase();
|
|
77
|
+
if (!q)
|
|
78
|
+
return models;
|
|
79
|
+
const exact = models.find((m) => m.id.toLowerCase() === q);
|
|
80
|
+
return exact ? [exact] : models.filter((m) => m.id.toLowerCase().includes(q) || (m.oicki.name ?? "").toLowerCase().includes(q));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Limits to plan with when `selected` is a router such as `auto`: the tighter of the router's own numbers and the
|
|
84
|
+
* model that actually serves it. The request still names `selected.id`.
|
|
85
|
+
*/
|
|
86
|
+
export function effectiveModel(selected, served) {
|
|
87
|
+
selected = withKnownLimits(selected);
|
|
88
|
+
if (!served || served.id === selected.id)
|
|
89
|
+
return selected;
|
|
90
|
+
served = withKnownLimits(served);
|
|
91
|
+
const min = (a, b) => (a && b ? Math.min(a, b) : (a ?? b));
|
|
92
|
+
return {
|
|
93
|
+
...selected,
|
|
94
|
+
oicki: {
|
|
95
|
+
...selected.oicki,
|
|
96
|
+
context_window: min(selected.oicki.context_window, served.oicki.context_window),
|
|
97
|
+
max_output_tokens: min(selected.oicki.max_output_tokens, served.oicki.max_output_tokens),
|
|
98
|
+
capabilities: {
|
|
99
|
+
...selected.oicki.capabilities,
|
|
100
|
+
tools: selected.oicki.capabilities.tools && served.oicki.capabilities.tools,
|
|
101
|
+
reasoning: !!selected.oicki.capabilities.reasoning && !!served.oicki.capabilities.reasoning,
|
|
102
|
+
input_modalities: selected.oicki.capabilities.input_modalities.filter((x) => served.oicki.capabilities.input_modalities.includes(x)),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/modes.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission modes: presets that decide, for every tool call, whether it runs, asks, or is refused.
|
|
3
|
+
*
|
|
4
|
+
* default reads run; edits, writes, commands and MCP calls follow your saved permissions, otherwise ask
|
|
5
|
+
* autoedit like default, but edits/writes inside the workspace run without asking
|
|
6
|
+
* plan only read-only tools run; everything that could change something is refused
|
|
7
|
+
* custom a per-tool table (Always Allow / Ask Every Time / Deny) that you edit with /permissions
|
|
8
|
+
*
|
|
9
|
+
* This is enforced where tools are executed (Agent.runTool -> App.authorize, and again in App.confirm), not by the prompt.
|
|
10
|
+
* Modes are not sandboxes: an approved shell command can do anything the user can.
|
|
11
|
+
*/
|
|
12
|
+
import { protectedPath } from "./permissions.js";
|
|
13
|
+
export const MODES = ["default", "autoedit", "plan", "custom"];
|
|
14
|
+
export const MODE_LABEL = { default: "Default", autoedit: "Auto Edit", plan: "Plan", custom: "Custom" };
|
|
15
|
+
export const MODE_SUMMARY = {
|
|
16
|
+
default: "reads run; edits, commands and MCP calls follow your saved permissions, otherwise ask",
|
|
17
|
+
autoedit: "edits and writes inside the workspace run without asking; commands and MCP calls still follow your permissions",
|
|
18
|
+
plan: "read-only: nothing that modifies files, runs commands or calls MCP tools can run",
|
|
19
|
+
custom: "your own per-tool policy (/permissions)",
|
|
20
|
+
};
|
|
21
|
+
export const POLICY_LABEL = { allow: "Always Allow", ask: "Ask Every Time", deny: "Deny" };
|
|
22
|
+
export const nextMode = (m) => MODES[(MODES.indexOf(m) + 1) % MODES.length];
|
|
23
|
+
export const parseMode = (s) => {
|
|
24
|
+
const k = s.toLowerCase().replace(/[\s_-]+/g, "");
|
|
25
|
+
return MODES.find((m) => m === k);
|
|
26
|
+
};
|
|
27
|
+
const READ_ONLY = new Set(["read", "grep", "glob", "ls", "skill", "job"]); // job only checks or stops what was approved
|
|
28
|
+
const norm = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
29
|
+
/** Relative, no `..`: inside the workspace as far as the path text can tell (the tools themselves also resolve symlinks). */
|
|
30
|
+
export const insideWorkspace = (p) => p !== "" && !p.startsWith("/") && !/^[A-Za-z]:/.test(p) && !p.split("/").includes("..");
|
|
31
|
+
export function subjectOf(name, args, mcp) {
|
|
32
|
+
if (mcp)
|
|
33
|
+
return { kind: "mcp", server: mcp.server, mcpTool: mcp.tool };
|
|
34
|
+
if (READ_ONLY.has(name))
|
|
35
|
+
return { kind: "read", name };
|
|
36
|
+
if (name === "write" || name === "edit")
|
|
37
|
+
return { kind: "file", tool: name, path: norm(String(args?.path ?? "")) };
|
|
38
|
+
if (name === "bash")
|
|
39
|
+
return { kind: "bash", command: String(args?.command ?? "") };
|
|
40
|
+
return { kind: "other", name };
|
|
41
|
+
}
|
|
42
|
+
export function subjectOfAction(a) {
|
|
43
|
+
if (a.tool === "bash")
|
|
44
|
+
return { kind: "bash", command: a.command ?? "" };
|
|
45
|
+
if (a.tool === "mcp")
|
|
46
|
+
return { kind: "mcp", server: a.server ?? "", mcpTool: a.mcpTool ?? "" };
|
|
47
|
+
return { kind: "file", tool: a.tool, path: norm(a.path ?? "") };
|
|
48
|
+
}
|
|
49
|
+
const actionOf = (s) => s.kind === "file" ? { tool: s.tool, path: s.path } : s.kind === "bash" ? { tool: "bash", command: s.command } : s.kind === "mcp" ? { tool: "mcp", server: s.server, mcpTool: s.mcpTool } : undefined;
|
|
50
|
+
/** The policy keys that apply to a subject, most specific first. */
|
|
51
|
+
export function policyKeys(s) {
|
|
52
|
+
switch (s.kind) {
|
|
53
|
+
case "read": return s.name === "skill" ? [] : [s.name];
|
|
54
|
+
case "file": return [s.tool];
|
|
55
|
+
case "bash": return [`bash:${s.command.trim()}`, "bash"];
|
|
56
|
+
case "mcp": return [`mcp:${s.mcpTool ? `${s.server}/${s.mcpTool}` : s.server}`, `mcp:${s.server}`, "mcp:*"];
|
|
57
|
+
case "other": return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const label = (s) => s.kind === "file" ? `${s.tool} ${s.path}` : s.kind === "bash" ? "shell commands" : s.kind === "mcp" ? `MCP tool ${s.server}/${s.mcpTool}` : s.kind === "read" ? s.name : s.name;
|
|
61
|
+
export function decide(mode, perms, s) {
|
|
62
|
+
const M = MODE_LABEL[mode];
|
|
63
|
+
if (s.kind === "read") {
|
|
64
|
+
if (mode === "custom") {
|
|
65
|
+
for (const k of policyKeys(s)) {
|
|
66
|
+
const p = perms.policyOf(k);
|
|
67
|
+
if (p)
|
|
68
|
+
return { decision: p.policy, why: `${M}: ${s.name} is set to ${POLICY_LABEL[p.policy]}` };
|
|
69
|
+
}
|
|
70
|
+
return { decision: "allow", why: `${M}: ${s.name} is read-only` };
|
|
71
|
+
}
|
|
72
|
+
return { decision: "allow", why: `${M}: ${s.name} is read-only` };
|
|
73
|
+
}
|
|
74
|
+
if (mode === "plan") {
|
|
75
|
+
return {
|
|
76
|
+
decision: "deny",
|
|
77
|
+
why: `Plan mode is read-only: ${label(s)} is blocked. Do not try to achieve the same effect with another tool (bash, an MCP tool, …). Inspect with read/grep/ls and describe the proposed changes in your answer; the user switches mode (Shift+Tab) to apply them.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const action = actionOf(s);
|
|
81
|
+
const grant = action ? perms.allowedBy(action) : undefined;
|
|
82
|
+
// a runner command (npm test, make …) whose command-defining files changed since it was approved asks again
|
|
83
|
+
const changed = s.kind === "bash" ? perms.runnerChanged(s.command) : undefined;
|
|
84
|
+
const rerun = () => ({ decision: "ask", why: `${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ` and ${changed.length - 5} more` : ""} changed since this command was allowed, and it runs what ${changed.length > 1 ? "they define" : "that file defines"}. Allowing it accepts the new contents` });
|
|
85
|
+
const viaGrant = () => (grant ? (changed ? rerun() : { decision: "allow", why: `allowed by a ${grant.scope} permission`, rule: grant }) : undefined);
|
|
86
|
+
if (s.kind === "other")
|
|
87
|
+
return { decision: "ask", why: `${M}: ${s.name} is not a known tool, so it asks` };
|
|
88
|
+
// A protected file (git metadata, project instructions, skills, MCP config) is never allowed by a mode or a broad
|
|
89
|
+
// policy: only an exact per-file grant or a fresh approval. A Deny still denies.
|
|
90
|
+
const prot = s.kind === "file" ? protectedPath(s.path) : undefined;
|
|
91
|
+
const askProtected = () => viaGrant() ?? { decision: "ask", why: `protected file (${prot}); it always asks` };
|
|
92
|
+
// installed programs (node_modules/.bin, a virtualenv's bin/) are what runner commands run: Auto Edit does not cover them
|
|
93
|
+
const installed = s.kind === "file" && s.path.split("/").some((seg) => /^(node_modules|\.venv|venv)$/i.test(seg));
|
|
94
|
+
if (mode === "autoedit" && s.kind === "file" && insideWorkspace(s.path) && !prot && !installed)
|
|
95
|
+
return { decision: "allow", why: `Auto Edit: ${s.tool} inside the workspace` };
|
|
96
|
+
if (mode === "custom") {
|
|
97
|
+
for (const k of policyKeys(s)) {
|
|
98
|
+
const p = perms.policyOf(k);
|
|
99
|
+
if (!p)
|
|
100
|
+
continue;
|
|
101
|
+
const src = `${M}: ${k} is set to ${POLICY_LABEL[p.policy]}${p.scope === "session" ? " (this session)" : ""}`;
|
|
102
|
+
if (p.policy === "ask")
|
|
103
|
+
return viaGrant() ?? { decision: "ask", why: src };
|
|
104
|
+
if (p.policy === "allow" && prot)
|
|
105
|
+
return askProtected();
|
|
106
|
+
if (p.policy === "allow" && changed && k !== "bash")
|
|
107
|
+
return rerun(); // "all of bash" stays as the person chose it
|
|
108
|
+
return { decision: p.policy, why: src };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (prot)
|
|
112
|
+
return askProtected();
|
|
113
|
+
return viaGrant() ?? { decision: "ask", why: `${M}: ${label(s)} asks first${mode === "autoedit" && s.kind === "file" ? (installed ? " (installed packages, which commands run)" : " (outside the workspace)") : ""}; no saved permission matches` };
|
|
114
|
+
}
|