@agentplate/cli 1.0.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 +54 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/agents/architect.md +108 -0
- package/agents/builder.md +97 -0
- package/agents/coordinator.md +113 -0
- package/agents/deployer.md +117 -0
- package/agents/devops.md +114 -0
- package/agents/lead.md +107 -0
- package/agents/merger.md +103 -0
- package/agents/reviewer.md +90 -0
- package/agents/scout.md +95 -0
- package/agents/verifier.md +106 -0
- package/package.json +64 -0
- package/src/agents/guard-rules.ts +55 -0
- package/src/agents/identity.test.ts +161 -0
- package/src/agents/identity.ts +229 -0
- package/src/agents/manifest.test.ts +260 -0
- package/src/agents/manifest.ts +286 -0
- package/src/agents/overlay.test.ts +190 -0
- package/src/agents/overlay.ts +212 -0
- package/src/agents/system-prompt.test.ts +53 -0
- package/src/agents/system-prompt.ts +95 -0
- package/src/agents/turn-runner.ts +79 -0
- package/src/commands/coordinator.test.ts +75 -0
- package/src/commands/coordinator.ts +259 -0
- package/src/commands/deploy.test.ts +504 -0
- package/src/commands/deploy.ts +874 -0
- package/src/commands/doctor.test.ts +106 -0
- package/src/commands/doctor.ts +208 -0
- package/src/commands/init.ts +71 -0
- package/src/commands/log.ts +51 -0
- package/src/commands/mail.ts +197 -0
- package/src/commands/merge.ts +127 -0
- package/src/commands/model.ts +58 -0
- package/src/commands/prime.ts +61 -0
- package/src/commands/reap.ts +87 -0
- package/src/commands/serve.ts +61 -0
- package/src/commands/setup.ts +48 -0
- package/src/commands/ship.test.ts +106 -0
- package/src/commands/ship.ts +202 -0
- package/src/commands/skill.test.ts +458 -0
- package/src/commands/skill.ts +730 -0
- package/src/commands/sling.ts +365 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/stop.ts +56 -0
- package/src/commands/tui.ts +199 -0
- package/src/commands/worktree.ts +77 -0
- package/src/config.test.ts +92 -0
- package/src/config.ts +202 -0
- package/src/db/sqlite.test.ts +77 -0
- package/src/db/sqlite.ts +102 -0
- package/src/deploy/audit.test.ts +233 -0
- package/src/deploy/audit.ts +245 -0
- package/src/deploy/context.test.ts +243 -0
- package/src/deploy/context.ts +72 -0
- package/src/deploy/registry.test.ts +101 -0
- package/src/deploy/registry.ts +86 -0
- package/src/deploy/secrets.test.ts +129 -0
- package/src/deploy/secrets.ts +69 -0
- package/src/deploy/targets/docker-gha.test.ts +323 -0
- package/src/deploy/targets/docker-gha.ts +841 -0
- package/src/deploy/types.ts +153 -0
- package/src/errors.test.ts +42 -0
- package/src/errors.ts +69 -0
- package/src/events/store.test.ts +183 -0
- package/src/events/store.ts +201 -0
- package/src/index.ts +137 -0
- package/src/insights/quality-gates.ts +73 -0
- package/src/json.test.ts +28 -0
- package/src/json.ts +50 -0
- package/src/logging/color.ts +62 -0
- package/src/logging/logger.ts +60 -0
- package/src/logging/sanitizer.test.ts +36 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/client.test.ts +192 -0
- package/src/mail/client.ts +188 -0
- package/src/mail/store.test.ts +279 -0
- package/src/mail/store.ts +311 -0
- package/src/merge/lock.test.ts +88 -0
- package/src/merge/lock.ts +84 -0
- package/src/merge/queue.test.ts +136 -0
- package/src/merge/queue.ts +177 -0
- package/src/merge/resolver.test.ts +219 -0
- package/src/merge/resolver.ts +274 -0
- package/src/paths.ts +36 -0
- package/src/providers/apply.test.ts +90 -0
- package/src/providers/apply.ts +66 -0
- package/src/providers/registry.test.ts +74 -0
- package/src/providers/registry.ts +254 -0
- package/src/runtimes/claude.ts +313 -0
- package/src/runtimes/codex.ts +280 -0
- package/src/runtimes/cursor.ts +247 -0
- package/src/runtimes/gemini.ts +173 -0
- package/src/runtimes/mock.ts +71 -0
- package/src/runtimes/opencode.ts +259 -0
- package/src/runtimes/registry.test.ts +924 -0
- package/src/runtimes/registry.ts +63 -0
- package/src/runtimes/resolve.ts +45 -0
- package/src/runtimes/types.ts +97 -0
- package/src/scaffold.ts +68 -0
- package/src/secrets.test.ts +51 -0
- package/src/secrets.ts +78 -0
- package/src/serve/api.ts +667 -0
- package/src/serve/server.test.ts +433 -0
- package/src/serve/server.ts +271 -0
- package/src/serve/system.ts +90 -0
- package/src/serve/weather.ts +140 -0
- package/src/sessions/reaper.test.ts +162 -0
- package/src/sessions/reaper.ts +149 -0
- package/src/sessions/store.test.ts +351 -0
- package/src/sessions/store.ts +350 -0
- package/src/skills/distiller.test.ts +498 -0
- package/src/skills/distiller.ts +426 -0
- package/src/skills/feedback.test.ts +300 -0
- package/src/skills/feedback.ts +168 -0
- package/src/skills/lifecycle.ts +169 -0
- package/src/skills/retrieval.test.ts +421 -0
- package/src/skills/retrieval.ts +365 -0
- package/src/skills/safety.test.ts +335 -0
- package/src/skills/safety.ts +216 -0
- package/src/skills/store.test.ts +425 -0
- package/src/skills/store.ts +684 -0
- package/src/skills/types.ts +107 -0
- package/src/types.ts +442 -0
- package/src/utils/detect.test.ts +35 -0
- package/src/utils/detect.ts +82 -0
- package/src/version.test.ts +19 -0
- package/src/version.ts +7 -0
- package/src/wizard/setup.ts +254 -0
- package/src/worktree/manager.test.ts +181 -0
- package/src/worktree/manager.ts +229 -0
- package/templates/overlay.md.tmpl +102 -0
- package/ui/dist/assets/index-C7rXIMER.css +1 -0
- package/ui/dist/assets/index-W4kbr4by.js +4526 -0
- package/ui/dist/favicon.svg +21 -0
- package/ui/dist/index.html +16 -0
- package/ui/dist/logo-clay.svg +21 -0
- package/ui/dist/logo.svg +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agentplate CLI — main entry point and command router.
|
|
5
|
+
*
|
|
6
|
+
* Usage: agentplate <command> [args...] (alias: ap)
|
|
7
|
+
*
|
|
8
|
+
* Phase 0 wires the program shell, global flags, version handling, a consistent
|
|
9
|
+
* top-level error handler, and the first real command (`doctor`). Subsequent
|
|
10
|
+
* phases register their commands here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import { createCoordinatorCommand } from "./commands/coordinator.ts";
|
|
15
|
+
import {
|
|
16
|
+
createDeployCommand,
|
|
17
|
+
createRollbackCommand,
|
|
18
|
+
createTargetCommand,
|
|
19
|
+
} from "./commands/deploy.ts";
|
|
20
|
+
import { createDoctorCommand } from "./commands/doctor.ts";
|
|
21
|
+
import { createInitCommand } from "./commands/init.ts";
|
|
22
|
+
import { createLogCommand } from "./commands/log.ts";
|
|
23
|
+
import { createMailCommand } from "./commands/mail.ts";
|
|
24
|
+
import { createMergeCommand } from "./commands/merge.ts";
|
|
25
|
+
import { createModelCommand } from "./commands/model.ts";
|
|
26
|
+
import { createPrimeCommand } from "./commands/prime.ts";
|
|
27
|
+
import { createReapCommand } from "./commands/reap.ts";
|
|
28
|
+
import { createServeCommand } from "./commands/serve.ts";
|
|
29
|
+
import { createSetupCommand } from "./commands/setup.ts";
|
|
30
|
+
import { createShipCommand } from "./commands/ship.ts";
|
|
31
|
+
import { createSkillCommand } from "./commands/skill.ts";
|
|
32
|
+
import { createSlingCommand } from "./commands/sling.ts";
|
|
33
|
+
import { createStatusCommand } from "./commands/status.ts";
|
|
34
|
+
import { createStopCommand } from "./commands/stop.ts";
|
|
35
|
+
import { createTuiCommand } from "./commands/tui.ts";
|
|
36
|
+
import { createWorktreeCommand } from "./commands/worktree.ts";
|
|
37
|
+
import { setProjectRootOverride } from "./config.ts";
|
|
38
|
+
import { isAgentplateError } from "./errors.ts";
|
|
39
|
+
import { jsonError } from "./json.ts";
|
|
40
|
+
import { brand, muted, printError, setQuiet } from "./logging/color.ts";
|
|
41
|
+
import { VERSION } from "./version.ts";
|
|
42
|
+
|
|
43
|
+
export { VERSION };
|
|
44
|
+
|
|
45
|
+
interface GlobalOptions {
|
|
46
|
+
json?: boolean;
|
|
47
|
+
verbose?: boolean;
|
|
48
|
+
quiet?: boolean;
|
|
49
|
+
project?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rawArgs = process.argv.slice(2);
|
|
53
|
+
|
|
54
|
+
// Handle `--version --json` before Commander consumes the flag.
|
|
55
|
+
if ((rawArgs.includes("-v") || rawArgs.includes("--version")) && rawArgs.includes("--json")) {
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
`${JSON.stringify({
|
|
58
|
+
name: "agentplate",
|
|
59
|
+
version: VERSION,
|
|
60
|
+
runtime: "bun",
|
|
61
|
+
platform: `${process.platform}-${process.arch}`,
|
|
62
|
+
})}\n`,
|
|
63
|
+
);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildProgram(): Command {
|
|
68
|
+
const program = new Command();
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.name("agentplate")
|
|
72
|
+
.description(
|
|
73
|
+
`${brand("Agentplate")} — self-improving multi-agent orchestration, from build to deploy.`,
|
|
74
|
+
)
|
|
75
|
+
.version(VERSION, "-v, --version", "output the version number")
|
|
76
|
+
.option("--json", "output machine-readable JSON")
|
|
77
|
+
.option("--verbose", "verbose output")
|
|
78
|
+
.option("-q, --quiet", "suppress non-error output")
|
|
79
|
+
.option("--project <path>", "target project root (overrides auto-detection)")
|
|
80
|
+
.configureHelp({ showGlobalOptions: true });
|
|
81
|
+
|
|
82
|
+
// Apply global flags before any subcommand action runs. optsWithGlobals()
|
|
83
|
+
// merges the subcommand's options with the program's global options.
|
|
84
|
+
program.hook("preAction", (thisCommand) => {
|
|
85
|
+
const opts = thisCommand.optsWithGlobals<GlobalOptions>();
|
|
86
|
+
if (opts.quiet) setQuiet(true);
|
|
87
|
+
if (opts.project) setProjectRootOverride(opts.project);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Onboarding
|
|
91
|
+
program.addCommand(createSetupCommand());
|
|
92
|
+
program.addCommand(createInitCommand());
|
|
93
|
+
program.addCommand(createModelCommand());
|
|
94
|
+
program.addCommand(createDoctorCommand());
|
|
95
|
+
// Orchestration
|
|
96
|
+
program.addCommand(createCoordinatorCommand());
|
|
97
|
+
program.addCommand(createSlingCommand());
|
|
98
|
+
program.addCommand(createStatusCommand());
|
|
99
|
+
program.addCommand(createMailCommand());
|
|
100
|
+
program.addCommand(createMergeCommand());
|
|
101
|
+
program.addCommand(createWorktreeCommand());
|
|
102
|
+
program.addCommand(createStopCommand());
|
|
103
|
+
program.addCommand(createReapCommand());
|
|
104
|
+
program.addCommand(createPrimeCommand());
|
|
105
|
+
program.addCommand(createLogCommand());
|
|
106
|
+
// Self-improving skills
|
|
107
|
+
program.addCommand(createSkillCommand());
|
|
108
|
+
// Build → CI/CD → Deploy
|
|
109
|
+
program.addCommand(createShipCommand());
|
|
110
|
+
program.addCommand(createTargetCommand());
|
|
111
|
+
program.addCommand(createDeployCommand());
|
|
112
|
+
program.addCommand(createRollbackCommand());
|
|
113
|
+
// Surfaces
|
|
114
|
+
program.addCommand(createServeCommand());
|
|
115
|
+
program.addCommand(createTuiCommand());
|
|
116
|
+
|
|
117
|
+
program.addHelpText(
|
|
118
|
+
"after",
|
|
119
|
+
`\n${muted("More commands (ship, skill, deploy, …) arrive as Agentplate grows.")}`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return program;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
const program = buildProgram();
|
|
127
|
+
await program.parseAsync(process.argv);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((error: unknown) => {
|
|
131
|
+
if (rawArgs.includes("--json")) {
|
|
132
|
+
jsonError(error);
|
|
133
|
+
} else {
|
|
134
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
135
|
+
}
|
|
136
|
+
process.exit(isAgentplateError(error) ? error.exitCode : 1);
|
|
137
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gates — run the project's configured test/lint/typecheck commands and
|
|
3
|
+
* fold their results into a single {@link OutcomeStatus}.
|
|
4
|
+
*
|
|
5
|
+
* The status threads through the self-improving loop: skills are only distilled
|
|
6
|
+
* from work that passed, and applied-skill outcomes are scored by it, so the
|
|
7
|
+
* confidence track record reflects whether changes actually held up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { OutcomeStatus, QualityGate } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
export interface GateResult {
|
|
13
|
+
name: string;
|
|
14
|
+
command: string;
|
|
15
|
+
passed: boolean;
|
|
16
|
+
exitCode: number;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QualityGateOutcome {
|
|
21
|
+
status: OutcomeStatus;
|
|
22
|
+
results: GateResult[];
|
|
23
|
+
totalDurationMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run each gate as a shell command in `cwd`. A gate passes on exit code 0.
|
|
28
|
+
* Aggregate status: all pass → success, all fail → failure, mixed → partial.
|
|
29
|
+
* Returns null when there are no gates configured (nothing to score).
|
|
30
|
+
*/
|
|
31
|
+
export async function runQualityGates(
|
|
32
|
+
gates: QualityGate[],
|
|
33
|
+
cwd: string,
|
|
34
|
+
): Promise<QualityGateOutcome | null> {
|
|
35
|
+
if (gates.length === 0) return null;
|
|
36
|
+
|
|
37
|
+
const results: GateResult[] = [];
|
|
38
|
+
let total = 0;
|
|
39
|
+
for (const gate of gates) {
|
|
40
|
+
const started = performance.now();
|
|
41
|
+
let exitCode = 1;
|
|
42
|
+
try {
|
|
43
|
+
// Run the gate through the platform shell: `cmd /c` on Windows (no bash
|
|
44
|
+
// there), `bash -lc` elsewhere. `.cmd` shims (biome/tsc) resolve under both.
|
|
45
|
+
const shellArgv =
|
|
46
|
+
process.platform === "win32"
|
|
47
|
+
? ["cmd", "/d", "/s", "/c", gate.command]
|
|
48
|
+
: ["bash", "-lc", gate.command];
|
|
49
|
+
const proc = Bun.spawn(shellArgv, {
|
|
50
|
+
cwd,
|
|
51
|
+
stdout: "pipe",
|
|
52
|
+
stderr: "pipe",
|
|
53
|
+
});
|
|
54
|
+
exitCode = await proc.exited;
|
|
55
|
+
} catch {
|
|
56
|
+
exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
const durationMs = Math.round(performance.now() - started);
|
|
59
|
+
total += durationMs;
|
|
60
|
+
results.push({
|
|
61
|
+
name: gate.name,
|
|
62
|
+
command: gate.command,
|
|
63
|
+
passed: exitCode === 0,
|
|
64
|
+
exitCode,
|
|
65
|
+
durationMs,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const passed = results.filter((r) => r.passed).length;
|
|
70
|
+
const status: OutcomeStatus =
|
|
71
|
+
passed === results.length ? "success" : passed === 0 ? "failure" : "partial";
|
|
72
|
+
return { status, results, totalDurationMs: total };
|
|
73
|
+
}
|
package/src/json.test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ConfigError } from "./errors.ts";
|
|
3
|
+
import { jsonFailure, jsonSuccess } from "./json.ts";
|
|
4
|
+
|
|
5
|
+
describe("json envelopes", () => {
|
|
6
|
+
test("jsonSuccess wraps data", () => {
|
|
7
|
+
expect(jsonSuccess({ a: 1 })).toEqual({ ok: true, data: { a: 1 } });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("jsonFailure preserves AgentplateError code", () => {
|
|
11
|
+
const env = jsonFailure(new ConfigError("bad config"));
|
|
12
|
+
expect(env.ok).toBe(false);
|
|
13
|
+
expect(env.error.code).toBe("CONFIG_ERROR");
|
|
14
|
+
expect(env.error.message).toBe("bad config");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("jsonFailure handles plain errors", () => {
|
|
18
|
+
const env = jsonFailure(new Error("oops"));
|
|
19
|
+
expect(env.error.code).toBe("UNKNOWN_ERROR");
|
|
20
|
+
expect(env.error.message).toBe("oops");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("jsonFailure handles non-error throwables", () => {
|
|
24
|
+
const env = jsonFailure("string thrown");
|
|
25
|
+
expect(env.error.code).toBe("UNKNOWN_ERROR");
|
|
26
|
+
expect(env.error.message).toBe("string thrown");
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized JSON output envelopes for `--json` mode.
|
|
3
|
+
*
|
|
4
|
+
* Every command that supports `--json` should emit exactly one envelope on
|
|
5
|
+
* stdout via {@link jsonOutput} or {@link jsonError}, so machine consumers get a
|
|
6
|
+
* predictable, versioned shape: `{ ok, data? , error? }`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isAgentplateError } from "./errors.ts";
|
|
10
|
+
|
|
11
|
+
/** Successful JSON envelope. */
|
|
12
|
+
export interface JsonSuccess<T> {
|
|
13
|
+
ok: true;
|
|
14
|
+
data: T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Error JSON envelope. */
|
|
18
|
+
export interface JsonFailure {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type JsonEnvelope<T> = JsonSuccess<T> | JsonFailure;
|
|
27
|
+
|
|
28
|
+
/** Build (do not print) a success envelope. */
|
|
29
|
+
export function jsonSuccess<T>(data: T): JsonSuccess<T> {
|
|
30
|
+
return { ok: true, data };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build (do not print) an error envelope from any thrown value. */
|
|
34
|
+
export function jsonFailure(error: unknown): JsonFailure {
|
|
35
|
+
if (isAgentplateError(error)) {
|
|
36
|
+
return { ok: false, error: { code: error.code, message: error.message } };
|
|
37
|
+
}
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return { ok: false, error: { code: "UNKNOWN_ERROR", message } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Print a success envelope to stdout. */
|
|
43
|
+
export function jsonOutput<T>(data: T): void {
|
|
44
|
+
process.stdout.write(`${JSON.stringify(jsonSuccess(data))}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Print an error envelope to stderr. */
|
|
48
|
+
export function jsonError(error: unknown): void {
|
|
49
|
+
process.stderr.write(`${JSON.stringify(jsonFailure(error))}\n`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central color + console output control.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `chalk` so color can be disabled globally (via `NO_COLOR`, a non-TTY
|
|
5
|
+
* stdout, or `--quiet`) from one place, and exposes a small palette + a set of
|
|
6
|
+
* semantic printers used across commands for a consistent look.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
let quiet = false;
|
|
12
|
+
|
|
13
|
+
/** Enable/disable quiet mode (suppresses non-error output). */
|
|
14
|
+
export function setQuiet(value: boolean): void {
|
|
15
|
+
quiet = value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Is quiet mode active? */
|
|
19
|
+
export function isQuiet(): boolean {
|
|
20
|
+
return quiet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Re-export chalk so callers don't import it directly. */
|
|
24
|
+
export { chalk };
|
|
25
|
+
|
|
26
|
+
// Palette ------------------------------------------------------------------
|
|
27
|
+
/** Brand color (primary). */
|
|
28
|
+
export const brand = chalk.hex("#e07a3f");
|
|
29
|
+
/** Accent color (secondary highlights). */
|
|
30
|
+
export const accent = chalk.hex("#f0b429");
|
|
31
|
+
/** Muted color (de-emphasized text). */
|
|
32
|
+
export const muted = chalk.dim;
|
|
33
|
+
|
|
34
|
+
// Semantic printers --------------------------------------------------------
|
|
35
|
+
/** Print a success line (skipped in quiet mode). */
|
|
36
|
+
export function printSuccess(message: string): void {
|
|
37
|
+
if (quiet) return;
|
|
38
|
+
process.stdout.write(`${chalk.green("✓")} ${message}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Print an informational line (skipped in quiet mode). */
|
|
42
|
+
export function printInfo(message: string): void {
|
|
43
|
+
if (quiet) return;
|
|
44
|
+
process.stdout.write(`${message}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Print a warning line (skipped in quiet mode). */
|
|
48
|
+
export function printWarning(message: string): void {
|
|
49
|
+
if (quiet) return;
|
|
50
|
+
process.stdout.write(`${accent("!")} ${message}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Print a de-emphasized hint line (skipped in quiet mode). */
|
|
54
|
+
export function printHint(message: string): void {
|
|
55
|
+
if (quiet) return;
|
|
56
|
+
process.stdout.write(`${muted(message)}\n`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Print an error line to stderr (always shown). */
|
|
60
|
+
export function printError(message: string): void {
|
|
61
|
+
process.stderr.write(`${chalk.red("✗")} ${message}\n`);
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structured logger.
|
|
3
|
+
*
|
|
4
|
+
* Phase 0 provides a lightweight, dependency-free logger that writes
|
|
5
|
+
* human-readable lines to stderr and respects the global redaction setting.
|
|
6
|
+
* Later phases can extend this with NDJSON file sinks per agent without changing
|
|
7
|
+
* the call sites.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { sanitize } from "./sanitizer.ts";
|
|
11
|
+
|
|
12
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
13
|
+
|
|
14
|
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
15
|
+
debug: 10,
|
|
16
|
+
info: 20,
|
|
17
|
+
warn: 30,
|
|
18
|
+
error: 40,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface LoggerOptions {
|
|
22
|
+
/** Minimum level to emit. */
|
|
23
|
+
level?: LogLevel;
|
|
24
|
+
/** Redact secrets from messages before writing. */
|
|
25
|
+
redact?: boolean;
|
|
26
|
+
/** Component name prefixed to each line. */
|
|
27
|
+
scope?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Logger {
|
|
31
|
+
debug(message: string): void;
|
|
32
|
+
info(message: string): void;
|
|
33
|
+
warn(message: string): void;
|
|
34
|
+
error(message: string): void;
|
|
35
|
+
child(scope: string): Logger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Create a logger. Defaults: `info` level, redaction on, no scope. */
|
|
39
|
+
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
40
|
+
const level: LogLevel = options.level ?? "info";
|
|
41
|
+
const redact = options.redact ?? true;
|
|
42
|
+
const scope = options.scope;
|
|
43
|
+
const threshold = LEVEL_ORDER[level];
|
|
44
|
+
|
|
45
|
+
function emit(messageLevel: LogLevel, message: string): void {
|
|
46
|
+
if (LEVEL_ORDER[messageLevel] < threshold) return;
|
|
47
|
+
const text = redact ? sanitize(message) : message;
|
|
48
|
+
const prefix = scope ? `[${scope}] ` : "";
|
|
49
|
+
process.stderr.write(`${messageLevel.toUpperCase()} ${prefix}${text}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
debug: (message) => emit("debug", message),
|
|
54
|
+
info: (message) => emit("info", message),
|
|
55
|
+
warn: (message) => emit("warn", message),
|
|
56
|
+
error: (message) => emit("error", message),
|
|
57
|
+
child: (childScope) =>
|
|
58
|
+
createLogger({ level, redact, scope: scope ? `${scope}:${childScope}` : childScope }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { containsSecret, sanitize } from "./sanitizer.ts";
|
|
3
|
+
|
|
4
|
+
describe("sanitizer", () => {
|
|
5
|
+
test("redacts Anthropic-style keys", () => {
|
|
6
|
+
const out = sanitize("key is sk-ant-abcdef0123456789ABCDEF done");
|
|
7
|
+
expect(out).not.toContain("abcdef0123456789");
|
|
8
|
+
expect(out).toContain("[REDACTED]");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("redacts KEY=value assignments but keeps the key prefix", () => {
|
|
12
|
+
const out = sanitize("ANTHROPIC_API_KEY=supersecretvalue123");
|
|
13
|
+
expect(out).toContain("ANTHROPIC_API_KEY=");
|
|
14
|
+
expect(out).not.toContain("supersecretvalue123");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("redacts bearer tokens", () => {
|
|
18
|
+
const out = sanitize("Authorization: Bearer abc.def.ghi");
|
|
19
|
+
expect(out).not.toContain("abc.def.ghi");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("redacts PEM private key blocks", () => {
|
|
23
|
+
const pem = "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBg\n-----END PRIVATE KEY-----";
|
|
24
|
+
expect(sanitize(pem)).toBe("[REDACTED]");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("leaves clean text untouched", () => {
|
|
28
|
+
const clean = "just a normal log line with no secrets";
|
|
29
|
+
expect(sanitize(clean)).toBe(clean);
|
|
30
|
+
expect(containsSecret(clean)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("containsSecret detects secrets", () => {
|
|
34
|
+
expect(containsSecret("token: ghp_0123456789abcdefghij0123")).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction.
|
|
3
|
+
*
|
|
4
|
+
* Best-effort scrubbing of credentials from text before it is logged, stored,
|
|
5
|
+
* or surfaced in mail/audit records. This is defense-in-depth, not a guarantee:
|
|
6
|
+
* the architecture keeps secrets out of these channels in the first place, and
|
|
7
|
+
* this is the backstop.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const REDACTED = "[REDACTED]";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Patterns whose ENTIRE match is a secret. The full match is replaced. Ordered
|
|
14
|
+
* most-specific first so a narrower pattern (sk-ant-) wins before a broader one.
|
|
15
|
+
*/
|
|
16
|
+
const WHOLE_SECRET_PATTERNS: RegExp[] = [
|
|
17
|
+
// PEM private key blocks.
|
|
18
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
19
|
+
// Provider API keys with recognizable prefixes.
|
|
20
|
+
/\bsk-ant-[a-zA-Z0-9_-]{16,}\b/g,
|
|
21
|
+
/\bsk-[a-zA-Z0-9]{20,}\b/g,
|
|
22
|
+
/\bghp_[a-zA-Z0-9]{20,}\b/g,
|
|
23
|
+
/\bgho_[a-zA-Z0-9]{20,}\b/g,
|
|
24
|
+
// AWS access key ids.
|
|
25
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Patterns where capture group 1 is a prefix to KEEP and the rest is the secret
|
|
30
|
+
* to redact (so logs stay readable: `ANTHROPIC_API_KEY=[REDACTED]`).
|
|
31
|
+
*/
|
|
32
|
+
const PREFIXED_SECRET_PATTERNS: RegExp[] = [
|
|
33
|
+
// Bearer tokens in Authorization headers.
|
|
34
|
+
/\b(Authorization:\s*Bearer\s+)[^\s"']+/gi,
|
|
35
|
+
// `KEY=value` / `TOKEN: value` / `SECRET="value"` assignments.
|
|
36
|
+
/\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*\s*[=:]\s*)["']?[^\s"']+["']?/gi,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return `text` with recognized secrets redacted. Whole-secret matches are
|
|
41
|
+
* replaced entirely; prefixed matches keep their key and redact the value.
|
|
42
|
+
*/
|
|
43
|
+
export function sanitize(text: string): string {
|
|
44
|
+
let result = text;
|
|
45
|
+
for (const pattern of WHOLE_SECRET_PATTERNS) {
|
|
46
|
+
result = result.replace(pattern, REDACTED);
|
|
47
|
+
}
|
|
48
|
+
for (const pattern of PREFIXED_SECRET_PATTERNS) {
|
|
49
|
+
result = result.replace(pattern, (_match, prefix: string) => `${prefix}${REDACTED}`);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True if the text appears to contain a secret (after-the-fact safety check). */
|
|
55
|
+
export function containsSecret(text: string): boolean {
|
|
56
|
+
return sanitize(text) !== text;
|
|
57
|
+
}
|