@dna113p/machines 0.1.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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/codex/machines/ui/machines.html +286 -0
- package/dist/examples/agent.d.ts +1 -0
- package/dist/examples/agent.js +19 -0
- package/dist/examples/choices.d.ts +1 -0
- package/dist/examples/choices.js +17 -0
- package/dist/examples/host.d.ts +3 -0
- package/dist/examples/host.js +32 -0
- package/dist/examples/human.d.ts +1 -0
- package/dist/examples/human.js +17 -0
- package/dist/examples/named-agents.d.ts +1 -0
- package/dist/examples/named-agents.js +38 -0
- package/dist/examples/operation.d.ts +1 -0
- package/dist/examples/operation.js +10 -0
- package/dist/examples/pi.d.ts +1 -0
- package/dist/examples/pi.js +29 -0
- package/dist/mcp/server.d.ts +17 -0
- package/dist/mcp/server.js +313 -0
- package/dist/pi-extension/index.d.ts +52 -0
- package/dist/pi-extension/index.js +390 -0
- package/dist/src/acp.d.ts +7 -0
- package/dist/src/acp.js +184 -0
- package/dist/src/agent-bindings.d.ts +15 -0
- package/dist/src/agent-bindings.js +113 -0
- package/dist/src/catalog.d.ts +31 -0
- package/dist/src/catalog.js +68 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +392 -0
- package/dist/src/discovery-worker.d.ts +3 -0
- package/dist/src/discovery-worker.js +52 -0
- package/dist/src/discovery.d.ts +8 -0
- package/dist/src/discovery.js +94 -0
- package/dist/src/host.d.ts +25 -0
- package/dist/src/host.js +357 -0
- package/dist/src/index.d.ts +210 -0
- package/dist/src/index.js +204 -0
- package/dist/src/launcher.d.ts +19 -0
- package/dist/src/launcher.js +98 -0
- package/dist/src/machine-module.d.ts +3 -0
- package/dist/src/machine-module.js +55 -0
- package/dist/src/session.d.ts +40 -0
- package/dist/src/session.js +165 -0
- package/dist/src/terminal-human.d.ts +2 -0
- package/dist/src/terminal-human.js +100 -0
- package/docs/architecture.md +111 -0
- package/docs/authoring.md +193 -0
- package/docs/integrations.md +142 -0
- package/package.json +58 -0
- package/skills/machine-builder/SKILL.md +34 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { stat } from "node:fs/promises";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { machinesUserHome, nearestMachinesDirectory } from "./discovery.js";
|
|
13
|
+
const presetFields = new Set(["description", "runner", "harness", "model", "thinking"]);
|
|
14
|
+
export async function loadAgentBindings(cwd = process.cwd(), home = machinesUserHome(), adapters = {}) {
|
|
15
|
+
const globalPath = join(home, ".machines", "agents.ts");
|
|
16
|
+
const projectDirectory = await nearestMachinesDirectory(cwd);
|
|
17
|
+
const projectPath = projectDirectory === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: join(projectDirectory, "agents.ts");
|
|
20
|
+
const paths = projectPath === undefined || resolve(projectPath) === resolve(globalPath)
|
|
21
|
+
? [globalPath]
|
|
22
|
+
: [globalPath, projectPath];
|
|
23
|
+
const agents = {};
|
|
24
|
+
const presets = {};
|
|
25
|
+
const sources = {};
|
|
26
|
+
for (const path of paths) {
|
|
27
|
+
if (!await isFile(path))
|
|
28
|
+
continue;
|
|
29
|
+
const bindings = await loadBindingFile(path, adapters);
|
|
30
|
+
for (const [name, preset] of Object.entries(bindings)) {
|
|
31
|
+
agents[name] = preset.runner;
|
|
32
|
+
presets[name] = preset;
|
|
33
|
+
sources[name] = path;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { agents, presets, sources };
|
|
37
|
+
}
|
|
38
|
+
async function loadBindingFile(path, adapters) {
|
|
39
|
+
let loaded;
|
|
40
|
+
try {
|
|
41
|
+
loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(path).href));
|
|
42
|
+
}
|
|
43
|
+
catch (cause) {
|
|
44
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
45
|
+
throw new Error(`Could not load Agent bindings from "${path}": ${message}`, { cause });
|
|
46
|
+
}
|
|
47
|
+
const configure = loaded.default;
|
|
48
|
+
if (typeof configure !== "function") {
|
|
49
|
+
throw new Error(`Agent bindings "${path}" must default-export a function`);
|
|
50
|
+
}
|
|
51
|
+
let value;
|
|
52
|
+
try {
|
|
53
|
+
value = await configure(adapters);
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
57
|
+
throw new Error(`Could not configure Agent bindings from "${path}": ${message}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
60
|
+
throw new Error(`Agent bindings "${path}" must return an object`);
|
|
61
|
+
}
|
|
62
|
+
const bindings = {};
|
|
63
|
+
for (const [name, preset] of Object.entries(value)) {
|
|
64
|
+
if (name.trim() === "" || name !== name.trim()) {
|
|
65
|
+
throw new Error(`Agent bindings "${path}" contain a non-trimmed or empty name`);
|
|
66
|
+
}
|
|
67
|
+
if (preset === null || typeof preset !== "object" || Array.isArray(preset)) {
|
|
68
|
+
throw new Error(`Agent preset "${name}" in "${path}" must be an object`);
|
|
69
|
+
}
|
|
70
|
+
const unknownFields = Object.keys(preset).filter((field) => !presetFields.has(field));
|
|
71
|
+
if (unknownFields.length > 0) {
|
|
72
|
+
throw new Error(`Agent preset "${name}" in "${path}" has unknown fields: ${unknownFields.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
const candidate = preset;
|
|
75
|
+
const description = oneLine(candidate.description, "description", name, path);
|
|
76
|
+
if (typeof candidate.runner !== "function") {
|
|
77
|
+
throw new Error(`Agent preset "${name}" in "${path}" must include a runner function`);
|
|
78
|
+
}
|
|
79
|
+
bindings[name] = {
|
|
80
|
+
description,
|
|
81
|
+
runner: candidate.runner,
|
|
82
|
+
...optionalField(candidate.harness, "harness", name, path),
|
|
83
|
+
...optionalField(candidate.model, "model", name, path),
|
|
84
|
+
...optionalField(candidate.thinking, "thinking", name, path),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return bindings;
|
|
88
|
+
}
|
|
89
|
+
function optionalField(value, field, name, path) {
|
|
90
|
+
return value === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { [field]: oneLine(value, field, name, path) };
|
|
93
|
+
}
|
|
94
|
+
function oneLine(value, field, name, path) {
|
|
95
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
96
|
+
throw new Error(`Agent preset "${name}" in "${path}" must have a non-empty ${field}`);
|
|
97
|
+
}
|
|
98
|
+
const normalized = value.trim();
|
|
99
|
+
if (/\r|\n|\t/u.test(normalized)) {
|
|
100
|
+
throw new Error(`Agent preset "${name}" ${field} in "${path}" must fit in one line`);
|
|
101
|
+
}
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
async function isFile(path) {
|
|
105
|
+
try {
|
|
106
|
+
return (await stat(path)).isFile();
|
|
107
|
+
}
|
|
108
|
+
catch (cause) {
|
|
109
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
110
|
+
return false;
|
|
111
|
+
throw cause;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type AgentPreset } from "./agent-bindings.ts";
|
|
2
|
+
import type { AgentRunner } from "./index.ts";
|
|
3
|
+
export interface MachineSummary {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly agentRoles: Readonly<Record<string, string>>;
|
|
8
|
+
readonly missingAgents: readonly string[];
|
|
9
|
+
readonly error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentPresetSummary {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly harness?: string;
|
|
15
|
+
readonly model?: string;
|
|
16
|
+
readonly thinking?: string;
|
|
17
|
+
readonly source: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LauncherLocation {
|
|
20
|
+
readonly cwd?: string;
|
|
21
|
+
readonly home?: string;
|
|
22
|
+
}
|
|
23
|
+
interface ResolvedAgentPresets {
|
|
24
|
+
readonly agents: Readonly<Record<string, AgentRunner>>;
|
|
25
|
+
readonly presets: Readonly<Record<string, AgentPreset>>;
|
|
26
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
27
|
+
}
|
|
28
|
+
export declare function inspectMachines(location?: LauncherLocation): Promise<readonly MachineSummary[]>;
|
|
29
|
+
export declare function inspectAgentPresets(location?: LauncherLocation): Promise<readonly AgentPresetSummary[]>;
|
|
30
|
+
export declare function configuredAgentPresets(cwd: string, home: string): Promise<ResolvedAgentPresets>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { loadAgentBindings } from "./agent-bindings.js";
|
|
2
|
+
import { acpAgent } from "./acp.js";
|
|
3
|
+
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
4
|
+
import { loadMachineModule, readAgentRoles, requireMachineDescription } from "./machine-module.js";
|
|
5
|
+
export async function inspectMachines(location = {}) {
|
|
6
|
+
const cwd = location.cwd ?? process.cwd();
|
|
7
|
+
const home = location.home ?? machinesUserHome();
|
|
8
|
+
const discovered = await discoverMachines(cwd, home);
|
|
9
|
+
const bindings = await configuredAgentPresets(cwd, home)
|
|
10
|
+
.then((value) => ({ value, error: undefined }), (cause) => ({ value: undefined, error: errorMessage(cause) }));
|
|
11
|
+
return Promise.all(discovered.map(async (found) => {
|
|
12
|
+
try {
|
|
13
|
+
const loaded = await loadMachineModule(found.path);
|
|
14
|
+
const description = requireMachineDescription(loaded, found.path);
|
|
15
|
+
const agentRoles = readAgentRoles(loaded, found.path);
|
|
16
|
+
if (typeof loaded.default !== "function") {
|
|
17
|
+
throw new Error(`Machine "${found.path}" must default-export a factory`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...found,
|
|
21
|
+
description,
|
|
22
|
+
agentRoles,
|
|
23
|
+
missingAgents: Object.keys(agentRoles).filter((name) => bindings.value !== undefined && !Object.hasOwn(bindings.value.agents, name)),
|
|
24
|
+
...(bindings.error === undefined ? {} : { error: bindings.error }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (cause) {
|
|
28
|
+
return { ...found, description: "", agentRoles: {}, missingAgents: [], error: errorMessage(cause) };
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
export async function inspectAgentPresets(location = {}) {
|
|
33
|
+
const cwd = location.cwd ?? process.cwd();
|
|
34
|
+
const home = location.home ?? machinesUserHome();
|
|
35
|
+
const configured = await configuredAgentPresets(cwd, home);
|
|
36
|
+
return Object.entries(configured.presets)
|
|
37
|
+
.map(([name, preset]) => ({
|
|
38
|
+
name,
|
|
39
|
+
description: preset.description,
|
|
40
|
+
...(preset.harness === undefined ? {} : { harness: preset.harness }),
|
|
41
|
+
...(preset.model === undefined ? {} : { model: preset.model }),
|
|
42
|
+
...(preset.thinking === undefined ? {} : { thinking: preset.thinking }),
|
|
43
|
+
source: configured.sources[name] ?? "unknown",
|
|
44
|
+
}))
|
|
45
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
46
|
+
}
|
|
47
|
+
export async function configuredAgentPresets(cwd, home) {
|
|
48
|
+
const configured = await loadAgentBindings(cwd, home, { acpAgent });
|
|
49
|
+
const defaultRunner = acpAgent("npx", ["-y", "pi-acp"], {
|
|
50
|
+
harness: "pi-acp",
|
|
51
|
+
output: "capture",
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
agents: { default: defaultRunner, ...configured.agents },
|
|
55
|
+
presets: {
|
|
56
|
+
default: {
|
|
57
|
+
description: "Runs the current Pi Agent through ACP",
|
|
58
|
+
harness: "pi-acp",
|
|
59
|
+
runner: defaultRunner,
|
|
60
|
+
},
|
|
61
|
+
...configured.presets,
|
|
62
|
+
},
|
|
63
|
+
sources: { default: "built in", ...configured.sources },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function errorMessage(cause) {
|
|
67
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
68
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { emitKeypressEvents } from "node:readline";
|
|
5
|
+
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import yoctoSpinner from "yocto-spinner";
|
|
7
|
+
import { bold, cyan, dim, green, magenta, red, yellow } from "yoctocolors";
|
|
8
|
+
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
9
|
+
import { listAgentPresets, listMachines, prepareMachineRun, } from "./launcher.js";
|
|
10
|
+
const [command, selector, ...argumentsAfterSelector] = process.argv.slice(2);
|
|
11
|
+
try {
|
|
12
|
+
if (command === "list" && selector === undefined) {
|
|
13
|
+
const machines = await listMachines({ cwd: process.cwd() });
|
|
14
|
+
if (machines.length === 0)
|
|
15
|
+
console.log("No Machines found.");
|
|
16
|
+
machines.forEach((found, index) => {
|
|
17
|
+
printMachineListing(found, index > 0);
|
|
18
|
+
});
|
|
19
|
+
if (machines.some((found) => found.error !== undefined))
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
else if (command === "agents" && selector === undefined) {
|
|
23
|
+
const presets = await listAgentPresets({ cwd: process.cwd() });
|
|
24
|
+
if (presets.length === 0)
|
|
25
|
+
console.log("No Agent presets found.");
|
|
26
|
+
presets.forEach((preset, index) => {
|
|
27
|
+
printAgentListing(preset, index > 0);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else if (command === "show" && selector !== undefined) {
|
|
31
|
+
const machinePath = await resolveMachine(selector);
|
|
32
|
+
console.log(`Machine: ${machinePath}`);
|
|
33
|
+
console.log(await readFile(machinePath, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
else if (command === "run" && selector !== undefined) {
|
|
36
|
+
await runMachine(await resolveMachine(selector), argumentsAfterSelector);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error([
|
|
40
|
+
"Usage:",
|
|
41
|
+
" machine list",
|
|
42
|
+
" machine agents",
|
|
43
|
+
" machine show <name-or-file>",
|
|
44
|
+
" machine run <name-or-file> [--agent role=preset]... [--] [input]",
|
|
45
|
+
].join("\n"));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (cause) {
|
|
49
|
+
console.error(cause instanceof Error ? cause.message : String(cause));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
async function resolveMachine(selector) {
|
|
53
|
+
if (isAbsolute(selector) || selector.includes("/") || selector.endsWith(".ts")) {
|
|
54
|
+
return resolve(selector);
|
|
55
|
+
}
|
|
56
|
+
const machines = await discoverMachines(process.cwd(), machinesUserHome());
|
|
57
|
+
const found = machines.find((candidate) => candidate.name === selector);
|
|
58
|
+
if (found === undefined) {
|
|
59
|
+
throw new Error(`Machine "${selector}" not found. Run "machine list" to see available Machines.`);
|
|
60
|
+
}
|
|
61
|
+
return found.path;
|
|
62
|
+
}
|
|
63
|
+
async function runMachine(machinePath, args) {
|
|
64
|
+
const invocation = parseRunArguments(args);
|
|
65
|
+
printMachineHeader(machinePath);
|
|
66
|
+
const prepared = await prepareMachineRun({
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
machine: machinePath,
|
|
69
|
+
input: invocation.input,
|
|
70
|
+
agents: Object.fromEntries(invocation.agentOverrides),
|
|
71
|
+
});
|
|
72
|
+
const status = createRunStatus(machinePath);
|
|
73
|
+
try {
|
|
74
|
+
const result = await prepared.start({
|
|
75
|
+
onAgentUpdate: status.onAgentUpdate,
|
|
76
|
+
onHumanInput: status.onHumanInput,
|
|
77
|
+
onState: status.onState,
|
|
78
|
+
});
|
|
79
|
+
status.succeed();
|
|
80
|
+
console.log(`--> ${String(result.value)}`);
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
status.fail();
|
|
84
|
+
throw cause;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
status.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseRunArguments(args) {
|
|
91
|
+
const agentOverrides = new Map();
|
|
92
|
+
let index = 0;
|
|
93
|
+
while (index < args.length) {
|
|
94
|
+
const argument = args[index];
|
|
95
|
+
if (argument === "--") {
|
|
96
|
+
index += 1;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (argument !== "--agent")
|
|
100
|
+
break;
|
|
101
|
+
const assignment = args[index + 1];
|
|
102
|
+
if (assignment === undefined) {
|
|
103
|
+
throw new Error('Expected "role=preset" after "--agent"');
|
|
104
|
+
}
|
|
105
|
+
const separator = assignment.indexOf("=");
|
|
106
|
+
const role = separator === -1 ? "" : assignment.slice(0, separator);
|
|
107
|
+
const preset = separator === -1 ? "" : assignment.slice(separator + 1);
|
|
108
|
+
if (role.trim() === ""
|
|
109
|
+
|| preset.trim() === ""
|
|
110
|
+
|| role !== role.trim()
|
|
111
|
+
|| preset !== preset.trim()) {
|
|
112
|
+
throw new Error(`Invalid Agent override "${assignment}"; expected "role=preset"`);
|
|
113
|
+
}
|
|
114
|
+
if (agentOverrides.has(role)) {
|
|
115
|
+
throw new Error(`Agent role "${role}" was overridden more than once`);
|
|
116
|
+
}
|
|
117
|
+
agentOverrides.set(role, preset);
|
|
118
|
+
index += 2;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
agentOverrides,
|
|
122
|
+
input: args.slice(index).join(" "),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function printMachineListing(found, separate) {
|
|
126
|
+
if (!process.stdout.isTTY) {
|
|
127
|
+
if (found.error !== undefined)
|
|
128
|
+
console.error(found.error);
|
|
129
|
+
const readiness = found.error !== undefined ? "invalid" : found.missingAgents.length === 0
|
|
130
|
+
? "ready"
|
|
131
|
+
: `missing Agents: ${found.missingAgents.join(", ")}`;
|
|
132
|
+
console.log(`${found.name}\t${found.description}\t${found.path}\t${readiness}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (separate)
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(bold(cyan(found.name)));
|
|
138
|
+
console.log(` ${found.description}`);
|
|
139
|
+
console.log(` ${dim(found.path)}`);
|
|
140
|
+
if (found.error !== undefined)
|
|
141
|
+
console.log(` ${red(found.error)}`);
|
|
142
|
+
if (found.missingAgents.length > 0) {
|
|
143
|
+
console.log(` ${yellow(`missing Agents: ${found.missingAgents.join(", ")}`)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function printAgentListing(preset, separate) {
|
|
147
|
+
const identity = [preset.harness, preset.model, preset.thinking].filter(Boolean);
|
|
148
|
+
if (!process.stdout.isTTY) {
|
|
149
|
+
console.log([
|
|
150
|
+
preset.name,
|
|
151
|
+
preset.description,
|
|
152
|
+
preset.harness ?? "",
|
|
153
|
+
preset.model ?? "",
|
|
154
|
+
preset.thinking ?? "",
|
|
155
|
+
preset.source,
|
|
156
|
+
].join("\t"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (separate)
|
|
160
|
+
console.log();
|
|
161
|
+
console.log(bold(cyan(preset.name)));
|
|
162
|
+
console.log(` ${preset.description}`);
|
|
163
|
+
if (identity.length > 0)
|
|
164
|
+
console.log(` ${magenta(identity.join(" · "))}`);
|
|
165
|
+
console.log(` ${dim(preset.source)}`);
|
|
166
|
+
}
|
|
167
|
+
function printMachineHeader(machinePath) {
|
|
168
|
+
if (!process.stdout.isTTY) {
|
|
169
|
+
console.log(`Machine: ${machinePath}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`${magenta("◆")} ${bold(machineName(machinePath))}`);
|
|
173
|
+
console.log(` ${dim(machinePath)}\n`);
|
|
174
|
+
}
|
|
175
|
+
function createRunStatus(machinePath) {
|
|
176
|
+
const name = machineName(machinePath);
|
|
177
|
+
if (!process.stderr.isTTY) {
|
|
178
|
+
return {
|
|
179
|
+
onState(state) {
|
|
180
|
+
console.error(`● ${name} · ${formatState(state)}`);
|
|
181
|
+
},
|
|
182
|
+
onAgentUpdate() { },
|
|
183
|
+
onHumanInput() { },
|
|
184
|
+
succeed() { },
|
|
185
|
+
fail() { },
|
|
186
|
+
close() { },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
let currentState = "starting";
|
|
190
|
+
let enteredAt = 0;
|
|
191
|
+
let humanInputActive = false;
|
|
192
|
+
let activityExpanded = false;
|
|
193
|
+
let agentOutput = "";
|
|
194
|
+
let agentIdentity = {};
|
|
195
|
+
let refresh;
|
|
196
|
+
const tools = new Map();
|
|
197
|
+
const hotkeysAvailable = Boolean(process.stdin.isTTY && process.stdin.setRawMode);
|
|
198
|
+
const initialRawMode = process.stdin.isRaw;
|
|
199
|
+
let hotkeysEnabled = false;
|
|
200
|
+
const signalHandlers = new Map();
|
|
201
|
+
const spinner = yoctoSpinner({ handleSignals: false, stream: process.stderr });
|
|
202
|
+
function runningText() {
|
|
203
|
+
const hint = hotkeysAvailable
|
|
204
|
+
? ` · ${dim(activityExpanded ? "[o] hide activity" : "[o] activity")}`
|
|
205
|
+
: "";
|
|
206
|
+
const status = `${cyan(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}${hint}`;
|
|
207
|
+
const identity = [
|
|
208
|
+
agentIdentity.harness,
|
|
209
|
+
agentIdentity.model,
|
|
210
|
+
agentIdentity.thinking === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: `thinking ${agentIdentity.thinking}`,
|
|
213
|
+
].filter(Boolean).join(" · ");
|
|
214
|
+
const identityLine = identity === ""
|
|
215
|
+
? ""
|
|
216
|
+
: `\n ${dim(truncate(identity, Math.max(20, (process.stderr.columns ?? 100) - 4)))}`;
|
|
217
|
+
return activityExpanded
|
|
218
|
+
? `${status}${identityLine}\n${activityText()}`
|
|
219
|
+
: `${status}${identityLine}`;
|
|
220
|
+
}
|
|
221
|
+
function completedText() {
|
|
222
|
+
return `${green(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}`;
|
|
223
|
+
}
|
|
224
|
+
function clearRefresh() {
|
|
225
|
+
if (refresh !== undefined)
|
|
226
|
+
clearInterval(refresh);
|
|
227
|
+
refresh = undefined;
|
|
228
|
+
}
|
|
229
|
+
function completeCurrent() {
|
|
230
|
+
clearRefresh();
|
|
231
|
+
if (spinner.isSpinning)
|
|
232
|
+
spinner.success(completedText());
|
|
233
|
+
}
|
|
234
|
+
function activityText() {
|
|
235
|
+
const toolLines = [...tools.values()].map((tool) => {
|
|
236
|
+
if (tool.status === "completed")
|
|
237
|
+
return `${green("✓")} ${tool.title}`;
|
|
238
|
+
if (tool.status === "failed")
|
|
239
|
+
return `${red("✖")} ${tool.title}`;
|
|
240
|
+
if (tool.status === "in_progress")
|
|
241
|
+
return `${cyan("…")} ${tool.title}`;
|
|
242
|
+
return `${dim("·")} ${tool.title}`;
|
|
243
|
+
});
|
|
244
|
+
const lastNewline = agentOutput.lastIndexOf("\n");
|
|
245
|
+
const completeOutput = lastNewline === -1 ? "" : agentOutput.slice(0, lastNewline);
|
|
246
|
+
const outputLines = completeOutput
|
|
247
|
+
.replace(/^MACHINES_EVENT .+$(?:\r?\n)?/gmu, "")
|
|
248
|
+
.replace(/^New version available:.+$(?:\r?\n)?/gmu, "")
|
|
249
|
+
.trim()
|
|
250
|
+
.split(/\r?\n/u)
|
|
251
|
+
.filter((line) => line !== "")
|
|
252
|
+
.slice(-4)
|
|
253
|
+
.map((line) => `${dim("│")} ${line}`);
|
|
254
|
+
const lines = [...toolLines, ...outputLines].slice(-8);
|
|
255
|
+
if (lines.length === 0)
|
|
256
|
+
return dim(" Waiting for Agent activity…");
|
|
257
|
+
const width = Math.max(20, (process.stderr.columns ?? 100) - 4);
|
|
258
|
+
return lines
|
|
259
|
+
.map((line) => ` ${truncate(stripVTControlCharacters(line), width)}`)
|
|
260
|
+
.join("\n");
|
|
261
|
+
}
|
|
262
|
+
function render() {
|
|
263
|
+
if (spinner.isSpinning)
|
|
264
|
+
spinner.text = runningText();
|
|
265
|
+
}
|
|
266
|
+
function onKeypress(_character, key) {
|
|
267
|
+
if (key?.ctrl && key.name === "c") {
|
|
268
|
+
disableHotkeys();
|
|
269
|
+
process.kill(process.pid, "SIGINT");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (key?.name !== "o")
|
|
273
|
+
return;
|
|
274
|
+
activityExpanded = !activityExpanded;
|
|
275
|
+
render();
|
|
276
|
+
}
|
|
277
|
+
function enableHotkeys() {
|
|
278
|
+
if (!hotkeysAvailable || hotkeysEnabled)
|
|
279
|
+
return;
|
|
280
|
+
emitKeypressEvents(process.stdin);
|
|
281
|
+
process.stdin.on("keypress", onKeypress);
|
|
282
|
+
process.stdin.setRawMode(true);
|
|
283
|
+
process.stdin.resume();
|
|
284
|
+
hotkeysEnabled = true;
|
|
285
|
+
}
|
|
286
|
+
function disableHotkeys() {
|
|
287
|
+
if (!hotkeysEnabled)
|
|
288
|
+
return;
|
|
289
|
+
process.stdin.off("keypress", onKeypress);
|
|
290
|
+
process.stdin.setRawMode(initialRawMode ?? false);
|
|
291
|
+
if (!initialRawMode)
|
|
292
|
+
process.stdin.pause();
|
|
293
|
+
hotkeysEnabled = false;
|
|
294
|
+
}
|
|
295
|
+
function closeHotkeys() {
|
|
296
|
+
disableHotkeys();
|
|
297
|
+
for (const [signal, handler] of signalHandlers)
|
|
298
|
+
process.off(signal, handler);
|
|
299
|
+
signalHandlers.clear();
|
|
300
|
+
}
|
|
301
|
+
if (hotkeysAvailable) {
|
|
302
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
303
|
+
const handler = () => {
|
|
304
|
+
closeHotkeys();
|
|
305
|
+
process.kill(process.pid, signal);
|
|
306
|
+
};
|
|
307
|
+
signalHandlers.set(signal, handler);
|
|
308
|
+
process.once(signal, handler);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
enableHotkeys();
|
|
312
|
+
return {
|
|
313
|
+
onAgentUpdate(update) {
|
|
314
|
+
if (update.type === "identity") {
|
|
315
|
+
agentIdentity = update;
|
|
316
|
+
}
|
|
317
|
+
else if (update.type === "output") {
|
|
318
|
+
agentOutput = (agentOutput + update.text).slice(-32_768);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const previous = tools.get(update.id);
|
|
322
|
+
tools.set(update.id, {
|
|
323
|
+
title: update.title ?? previous?.title ?? "tool",
|
|
324
|
+
status: update.status ?? previous?.status,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
render();
|
|
328
|
+
},
|
|
329
|
+
onHumanInput(active) {
|
|
330
|
+
humanInputActive = active;
|
|
331
|
+
if (active) {
|
|
332
|
+
disableHotkeys();
|
|
333
|
+
completeCurrent();
|
|
334
|
+
process.stderr.write(`${yellow("◆")} ${yellow("input needed")}\n`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
enableHotkeys();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onState(state) {
|
|
341
|
+
if (!humanInputActive)
|
|
342
|
+
completeCurrent();
|
|
343
|
+
currentState = formatState(state);
|
|
344
|
+
enteredAt = Date.now();
|
|
345
|
+
agentOutput = "";
|
|
346
|
+
agentIdentity = {};
|
|
347
|
+
tools.clear();
|
|
348
|
+
if (humanInputActive)
|
|
349
|
+
return;
|
|
350
|
+
spinner.start(runningText());
|
|
351
|
+
refresh = setInterval(() => {
|
|
352
|
+
spinner.text = runningText();
|
|
353
|
+
}, 1_000);
|
|
354
|
+
refresh.unref();
|
|
355
|
+
},
|
|
356
|
+
succeed() {
|
|
357
|
+
completeCurrent();
|
|
358
|
+
},
|
|
359
|
+
fail() {
|
|
360
|
+
clearRefresh();
|
|
361
|
+
const failed = `${red(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}`;
|
|
362
|
+
if (spinner.isSpinning)
|
|
363
|
+
spinner.error(failed);
|
|
364
|
+
else
|
|
365
|
+
process.stderr.write(`${red("✖")} ${failed}\n`);
|
|
366
|
+
},
|
|
367
|
+
close() {
|
|
368
|
+
closeHotkeys();
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function formatState(state) {
|
|
373
|
+
return typeof state === "string" ? state : JSON.stringify(state);
|
|
374
|
+
}
|
|
375
|
+
function displayState(state) {
|
|
376
|
+
return state.replace(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase();
|
|
377
|
+
}
|
|
378
|
+
function truncate(text, width) {
|
|
379
|
+
return text.length <= width ? text : `${text.slice(0, width - 1)}…`;
|
|
380
|
+
}
|
|
381
|
+
function machineName(machinePath) {
|
|
382
|
+
const file = basename(machinePath);
|
|
383
|
+
if (file === "index.ts")
|
|
384
|
+
return basename(dirname(machinePath));
|
|
385
|
+
return file.endsWith(".ts") ? file.slice(0, -3) : file;
|
|
386
|
+
}
|
|
387
|
+
function formatElapsed(milliseconds) {
|
|
388
|
+
const seconds = Math.round(milliseconds / 1_000);
|
|
389
|
+
if (seconds < 60)
|
|
390
|
+
return `${seconds}s`;
|
|
391
|
+
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
392
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type AgentPresetSummary, type LauncherLocation, type MachineSummary } from "./catalog.ts";
|
|
2
|
+
export declare function queryCatalog(kind: "machines", location: LauncherLocation): Promise<readonly MachineSummary[]>;
|
|
3
|
+
export declare function queryCatalog(kind: "agents", location: LauncherLocation): Promise<readonly AgentPresetSummary[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { machinesUserHome } from "./discovery.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
4
|
+
import { inspectAgentPresets, inspectMachines, } from "./catalog.js";
|
|
5
|
+
export function queryCatalog(kind, location) {
|
|
6
|
+
// A fresh module graph also refreshes transitive imports and releases their cache afterward.
|
|
7
|
+
// This isolates discovery output/lifetime; the imported code remains trusted local code.
|
|
8
|
+
const job = {
|
|
9
|
+
machinesCatalog: true,
|
|
10
|
+
kind,
|
|
11
|
+
location: {
|
|
12
|
+
cwd: resolve(location.cwd ?? process.cwd()),
|
|
13
|
+
home: resolve(location.home ?? machinesUserHome()),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const worker = new Worker(new URL(import.meta.url), {
|
|
17
|
+
workerData: job,
|
|
18
|
+
execArgv: [],
|
|
19
|
+
stdout: true,
|
|
20
|
+
stderr: true,
|
|
21
|
+
});
|
|
22
|
+
worker.stdout.resume();
|
|
23
|
+
worker.stderr.resume();
|
|
24
|
+
return new Promise((resolveReply, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
const finish = (reply) => {
|
|
27
|
+
if (settled)
|
|
28
|
+
return;
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
void worker.terminate();
|
|
32
|
+
if ("error" in reply)
|
|
33
|
+
reject(new Error(reply.error));
|
|
34
|
+
else
|
|
35
|
+
resolveReply(reply.entries);
|
|
36
|
+
};
|
|
37
|
+
const timer = setTimeout(() => finish({ error: "Machine discovery timed out after 10 seconds" }), 10_000);
|
|
38
|
+
worker.once("message", (reply) => finish(reply));
|
|
39
|
+
worker.once("error", (error) => finish({ error: error.message }));
|
|
40
|
+
worker.once("exit", (code) => finish({ error: `Machine discovery exited before returning results (code ${code})` }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!isMainThread && parentPort !== null && workerData?.machinesCatalog === true) {
|
|
44
|
+
const job = workerData;
|
|
45
|
+
const port = parentPort;
|
|
46
|
+
const result = job.kind === "machines"
|
|
47
|
+
? inspectMachines(job.location)
|
|
48
|
+
: inspectAgentPresets(job.location);
|
|
49
|
+
void result.then((entries) => port.postMessage({ entries }), (cause) => port.postMessage({
|
|
50
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Select a separate global catalog without changing the process's real home. */
|
|
2
|
+
export declare function machinesUserHome(): string;
|
|
3
|
+
export interface DiscoveredMachine {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function discoverMachines(cwd: string, home: string): Promise<readonly DiscoveredMachine[]>;
|
|
8
|
+
export declare function nearestMachinesDirectory(cwd: string): Promise<string | undefined>;
|