agenttwin 0.2.0 → 0.2.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/README.md +14 -2
- package/dist/api.js +53 -0
- package/dist/cli.js +389 -179
- package/dist/dashboard/assets/dashboard/dashboard.css +5 -0
- package/dist/dashboard/assets/dashboard/dashboard.js +21 -4
- package/dist/dashboard/assets/dashboard/hosted.js +7 -2
- package/dist/dashboard/index.html +3 -4
- package/dist/gateway.js +53 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -53,22 +53,13 @@ var init_answers = __esm({
|
|
|
53
53
|
|
|
54
54
|
// src/cli.ts
|
|
55
55
|
import { readFileSync as readFileSync3 } from "fs";
|
|
56
|
-
import { dirname as dirname4, join as
|
|
56
|
+
import { dirname as dirname4, join as join17 } from "path";
|
|
57
57
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
58
58
|
import { parseArgs } from "util";
|
|
59
59
|
|
|
60
|
-
// src/commands/
|
|
61
|
-
import {
|
|
62
|
-
import { join as join4
|
|
63
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
64
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
65
|
-
|
|
66
|
-
// src/commands/run.ts
|
|
67
|
-
import { mkdtemp, mkdir, readFile as readFile3, writeFile, realpath, lstat, rm, copyFile } from "fs/promises";
|
|
68
|
-
import { tmpdir } from "os";
|
|
69
|
-
import { dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
|
|
70
|
-
import { fileURLToPath } from "url";
|
|
71
|
-
import { createHash, randomUUID } from "crypto";
|
|
60
|
+
// src/commands/sync.ts
|
|
61
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
62
|
+
import { join as join4 } from "path";
|
|
72
63
|
|
|
73
64
|
// src/runtime/remote.ts
|
|
74
65
|
import { readFile } from "fs/promises";
|
|
@@ -172,19 +163,206 @@ async function executeRemote(stage, spec, log) {
|
|
|
172
163
|
throw new CliError(`Timed out waiting for hosted run ${job.id}; inspect this run in the API before retrying`);
|
|
173
164
|
}
|
|
174
165
|
|
|
166
|
+
// src/commands/session.ts
|
|
167
|
+
import { spawn, spawnSync } from "child_process";
|
|
168
|
+
import { writeFile as writeFile2, readFile as readFile3 } from "fs/promises";
|
|
169
|
+
import { join as join3 } from "path";
|
|
170
|
+
|
|
171
|
+
// src/inventory/dashboard-transfer.ts
|
|
172
|
+
function discoveryTransfer(value) {
|
|
173
|
+
const input = value;
|
|
174
|
+
if (!input || input.schema !== "company-inventory/v1" || typeof input.name !== "string" || !Array.isArray(input.tools) || !Array.isArray(input.workflows) || input.tools.length > 1e3 || input.workflows.length > 5e3) throw new Error("Invalid discovery metadata");
|
|
175
|
+
const text = (v, limit = 240) => {
|
|
176
|
+
if (typeof v !== "string" || v.length > limit || /[\x00-\x1f]/.test(v)) throw new Error("Invalid discovery string");
|
|
177
|
+
return v;
|
|
178
|
+
};
|
|
179
|
+
const strings = (v) => {
|
|
180
|
+
if (!Array.isArray(v) || v.length > 1e3) throw new Error("Invalid discovery list");
|
|
181
|
+
return v.map((s) => text(s));
|
|
182
|
+
};
|
|
183
|
+
const entrypoint = (v) => {
|
|
184
|
+
const path = text(v, 500);
|
|
185
|
+
if (path.startsWith("/") || path.includes("\\") || /(^|[\/])\.\.([\/#]|$)/.test(path) || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(path)) throw new Error("Entrypoint must be relative to the project");
|
|
186
|
+
return path;
|
|
187
|
+
};
|
|
188
|
+
const tools = input.tools.map((raw) => {
|
|
189
|
+
const t = raw;
|
|
190
|
+
return { id: text(t.id), title: text(t.title), kind: text(t.kind), category: text(t.category), confidence: text(t.confidence), interface: text(t.interface) };
|
|
191
|
+
});
|
|
192
|
+
const workflows = input.workflows.map((raw) => {
|
|
193
|
+
const w = raw;
|
|
194
|
+
if (!Array.isArray(w.steps) || w.steps.length > 1e3) throw new Error("Invalid workflow steps");
|
|
195
|
+
return { id: text(w.id), title: text(w.title), confidence: text(w.confidence), supported: w.supported === true, verified: false, tools: strings(w.tools), steps: w.steps.map((rawStep) => {
|
|
196
|
+
const step = rawStep;
|
|
197
|
+
return { tool: text(step.tool), operations: strings(step.operations) };
|
|
198
|
+
}), ...w.entrypoint ? { entrypoint: entrypoint(w.entrypoint) } : {} };
|
|
199
|
+
});
|
|
200
|
+
if (new Set(tools.map((t) => t.id)).size !== tools.length || new Set(workflows.map((w) => w.id)).size !== workflows.length) throw new Error("Duplicate discovery IDs");
|
|
201
|
+
const ids = new Set(tools.map((t) => t.id));
|
|
202
|
+
if (workflows.some((w) => w.tools.some((id) => !ids.has(id)) || w.steps.some((s) => !ids.has(s.tool)))) throw new Error("Unknown workflow tool");
|
|
203
|
+
const result = { schema: "company-inventory/v1", name: text(input.name), generatedAt: input.generatedAt ? text(input.generatedAt) : "", tools, workflows };
|
|
204
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 2 * 1024 * 1024) throw new Error("Discovery exceeds 2 MiB");
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/commands/dashboard.ts
|
|
209
|
+
import { cp, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
210
|
+
import { basename, join as join2 } from "path";
|
|
211
|
+
import { fileURLToPath } from "url";
|
|
212
|
+
|
|
213
|
+
// src/inspect/sanitize.ts
|
|
214
|
+
var URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
|
|
215
|
+
var TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
216
|
+
var EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
217
|
+
function stripUrlCredentials(value) {
|
|
218
|
+
return value.replace(URL_CREDENTIALS, "$1");
|
|
219
|
+
}
|
|
220
|
+
function sanitizeOutput(value) {
|
|
221
|
+
return stripUrlCredentials(value).replace(TOKENISH, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
|
|
222
|
+
}
|
|
223
|
+
function excerpt(value, max = 200) {
|
|
224
|
+
const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
|
|
225
|
+
return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/commands/dashboard.ts
|
|
229
|
+
function dashboardInventory(cwd, inventory) {
|
|
230
|
+
return JSON.parse(sanitizeOutput(JSON.stringify({
|
|
231
|
+
schema: "company-inventory/v1",
|
|
232
|
+
generatedAt: inventory.generatedAt,
|
|
233
|
+
name: basename(cwd),
|
|
234
|
+
tools: inventory.tools.map((t) => ({
|
|
235
|
+
id: t.id,
|
|
236
|
+
title: t.title,
|
|
237
|
+
kind: t.kind,
|
|
238
|
+
category: t.category,
|
|
239
|
+
confidence: t.confidence ?? "low",
|
|
240
|
+
interface: t.kind === "runtime" ? "terminal" : "api"
|
|
241
|
+
})),
|
|
242
|
+
workflows: inventory.workflows.map((w) => ({ id: w.id, title: w.title ?? w.id, confidence: w.confidence ?? "low", supported: w.supported, verified: false, ...w.entrypoint ? { entrypoint: w.entrypoint } : {}, tools: w.tools, steps: w.steps.map((s) => ({ tool: s.tool, operations: s.operations })) }))
|
|
243
|
+
})));
|
|
244
|
+
}
|
|
245
|
+
async function writeDashboard(cwd, inventory) {
|
|
246
|
+
const directory = join2(cwd, ".agenttwin", "dashboard");
|
|
247
|
+
let bundle = fileURLToPath(new URL("./dashboard/", import.meta.url));
|
|
248
|
+
try {
|
|
249
|
+
await readFile2(join2(bundle, "index.html"));
|
|
250
|
+
} catch {
|
|
251
|
+
bundle = fileURLToPath(new URL("../../../../site/", import.meta.url));
|
|
252
|
+
}
|
|
253
|
+
await mkdir(join2(directory, "assets", "dashboard"), { recursive: true });
|
|
254
|
+
const html = await readFile2(join2(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
|
|
255
|
+
for (const folder of ["dashboard", "icons", "logo", "fonts"]) {
|
|
256
|
+
await cp(join2(bundle, "assets", folder), join2(directory, "assets", folder), { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
await cp(join2(bundle, "assets", "brand.css"), join2(directory, "assets", "brand.css"));
|
|
259
|
+
const data = await loadDashboardDiscovery(cwd, inventory);
|
|
260
|
+
const payload = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
261
|
+
const page2 = html.replace('type="module"', "defer").replace('<script src="assets/dashboard/dashboard.js', `<script id="agenttwin-discovery" type="application/json">${payload}</script><script src="assets/dashboard/dashboard.js`).replace(/href="index.html"/g, 'href="../report.html"').replace(/href="docs\/"/g, 'href="../report.html"').replace("Docs \u2197", "Discovery report \u2197").replace("Runtime documentation \u2197", "Coverage report \u2197").replace("Back to website \u2197", "Discovery report \u2197");
|
|
262
|
+
await writeFile(join2(directory, "index.html"), page2, "utf8");
|
|
263
|
+
await writeFile(join2(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
|
|
264
|
+
await writeFile(join2(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
|
|
265
|
+
return join2(directory, "index.html");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/commands/session.ts
|
|
269
|
+
function sessionTask() {
|
|
270
|
+
return `# Prepare this repository for AgentTwin
|
|
271
|
+
|
|
272
|
+
Read project instructions and .agenttwin/inventory.json first. Inspect tracked application source and configuration; skip secrets, .env values, private data, dependencies and build outputs. Use your configured coding-agent provider and normal permission controls.
|
|
273
|
+
|
|
274
|
+
Find distinct business workflows across the repository, including routes, queues, schedules, agent tools and exported functions. Do not cap discovery at a handful of examples. Trace callers and side effects across files. For each candidate record its real relative entrypoint, intent, inputs, dependency operations, expected observable outcome and plausible production failures. Distinguish observed code from hypotheses. Do not claim discovery is exhaustive; report skipped files and unresolved dynamic paths.
|
|
275
|
+
|
|
276
|
+
Write a human-readable .agenttwin/WORKFLOWS.md. For dashboard handoff, write .agenttwin/agent-discovery.json using the metadata-only company-inventory/v1 schema below. Start with the tools in .agenttwin/dashboard/assets/dashboard/inventory.json and retain their IDs; add candidates to workflows. Include no source bodies, credentials, PII or absolute paths. Never mark a candidate verified or executable merely because it was discovered.
|
|
277
|
+
|
|
278
|
+
Example shape (replace these descriptive placeholders with evidence from this repo):
|
|
279
|
+
{"schema":"company-inventory/v1","name":"project","tools":[],"workflows":[{"id":"agent:unique_workflow","title":"Workflow intent","entrypoint":"src/path.ts#function","confidence":"medium","supported":false,"verified":false,"tools":[],"steps":[]}]}
|
|
280
|
+
Each tool must have id, title, kind, category, confidence, interface. Each step must have tool (an existing tool ID) and operations (string array).
|
|
281
|
+
|
|
282
|
+
Before execution, ask the user for missing input, expected outcomes and any required synthetic fixtures. Prepare one real workflow at a time: an executable Node entrypoint plus its required application files, reviewed dependency mappings, synthetic starting state, assertions and an explicit fault when appropriate. Read agenttwin prepare --help and the existing preparation schema rather than guessing fields. The current hosted runtime supports Node, PostgreSQL, Redis, bounded Slack operations and explicit custom JSON collections. Unsupported vendor SDKs, browser and desktop interactions are blockers; never represent generic JSON endpoints as full vendor twins.
|
|
283
|
+
|
|
284
|
+
Do not run application workflows, Docker or containers locally. Do not access production accounts or copy production data. Do not print credentials. Execute reviewed workflows only through npx agenttwin run in a fresh hosted sandbox with AgentTwin account credentials or E2B credentials. Upload application files only after the user reviews the bundle. When a run fails, use the actual run receipt, logs, failed assertions and source digests to propose a fix. Explain uncertainty, edit the approved code, rebuild the bundle and retest in a new sandbox; never report success without passing evidence.
|
|
285
|
+
|
|
286
|
+
After review, npx agenttwin sync sends discovery metadata to the account dashboard using AGENTTWIN_TOKEN (and optional AGENTTWIN_API_URL). It does not upload application source. The dashboard will list all candidate workflows and populate the environment map from these findings.
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
function installedOpenCode() {
|
|
290
|
+
return spawnSync("opencode", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
|
|
291
|
+
}
|
|
292
|
+
async function writeSessionTask(cwd) {
|
|
293
|
+
const path = join3(cwd, ".agenttwin/AGENT-TASK.md");
|
|
294
|
+
await writeFile2(path, sessionTask(), "utf8");
|
|
295
|
+
return path;
|
|
296
|
+
}
|
|
297
|
+
async function launchSession(cwd) {
|
|
298
|
+
if (!installedOpenCode()) throw new Error("OpenCode is not installed. Open .agenttwin/AGENT-TASK.md with your coding agent, or install OpenCode and retry init --agent opencode.");
|
|
299
|
+
const prompt = "Read .agenttwin/AGENT-TASK.md and follow it to discover and prepare this project for AgentTwin. Start by analyzing the repository and presenting workflow findings and missing execution requirements.";
|
|
300
|
+
await new Promise((resolve5, reject) => {
|
|
301
|
+
const child = spawn("opencode", ["--prompt", prompt], { cwd, stdio: "inherit", shell: false });
|
|
302
|
+
child.once("error", reject);
|
|
303
|
+
child.once("exit", (code, signal) => code === 0 ? resolve5() : reject(new Error(`Coding session ended ${signal || `with exit code ${code}`}. Discovery files remain available.`)));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async function loadDashboardDiscovery(cwd, inventory) {
|
|
307
|
+
const base = discoveryTransfer(dashboardInventory(cwd, inventory));
|
|
308
|
+
let raw;
|
|
309
|
+
try {
|
|
310
|
+
raw = await readFile3(join3(cwd, ".agenttwin/agent-discovery.json"), "utf8");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error.code === "ENOENT") return base;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
if (Buffer.byteLength(raw) > 2 * 1024 * 1024) throw new Error("Agent discovery exceeds 2 MiB");
|
|
316
|
+
const extra = discoveryTransfer(JSON.parse(raw));
|
|
317
|
+
const tools = new Map(base.tools.map((t) => [t.id, t]));
|
|
318
|
+
for (const tool of extra.tools) if (!tools.has(tool.id)) tools.set(tool.id, tool);
|
|
319
|
+
const workflows = new Map(base.workflows.map((w) => [w.id, w]));
|
|
320
|
+
for (const workflow of extra.workflows) if (!workflows.has(workflow.id)) workflows.set(workflow.id, { ...workflow, supported: false, verified: false });
|
|
321
|
+
const result = discoveryTransfer({ ...base, tools: [...tools.values()], workflows: [...workflows.values()] });
|
|
322
|
+
const ids = new Set(result.tools.map((t) => t.id));
|
|
323
|
+
if (result.workflows.some((w) => w.tools.some((id) => !ids.has(id)) || w.steps.some((s) => !ids.has(s.tool)))) throw new Error("Agent discovery references unknown tools. Correct .agenttwin/agent-discovery.json before syncing.");
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/commands/sync.ts
|
|
328
|
+
async function runSync(cwd) {
|
|
329
|
+
const token = process.env.AGENTTWIN_TOKEN;
|
|
330
|
+
if (!token || !/^at_user_[a-f0-9]{64}$/.test(token)) throw new Error("Set AGENTTWIN_TOKEN to your individual account token.");
|
|
331
|
+
const base = apiBase(process.env.AGENTTWIN_API_URL || "https://agenttwin-api.212.56.40.125.sslip.io");
|
|
332
|
+
const inventory = JSON.parse(await readFile4(join4(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
333
|
+
const payload = await loadDashboardDiscovery(cwd, inventory);
|
|
334
|
+
const response = await fetch(base + "/v1/discovery", { method: "POST", headers: { authorization: "Bearer " + token, "content-type": "application/json" }, body: JSON.stringify(payload), redirect: "error", signal: AbortSignal.timeout(2e4) });
|
|
335
|
+
const result = await response.json();
|
|
336
|
+
if (!response.ok) throw new Error(result.error || "Discovery sync failed: " + response.status);
|
|
337
|
+
return { synced: true, tools: payload.tools.length, workflows: payload.workflows.length, dashboard: "https://agenttwin.212.56.40.125.sslip.io/site/dashboard.html", uploaded: "discovery metadata only; no source files or credentials" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/commands/prepare.ts
|
|
341
|
+
import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile7, writeFile as writeFile4, rm as rm2 } from "fs/promises";
|
|
342
|
+
import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
|
|
343
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
344
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
345
|
+
|
|
346
|
+
// src/commands/run.ts
|
|
347
|
+
import { mkdtemp, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile3, realpath, lstat, rm, copyFile } from "fs/promises";
|
|
348
|
+
import { tmpdir } from "os";
|
|
349
|
+
import { dirname, isAbsolute, join as join6, relative, resolve, sep } from "path";
|
|
350
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
351
|
+
import { createHash, randomUUID } from "crypto";
|
|
352
|
+
|
|
175
353
|
// src/runtime/e2b.ts
|
|
176
354
|
import { Sandbox } from "e2b";
|
|
177
|
-
import { readFile as
|
|
355
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
178
356
|
import { randomBytes } from "crypto";
|
|
179
357
|
import { homedir } from "os";
|
|
180
|
-
import { join as
|
|
358
|
+
import { join as join5 } from "path";
|
|
181
359
|
async function executeE2B(stage, spec, log, signal) {
|
|
182
360
|
signal?.throwIfAborted();
|
|
183
361
|
const apiKey = process.env.E2B_API_KEY;
|
|
184
362
|
if (!apiKey) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Set E2B_API_KEY in your shell. It remains on the host and is never passed to the application." });
|
|
185
363
|
let saved = {};
|
|
186
364
|
try {
|
|
187
|
-
saved = JSON.parse(await
|
|
365
|
+
saved = JSON.parse(await readFile5(join5(homedir(), ".agenttwin/runtime.json"), "utf8"));
|
|
188
366
|
} catch {
|
|
189
367
|
}
|
|
190
368
|
const template = process.env.E2B_TEMPLATE_ID ?? saved.templateId;
|
|
@@ -203,7 +381,7 @@ async function executeE2B(stage, spec, log, signal) {
|
|
|
203
381
|
signal?.throwIfAborted();
|
|
204
382
|
await sandbox.commands.run("mkdir -p /input/app && chmod 755 /input /input/app", { user: "root" });
|
|
205
383
|
for (const file of ["world.json", "worker.js", ...spec.files.map((f) => `app/${f}`)]) {
|
|
206
|
-
await sandbox.files.write(`/input/${file}`, new Uint8Array(await
|
|
384
|
+
await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile5(join5(stage, file))).buffer, { user: "root" });
|
|
207
385
|
}
|
|
208
386
|
await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
|
|
209
387
|
const runtimeEnv = {};
|
|
@@ -326,12 +504,12 @@ async function stageApplication(root, files, destination) {
|
|
|
326
504
|
const source = await realpath(resolve(canonicalRoot, file));
|
|
327
505
|
const rel = relative(canonicalRoot, source);
|
|
328
506
|
if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel) || source !== resolve(canonicalRoot, file) || !(await lstat(source)).isFile()) throw new CliError(`Files must be regular files within the project, without symlinks: ${file}`, { code: EXIT.USAGE });
|
|
329
|
-
const contents = await
|
|
507
|
+
const contents = await readFile6(source);
|
|
330
508
|
bytes += contents.length;
|
|
331
509
|
if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
|
|
332
|
-
const target =
|
|
333
|
-
await
|
|
334
|
-
await
|
|
510
|
+
const target = join6(destination, file);
|
|
511
|
+
await mkdir2(dirname(target), { recursive: true });
|
|
512
|
+
await writeFile3(target, contents, { mode: 420 });
|
|
335
513
|
digests[file] = createHash("sha256").update(contents).digest("hex");
|
|
336
514
|
}
|
|
337
515
|
return digests;
|
|
@@ -340,14 +518,14 @@ async function runWorld(cwd, opts) {
|
|
|
340
518
|
const file = resolve(cwd, opts.file ?? "agenttwin.world.json");
|
|
341
519
|
let spec;
|
|
342
520
|
try {
|
|
343
|
-
spec = parseWorld(JSON.parse(await
|
|
521
|
+
spec = parseWorld(JSON.parse(await readFile6(file, "utf8")));
|
|
344
522
|
} catch (error) {
|
|
345
523
|
throw new CliError(String(error), { code: EXIT.USAGE, action: "Create agenttwin.world.json; run agenttwin example --out ./agenttwin-example for a working example." });
|
|
346
524
|
}
|
|
347
525
|
if (spec.workflowId) {
|
|
348
526
|
let inventory;
|
|
349
527
|
try {
|
|
350
|
-
inventory = JSON.parse(await
|
|
528
|
+
inventory = JSON.parse(await readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
351
529
|
} catch {
|
|
352
530
|
throw new CliError("Linked workflow needs discovery inventory; run init first", { code: EXIT.BLOCKED });
|
|
353
531
|
}
|
|
@@ -356,12 +534,12 @@ async function runWorld(cwd, opts) {
|
|
|
356
534
|
const runtime = opts.runtime ?? (process.env.AGENTTWIN_API_URL ? "hosted" : "e2b");
|
|
357
535
|
if (!["e2b", "hosted"].includes(runtime)) throw new CliError("runtime must be hosted or e2b; no local application execution", { code: EXIT.USAGE });
|
|
358
536
|
if (runtime === "e2b" && !process.env.E2B_API_KEY) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Configure your E2B key in the shell; it will not be uploaded." });
|
|
359
|
-
const stage = await mkdtemp(
|
|
537
|
+
const stage = await mkdtemp(join6(tmpdir(), "agenttwin-world-"));
|
|
360
538
|
try {
|
|
361
|
-
const sourceDigests = await stageApplication(dirname(file), spec.files,
|
|
362
|
-
await
|
|
363
|
-
const worker =
|
|
364
|
-
await copyFile(worker,
|
|
539
|
+
const sourceDigests = await stageApplication(dirname(file), spec.files, join6(stage, "app"));
|
|
540
|
+
await writeFile3(join6(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
|
|
541
|
+
const worker = join6(dirname(fileURLToPath2(import.meta.url)), "worker.js");
|
|
542
|
+
await copyFile(worker, join6(stage, "worker.js"));
|
|
365
543
|
if (runtime === "hosted") {
|
|
366
544
|
const receipt2 = await executeRemote(stage, spec, opts.log);
|
|
367
545
|
if (receipt2.schema !== "agenttwin/run-v1" || typeof receipt2.runId !== "string" || !/^[a-f0-9-]{36}$/.test(receipt2.runId) || !["passed", "failed", "error", "incomplete"].includes(String(receipt2.status))) throw new CliError("Invalid hosted receipt");
|
|
@@ -369,11 +547,11 @@ async function runWorld(cwd, opts) {
|
|
|
369
547
|
const reportedSources = receipt2.sourceDigests;
|
|
370
548
|
if (receipt2.specDigest !== expectedSpec || !reportedSources || Object.keys(reportedSources).length !== Object.keys(sourceDigests).length || Object.entries(sourceDigests).some(([path, digest]) => reportedSources[path] !== digest)) throw new CliError("Hosted receipt does not match the submitted source/specification");
|
|
371
549
|
const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
|
|
372
|
-
await
|
|
373
|
-
const evidencePath =
|
|
374
|
-
const reportPath =
|
|
375
|
-
await
|
|
376
|
-
await
|
|
550
|
+
await mkdir2(out2, { recursive: true });
|
|
551
|
+
const evidencePath = join6(out2, "run.json");
|
|
552
|
+
const reportPath = join6(out2, "report.md");
|
|
553
|
+
await writeFile3(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
|
|
554
|
+
await writeFile3(reportPath, `# ${spec.name}: ${receipt2.status}
|
|
377
555
|
|
|
378
556
|
Run: ${receipt2.runId}
|
|
379
557
|
|
|
@@ -405,12 +583,12 @@ See run.json for assertions, service state, application output and isolation met
|
|
|
405
583
|
isolation
|
|
406
584
|
};
|
|
407
585
|
const out = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
|
|
408
|
-
await
|
|
409
|
-
await
|
|
410
|
-
await
|
|
586
|
+
await mkdir2(out, { recursive: true });
|
|
587
|
+
await writeFile3(join6(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
|
|
588
|
+
await writeFile3(join6(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
|
|
411
589
|
const summary = [`# ${spec.name}: ${report.status}`, "", `Run: ${runId}`, `Image: ${image}`, "", "## Assertions", "", ...report.assertions.map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}${a.path}: expected ${a.count}, observed ${a.actual}`), "", "## Infrastructure assertions", "", ...(report.infrastructureAssertions ?? []).map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}: ${JSON.stringify(a.actual)}`), "", "## Coverage", "", report.coverage, "", "External networking disabled. Only explicitly listed source files were uploaded. New state and tokens are created for each run.", "", "See run.json for application output, faults, events, and initial/final state.", ""].join("\n");
|
|
412
|
-
await
|
|
413
|
-
return { ...receipt, reportPath:
|
|
590
|
+
await writeFile3(join6(out, "report.md"), summary);
|
|
591
|
+
return { ...receipt, reportPath: join6(out, "report.md"), evidencePath: join6(out, "run.json") };
|
|
414
592
|
} finally {
|
|
415
593
|
await rm(stage, { recursive: true, force: true });
|
|
416
594
|
}
|
|
@@ -463,30 +641,36 @@ function reviewPreparation(inventory, input) {
|
|
|
463
641
|
async function runPrepare(cwd, opts) {
|
|
464
642
|
if (!opts.file) throw new CliError("prepare requires --file preparation.json", { code: EXIT.USAGE });
|
|
465
643
|
const file = resolve2(cwd, opts.file);
|
|
466
|
-
const inventory = JSON.parse(await
|
|
467
|
-
const
|
|
644
|
+
const inventory = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
645
|
+
const discovered = await loadDashboardDiscovery(cwd, inventory);
|
|
646
|
+
const known = new Set(inventory.workflows.map((w) => w.id));
|
|
647
|
+
for (const w of discovered.workflows) if (!known.has(w.id)) {
|
|
648
|
+
inventory.workflows.push({ ...w, confidence: "low", apps: [], sharedEntities: [], evidence: [{ source: "agent-analysis", ref: ".agenttwin/agent-discovery.json" }], steps: w.steps.map((s) => ({ ...s, app: "", evidence: [] })), risk: "high", rank: 0, supported: false, hypothesis: true, linkingSignals: ["Coding-agent analysis requires user review"], scopeUncertain: true, verified: false });
|
|
649
|
+
}
|
|
650
|
+
for (const t of discovered.tools) if (!inventory.tools.some((tool) => tool.id === t.id)) throw new Error("Agent-discovered dependencies must be classified in init before preparation. Review the additional tool, then rerun init.");
|
|
651
|
+
const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile7(file, "utf8")));
|
|
468
652
|
const directory = resolve2(cwd, opts.out ?? `.agenttwin/prepared/${spec.name}-${randomUUID2().slice(0, 8)}`);
|
|
469
|
-
const stage = await mkdtemp2(
|
|
653
|
+
const stage = await mkdtemp2(join7(tmpdir2(), "agenttwin-prepare-"));
|
|
470
654
|
try {
|
|
471
655
|
const sourceDigests = await stageApplication(dirname2(file), spec.files, stage);
|
|
472
656
|
const files = /* @__PURE__ */ Object.create(null);
|
|
473
|
-
for (const name of spec.files) files[name] = (await
|
|
657
|
+
for (const name of spec.files) files[name] = (await readFile7(join7(stage, name))).toString("base64");
|
|
474
658
|
const manifest = { schema: "agenttwin/prepared-v1", workflowId: workflow.id, invocation: spec.command, sourceDigests, specDigest: createHash2("sha256").update(JSON.stringify(spec)).digest("hex"), mappings, provenance: "user-reviewed invocation and operation contracts; discovery does not verify runtime fidelity", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
475
|
-
await
|
|
476
|
-
await
|
|
477
|
-
await
|
|
478
|
-
await
|
|
479
|
-
await
|
|
480
|
-
await
|
|
481
|
-
await
|
|
659
|
+
await writeFile4(join7(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
|
|
660
|
+
await writeFile4(join7(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
|
|
661
|
+
await writeFile4(join7(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
|
|
662
|
+
await mkdir3(join7(stage, ".agenttwin"), { recursive: true });
|
|
663
|
+
await writeFile4(join7(stage, ".agenttwin/inventory.json"), JSON.stringify({ workflows: [{ id: workflow.id }] }), { mode: 384 });
|
|
664
|
+
await mkdir3(dirname2(directory), { recursive: true });
|
|
665
|
+
await mkdir3(directory, { mode: 448 });
|
|
482
666
|
try {
|
|
483
667
|
const { cp: cp2, readdir: readdir2 } = await import("fs/promises");
|
|
484
|
-
for (const entry of await readdir2(stage)) await cp2(
|
|
668
|
+
for (const entry of await readdir2(stage)) await cp2(join7(stage, entry), join7(directory, entry), { recursive: true, force: false, errorOnExist: true });
|
|
485
669
|
} catch (error) {
|
|
486
670
|
await rm2(directory, { recursive: true, force: true });
|
|
487
671
|
throw error;
|
|
488
672
|
}
|
|
489
|
-
return { directory, worldPath:
|
|
673
|
+
return { directory, worldPath: join7(directory, "agenttwin.world.json"), bundlePath: join7(directory, "agenttwin.bundle.json"), manifest };
|
|
490
674
|
} finally {
|
|
491
675
|
await rm2(stage, { recursive: true, force: true });
|
|
492
676
|
}
|
|
@@ -494,9 +678,9 @@ async function runPrepare(cwd, opts) {
|
|
|
494
678
|
|
|
495
679
|
// src/commands/setup.ts
|
|
496
680
|
import { Template } from "e2b";
|
|
497
|
-
import { mkdir as
|
|
681
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
|
|
498
682
|
import { homedir as homedir2 } from "os";
|
|
499
|
-
import { join as
|
|
683
|
+
import { join as join8 } from "path";
|
|
500
684
|
async function runSetup(log) {
|
|
501
685
|
if (!process.env.E2B_API_KEY) throw new CliError("Set E2B_API_KEY before creating your hosted runtime template", { code: EXIT.BLOCKED });
|
|
502
686
|
log("Building the AgentTwin template on E2B (Node 22, PostgreSQL 15, Redis). No local containers are used.");
|
|
@@ -504,16 +688,16 @@ async function runSetup(log) {
|
|
|
504
688
|
const result = await Template.build(template, "agenttwin-node-v2", { apiKey: process.env.E2B_API_KEY, cpuCount: 2, memoryMB: 2048, onBuildLogs: (entry) => {
|
|
505
689
|
if (!entry.message.includes("[stdout]") && !entry.message.includes("[stderr]")) log(entry.message);
|
|
506
690
|
} });
|
|
507
|
-
const directory =
|
|
508
|
-
await
|
|
509
|
-
const path =
|
|
510
|
-
await
|
|
691
|
+
const directory = join8(homedir2(), ".agenttwin");
|
|
692
|
+
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
693
|
+
const path = join8(directory, "runtime.json");
|
|
694
|
+
await writeFile5(path, JSON.stringify({ provider: "e2b", templateId: result.templateId, buildId: result.buildId }, null, 2) + "\n", { mode: 384 });
|
|
511
695
|
return { provider: "e2b", templateId: result.templateId, buildId: result.buildId, configPath: path };
|
|
512
696
|
}
|
|
513
697
|
|
|
514
698
|
// src/commands/example.ts
|
|
515
|
-
import { mkdir as
|
|
516
|
-
import { resolve as resolve3, join as
|
|
699
|
+
import { mkdir as mkdir5, writeFile as writeFile6, stat } from "fs/promises";
|
|
700
|
+
import { resolve as resolve3, join as join9 } from "path";
|
|
517
701
|
var exampleSource = `// Replace this file with your own bundled Node workflow.
|
|
518
702
|
const safe = process.argv.includes('--safe');
|
|
519
703
|
const crm = process.env.AGENTTWIN_CRM_URL;
|
|
@@ -557,27 +741,27 @@ function exampleWorld(safe = false) {
|
|
|
557
741
|
async function runExample(cwd, out) {
|
|
558
742
|
const directory = resolve3(cwd, out ?? "agenttwin-example");
|
|
559
743
|
if (await stat(directory).then(() => true, () => false)) throw new CliError(`Destination exists: ${directory}`, { code: EXIT.USAGE, action: "Choose a new --out directory." });
|
|
560
|
-
await
|
|
561
|
-
await
|
|
562
|
-
await
|
|
563
|
-
await
|
|
564
|
-
await
|
|
744
|
+
await mkdir5(directory, { recursive: true });
|
|
745
|
+
await writeFile6(join9(directory, "workflow.mjs"), exampleSource);
|
|
746
|
+
await writeFile6(join9(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
|
|
747
|
+
await writeFile6(join9(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
|
|
748
|
+
await writeFile6(join9(directory, "README.md"), `# Support workflow
|
|
565
749
|
|
|
566
750
|
Run with agenttwin run --file agenttwin.world.json (expected FAIL), then --file safe.world.json (expected PASS). Requires AGENTTWIN_API_URL and AGENTTWIN_TOKEN, or your own E2B_API_KEY plus a template from agenttwin setup. Each run creates a fresh hosted sandbox and deletes it afterward.
|
|
567
751
|
|
|
568
752
|
This is actual application code using a synthetic custom ticket API plus the supported Slack JSON subset. It is not a clone of a CRM vendor. Replace workflow.mjs with your own built code and edit the explicit file list, endpoints, seed records and assertions. No npm install runs in the sandbox; dependencies must be bundled into your application.
|
|
569
753
|
`);
|
|
570
|
-
return { directory, worldPath:
|
|
754
|
+
return { directory, worldPath: join9(directory, "agenttwin.world.json") };
|
|
571
755
|
}
|
|
572
756
|
|
|
573
757
|
// src/commands/init.ts
|
|
574
758
|
import { readFileSync as readFileSync2 } from "fs";
|
|
575
|
-
import { writeFile as
|
|
576
|
-
import { join as
|
|
759
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
760
|
+
import { join as join16 } from "path";
|
|
577
761
|
|
|
578
762
|
// src/lib/files.ts
|
|
579
|
-
import { readdir, readFile as
|
|
580
|
-
import { join as
|
|
763
|
+
import { readdir, readFile as readFile8, stat as stat2 } from "fs/promises";
|
|
764
|
+
import { join as join10, relative as relative2, sep as sep2 } from "path";
|
|
581
765
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
582
766
|
".git",
|
|
583
767
|
".agenttwin",
|
|
@@ -635,7 +819,7 @@ async function walkDir(root, maxFiles, budgetMs) {
|
|
|
635
819
|
}
|
|
636
820
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
637
821
|
for (const entry of entries) {
|
|
638
|
-
const full =
|
|
822
|
+
const full = join10(dir, entry.name);
|
|
639
823
|
if (entry.isDirectory()) {
|
|
640
824
|
if (!shouldIgnoreDir(entry.name)) stack.push(full);
|
|
641
825
|
} else if (entry.isFile()) {
|
|
@@ -669,7 +853,7 @@ var RepoFileIndex = class {
|
|
|
669
853
|
}
|
|
670
854
|
async exists(path) {
|
|
671
855
|
try {
|
|
672
|
-
await stat2(
|
|
856
|
+
await stat2(join10(this.cwd, path));
|
|
673
857
|
return true;
|
|
674
858
|
} catch {
|
|
675
859
|
return false;
|
|
@@ -678,7 +862,7 @@ var RepoFileIndex = class {
|
|
|
678
862
|
async read(path, maxBytes = MAX_BYTES) {
|
|
679
863
|
const cached = this.cache.contents.get(path);
|
|
680
864
|
if (cached !== void 0) return cached;
|
|
681
|
-
const full =
|
|
865
|
+
const full = join10(this.cwd, path);
|
|
682
866
|
try {
|
|
683
867
|
const info2 = await stat2(full);
|
|
684
868
|
if (!info2.isFile()) return void 0;
|
|
@@ -686,7 +870,7 @@ var RepoFileIndex = class {
|
|
|
686
870
|
this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
|
|
687
871
|
return void 0;
|
|
688
872
|
}
|
|
689
|
-
const text = await
|
|
873
|
+
const text = await readFile8(full, "utf8");
|
|
690
874
|
this.cache.contents.set(path, text);
|
|
691
875
|
return text;
|
|
692
876
|
} catch {
|
|
@@ -1546,13 +1730,13 @@ var containerDetector = {
|
|
|
1546
1730
|
// src/discovery/detectors/cloud-creds.ts
|
|
1547
1731
|
import { stat as stat3 } from "fs/promises";
|
|
1548
1732
|
import { homedir as homedir3 } from "os";
|
|
1549
|
-
import { join as
|
|
1733
|
+
import { join as join11 } from "path";
|
|
1550
1734
|
var CREDENTIALS = [
|
|
1551
|
-
{ path:
|
|
1552
|
-
{ path:
|
|
1553
|
-
{ path:
|
|
1554
|
-
{ path:
|
|
1555
|
-
{ path:
|
|
1735
|
+
{ path: join11(homedir3(), ".aws", "config"), subject: "aws", title: "AWS" },
|
|
1736
|
+
{ path: join11(homedir3(), ".aws", "credentials"), subject: "aws", title: "AWS" },
|
|
1737
|
+
{ path: join11(homedir3(), ".config", "gcloud"), subject: "gcp", title: "Google Cloud" },
|
|
1738
|
+
{ path: join11(homedir3(), ".azure"), subject: "azure", title: "Azure" },
|
|
1739
|
+
{ path: join11(homedir3(), ".kube", "config"), subject: "kubernetes", title: "Kubernetes" }
|
|
1556
1740
|
];
|
|
1557
1741
|
async function exists(path) {
|
|
1558
1742
|
try {
|
|
@@ -1603,13 +1787,13 @@ function registerDetectors() {
|
|
|
1603
1787
|
}
|
|
1604
1788
|
|
|
1605
1789
|
// src/discovery/masking.ts
|
|
1606
|
-
var
|
|
1607
|
-
var
|
|
1790
|
+
var EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
1791
|
+
var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
1608
1792
|
function maskSecret(value) {
|
|
1609
|
-
return value.replace(
|
|
1793
|
+
return value.replace(TOKENISH2, (match) => `${match.slice(0, 4)}***`);
|
|
1610
1794
|
}
|
|
1611
1795
|
function maskEmail(value) {
|
|
1612
|
-
return value.replace(
|
|
1796
|
+
return value.replace(EMAIL2, "$1***$2");
|
|
1613
1797
|
}
|
|
1614
1798
|
function maskUrl(value) {
|
|
1615
1799
|
try {
|
|
@@ -2111,12 +2295,12 @@ var SimpleDetectorRegistry = class {
|
|
|
2111
2295
|
};
|
|
2112
2296
|
|
|
2113
2297
|
// src/discovery/passes/runtime.ts
|
|
2114
|
-
import { spawn } from "child_process";
|
|
2298
|
+
import { spawn as spawn2 } from "child_process";
|
|
2115
2299
|
import { createServer } from "http";
|
|
2116
2300
|
import { connect } from "net";
|
|
2117
2301
|
import { readFileSync, rmSync, writeFileSync } from "fs";
|
|
2118
2302
|
import { tmpdir as tmpdir3 } from "os";
|
|
2119
|
-
import { join as
|
|
2303
|
+
import { join as join12 } from "path";
|
|
2120
2304
|
var EGRESS_HOOK = `
|
|
2121
2305
|
const fs = require('fs');
|
|
2122
2306
|
const dns = require('dns');
|
|
@@ -2186,8 +2370,8 @@ function startProxy(onHost) {
|
|
|
2186
2370
|
async function observeEgress(opts) {
|
|
2187
2371
|
const hosts = /* @__PURE__ */ new Set();
|
|
2188
2372
|
const { server, url } = await startProxy((host) => hosts.add(host));
|
|
2189
|
-
const logFile =
|
|
2190
|
-
const hookFile =
|
|
2373
|
+
const logFile = join12(tmpdir3(), `agenttwin-egress-${process.pid}.log`);
|
|
2374
|
+
const hookFile = join12(tmpdir3(), `agenttwin-egress-hook-${process.pid}.cjs`);
|
|
2191
2375
|
try {
|
|
2192
2376
|
writeFileSync(hookFile, EGRESS_HOOK, "utf8");
|
|
2193
2377
|
writeFileSync(logFile, "", "utf8");
|
|
@@ -2196,7 +2380,7 @@ async function observeEgress(opts) {
|
|
|
2196
2380
|
let child;
|
|
2197
2381
|
if (opts.run) {
|
|
2198
2382
|
opts.log(`Observing egress: spawning \`${opts.run}\` through ${url}`);
|
|
2199
|
-
child =
|
|
2383
|
+
child = spawn2(opts.run, {
|
|
2200
2384
|
shell: true,
|
|
2201
2385
|
env: {
|
|
2202
2386
|
...process.env,
|
|
@@ -2654,6 +2838,32 @@ function singularize(word) {
|
|
|
2654
2838
|
return word;
|
|
2655
2839
|
}
|
|
2656
2840
|
|
|
2841
|
+
// src/inventory/relationships.ts
|
|
2842
|
+
import { createHash as createHash4 } from "crypto";
|
|
2843
|
+
|
|
2844
|
+
// src/inventory/entrypoints.ts
|
|
2845
|
+
function codeOnly(source) {
|
|
2846
|
+
return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
|
|
2847
|
+
}
|
|
2848
|
+
function exportedEntrypoints(source) {
|
|
2849
|
+
const code = codeOnly(source);
|
|
2850
|
+
const pattern = /\bexport\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)[^{;]*\{|\bexport\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::[^=;]+)?=>\s*\{/g;
|
|
2851
|
+
const result = [];
|
|
2852
|
+
let match;
|
|
2853
|
+
while (match = pattern.exec(code)) {
|
|
2854
|
+
let depth = 1, end = pattern.lastIndex;
|
|
2855
|
+
while (end < code.length && depth) {
|
|
2856
|
+
if (code[end] === "{") depth++;
|
|
2857
|
+
if (code[end] === "}") depth--;
|
|
2858
|
+
end++;
|
|
2859
|
+
}
|
|
2860
|
+
if (depth) continue;
|
|
2861
|
+
result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
|
|
2862
|
+
pattern.lastIndex = end;
|
|
2863
|
+
}
|
|
2864
|
+
return result;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2657
2867
|
// src/inventory/operations.ts
|
|
2658
2868
|
var WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
2659
2869
|
"create",
|
|
@@ -2708,7 +2918,7 @@ function scanOperations(text, path) {
|
|
|
2708
2918
|
seen.add(key);
|
|
2709
2919
|
out.push({ receiver, resource, verb, kind, ref: `${path}:${line}` });
|
|
2710
2920
|
}
|
|
2711
|
-
return out
|
|
2921
|
+
return out;
|
|
2712
2922
|
}
|
|
2713
2923
|
|
|
2714
2924
|
// src/inventory/relationships.ts
|
|
@@ -2888,9 +3098,12 @@ async function buildRelationships(apps, tools, files, decisions) {
|
|
|
2888
3098
|
const roles = /* @__PURE__ */ new Map();
|
|
2889
3099
|
const fileEntities = /* @__PURE__ */ new Map();
|
|
2890
3100
|
const uncertainOperations = [];
|
|
3101
|
+
const entrypointCalls = [];
|
|
3102
|
+
const sourceText = /* @__PURE__ */ new Map();
|
|
2891
3103
|
for (const path of sourceFiles) {
|
|
2892
3104
|
const text = await files.read(path, 2e5);
|
|
2893
3105
|
if (!text) continue;
|
|
3106
|
+
sourceText.set(path, text);
|
|
2894
3107
|
const app = appForPath(apps, path);
|
|
2895
3108
|
const role = roles.get(app.id) ?? { produced: /* @__PURE__ */ new Set(), consumed: /* @__PURE__ */ new Set() };
|
|
2896
3109
|
collect(PRODUCE, text, role.produced);
|
|
@@ -2923,6 +3136,7 @@ async function buildRelationships(apps, tools, files, decisions) {
|
|
|
2923
3136
|
evidence: []
|
|
2924
3137
|
};
|
|
2925
3138
|
const label = `${op.resource}.${op.verb}`;
|
|
3139
|
+
entrypointCalls.push({ path, line: Number(op.ref.slice(path.length + 1)), providerId, toolId: tool.id, kind: op.kind, label, resource: op.resource, ref: op.ref });
|
|
2926
3140
|
if (!record2.operations.includes(label)) record2.operations.push(label);
|
|
2927
3141
|
if (!record2.resources.includes(op.resource)) record2.resources.push(op.resource);
|
|
2928
3142
|
if (!record2.evidence.some((e) => e.ref === op.ref)) record2.evidence.push({ source: "static", ref: op.ref });
|
|
@@ -3066,6 +3280,34 @@ async function buildRelationships(apps, tools, files, decisions) {
|
|
|
3066
3280
|
verified: false
|
|
3067
3281
|
});
|
|
3068
3282
|
}
|
|
3283
|
+
for (const [path, text] of sourceText) {
|
|
3284
|
+
const app = appForPath(apps, path);
|
|
3285
|
+
for (const entry of exportedEntrypoints(text)) {
|
|
3286
|
+
const calls = entrypointCalls.filter((call) => call.path === path && call.line >= entry.startLine && call.line <= entry.endLine && decidedRecords.some((record2) => record2.toolId === call.toolId && record2.kind === call.kind && record2.evidence.some((e) => e.ref === call.ref)));
|
|
3287
|
+
if (!calls.some((call) => call.kind === "writes" || call.kind === "emits")) continue;
|
|
3288
|
+
const dependencies = [...new Set(calls.map((call) => call.toolId))];
|
|
3289
|
+
const evidence = [...new Set(calls.map((call) => call.ref))].map((ref) => ({ source: "static", ref }));
|
|
3290
|
+
workflows.push({
|
|
3291
|
+
id: "entry:" + createHash4("sha256").update(path + ":" + entry.name).digest("hex").slice(0, 20),
|
|
3292
|
+
title: entry.name,
|
|
3293
|
+
entrypoint: path + "#" + entry.name,
|
|
3294
|
+
apps: [app.id],
|
|
3295
|
+
tools: dependencies,
|
|
3296
|
+
steps: dependencies.map((tool) => ({ app: app.id, tool, operations: [...new Set(calls.filter((c) => c.toolId === tool).map((c) => c.label))], evidence })),
|
|
3297
|
+
sharedEntities: [...new Set(calls.map((call) => singularize(call.resource)))],
|
|
3298
|
+
evidence,
|
|
3299
|
+
confidence: "medium",
|
|
3300
|
+
risk: "high",
|
|
3301
|
+
rank: 0,
|
|
3302
|
+
supported: false,
|
|
3303
|
+
hypothesis: true,
|
|
3304
|
+
linkingSignals: ["exported function containing binding-resolved side effects; invocation and correctness require review"],
|
|
3305
|
+
scopeUncertain: true,
|
|
3306
|
+
verified: false
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (sourceFiles.length === MAX_FILES3) gaps.push("Source scan is limited to 2500 files; split large repositories into explicit roots. Not every workflow can be proven by static discovery.");
|
|
3069
3311
|
const finalWorkflows = workflows.filter((w) => !rejected.has(w.id)).map(
|
|
3070
3312
|
(w) => confirmed.has(w.id) ? { ...w, supported: true, hypothesis: false, linkingSignals: [...w.linkingSignals, "confirmed by decision"] } : w
|
|
3071
3313
|
);
|
|
@@ -3379,8 +3621,8 @@ async function buildInventory({
|
|
|
3379
3621
|
}
|
|
3380
3622
|
|
|
3381
3623
|
// src/inventory/store.ts
|
|
3382
|
-
import { mkdir as
|
|
3383
|
-
import { join as
|
|
3624
|
+
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
|
|
3625
|
+
import { join as join13 } from "path";
|
|
3384
3626
|
var STORE_DIR = ".agenttwin";
|
|
3385
3627
|
var STORE_GITIGNORE = [
|
|
3386
3628
|
"inventory.json",
|
|
@@ -3396,24 +3638,24 @@ var STORE_GITIGNORE = [
|
|
|
3396
3638
|
""
|
|
3397
3639
|
].join("\n");
|
|
3398
3640
|
function storeDir(cwd) {
|
|
3399
|
-
return
|
|
3641
|
+
return join13(cwd, STORE_DIR);
|
|
3400
3642
|
}
|
|
3401
3643
|
function storePath(cwd, name) {
|
|
3402
|
-
return
|
|
3644
|
+
return join13(storeDir(cwd), name);
|
|
3403
3645
|
}
|
|
3404
3646
|
async function ensureStore(cwd) {
|
|
3405
|
-
await
|
|
3406
|
-
await
|
|
3647
|
+
await mkdir6(storeDir(cwd), { recursive: true });
|
|
3648
|
+
await writeFile7(storePath(cwd, ".gitignore"), STORE_GITIGNORE, "utf8");
|
|
3407
3649
|
}
|
|
3408
3650
|
async function readJson(path) {
|
|
3409
3651
|
try {
|
|
3410
|
-
return JSON.parse(await
|
|
3652
|
+
return JSON.parse(await readFile9(path, "utf8"));
|
|
3411
3653
|
} catch {
|
|
3412
3654
|
return void 0;
|
|
3413
3655
|
}
|
|
3414
3656
|
}
|
|
3415
3657
|
async function writeJson(path, value) {
|
|
3416
|
-
await
|
|
3658
|
+
await writeFile7(path, `${JSON.stringify(value, null, 2)}
|
|
3417
3659
|
`, "utf8");
|
|
3418
3660
|
}
|
|
3419
3661
|
var readInventory = (cwd) => readJson(storePath(cwd, "inventory.json"));
|
|
@@ -3616,30 +3858,13 @@ var REGISTRY = [
|
|
|
3616
3858
|
];
|
|
3617
3859
|
|
|
3618
3860
|
// src/inspect/runner.ts
|
|
3619
|
-
import { spawn as
|
|
3620
|
-
|
|
3621
|
-
// src/inspect/sanitize.ts
|
|
3622
|
-
var URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
|
|
3623
|
-
var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
3624
|
-
var EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
3625
|
-
function stripUrlCredentials(value) {
|
|
3626
|
-
return value.replace(URL_CREDENTIALS, "$1");
|
|
3627
|
-
}
|
|
3628
|
-
function sanitizeOutput(value) {
|
|
3629
|
-
return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL2, "$1***$2");
|
|
3630
|
-
}
|
|
3631
|
-
function excerpt(value, max = 200) {
|
|
3632
|
-
const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
|
|
3633
|
-
return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
// src/inspect/runner.ts
|
|
3861
|
+
import { spawn as spawn3 } from "child_process";
|
|
3637
3862
|
function runCommand(spec, cwd, signal) {
|
|
3638
3863
|
return new Promise((resolve5) => {
|
|
3639
3864
|
if (signal.aborted) {
|
|
3640
3865
|
return resolve5({ code: null, stdout: "", stderr: "", missing: false, timedOut: false, cancelled: true });
|
|
3641
3866
|
}
|
|
3642
|
-
const child =
|
|
3867
|
+
const child = spawn3(spec.cmd, spec.args, {
|
|
3643
3868
|
cwd,
|
|
3644
3869
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3645
3870
|
env: process.env
|
|
@@ -4177,12 +4402,12 @@ async function runExport(opts) {
|
|
|
4177
4402
|
const plan = await readPlan(opts.cwd);
|
|
4178
4403
|
const bundle = { schema: "export/v1", inventory, plan: plan ?? null };
|
|
4179
4404
|
if (opts.out) {
|
|
4180
|
-
const { mkdir: mkdir8, writeFile:
|
|
4181
|
-
const { join:
|
|
4405
|
+
const { mkdir: mkdir8, writeFile: writeFile11 } = await import("fs/promises");
|
|
4406
|
+
const { join: join18 } = await import("path");
|
|
4182
4407
|
await mkdir8(opts.out, { recursive: true });
|
|
4183
|
-
await
|
|
4408
|
+
await writeFile11(join18(opts.out, "inventory.json"), `${JSON.stringify(inventory, null, 2)}
|
|
4184
4409
|
`, "utf8");
|
|
4185
|
-
if (plan) await
|
|
4410
|
+
if (plan) await writeFile11(join18(opts.out, "plan.json"), `${JSON.stringify(plan, null, 2)}
|
|
4186
4411
|
`, "utf8");
|
|
4187
4412
|
return { schema: "export/v1", out: opts.out, wrote: ["inventory.json", ...plan ? ["plan.json"] : []] };
|
|
4188
4413
|
}
|
|
@@ -4582,16 +4807,16 @@ async function askQuestions(questions) {
|
|
|
4582
4807
|
init_answers();
|
|
4583
4808
|
|
|
4584
4809
|
// src/lib/config.ts
|
|
4585
|
-
import { readFile as
|
|
4586
|
-
import { join as
|
|
4810
|
+
import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
4811
|
+
import { join as join14 } from "path";
|
|
4587
4812
|
var CONFIG_FILENAME = "agenttwin.config.json";
|
|
4588
4813
|
var CONFIG_VERSION = 2;
|
|
4589
4814
|
function configPath(cwd) {
|
|
4590
|
-
return
|
|
4815
|
+
return join14(cwd, CONFIG_FILENAME);
|
|
4591
4816
|
}
|
|
4592
4817
|
async function readConfig(cwd) {
|
|
4593
4818
|
try {
|
|
4594
|
-
const text = await
|
|
4819
|
+
const text = await readFile10(configPath(cwd), "utf8");
|
|
4595
4820
|
return JSON.parse(text);
|
|
4596
4821
|
} catch {
|
|
4597
4822
|
return void 0;
|
|
@@ -4627,57 +4852,14 @@ function buildConfigFromInventory(inventory, existing) {
|
|
|
4627
4852
|
}
|
|
4628
4853
|
async function writeConfig(cwd, config, _options = {}) {
|
|
4629
4854
|
const path = configPath(cwd);
|
|
4630
|
-
await
|
|
4855
|
+
await writeFile8(path, `${JSON.stringify(config, null, 2)}
|
|
4631
4856
|
`, "utf8");
|
|
4632
4857
|
return { path };
|
|
4633
4858
|
}
|
|
4634
4859
|
|
|
4635
|
-
// src/commands/dashboard.ts
|
|
4636
|
-
import { cp, mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
4637
|
-
import { basename, join as join12 } from "path";
|
|
4638
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4639
|
-
function dashboardInventory(cwd, inventory) {
|
|
4640
|
-
return JSON.parse(sanitizeOutput(JSON.stringify({
|
|
4641
|
-
schema: "company-inventory/v1",
|
|
4642
|
-
generatedAt: inventory.generatedAt,
|
|
4643
|
-
name: basename(cwd),
|
|
4644
|
-
tools: inventory.tools.map((t) => ({
|
|
4645
|
-
id: t.id,
|
|
4646
|
-
title: t.title,
|
|
4647
|
-
kind: t.kind,
|
|
4648
|
-
category: t.category,
|
|
4649
|
-
confidence: t.confidence,
|
|
4650
|
-
interface: t.kind === "runtime" ? "terminal" : "api"
|
|
4651
|
-
})),
|
|
4652
|
-
workflows: inventory.workflows.map((w) => ({ id: w.id, title: w.title, confidence: w.confidence, supported: w.supported, verified: false, tools: w.tools, steps: w.steps.map((s) => ({ tool: s.tool, operations: s.operations })) }))
|
|
4653
|
-
})));
|
|
4654
|
-
}
|
|
4655
|
-
async function writeDashboard(cwd, inventory) {
|
|
4656
|
-
const directory = join12(cwd, ".agenttwin", "dashboard");
|
|
4657
|
-
let bundle = fileURLToPath2(new URL("./dashboard/", import.meta.url));
|
|
4658
|
-
try {
|
|
4659
|
-
await readFile8(join12(bundle, "index.html"));
|
|
4660
|
-
} catch {
|
|
4661
|
-
bundle = fileURLToPath2(new URL("../../../../site/", import.meta.url));
|
|
4662
|
-
}
|
|
4663
|
-
await mkdir6(join12(directory, "assets", "dashboard"), { recursive: true });
|
|
4664
|
-
const html = await readFile8(join12(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
|
|
4665
|
-
for (const folder of ["dashboard", "icons", "logo", "fonts"]) {
|
|
4666
|
-
await cp(join12(bundle, "assets", folder), join12(directory, "assets", folder), { recursive: true });
|
|
4667
|
-
}
|
|
4668
|
-
await cp(join12(bundle, "assets", "brand.css"), join12(directory, "assets", "brand.css"));
|
|
4669
|
-
const data = dashboardInventory(cwd, inventory);
|
|
4670
|
-
const payload = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
4671
|
-
const page2 = html.replace('type="module"', "defer").replace('<script src="assets/dashboard/dashboard.js"', `<script id="agenttwin-discovery" type="application/json">${payload}</script><script src="assets/dashboard/dashboard.js"`).replace(/href="index.html"/g, 'href="../report.html"').replace(/href="docs\/"/g, 'href="../report.html"').replace("Docs \u2197", "Discovery report \u2197").replace("Runtime documentation \u2197", "Coverage report \u2197").replace("Back to website \u2197", "Discovery report \u2197");
|
|
4672
|
-
await writeFile7(join12(directory, "index.html"), page2, "utf8");
|
|
4673
|
-
await writeFile7(join12(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
|
|
4674
|
-
await writeFile7(join12(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
|
|
4675
|
-
return join12(directory, "index.html");
|
|
4676
|
-
}
|
|
4677
|
-
|
|
4678
4860
|
// src/commands/preview.ts
|
|
4679
|
-
import { mkdir as mkdir7, readFile as
|
|
4680
|
-
import { dirname as dirname3, join as
|
|
4861
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
4862
|
+
import { dirname as dirname3, join as join15, resolve as resolve4 } from "path";
|
|
4681
4863
|
|
|
4682
4864
|
// src/preview/html.ts
|
|
4683
4865
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -4743,7 +4925,7 @@ async function writeBrowserReport(cwd, inventory, plan, out) {
|
|
|
4743
4925
|
await mkdir7(dirname3(path), { recursive: true });
|
|
4744
4926
|
const cleanInventory = JSON.parse(sanitizeOutput(JSON.stringify(inventory)));
|
|
4745
4927
|
const cleanPlan = JSON.parse(sanitizeOutput(JSON.stringify(plan)));
|
|
4746
|
-
await
|
|
4928
|
+
await writeFile9(path, renderBrowserReport(cleanInventory, cleanPlan), "utf8");
|
|
4747
4929
|
return path;
|
|
4748
4930
|
}
|
|
4749
4931
|
async function runReport(cwd, out) {
|
|
@@ -4758,13 +4940,13 @@ async function runFeedback(cwd, note) {
|
|
|
4758
4940
|
const path = storePath(cwd, "feedback.json");
|
|
4759
4941
|
let entries = [];
|
|
4760
4942
|
try {
|
|
4761
|
-
const existing = JSON.parse(await
|
|
4943
|
+
const existing = JSON.parse(await readFile11(path, "utf8"));
|
|
4762
4944
|
entries = Array.isArray(existing.items) ? existing.items : [];
|
|
4763
4945
|
} catch (error) {
|
|
4764
4946
|
if (error.code !== "ENOENT") throw new CliError("Existing feedback could not be read; it was not overwritten");
|
|
4765
4947
|
}
|
|
4766
4948
|
entries.push({ createdAt: (/* @__PURE__ */ new Date()).toISOString(), note: sanitizeOutput(note.trim()) });
|
|
4767
|
-
await
|
|
4949
|
+
await writeFile9(path, `${JSON.stringify({ schema: "preview-feedback/v1", items: entries }, null, 2)}
|
|
4768
4950
|
`, "utf8");
|
|
4769
4951
|
return { schema: "feedback-result/v1", saved: true, uploaded: false, path, notes: entries.length };
|
|
4770
4952
|
}
|
|
@@ -4853,6 +5035,8 @@ function mergeDecisions(existing, incoming) {
|
|
|
4853
5035
|
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
4854
5036
|
}
|
|
4855
5037
|
async function runInit(opts) {
|
|
5038
|
+
if (opts.agent && opts.agent !== "opencode") throw new Error("Supported launcher: --agent opencode. Use .agenttwin/AGENT-TASK.md with any other coding agent.");
|
|
5039
|
+
if (opts.agent && (opts.dryRun || opts.json)) throw new Error("A coding session requires a writing, interactive init (omit --dry-run and --json).");
|
|
4856
5040
|
setQuiet(opts.json);
|
|
4857
5041
|
const interactive = !opts.json && !opts.yes && !opts.dryRun && (opts.interactive === true || !opts.json && process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
4858
5042
|
const log = (message) => {
|
|
@@ -4915,8 +5099,8 @@ async function runInit(opts) {
|
|
|
4915
5099
|
await writeInventory(opts.cwd, finalInventory);
|
|
4916
5100
|
await writePlan(opts.cwd, plan);
|
|
4917
5101
|
await writeConfig(opts.cwd, config, { force: opts.force });
|
|
4918
|
-
await
|
|
4919
|
-
await
|
|
5102
|
+
await writeFile10(join16(opts.cwd, "ENVIRONMENT.md"), renderEnvironment(finalInventory), "utf8");
|
|
5103
|
+
await writeFile10(join16(opts.cwd, "REPLICATION.md"), renderReplication(finalInventory, plan), "utf8");
|
|
4920
5104
|
reportPath = await writeBrowserReport(opts.cwd, finalInventory, plan);
|
|
4921
5105
|
dashboardPath = await writeDashboard(opts.cwd, finalInventory);
|
|
4922
5106
|
wrote = true;
|
|
@@ -4960,6 +5144,19 @@ async function runInit(opts) {
|
|
|
4960
5144
|
if (dashboardPath) info(`Open ${dashboardPath} to review detected connectors and workflows.`);
|
|
4961
5145
|
if (reportPath) info(`Open ${reportPath} in your browser. To save review notes: ${bold('agenttwin feedback --note "..."')}`);
|
|
4962
5146
|
}
|
|
5147
|
+
if (!opts.dryRun) {
|
|
5148
|
+
const taskPath = await writeSessionTask(opts.cwd);
|
|
5149
|
+
if (!opts.json) info(`Coding-agent task: ${taskPath}`);
|
|
5150
|
+
const launch = opts.agent === "opencode" || interactive && installedOpenCode() && await confirm("Start OpenCode with your configured provider to analyze workflows? Repository context may be sent to that provider.", false);
|
|
5151
|
+
if (launch) {
|
|
5152
|
+
await launchSession(opts.cwd);
|
|
5153
|
+
await writeDashboard(opts.cwd, finalInventory);
|
|
5154
|
+
}
|
|
5155
|
+
if (opts.sync) {
|
|
5156
|
+
const synced = await runSync(opts.cwd);
|
|
5157
|
+
if (!opts.json) ok(`Synced ${synced.workflows} candidates: ${synced.dashboard}`);
|
|
5158
|
+
} else if (!opts.json) info("Send reviewed discovery to your account: npx agenttwin sync (requires AGENTTWIN_TOKEN).");
|
|
5159
|
+
}
|
|
4963
5160
|
process.exitCode = result.exitCode;
|
|
4964
5161
|
}
|
|
4965
5162
|
|
|
@@ -4967,7 +5164,7 @@ async function runInit(opts) {
|
|
|
4967
5164
|
function readVersion() {
|
|
4968
5165
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4969
5166
|
const pkg = JSON.parse(
|
|
4970
|
-
readFileSync3(
|
|
5167
|
+
readFileSync3(join17(here, "..", "package.json"), "utf8")
|
|
4971
5168
|
);
|
|
4972
5169
|
return pkg.version ?? "0.0.0";
|
|
4973
5170
|
}
|
|
@@ -4977,12 +5174,14 @@ function helpText() {
|
|
|
4977
5174
|
"",
|
|
4978
5175
|
bold("Usage"),
|
|
4979
5176
|
" $ npx agenttwin <command> [options]",
|
|
5177
|
+
" $ npx agenttwin init --agent opencode [--sync]",
|
|
4980
5178
|
"",
|
|
4981
5179
|
bold("Commands"),
|
|
4982
5180
|
` ${"setup".padEnd(12)}Build your hosted E2B runtime template (once per account)`,
|
|
4983
5181
|
` ${"prepare".padEnd(12)}Bundle a reviewed discovered workflow (--file preparation.json)`,
|
|
4984
5182
|
` ${"run".padEnd(12)}Execute your Node workflow in a hosted E2B sandbox (--file world.json)`,
|
|
4985
5183
|
` ${"example".padEnd(12)}Write an editable multi-service workflow (--out directory)`,
|
|
5184
|
+
` ${"sync".padEnd(12)}Send discovery metadata to your account dashboard`,
|
|
4986
5185
|
` ${"init".padEnd(12)}Discover and write agenttwin.config.json + reports`,
|
|
4987
5186
|
` ${"discover".padEnd(12)}Build the company inventory (writes .agenttwin/inventory.json)`,
|
|
4988
5187
|
` ${"decide".padEnd(12)}Apply structured decisions (--file or --stdin)`,
|
|
@@ -5029,6 +5228,8 @@ async function main() {
|
|
|
5029
5228
|
answers: { type: "string" },
|
|
5030
5229
|
"max-questions": { type: "string" },
|
|
5031
5230
|
interactive: { type: "boolean" },
|
|
5231
|
+
agent: { type: "string" },
|
|
5232
|
+
sync: { type: "boolean" },
|
|
5032
5233
|
"non-interactive": { type: "boolean" },
|
|
5033
5234
|
note: { type: "string" },
|
|
5034
5235
|
incremental: { type: "boolean" },
|
|
@@ -5115,6 +5316,13 @@ Evidence: ${result.evidencePath}`);
|
|
|
5115
5316
|
Run: agenttwin run --file "${result.worldPath}"`);
|
|
5116
5317
|
return;
|
|
5117
5318
|
}
|
|
5319
|
+
case "sync": {
|
|
5320
|
+
const result = await runSync(cwd);
|
|
5321
|
+
printResult(result, `Synced ${result.tools} resources and ${result.workflows} workflow candidates.
|
|
5322
|
+
${result.dashboard}
|
|
5323
|
+
${result.uploaded}`);
|
|
5324
|
+
return;
|
|
5325
|
+
}
|
|
5118
5326
|
case "init":
|
|
5119
5327
|
await runInit({
|
|
5120
5328
|
cwd,
|
|
@@ -5127,7 +5335,9 @@ Run: agenttwin run --file "${result.worldPath}"`);
|
|
|
5127
5335
|
observeRun: str("observe-run"),
|
|
5128
5336
|
answers: str("answers"),
|
|
5129
5337
|
maxQuestions: Number(str("max-questions") ?? 8) || 8,
|
|
5130
|
-
interactive: flag("interactive")
|
|
5338
|
+
interactive: flag("interactive"),
|
|
5339
|
+
agent: str("agent"),
|
|
5340
|
+
sync: flag("sync")
|
|
5131
5341
|
});
|
|
5132
5342
|
return;
|
|
5133
5343
|
case "discover": {
|
|
@@ -5147,7 +5357,7 @@ Run: agenttwin run --file "${result.worldPath}"`);
|
|
|
5147
5357
|
result,
|
|
5148
5358
|
[
|
|
5149
5359
|
`Discovered ${counts.tools} tools (${counts.classified} classified) \xB7 ${counts.unclassified} unclassified \xB7 ${counts.withTwin} with twins`,
|
|
5150
|
-
`Wrote ${
|
|
5360
|
+
`Wrote ${join17(".agenttwin", "inventory.json")}`,
|
|
5151
5361
|
result.blocked.length > 0 ? `Blocked: ${result.blocked.join("; ")}` : "",
|
|
5152
5362
|
result.opportunities.length > 0 ? `Opportunities: ${result.opportunities.join("; ")}` : "",
|
|
5153
5363
|
result.reconciliation.splits.length > 0 ? `Splits: ${result.reconciliation.splits.map((s) => s.from).join(", ")}` : ""
|
|
@@ -5170,7 +5380,7 @@ Run: agenttwin run --file "${result.worldPath}"`);
|
|
|
5170
5380
|
printResult(
|
|
5171
5381
|
result,
|
|
5172
5382
|
`Plan: ${result.plan.items.length} tools \xB7 ${Object.entries(byStrategy).map(([k, v]) => `${k} ${v}`).join(", ")}
|
|
5173
|
-
Wrote ${
|
|
5383
|
+
Wrote ${join17(".agenttwin", "plan.json")}`
|
|
5174
5384
|
);
|
|
5175
5385
|
return;
|
|
5176
5386
|
}
|