amness 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// `amness plugin new <name>` and `amness plugin list`.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { scanSource, BUILTIN_GRADERS, gradeSessions, sessionFiles, describeProjectPlugins, checkPolicies, importPlugins, loadPlugins, pluginTemplate, projectPluginDir } from "../src/index.js";
|
|
6
|
+
import { dim, green, red } from "./ui.js";
|
|
7
|
+
export const PLUGIN_HELP = `usage: amness plugin <command>
|
|
8
|
+
new <name> create .amness/plugins/<name>.ts from a template (never overwrites)
|
|
9
|
+
policy <traces...> --plugin <file> replay the tool calls recorded in traces (a file, or a folder such as .amness/traces) through the plugin policies and show what each would allow, ask or deny
|
|
10
|
+
grade <traces...> [--plugin <file>] [--grader a,b] score real recorded sessions (a trace file, or a folder such as .amness/traces) with built-in or plugin graders
|
|
11
|
+
add <file|https-url> copy a plugin file into .amness/plugins/ (never overwrites) and show what it appears to touch; it still needs your approval the first time you start Amness
|
|
12
|
+
list show the plugins that load here (project plugins need --trust-project to be listed as loaded)
|
|
13
|
+
A plugin is a TypeScript file that runs inside Amness: guardrails, loop rules, tools, slash commands and eval variants. See docs/plugins.md.`;
|
|
14
|
+
export async function runPlugin(argv, cwd = process.cwd()) {
|
|
15
|
+
const [cmd, name] = argv, root = resolve(cwd);
|
|
16
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
17
|
+
console.log(PLUGIN_HELP);
|
|
18
|
+
return cmd ? 0 : 2;
|
|
19
|
+
}
|
|
20
|
+
if (cmd === "new") {
|
|
21
|
+
if (!name || !/^[\w.-]{1,40}$/.test(name)) {
|
|
22
|
+
console.error(red("✗ ") + "give a name made of letters, numbers, . _ - (for example: amness plugin new house-rules)");
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
const file = join(projectPluginDir(root), `${name}.ts`);
|
|
26
|
+
if (existsSync(file)) {
|
|
27
|
+
console.error(red("✗ ") + `${file} already exists`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
mkdirSync(projectPluginDir(root), { recursive: true });
|
|
31
|
+
writeFileSync(file, pluginTemplate(name));
|
|
32
|
+
console.log(green("created ") + file + "\n" + dim("Start the chat: it will ask you to approve this project plugin. Edit it, then /reload-plugins."));
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (cmd === "list") {
|
|
36
|
+
const r = await loadPlugins(root, { trustProject: argv.includes("--trust-project") });
|
|
37
|
+
for (const p of r.plugins)
|
|
38
|
+
console.log(`${p.name.padEnd(24)} ${p.description ?? ""}`);
|
|
39
|
+
for (const w of r.warnings)
|
|
40
|
+
console.error(red("! ") + w);
|
|
41
|
+
if (r.untrusted) {
|
|
42
|
+
console.log(dim(`${r.untrusted.count} project plugin file(s) not loaded (not approved); add --trust-project to load them here. What they appear to touch (a text scan, not a guarantee):`));
|
|
43
|
+
for (const l of describeProjectPlugins(root))
|
|
44
|
+
console.log(" " + l);
|
|
45
|
+
}
|
|
46
|
+
if (!r.plugins.length && !r.untrusted)
|
|
47
|
+
console.log("no plugins");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
if (cmd === "add") {
|
|
51
|
+
if (!name) {
|
|
52
|
+
console.error(red("✗ ") + "usage: amness plugin add <file or https URL>");
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
let text, base;
|
|
56
|
+
try {
|
|
57
|
+
if (/^https:\/\//.test(name)) {
|
|
58
|
+
const r = await fetch(name);
|
|
59
|
+
if (!r.ok)
|
|
60
|
+
throw new Error(`HTTP ${r.status}`);
|
|
61
|
+
text = await r.text();
|
|
62
|
+
base = basename(new URL(name).pathname) || "plugin.ts";
|
|
63
|
+
}
|
|
64
|
+
else if (/^http:\/\//.test(name)) {
|
|
65
|
+
console.error(red("✗ ") + "only https URLs are accepted");
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
text = readFileSync(resolve(name), "utf8");
|
|
70
|
+
base = basename(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.error(red("✗ ") + `could not read ${name}: ${e.message}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (text.length > 300_000) {
|
|
78
|
+
console.error(red("✗ ") + "that file is too large for a plugin (300 KB limit)");
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (!/\bexport\s+default\b|\bexport\s*\{[^}]*\bas default\b|module\.exports/.test(text)) {
|
|
82
|
+
console.error(red("✗ ") + "this does not look like a plugin: it has no `export default`");
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
const file = join(projectPluginDir(root), /\.(m?[jt]s)$/.test(base) ? base : base + ".ts");
|
|
86
|
+
if (existsSync(file)) {
|
|
87
|
+
console.error(red("✗ ") + `${file} already exists`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
mkdirSync(projectPluginDir(root), { recursive: true });
|
|
91
|
+
writeFileSync(file, text);
|
|
92
|
+
const caps = scanSource(text);
|
|
93
|
+
console.log(green("added ") + file);
|
|
94
|
+
console.log(caps.length ? "It appears to: " + caps.map((c) => c.label).join("; ") : "It shows no shell, network, file or environment access (a text scan, not a guarantee).");
|
|
95
|
+
console.log(dim("Read it before you trust it. Amness asks for your approval the first time it starts in this project."));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
if (cmd === "grade") {
|
|
99
|
+
const rest = argv.slice(1), take = (f) => { const i = rest.indexOf(f); return i >= 0 ? rest.splice(i, 2)[1] : undefined; };
|
|
100
|
+
const pluginFiles = (take("--plugin") ?? "").split(",").filter(Boolean), pick = (take("--grader") ?? "").split(",").filter(Boolean), inputs = rest.length ? rest : [join(root, ".amness", "traces")];
|
|
101
|
+
const graders = { ...BUILTIN_GRADERS };
|
|
102
|
+
for (const f of pluginFiles) {
|
|
103
|
+
const r = await importPlugins(f);
|
|
104
|
+
for (const w of r.warnings)
|
|
105
|
+
console.error(red("! ") + w);
|
|
106
|
+
for (const p of r.plugins)
|
|
107
|
+
Object.assign(graders, p.graders ?? {});
|
|
108
|
+
}
|
|
109
|
+
const chosen = pick.length ? Object.fromEntries(pick.map((n) => { if (!graders[n])
|
|
110
|
+
throw new Error(`unknown grader "${n}". Known: ${Object.keys(graders).join(", ")}`); return [n, graders[n]]; })) : graders;
|
|
111
|
+
const files = sessionFiles(inputs);
|
|
112
|
+
if (!files.length) {
|
|
113
|
+
console.error(red("✗ ") + "no trace files found");
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
const reports = await gradeSessions(files, chosen);
|
|
117
|
+
console.log(`${files.length} session file(s)`);
|
|
118
|
+
for (const r of reports) {
|
|
119
|
+
console.log(`${r.grader.padEnd(18)} mean ${Number.isNaN(r.mean) ? "n/a" : (r.mean * 100).toFixed(0) + "%"} over ${r.sessions} session(s)`);
|
|
120
|
+
for (const l of r.low)
|
|
121
|
+
console.log(dim(` ${(l.score * 100).toFixed(0).padStart(3)}% ${l.session} ${l.task.slice(0, 60)}${l.note ? " - " + l.note : ""}`));
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (cmd === "policy") {
|
|
126
|
+
const rest = argv.slice(1), pi = rest.indexOf("--plugin"), pluginFiles = pi >= 0 ? (rest.splice(pi, 2)[1] ?? "").split(",").filter(Boolean) : [], inputs = rest;
|
|
127
|
+
if (!pluginFiles.length || !inputs.length) {
|
|
128
|
+
console.error(red("✗ ") + "usage: amness plugin policy <trace file or folder>... --plugin <file>");
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const plugins = [];
|
|
132
|
+
for (const f of pluginFiles) {
|
|
133
|
+
const r = await importPlugins(f);
|
|
134
|
+
for (const w of r.warnings)
|
|
135
|
+
console.error(red("! ") + w);
|
|
136
|
+
plugins.push(...r.plugins);
|
|
137
|
+
}
|
|
138
|
+
if (!plugins.some((p) => p.policy)) {
|
|
139
|
+
console.error(red("✗ ") + "none of these plugins defines a policy");
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
const files = inputs.flatMap((p) => (existsSync(p) && statSync(p).isDirectory() ? readdirSync(p).filter((f) => f.endsWith(".jsonl")).map((f) => join(p, f)) : [p]));
|
|
143
|
+
const tally = { allow: 0, ask: 0, deny: 0, none: 0 }, shown = [];
|
|
144
|
+
for (const f of files)
|
|
145
|
+
for (const line of readFileSync(f, "utf8").split("\n")) {
|
|
146
|
+
if (!line.includes('"tool_calls"'))
|
|
147
|
+
continue;
|
|
148
|
+
let ev;
|
|
149
|
+
try {
|
|
150
|
+
ev = JSON.parse(line);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const c of ev.message?.tool_calls ?? []) {
|
|
156
|
+
let args = {};
|
|
157
|
+
try {
|
|
158
|
+
args = JSON.parse(c.function.arguments || "{}");
|
|
159
|
+
}
|
|
160
|
+
catch { /* keep empty */ }
|
|
161
|
+
const d = await checkPolicies(plugins, { tool: c.function.name, args, cwd: root });
|
|
162
|
+
tally[d?.action ?? "none"]++;
|
|
163
|
+
if (d && d.action !== "allow" && shown.length < 25)
|
|
164
|
+
shown.push(`${d.action.padEnd(5)} ${c.function.name} ${JSON.stringify(args).slice(0, 90)} ${dim(d.reason ?? "")}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log(`${files.length} trace file(s): ${tally.deny} denied, ${tally.ask} would ask, ${tally.allow} allowed by the policy, ${tally.none} without an opinion`);
|
|
168
|
+
for (const s of shown)
|
|
169
|
+
console.log(" " + s);
|
|
170
|
+
if (!shown.length)
|
|
171
|
+
console.log(dim(" nothing was denied or asked"));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
console.error(PLUGIN_HELP);
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const SCHEDULE_HELP = "usage: amness schedule <command>\n add <name> \"<cron>\" \"<task>\" [--verify <cmd>] save a recurring task for this project (5-field cron, e.g. \"0 3 * * 1-5\")\n list show this project's schedules\n remove <name> delete one\n install [--write] print the crontab lines; with --write, put them in your crontab (only a marked block for this project is touched)\n uninstall remove this project's block from your crontab\n Runs are headless (`amness run --json`), risky commands are denied, and each run's output is appended to .amness/schedule-logs/<name>.jsonl.\n Cron must be able to find `amness` and your API key; set them in the crontab environment or use a wrapper script.";
|
|
2
|
+
export declare function runSchedule(argv: string[], cwd?: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `amness schedule`: recurring headless runs via cron.
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { add, cronLines, ensureLogDir, load, mergeCrontab, remove } from "../src/index.js";
|
|
5
|
+
import { bold, dim, green, red } from "./ui.js";
|
|
6
|
+
export const SCHEDULE_HELP = `usage: amness schedule <command>
|
|
7
|
+
add <name> "<cron>" "<task>" [--verify <cmd>] save a recurring task for this project (5-field cron, e.g. "0 3 * * 1-5")
|
|
8
|
+
list show this project's schedules
|
|
9
|
+
remove <name> delete one
|
|
10
|
+
install [--write] print the crontab lines; with --write, put them in your crontab (only a marked block for this project is touched)
|
|
11
|
+
uninstall remove this project's block from your crontab
|
|
12
|
+
Runs are headless (\`amness run --json\`), risky commands are denied, and each run's output is appended to .amness/schedule-logs/<name>.jsonl.
|
|
13
|
+
Cron must be able to find \`amness\` and your API key; set them in the crontab environment or use a wrapper script.`;
|
|
14
|
+
export function runSchedule(argv, cwd = process.cwd()) {
|
|
15
|
+
const root = resolve(cwd), [cmd, ...rest] = argv;
|
|
16
|
+
const val = (f) => { const i = rest.indexOf(f); return i >= 0 ? rest[i + 1] : undefined; };
|
|
17
|
+
try {
|
|
18
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
19
|
+
console.log(SCHEDULE_HELP);
|
|
20
|
+
return cmd ? 0 : 2;
|
|
21
|
+
}
|
|
22
|
+
if (cmd === "add") {
|
|
23
|
+
const [name, cron, task] = rest;
|
|
24
|
+
add(root, { name, cron, task, verify: val("--verify") });
|
|
25
|
+
console.log(`saved "${name}". Run \`amness schedule install\` to see the crontab line.`);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if (cmd === "list") {
|
|
29
|
+
const l = load(root);
|
|
30
|
+
if (!l.length)
|
|
31
|
+
console.log("no schedules for this project");
|
|
32
|
+
for (const s of l)
|
|
33
|
+
console.log(`${bold(s.name.padEnd(16))} ${s.cron.padEnd(14)} ${s.task}${s.verify ? dim(" verify: " + s.verify) : ""}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (cmd === "remove") {
|
|
37
|
+
console.log(remove(root, rest[0]) ? `removed "${rest[0]}"` : `no schedule named "${rest[0]}"`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (cmd === "install" || cmd === "uninstall") {
|
|
41
|
+
const lines = cmd === "install" ? cronLines(root, load(root)) : [];
|
|
42
|
+
if (cmd === "install" && !rest.includes("--write")) {
|
|
43
|
+
console.log(lines.length ? lines.join("\n") : "no schedules to install");
|
|
44
|
+
console.log(dim("\n(not written; pass --write to add these to your crontab)"));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
let current = "";
|
|
48
|
+
try {
|
|
49
|
+
current = execFileSync("crontab", ["-l"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
50
|
+
}
|
|
51
|
+
catch { /* none yet */ }
|
|
52
|
+
ensureLogDir(root);
|
|
53
|
+
const r = spawnSync("crontab", ["-"], { input: mergeCrontab(current, root, lines), encoding: "utf8" });
|
|
54
|
+
if (r.status !== 0)
|
|
55
|
+
throw new Error("crontab refused the update: " + (r.stderr || "unknown error").trim());
|
|
56
|
+
console.log(green(cmd === "install" ? `installed ${lines.length} schedule(s) in your crontab` : "removed this project's schedules from your crontab"));
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
console.error(SCHEDULE_HELP);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.error(red("✗ " + e.message));
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type State = {
|
|
2
|
+
lastDir?: string;
|
|
3
|
+
verifyByDir: Record<string, string>;
|
|
4
|
+
tasks: string[]; /** project hooks the user approved: directory -> fingerprint of the hooks.json that was approved */
|
|
5
|
+
hookTrust?: Record<string, string>; /** project plugins the user approved: directory -> fingerprint of the plugin files */
|
|
6
|
+
pluginTrust?: Record<string, string>;
|
|
7
|
+
};
|
|
8
|
+
export declare function loadState(): State;
|
|
9
|
+
export declare function saveState(s: State): void;
|
|
10
|
+
/** Most-recent-first, de-duplicated, capped: the shape readline's `history` wants. */
|
|
11
|
+
export declare function rememberTask(s: State, task: string): State;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Small remembered state so the TUI stops asking things it already knows: last project, per-project verify command, task history.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { home } from "./config.js";
|
|
5
|
+
const file = () => join(home(), "state.json");
|
|
6
|
+
export function loadState() {
|
|
7
|
+
try {
|
|
8
|
+
if (existsSync(file()))
|
|
9
|
+
return { verifyByDir: {}, tasks: [], ...JSON.parse(readFileSync(file(), "utf8")) };
|
|
10
|
+
}
|
|
11
|
+
catch { /* corrupt: start fresh */ }
|
|
12
|
+
return { verifyByDir: {}, tasks: [] };
|
|
13
|
+
}
|
|
14
|
+
export function saveState(s) { mkdirSync(home(), { recursive: true }); writeFileSync(file(), JSON.stringify(s, null, 2)); }
|
|
15
|
+
/** Most-recent-first, de-duplicated, capped: the shape readline's `history` wants. */
|
|
16
|
+
export function rememberTask(s, task) {
|
|
17
|
+
const t = task.trim();
|
|
18
|
+
return t ? { ...s, tasks: [t, ...s.tasks.filter((x) => x !== t)].slice(0, 50) } : s;
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Colour: -1 = terminal default, 0x000000..0xFFFFFF = truecolor, 0x1000000 + n = 256-palette index n. */
|
|
2
|
+
export declare const DEFAULT = -1;
|
|
3
|
+
export declare const A: {
|
|
4
|
+
readonly bold: 1;
|
|
5
|
+
readonly dim: 2;
|
|
6
|
+
readonly italic: 4;
|
|
7
|
+
readonly underline: 8;
|
|
8
|
+
readonly strike: 16;
|
|
9
|
+
readonly inverse: 32;
|
|
10
|
+
};
|
|
11
|
+
export type Cell = {
|
|
12
|
+
ch: string;
|
|
13
|
+
w: number;
|
|
14
|
+
fg: number;
|
|
15
|
+
bg: number;
|
|
16
|
+
attr: number;
|
|
17
|
+
};
|
|
18
|
+
/** Resolve any colour value to 0xRRGGBB (for dimming/compositing and for screenshots). `def` stands in for the terminal default. */
|
|
19
|
+
export declare function toRgb(c: number, def: number): number;
|
|
20
|
+
export declare const rgb: (r: number, g: number, b: number) => number;
|
|
21
|
+
export declare const mix: (a: number, b: number, t: number) => number;
|
|
22
|
+
type St = {
|
|
23
|
+
fg: number;
|
|
24
|
+
bg: number;
|
|
25
|
+
attr: number;
|
|
26
|
+
};
|
|
27
|
+
/** Apply the parameters of one SGR sequence (the part between `ESC[` and `m`) to a style. */
|
|
28
|
+
export declare function applySgr(st: St, params: string): void;
|
|
29
|
+
/** Parse an ANSI string into styled cells. Wide characters become one cell with w = 2. Unknown escape sequences are dropped. */
|
|
30
|
+
export declare function parseAnsi(s: string, base?: Partial<St>): Cell[];
|
|
31
|
+
/** Cells back to an ANSI string (minimal SGR changes). */
|
|
32
|
+
export declare function cellsToAnsi(cells: Cell[]): string;
|
|
33
|
+
/** Full SGR (reset + everything) to reach a given style from any state. */
|
|
34
|
+
export declare function sgrFor(fg: number, bg: number, attr: number): string;
|
|
35
|
+
/** SGR fragments for a colour (truecolor, 256-palette index, or default). */
|
|
36
|
+
export declare const fgSeq: (c: number) => string;
|
|
37
|
+
export declare const bgSeq: (c: number) => string;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ANSI strings <-> styled cells. The renderers produce ANSI strings; the full-screen canvas and the screenshot tool need cells.
|
|
2
|
+
import { cpWidth } from "./text.js";
|
|
3
|
+
/** Colour: -1 = terminal default, 0x000000..0xFFFFFF = truecolor, 0x1000000 + n = 256-palette index n. */
|
|
4
|
+
export const DEFAULT = -1;
|
|
5
|
+
export const A = { bold: 1, dim: 2, italic: 4, underline: 8, strike: 16, inverse: 32 };
|
|
6
|
+
const BASE16 = [0x000000, 0xcd3131, 0x0dbc79, 0xe5e510, 0x2472c8, 0xbc3fbc, 0x11a8cd, 0xe5e5e5, 0x666666, 0xf14c4c, 0x23d18b, 0xf5f543, 0x3b8eea, 0xd670d6, 0x29b8db, 0xffffff];
|
|
7
|
+
/** Resolve any colour value to 0xRRGGBB (for dimming/compositing and for screenshots). `def` stands in for the terminal default. */
|
|
8
|
+
export function toRgb(c, def) {
|
|
9
|
+
if (c === DEFAULT)
|
|
10
|
+
return def;
|
|
11
|
+
if (c < 0x1000000)
|
|
12
|
+
return c;
|
|
13
|
+
const i = c - 0x1000000;
|
|
14
|
+
if (i < 16)
|
|
15
|
+
return BASE16[i];
|
|
16
|
+
if (i >= 232) {
|
|
17
|
+
const v = 8 + (i - 232) * 10;
|
|
18
|
+
return (v << 16) | (v << 8) | v;
|
|
19
|
+
}
|
|
20
|
+
const n = i - 16, lv = (x) => (x === 0 ? 0 : 55 + x * 40);
|
|
21
|
+
return (lv(Math.floor(n / 36)) << 16) | (lv(Math.floor(n / 6) % 6) << 8) | lv(n % 6);
|
|
22
|
+
}
|
|
23
|
+
export const rgb = (r, g, b) => (r << 16) | (g << 8) | b;
|
|
24
|
+
export const mix = (a, b, t) => rgb(...[16, 8, 0].map((s) => Math.round(((a >> s) & 255) * (1 - t) + ((b >> s) & 255) * t)));
|
|
25
|
+
/** Apply the parameters of one SGR sequence (the part between `ESC[` and `m`) to a style. */
|
|
26
|
+
export function applySgr(st, params) {
|
|
27
|
+
const p = params === "" ? [0] : params.split(/[;:]/).map((x) => (x === "" ? 0 : Number(x)));
|
|
28
|
+
for (let i = 0; i < p.length; i++) {
|
|
29
|
+
const n = p[i];
|
|
30
|
+
if (n === 0) {
|
|
31
|
+
st.fg = DEFAULT;
|
|
32
|
+
st.bg = DEFAULT;
|
|
33
|
+
st.attr = 0;
|
|
34
|
+
}
|
|
35
|
+
else if (n === 1)
|
|
36
|
+
st.attr |= A.bold;
|
|
37
|
+
else if (n === 2)
|
|
38
|
+
st.attr |= A.dim;
|
|
39
|
+
else if (n === 3)
|
|
40
|
+
st.attr |= A.italic;
|
|
41
|
+
else if (n === 4)
|
|
42
|
+
st.attr |= A.underline;
|
|
43
|
+
else if (n === 7)
|
|
44
|
+
st.attr |= A.inverse;
|
|
45
|
+
else if (n === 9)
|
|
46
|
+
st.attr |= A.strike;
|
|
47
|
+
else if (n === 22)
|
|
48
|
+
st.attr &= ~(A.bold | A.dim);
|
|
49
|
+
else if (n === 23)
|
|
50
|
+
st.attr &= ~A.italic;
|
|
51
|
+
else if (n === 24)
|
|
52
|
+
st.attr &= ~A.underline;
|
|
53
|
+
else if (n === 27)
|
|
54
|
+
st.attr &= ~A.inverse;
|
|
55
|
+
else if (n === 29)
|
|
56
|
+
st.attr &= ~A.strike;
|
|
57
|
+
else if (n >= 30 && n <= 37)
|
|
58
|
+
st.fg = 0x1000000 + n - 30;
|
|
59
|
+
else if (n >= 90 && n <= 97)
|
|
60
|
+
st.fg = 0x1000000 + n - 90 + 8;
|
|
61
|
+
else if (n === 39)
|
|
62
|
+
st.fg = DEFAULT;
|
|
63
|
+
else if (n >= 40 && n <= 47)
|
|
64
|
+
st.bg = 0x1000000 + n - 40;
|
|
65
|
+
else if (n >= 100 && n <= 107)
|
|
66
|
+
st.bg = 0x1000000 + n - 100 + 8;
|
|
67
|
+
else if (n === 49)
|
|
68
|
+
st.bg = DEFAULT;
|
|
69
|
+
else if (n === 38 || n === 48) {
|
|
70
|
+
const key = n === 38 ? "fg" : "bg";
|
|
71
|
+
if (p[i + 1] === 2) {
|
|
72
|
+
st[key] = rgb(p[i + 2] & 255, p[i + 3] & 255, p[i + 4] & 255);
|
|
73
|
+
i += 4;
|
|
74
|
+
}
|
|
75
|
+
else if (p[i + 1] === 5) {
|
|
76
|
+
st[key] = 0x1000000 + (p[i + 2] & 255);
|
|
77
|
+
i += 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Parse an ANSI string into styled cells. Wide characters become one cell with w = 2. Unknown escape sequences are dropped. */
|
|
83
|
+
export function parseAnsi(s, base = {}) {
|
|
84
|
+
const st = { fg: base.fg ?? DEFAULT, bg: base.bg ?? DEFAULT, attr: base.attr ?? 0 }, out = [];
|
|
85
|
+
const re = /\x1b\[([0-9;:?]*)([A-Za-z])|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|([\s\S])/gu;
|
|
86
|
+
for (let m; (m = re.exec(s));) {
|
|
87
|
+
if (m[2] === "m") {
|
|
88
|
+
const b = { ...base };
|
|
89
|
+
applySgr(st, m[1]);
|
|
90
|
+
void b;
|
|
91
|
+
}
|
|
92
|
+
else if (m[3] !== undefined) {
|
|
93
|
+
const cp = m[3].codePointAt(0), w = cpWidth(cp);
|
|
94
|
+
if (w === 0) {
|
|
95
|
+
if (out.length && cp >= 0x300)
|
|
96
|
+
out[out.length - 1].ch += m[3];
|
|
97
|
+
continue;
|
|
98
|
+
} // combining marks join the previous cell
|
|
99
|
+
out.push({ ch: m[3], w, fg: st.fg, bg: st.bg, attr: st.attr });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** Cells back to an ANSI string (minimal SGR changes). */
|
|
105
|
+
export function cellsToAnsi(cells) {
|
|
106
|
+
let out = "", fg = DEFAULT, bg = DEFAULT, attr = 0;
|
|
107
|
+
for (const c of cells) {
|
|
108
|
+
if (c.fg !== fg || c.bg !== bg || c.attr !== attr) {
|
|
109
|
+
out += sgrFor(c.fg, c.bg, c.attr);
|
|
110
|
+
fg = c.fg;
|
|
111
|
+
bg = c.bg;
|
|
112
|
+
attr = c.attr;
|
|
113
|
+
}
|
|
114
|
+
out += c.ch;
|
|
115
|
+
}
|
|
116
|
+
return out + (fg !== DEFAULT || bg !== DEFAULT || attr ? "\x1b[0m" : "");
|
|
117
|
+
}
|
|
118
|
+
const colorSeq = (c, base) => (c === DEFAULT ? String(base + 9) : c >= 0x1000000 ? (c - 0x1000000 < 8 ? String(base + c - 0x1000000) : c - 0x1000000 < 16 ? String(base + 60 + c - 0x1000000 - 8) : `${base + 8};5;${c - 0x1000000}`) : `${base + 8};2;${(c >> 16) & 255};${(c >> 8) & 255};${c & 255}`);
|
|
119
|
+
/** Full SGR (reset + everything) to reach a given style from any state. */
|
|
120
|
+
export function sgrFor(fg, bg, attr) {
|
|
121
|
+
const p = ["0"];
|
|
122
|
+
if (attr & A.bold)
|
|
123
|
+
p.push("1");
|
|
124
|
+
if (attr & A.dim)
|
|
125
|
+
p.push("2");
|
|
126
|
+
if (attr & A.italic)
|
|
127
|
+
p.push("3");
|
|
128
|
+
if (attr & A.underline)
|
|
129
|
+
p.push("4");
|
|
130
|
+
if (attr & A.inverse)
|
|
131
|
+
p.push("7");
|
|
132
|
+
if (attr & A.strike)
|
|
133
|
+
p.push("9");
|
|
134
|
+
if (fg !== DEFAULT)
|
|
135
|
+
p.push(colorSeq(fg, 30));
|
|
136
|
+
if (bg !== DEFAULT)
|
|
137
|
+
p.push(colorSeq(bg, 40));
|
|
138
|
+
return `\x1b[${p.join(";")}m`;
|
|
139
|
+
}
|
|
140
|
+
/** SGR fragments for a colour (truecolor, 256-palette index, or default). */
|
|
141
|
+
export const fgSeq = (c) => (c === DEFAULT ? "\x1b[39m" : c >= 0x1000000 ? `\x1b[38;5;${c - 0x1000000}m` : `\x1b[38;2;${(c >> 16) & 255};${(c >> 8) & 255};${c & 255}m`);
|
|
142
|
+
export const bgSeq = (c) => (c === DEFAULT ? "\x1b[49m" : c >= 0x1000000 ? `\x1b[48;5;${c - 0x1000000}m` : `\x1b[48;2;${(c >> 16) & 255};${(c >> 8) & 255};${c & 255}m`);
|