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.
Files changed (53) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +9 -0
  3. package/README.md +243 -2
  4. package/SECURITY.md +13 -0
  5. package/dist/account.js +34 -0
  6. package/dist/agent.js +570 -0
  7. package/dist/agents/approval.js +116 -0
  8. package/dist/agents/budget.js +293 -0
  9. package/dist/agents/child.js +102 -0
  10. package/dist/agents/host.js +289 -0
  11. package/dist/agents/orchestrator.js +286 -0
  12. package/dist/agents/plan.js +143 -0
  13. package/dist/agents/types.js +20 -0
  14. package/dist/agents/verify.js +94 -0
  15. package/dist/app.js +619 -0
  16. package/dist/auth.js +456 -0
  17. package/dist/brief.js +97 -0
  18. package/dist/commands.js +587 -0
  19. package/dist/compact.js +231 -0
  20. package/dist/config.js +82 -0
  21. package/dist/diff.js +98 -0
  22. package/dist/doctor.js +106 -0
  23. package/dist/errors.js +15 -0
  24. package/dist/fileio.js +688 -0
  25. package/dist/input.js +244 -0
  26. package/dist/jobs.js +262 -0
  27. package/dist/main.js +560 -0
  28. package/dist/mask.js +63 -0
  29. package/dist/mcp.js +721 -0
  30. package/dist/models.js +106 -0
  31. package/dist/modes.js +114 -0
  32. package/dist/oicki.js +475 -0
  33. package/dist/output.js +101 -0
  34. package/dist/permissions.js +389 -0
  35. package/dist/permissions_ui.js +134 -0
  36. package/dist/print.js +76 -0
  37. package/dist/proc.js +63 -0
  38. package/dist/runners.js +264 -0
  39. package/dist/serial.js +9 -0
  40. package/dist/session.js +452 -0
  41. package/dist/shell.js +40 -0
  42. package/dist/skills.js +322 -0
  43. package/dist/syntax.js +80 -0
  44. package/dist/tools.js +480 -0
  45. package/dist/tui/editor.js +279 -0
  46. package/dist/tui/highlight.js +78 -0
  47. package/dist/tui/markdown.js +254 -0
  48. package/dist/tui/screen.js +146 -0
  49. package/dist/tui/tui.js +505 -0
  50. package/dist/tui/view.js +337 -0
  51. package/dist/ui.js +105 -0
  52. package/dist/update.js +64 -0
  53. package/package.json +57 -4
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Runner commands (`npm test`, `make`, `cargo test`, `./run.sh` …) do what command-defining files say: package.json's
3
+ * scripts, a Makefile, a build file, the script itself. A saved approval of such a command is tied to those files as
4
+ * they were when it was approved; when one changes, the command asks again (see Permissions.runnerChanged).
5
+ *
6
+ * A deliberate trust/UX tradeoff, not a sandbox: ordinary source and test edits never invalidate an approval, so the
7
+ * edit-test-fix loop keeps running, even though `npm test` runs whatever the edited tests do.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { readFileSync, statSync } from "node:fs";
11
+ import { isAbsolute, join, normalize, relative } from "node:path";
12
+ // Executable tool configs that npm scripts run (lint, build, format); not test configs, which change in the test loop
13
+ const JS = ["js", "cjs", "mjs", "ts", "mts", "cts"];
14
+ const NODE_CONFIGS = ["eslint.config", "prettier.config", "vite.config", "webpack.config", "rollup.config", "tsup.config", "babel.config", "postcss.config"].flatMap((b) => JS.map((e) => `${b}.${e}`));
15
+ const NODE = ["package.json", ".npmrc", ".yarnrc", ".yarnrc.yml", "pnpm-workspace.yaml", "bunfig.toml", ".eslintrc.js", ".eslintrc.cjs", ".prettierrc.js", ".prettierrc.cjs", ".babelrc.js", ...NODE_CONFIGS];
16
+ const PYTHON = ["pyproject.toml", "setup.cfg", "setup.py", "tox.ini", "pytest.ini", "noxfile.py", "Pipfile"];
17
+ const GRADLE = ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradle.properties", "gradle/wrapper/gradle-wrapper.properties"];
18
+ const MAVEN = ["pom.xml", ".mvn/maven.config", ".mvn/extensions.xml", ".mvn/wrapper/maven-wrapper.properties"];
19
+ const MAKE = ["Makefile", "makefile", "GNUmakefile"];
20
+ /** Program name (lower case) → the files, relative to the directory it runs in or a parent, that define what it does. */
21
+ const RUNNERS = {
22
+ npm: NODE, npx: NODE, pnpm: NODE, pnpx: NODE, yarn: NODE, bun: NODE, bunx: NODE, turbo: ["turbo.json", ...NODE], nx: ["nx.json", ...NODE],
23
+ deno: ["deno.json", "deno.jsonc"],
24
+ make: MAKE, gmake: MAKE,
25
+ cargo: ["Cargo.toml", "build.rs", ".cargo/config.toml", ".cargo/config", "rust-toolchain.toml", "rust-toolchain"],
26
+ go: ["go.mod", "go.work"],
27
+ gradle: GRADLE, gradlew: GRADLE, mvn: MAVEN, mvnw: MAVEN,
28
+ just: ["justfile", "Justfile", ".justfile"],
29
+ task: ["Taskfile.yml", "Taskfile.yaml", "taskfile.yml", "taskfile.yaml"],
30
+ rake: ["Rakefile", "Gemfile"], bundle: ["Gemfile"],
31
+ pytest: PYTHON, tox: PYTHON, nox: PYTHON, poetry: PYTHON, uv: PYTHON, pipenv: PYTHON, hatch: PYTHON, pdm: PYTHON,
32
+ composer: ["composer.json"],
33
+ };
34
+ const NODE_PMS = new Set(["npm", "pnpm", "yarn", "bun"]);
35
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "fish"]);
36
+ const PYTHONS = /^python(\d+(\.\d+)?)?$/;
37
+ /** Programs that run the command after them, with the options of theirs that take a value. */
38
+ const WRAPPERS = { env: ["-u", "-C", "--unset", "--chdir"], time: [], nice: ["-n"], nohup: [], command: [], exec: ["-a"], timeout: ["-s", "-k", "--signal", "--kill-after"], sudo: ["-u", "-g"], corepack: [], xargs: [] };
39
+ /** Splits a command line into simple commands' words (quotes respected; good enough to find program names). */
40
+ function segments(line) {
41
+ const out = [[]];
42
+ let word = "", quote = "", has = false;
43
+ const end = () => {
44
+ if (has)
45
+ out.at(-1).push(word);
46
+ word = "";
47
+ has = false;
48
+ };
49
+ for (let i = 0; i < line.length; i++) {
50
+ const c = line[i];
51
+ if (quote) {
52
+ if (c === quote)
53
+ quote = "";
54
+ else if (c === "\\" && quote === '"' && i + 1 < line.length)
55
+ word += line[++i];
56
+ else
57
+ word += c;
58
+ continue;
59
+ }
60
+ if (c === "'" || c === '"')
61
+ (quote = c), (has = true);
62
+ else if (c === "\\" && i + 1 < line.length)
63
+ (word += line[++i]), (has = true);
64
+ else if (/\s/.test(c) && c !== "\n")
65
+ end();
66
+ else if ("\n;|&()`".includes(c)) {
67
+ end();
68
+ if (out.at(-1).length)
69
+ out.push([]);
70
+ }
71
+ else
72
+ (word += c), (has = true);
73
+ }
74
+ end();
75
+ return out.filter((s) => s.length);
76
+ }
77
+ /** Drops wrapper programs (`timeout 120`, `nice -n 5`, `env -i A=1`, `bundle exec`, `corepack`) and assignments. */
78
+ function unwrap(words) {
79
+ for (;;) {
80
+ if (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))
81
+ words = words.slice(1);
82
+ else if (words.length > 1 && words[0].toLowerCase() === "bundle" && words[1] === "exec")
83
+ words = words.slice(2);
84
+ else if (words.length && Object.hasOwn(WRAPPERS, words[0].toLowerCase())) {
85
+ const w = words[0].toLowerCase();
86
+ let i = 1;
87
+ while (i < words.length && words[i].startsWith("-"))
88
+ i += WRAPPERS[w].includes(words[i]) ? 2 : 1;
89
+ if (w === "timeout" && i < words.length)
90
+ i++; // the duration
91
+ words = words.slice(i);
92
+ }
93
+ else
94
+ return words;
95
+ }
96
+ }
97
+ /** The value of an option given as `--opt value` or `--opt=value`. */
98
+ function opt(args, names) {
99
+ for (let i = 0; i < args.length; i++) {
100
+ for (const n of names) {
101
+ if (args[i] === n)
102
+ return args[i + 1];
103
+ if (n.startsWith("--") && args[i].startsWith(n + "="))
104
+ return args[i].slice(n.length + 1);
105
+ }
106
+ }
107
+ return undefined;
108
+ }
109
+ /** The package.json script an npm-style command runs (`npm test`, `npm run build`, `yarn lint`, `node --run x`). */
110
+ function scriptName(pm, args) {
111
+ const rest = args.filter((a) => !a.startsWith("-"));
112
+ const [sub, name] = rest;
113
+ if (!sub)
114
+ return undefined;
115
+ if (["test", "t", "tst"].includes(sub))
116
+ return "test";
117
+ if (["start", "stop", "restart"].includes(sub))
118
+ return sub;
119
+ if (sub === "run" || sub === "run-script")
120
+ return name;
121
+ if (pm !== "npm" && !["install", "add", "remove", "exec", "dlx", "x", "create", "init", "why", "info"].includes(sub))
122
+ return sub; // `yarn lint`
123
+ return undefined;
124
+ }
125
+ function scriptsOf(cwd, dir) {
126
+ try {
127
+ const j = JSON.parse(readFileSync(join(cwd, dir, "package.json"), "utf8"));
128
+ return j && typeof j.scripts === "object" && j.scripts ? j.scripts : {};
129
+ }
130
+ catch {
131
+ return {};
132
+ }
133
+ }
134
+ /**
135
+ * The command-defining files of a command line, relative to the workspace, or [] when it runs no runner. `cd dir`
136
+ * moves later parts into that directory (outside the workspace nothing is followed). With `cwd`, the package.json
137
+ * scripts a command runs are looked into too (`"lint": "./scripts/lint.sh"`).
138
+ */
139
+ export function runnerFiles(command, cwd, depth = 0, start = ".") {
140
+ const files = new Set();
141
+ let dir = start;
142
+ const inside = (d, to) => {
143
+ let p = to;
144
+ if (isAbsolute(to)) {
145
+ if (!cwd)
146
+ return undefined;
147
+ const r = relative(cwd, to);
148
+ if (r.startsWith("..") || isAbsolute(r))
149
+ return undefined;
150
+ p = r || ".";
151
+ }
152
+ else if (to.startsWith("~"))
153
+ return undefined;
154
+ else
155
+ p = join(d, to);
156
+ const n = normalize(p).replace(/\\/g, "/");
157
+ return n === ".." || n.startsWith("../") ? undefined : n;
158
+ };
159
+ const add = (d, f) => {
160
+ const p = d === undefined ? undefined : inside(d, f);
161
+ if (p)
162
+ files.add(p);
163
+ };
164
+ // a runner finds its files in the directory it runs in or the nearest parent that has them
165
+ const addUp = (d, list) => {
166
+ for (let x = d; x !== undefined; x = x === "." ? undefined : normalize(join(x, "..")).replace(/\\/g, "/"))
167
+ for (const f of list)
168
+ add(x, f);
169
+ };
170
+ for (const raw of segments(command)) {
171
+ const words = unwrap(raw);
172
+ if (!words.length)
173
+ continue;
174
+ const [prog, ...args] = words;
175
+ if (prog === "cd" || prog === "pushd") {
176
+ dir = args[0] && dir !== undefined ? inside(dir, args[0]) : undefined;
177
+ continue;
178
+ }
179
+ const base = prog.replace(/^.*[\\/]/, "").replace(/\.(cmd|bat|exe|ps1)$/i, "").toLowerCase();
180
+ const sub = (s) => {
181
+ if (s !== undefined && depth < 3 && dir !== undefined)
182
+ for (const f of runnerFiles(s, cwd, depth + 1, dir))
183
+ files.add(f);
184
+ };
185
+ // a program run by path is its own definition (./run.sh, scripts/test, ./gradlew)
186
+ if (prog.includes("/") || prog.includes("\\"))
187
+ add(dir, prog);
188
+ if (RUNNERS[base]) {
189
+ // the directory it is pointed at (npm --prefix web, yarn --cwd web, pnpm -C web, make -C sub)
190
+ const to = RUNNERS[base].includes("package.json") ? opt(args, ["--prefix", "--cwd", "-C", "--dir"]) : base.endsWith("make") ? opt(args, ["-C", "--directory"]) : undefined;
191
+ const runDir = to !== undefined && dir !== undefined ? inside(dir, to) : dir;
192
+ addUp(runDir, RUNNERS[base]);
193
+ if (base.endsWith("make")) {
194
+ const mf = opt(args, ["-f", "--file", "--makefile"]);
195
+ if (mf)
196
+ add(runDir, mf);
197
+ }
198
+ if (cwd && runDir !== undefined && NODE_PMS.has(base)) {
199
+ const name = scriptName(base, args);
200
+ const scripts = name ? scriptsOf(cwd, runDir) : {};
201
+ for (const n of name ? [`pre${name}`, name, `post${name}`] : [])
202
+ if (typeof scripts[n] === "string" && depth < 3)
203
+ for (const f of runnerFiles(scripts[n], cwd, depth + 1, runDir))
204
+ files.add(f);
205
+ }
206
+ }
207
+ if (base === "node" && opt(args, ["--run"]) !== undefined) {
208
+ addUp(dir, NODE);
209
+ const scripts = cwd && dir !== undefined ? scriptsOf(cwd, dir) : {};
210
+ const n = opt(args, ["--run"]);
211
+ if (typeof scripts[n] === "string")
212
+ sub(scripts[n]);
213
+ }
214
+ // a shell: the string after -c (also -lc, -ec …), else the script file it runs
215
+ if (SHELLS.has(base)) {
216
+ const c = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
217
+ if (c >= 0)
218
+ sub(args[c + 1]);
219
+ else {
220
+ const script = args.find((a) => !a.startsWith("-"));
221
+ if (script)
222
+ add(dir, script);
223
+ }
224
+ }
225
+ if (base === "cmd")
226
+ sub(args.slice(args.findIndex((a) => /^\/[ck]$/i.test(a)) + 1).join(" ") || undefined);
227
+ if (base === "powershell" || base === "pwsh") {
228
+ const c = args.findIndex((a) => /^-(c|command)$/i.test(a));
229
+ if (c >= 0)
230
+ sub(args.slice(c + 1).join(" "));
231
+ const f = opt(args, ["-File", "-file", "-f"]);
232
+ if (f)
233
+ add(dir, f);
234
+ }
235
+ // `python -m pytest` and friends read the Python project files; `python main.py` is ordinary source
236
+ if (PYTHONS.test(base) && args[0] === "-m" && RUNNERS[args[1]?.split(".")[0]?.toLowerCase()])
237
+ addUp(dir, PYTHON);
238
+ }
239
+ return [...files].sort();
240
+ }
241
+ const MAX_HASHED = 5 * 1024 * 1024;
242
+ /** Each file's content hash ("" when absent), so a later change can be named. */
243
+ export function runnerHashes(cwd, files) {
244
+ const out = {};
245
+ for (const f of files) {
246
+ try {
247
+ const p = join(cwd, f);
248
+ const st = statSync(p);
249
+ if (!st.isFile())
250
+ continue;
251
+ const h = createHash("sha256").update(st.size > MAX_HASHED ? `size:${st.size}:${st.mtimeMs}` : readFileSync(p));
252
+ out[f] = h.digest("hex").slice(0, 32);
253
+ }
254
+ catch {
255
+ out[f] = "";
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ /** The files whose state differs from the baseline (changed, added or removed). */
261
+ export function changedFiles(base, now) {
262
+ const keys = new Set([...Object.keys(base), ...Object.keys(now)]);
263
+ return [...keys].filter((k) => (base[k] ?? "") !== (now[k] ?? "")).sort();
264
+ }
package/dist/serial.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Runs async jobs one at a time, in call order. A job that fails does not block the ones after it. */
2
+ export function serial() {
3
+ let tail = Promise.resolve();
4
+ return (job) => {
5
+ const p = tail.then(job, job);
6
+ tail = p.catch(() => { });
7
+ return p;
8
+ };
9
+ }