agenttwin 0.3.0 → 0.3.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/dist/cli.js CHANGED
@@ -48,6 +48,704 @@ var init_git = __esm({
48
48
  }
49
49
  });
50
50
 
51
+ // src/lib/files.ts
52
+ import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
53
+ import { join as join3, relative, sep } from "path";
54
+ function shouldIgnoreDir(name) {
55
+ return IGNORED_DIRS.has(name) || name === ".git";
56
+ }
57
+ async function walkDir(root, maxFiles, budgetMs) {
58
+ const out = [];
59
+ const limits = [];
60
+ const stack = [root];
61
+ const started = Date.now();
62
+ while (stack.length > 0 && out.length < maxFiles && Date.now() - started < budgetMs) {
63
+ const dir = stack.pop();
64
+ let entries;
65
+ try {
66
+ entries = await readdir2(dir, { withFileTypes: true });
67
+ } catch {
68
+ limits.push(`Directory could not be read: ${relative(root, dir) || "."}`);
69
+ continue;
70
+ }
71
+ entries.sort((a, b) => a.name.localeCompare(b.name));
72
+ for (const entry of entries) {
73
+ const full = join3(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ if (!shouldIgnoreDir(entry.name)) stack.push(full);
76
+ } else if (entry.isFile()) {
77
+ out.push(relative(root, full).split(sep).join("/"));
78
+ if (out.length >= maxFiles) break;
79
+ }
80
+ }
81
+ }
82
+ if (out.length >= maxFiles) limits.push(`File scan limit reached (${maxFiles} files); additional files may be unscanned`);
83
+ if (stack.length && Date.now() - started >= budgetMs) limits.push(`File walk time budget reached (${budgetMs}ms); directories remain unscanned`);
84
+ return { files: out.sort(), limits };
85
+ }
86
+ async function createFileIndex(cwd) {
87
+ return new RepoFileIndex(cwd);
88
+ }
89
+ var IGNORED_DIRS, MAX_FILES, MAX_BYTES, WALK_BUDGET_MS, RepoFileIndex;
90
+ var init_files = __esm({
91
+ "src/lib/files.ts"() {
92
+ "use strict";
93
+ IGNORED_DIRS = /* @__PURE__ */ new Set([
94
+ ".git",
95
+ ".agenttwin",
96
+ "node_modules",
97
+ "dist",
98
+ "build",
99
+ "out",
100
+ "coverage",
101
+ ".next",
102
+ ".nuxt",
103
+ ".venv",
104
+ "venv",
105
+ "env",
106
+ "site-packages",
107
+ "__pycache__",
108
+ ".pytest_cache",
109
+ ".mypy_cache",
110
+ ".ruff_cache",
111
+ ".tox",
112
+ "vendor",
113
+ "target",
114
+ ".turbo",
115
+ ".cache",
116
+ ".idea",
117
+ ".vscode",
118
+ ".terraform",
119
+ "models",
120
+ "data",
121
+ "datasets",
122
+ "media",
123
+ "uploads",
124
+ "tmp",
125
+ "logs",
126
+ "gen"
127
+ ]);
128
+ MAX_FILES = 2e4;
129
+ MAX_BYTES = 1e6;
130
+ WALK_BUDGET_MS = 8e3;
131
+ RepoFileIndex = class {
132
+ cwd;
133
+ cache = { contents: /* @__PURE__ */ new Map(), limits: [] };
134
+ bounds;
135
+ constructor(cwd, bounds = { maxFiles: MAX_FILES, budgetMs: WALK_BUDGET_MS }) {
136
+ this.cwd = cwd;
137
+ this.bounds = bounds;
138
+ }
139
+ async walk() {
140
+ if (!this.cache.files) {
141
+ const result = await walkDir(this.cwd, this.bounds.maxFiles, this.bounds.budgetMs);
142
+ this.cache.files = result.files;
143
+ this.cache.limits.push(...result.limits);
144
+ }
145
+ return this.cache.files;
146
+ }
147
+ scanLimits() {
148
+ return [...new Set(this.cache.limits)].sort();
149
+ }
150
+ async exists(path) {
151
+ try {
152
+ await stat(join3(this.cwd, path));
153
+ return true;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ async read(path, maxBytes = MAX_BYTES) {
159
+ const cached = this.cache.contents.get(path);
160
+ if (cached !== void 0) return cached;
161
+ const full = join3(this.cwd, path);
162
+ try {
163
+ const info2 = await stat(full);
164
+ if (!info2.isFile()) return void 0;
165
+ if (info2.size > maxBytes) {
166
+ this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
167
+ return void 0;
168
+ }
169
+ const text = await readFile3(full, "utf8");
170
+ this.cache.contents.set(path, text);
171
+ return text;
172
+ } catch {
173
+ return void 0;
174
+ }
175
+ }
176
+ async byExt(...exts) {
177
+ const wanted = new Set(exts.map((e) => e.replace(/^\./, "").toLowerCase()));
178
+ const files = await this.walk();
179
+ return files.filter((f) => {
180
+ const dot = f.lastIndexOf(".");
181
+ if (dot < 0) return false;
182
+ return wanted.has(f.slice(dot + 1).toLowerCase());
183
+ });
184
+ }
185
+ async byName(pattern) {
186
+ const files = await this.walk();
187
+ return files.filter((f) => pattern.test(f));
188
+ }
189
+ line(ref) {
190
+ const idx = ref.lastIndexOf(":");
191
+ if (idx < 0) return void 0;
192
+ const path = ref.slice(0, idx);
193
+ const num = Number(ref.slice(idx + 1));
194
+ if (!Number.isInteger(num)) return void 0;
195
+ const text = this.cache.contents.get(path);
196
+ if (text === void 0) return void 0;
197
+ const lines = text.split(/\r?\n/);
198
+ return lines[num - 1];
199
+ }
200
+ };
201
+ }
202
+ });
203
+
204
+ // src/inventory/entrypoints.ts
205
+ function codeOnly(source) {
206
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
207
+ }
208
+ function exportedEntrypoints(source) {
209
+ const code = codeOnly(source);
210
+ 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;
211
+ const result = [];
212
+ let match;
213
+ while (match = pattern.exec(code)) {
214
+ let depth = 1, end = pattern.lastIndex;
215
+ while (end < code.length && depth) {
216
+ if (code[end] === "{") depth++;
217
+ if (code[end] === "}") depth--;
218
+ end++;
219
+ }
220
+ if (depth) continue;
221
+ result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
222
+ pattern.lastIndex = end;
223
+ }
224
+ return result;
225
+ }
226
+ var init_entrypoints = __esm({
227
+ "src/inventory/entrypoints.ts"() {
228
+ "use strict";
229
+ }
230
+ });
231
+
232
+ // src/discovery/masking.ts
233
+ function maskSecret(value) {
234
+ return value.replace(TOKENISH, (match) => `${match.slice(0, 4)}***`);
235
+ }
236
+ function maskEmail(value) {
237
+ return value.replace(EMAIL, "$1***$2");
238
+ }
239
+ function maskUrl(value) {
240
+ try {
241
+ const url = new URL(value);
242
+ return `${url.protocol}//${url.host}${url.pathname}`;
243
+ } catch {
244
+ return maskSecret(value);
245
+ }
246
+ }
247
+ function redactDetail(value) {
248
+ return maskSecret(maskEmail(value)).slice(0, 200);
249
+ }
250
+ var EMAIL, TOKENISH;
251
+ var init_masking = __esm({
252
+ "src/discovery/masking.ts"() {
253
+ "use strict";
254
+ EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
255
+ TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
256
+ }
257
+ });
258
+
259
+ // src/discovery/probes/providers/stripe.ts
260
+ var stripeProbe;
261
+ var init_stripe = __esm({
262
+ "src/discovery/probes/providers/stripe.ts"() {
263
+ "use strict";
264
+ init_masking();
265
+ stripeProbe = {
266
+ id: "stripe.readonly",
267
+ providerId: "stripe",
268
+ title: "Enumerate Stripe account configuration (read-only)",
269
+ description: "Reads products, prices and webhook endpoints. Read-only; issues no writes.",
270
+ readScope: {
271
+ endpoints: [
272
+ "GET /v1/products",
273
+ "GET /v1/prices",
274
+ "GET /v1/webhook_endpoints"
275
+ ],
276
+ methods: ["GET"]
277
+ },
278
+ credentials: [
279
+ {
280
+ envVar: "STRIPE_SECRET_KEY",
281
+ label: "Stripe restricted key (read scopes)",
282
+ required: true,
283
+ scopes: ["products:read", "prices:read", "webhook_endpoints:read"],
284
+ hint: "Prefer a restricted key (rk_...) with read scopes over a full sk_live_ key."
285
+ }
286
+ ],
287
+ async run(ctx) {
288
+ const spec = this.credentials[0];
289
+ const key = ctx.credential(spec);
290
+ if (!key) {
291
+ return {
292
+ providerId: this.providerId,
293
+ ok: false,
294
+ summary: "no credential present",
295
+ resources: [],
296
+ evidence: []
297
+ };
298
+ }
299
+ const get = async (path) => {
300
+ const response = await ctx.fetch(`https://api.stripe.com${path}`, {
301
+ method: "GET",
302
+ headers: { Authorization: `Bearer ${key}` },
303
+ signal: ctx.signal
304
+ });
305
+ if (!response.ok) {
306
+ throw new Error(`Stripe ${path} -> HTTP ${response.status}`);
307
+ }
308
+ return await response.json();
309
+ };
310
+ try {
311
+ const [products, prices, hooks] = await Promise.all([
312
+ get("/v1/products?limit=100"),
313
+ get("/v1/prices?limit=100"),
314
+ get("/v1/webhook_endpoints?limit=100")
315
+ ]);
316
+ return {
317
+ providerId: this.providerId,
318
+ ok: true,
319
+ summary: `${products.data.length} products, ${prices.data.length} prices, ${hooks.data.length} webhook endpoints`,
320
+ resources: [
321
+ {
322
+ kind: "products",
323
+ count: products.data.length,
324
+ sample: products.data.slice(0, 5).map((p) => ({
325
+ id: p.id,
326
+ name: p.name,
327
+ active: p.active
328
+ }))
329
+ },
330
+ { kind: "prices", count: prices.data.length },
331
+ {
332
+ kind: "webhook_endpoints",
333
+ count: hooks.data.length,
334
+ sample: hooks.data.slice(0, 5).map((h2) => ({
335
+ url: typeof h2.url === "string" ? maskUrl(h2.url) : void 0,
336
+ status: h2.status
337
+ }))
338
+ }
339
+ ],
340
+ evidence: [
341
+ {
342
+ ref: "probe:stripe",
343
+ pass: "probe",
344
+ signal: "probe",
345
+ detail: "account reachable via read scopes"
346
+ }
347
+ ]
348
+ };
349
+ } catch (error) {
350
+ return {
351
+ providerId: this.providerId,
352
+ ok: false,
353
+ error: { message: error instanceof Error ? error.message : String(error) },
354
+ summary: "probe failed",
355
+ resources: [],
356
+ evidence: []
357
+ };
358
+ }
359
+ }
360
+ };
361
+ }
362
+ });
363
+
364
+ // src/discovery/probes/providers/github.ts
365
+ var githubProbe;
366
+ var init_github = __esm({
367
+ "src/discovery/probes/providers/github.ts"() {
368
+ "use strict";
369
+ githubProbe = {
370
+ id: "github.readonly",
371
+ providerId: "github",
372
+ title: "Enumerate GitHub account (read-only)",
373
+ description: "Reads the authenticated user and their repositories. Read-only.",
374
+ readScope: {
375
+ endpoints: ["GET /user", "GET /user/repos"],
376
+ methods: ["GET"]
377
+ },
378
+ credentials: [
379
+ {
380
+ envVar: "GITHUB_TOKEN",
381
+ label: "GitHub token (read:user, repo:read)",
382
+ required: true,
383
+ scopes: ["read:user", "public_repo"],
384
+ hint: "A fine-grained token with read-only repository access is sufficient."
385
+ }
386
+ ],
387
+ async run(ctx) {
388
+ const key = ctx.credential(this.credentials[0]);
389
+ if (!key) {
390
+ return {
391
+ providerId: this.providerId,
392
+ ok: false,
393
+ summary: "no credential present",
394
+ resources: [],
395
+ evidence: []
396
+ };
397
+ }
398
+ const headers = {
399
+ Authorization: `Bearer ${key}`,
400
+ Accept: "application/vnd.github+json",
401
+ "User-Agent": "agenttwin-discovery"
402
+ };
403
+ try {
404
+ const userResponse = await ctx.fetch("https://api.github.com/user", {
405
+ method: "GET",
406
+ headers,
407
+ signal: ctx.signal
408
+ });
409
+ if (!userResponse.ok) {
410
+ throw new Error(`GitHub /user -> HTTP ${userResponse.status}`);
411
+ }
412
+ const user = await userResponse.json();
413
+ const reposResponse = await ctx.fetch(
414
+ "https://api.github.com/user/repos?per_page=100&sort=updated",
415
+ { method: "GET", headers, signal: ctx.signal }
416
+ );
417
+ const repos = reposResponse.ok ? await reposResponse.json() : [];
418
+ return {
419
+ providerId: this.providerId,
420
+ ok: true,
421
+ summary: `${repos.length} repositories readable for @${user.login ?? "unknown"}`,
422
+ resources: [
423
+ {
424
+ kind: "repositories",
425
+ count: repos.length,
426
+ sample: repos.slice(0, 5).map((r) => ({
427
+ name: r.name,
428
+ private: r.private,
429
+ archived: r.archived
430
+ }))
431
+ }
432
+ ],
433
+ evidence: [
434
+ {
435
+ ref: "probe:github",
436
+ pass: "probe",
437
+ signal: "probe",
438
+ detail: "account reachable via read scopes"
439
+ }
440
+ ]
441
+ };
442
+ } catch (error) {
443
+ return {
444
+ providerId: this.providerId,
445
+ ok: false,
446
+ error: { message: error instanceof Error ? error.message : String(error) },
447
+ summary: "probe failed",
448
+ resources: [],
449
+ evidence: []
450
+ };
451
+ }
452
+ }
453
+ };
454
+ }
455
+ });
456
+
457
+ // src/discovery/providers/catalog.ts
458
+ function planned(unknown) {
459
+ return { supported: [], verified: [], unknown, implemented: false };
460
+ }
461
+ var NO_TWIN, PROVIDERS;
462
+ var init_catalog = __esm({
463
+ "src/discovery/providers/catalog.ts"() {
464
+ "use strict";
465
+ init_stripe();
466
+ init_github();
467
+ NO_TWIN = ["all behavior (no twin implemented)"];
468
+ PROVIDERS = [
469
+ {
470
+ id: "stripe",
471
+ title: "Stripe",
472
+ category: "payments",
473
+ twin: planned(["refunds", "payouts", "disputes", "webhook ordering"]),
474
+ probe: stripeProbe,
475
+ signatures: {
476
+ packages: {
477
+ node: ["stripe"],
478
+ python: ["stripe"],
479
+ go: ["stripe-go"],
480
+ java: ["stripe-java"],
481
+ ruby: ["stripe"],
482
+ php: ["stripe/stripe-php"],
483
+ dotnet: ["Stripe.net"]
484
+ },
485
+ imports: ["new Stripe\\(", `from ["']stripe["']`, `require\\(['"]stripe['"]\\)`, "stripe-go"],
486
+ hosts: ["api.stripe.com"],
487
+ envPrefixes: ["STRIPE_"],
488
+ webhookPaths: ["/webhooks/stripe", "/stripe/webhook", "/webhook/stripe"]
489
+ },
490
+ docs: "https://stripe.com/docs/api"
491
+ },
492
+ {
493
+ id: "slack",
494
+ title: "Slack",
495
+ category: "comms",
496
+ twin: planned(NO_TWIN),
497
+ signatures: {
498
+ packages: {
499
+ node: ["@slack/web-api", "@slack/bolt", "@slack/events-api", "@slack/socket-mode"],
500
+ python: ["slack_sdk", "slack-bolt"],
501
+ go: ["slack-go"],
502
+ java: ["slack-java-sdk"],
503
+ ruby: ["slack-ruby-client"]
504
+ },
505
+ imports: ["@slack/web-api", "WebClient\\(", "slack_sdk"],
506
+ hosts: ["slack.com", "api.slack.com", "hooks.slack.com"],
507
+ envPrefixes: ["SLACK_"]
508
+ }
509
+ },
510
+ {
511
+ id: "github",
512
+ title: "GitHub",
513
+ category: "vcs",
514
+ twin: planned(NO_TWIN),
515
+ probe: githubProbe,
516
+ signatures: {
517
+ packages: {
518
+ node: ["@octokit/rest", "@octokit/core", "@octokit/auth", "octokit"],
519
+ python: ["PyGithub", "github"],
520
+ go: ["google/go-github"],
521
+ java: ["github-api"],
522
+ ruby: ["octokit"]
523
+ },
524
+ imports: ["Octokit", "@octokit/", "PyGithub"],
525
+ hosts: ["api.github.com"],
526
+ envPrefixes: ["GITHUB_", "GH_"],
527
+ webhookPaths: ["/webhooks/github", "/github/webhook"]
528
+ }
529
+ },
530
+ {
531
+ id: "aws",
532
+ title: "AWS",
533
+ category: "cloud",
534
+ twin: planned(["service coverage (LocalStack passthrough planned)"]),
535
+ envWeak: true,
536
+ signatures: {
537
+ packages: {
538
+ node: ["@aws-sdk/", "aws-sdk"],
539
+ python: ["boto3", "botocore"],
540
+ go: ["aws-sdk-go", "aws-sdk-go-v2"],
541
+ java: ["software.amazon.awssdk", "aws-java-sdk"],
542
+ ruby: ["aws-sdk-"],
543
+ php: ["aws/aws-sdk-php"],
544
+ dotnet: ["AWSSDK."]
545
+ },
546
+ imports: ["@aws-sdk/", "boto3", "aws-sdk-go"],
547
+ hosts: ["amazonaws.com"],
548
+ envPrefixes: ["AWS_"]
549
+ }
550
+ },
551
+ {
552
+ id: "google",
553
+ title: "Google (Gmail / Calendar / Workspace)",
554
+ category: "comms",
555
+ twin: planned(NO_TWIN),
556
+ envWeak: true,
557
+ signatures: {
558
+ packages: {
559
+ node: ["googleapis", "google-auth-library"],
560
+ python: ["google-api-python-client", "google-cloud", "google-auth"],
561
+ go: ["google.golang.org/api"],
562
+ java: ["google-api-client"],
563
+ ruby: ["google-apis-"]
564
+ },
565
+ imports: ["googleapis", "google-api-python-client", "google.golang.org/api"],
566
+ hosts: ["googleapis.com", "gmail.googleapis.com", "www.googleapis.com"],
567
+ envPrefixes: ["GOOGLE_", "GCP_", "GMAIL_"]
568
+ }
569
+ },
570
+ {
571
+ id: "openai",
572
+ title: "OpenAI",
573
+ category: "ai",
574
+ twin: planned(NO_TWIN),
575
+ signatures: {
576
+ packages: {
577
+ node: ["openai"],
578
+ python: ["openai"],
579
+ go: ["go-openai"],
580
+ ruby: ["ruby-openai"]
581
+ },
582
+ imports: ["OpenAI\\(", `from ["']openai["']`, "go-openai"],
583
+ hosts: ["api.openai.com"],
584
+ envPrefixes: ["OPENAI_"]
585
+ }
586
+ },
587
+ {
588
+ id: "anthropic",
589
+ title: "Anthropic",
590
+ category: "ai",
591
+ twin: planned(NO_TWIN),
592
+ signatures: {
593
+ packages: {
594
+ node: ["@anthropic-ai/sdk"],
595
+ python: ["anthropic"],
596
+ go: ["anthropic-sdk-go"]
597
+ },
598
+ imports: ["@anthropic-ai/sdk", "Anthropic\\("],
599
+ hosts: ["api.anthropic.com"],
600
+ envPrefixes: ["ANTHROPIC_"]
601
+ }
602
+ },
603
+ {
604
+ id: "shopify",
605
+ title: "Shopify",
606
+ category: "commerce",
607
+ twin: planned(NO_TWIN),
608
+ signatures: {
609
+ packages: {
610
+ node: ["@shopify/", "shopify-api"],
611
+ python: ["ShopifyAPI"],
612
+ ruby: ["shopify_api"]
613
+ },
614
+ imports: ["@shopify/", "ShopifyAPI"],
615
+ hosts: ["myshopify.com", "shopify.com"],
616
+ envPrefixes: ["SHOPIFY_"]
617
+ }
618
+ },
619
+ {
620
+ id: "twilio",
621
+ title: "Twilio",
622
+ category: "comms",
623
+ twin: planned(NO_TWIN),
624
+ signatures: {
625
+ packages: {
626
+ node: ["twilio"],
627
+ python: ["twilio"],
628
+ java: ["twilio"],
629
+ ruby: ["twilio-ruby"],
630
+ php: ["twilio/sdk"]
631
+ },
632
+ imports: ["twilio"],
633
+ hosts: ["api.twilio.com"],
634
+ envPrefixes: ["TWILIO_"]
635
+ }
636
+ },
637
+ {
638
+ id: "sendgrid",
639
+ title: "SendGrid",
640
+ category: "comms",
641
+ twin: planned(NO_TWIN),
642
+ signatures: {
643
+ packages: {
644
+ node: ["@sendgrid/mail", "@sendgrid/client"],
645
+ python: ["sendgrid"],
646
+ ruby: ["sendgrid-ruby"]
647
+ },
648
+ imports: ["@sendgrid/", "sendgrid"],
649
+ hosts: ["api.sendgrid.com"],
650
+ envPrefixes: ["SENDGRID_"]
651
+ }
652
+ },
653
+ {
654
+ id: "hubspot",
655
+ title: "HubSpot",
656
+ category: "crm",
657
+ twin: planned(NO_TWIN),
658
+ signatures: {
659
+ packages: {
660
+ node: ["@hubspot/api-client"],
661
+ python: ["hubspot-api-client"],
662
+ ruby: ["hubspot-api-client"]
663
+ },
664
+ imports: ["@hubspot/api-client", "hubspot-api-client"],
665
+ hosts: ["api.hubapi.com"],
666
+ envPrefixes: ["HUBSPOT_"]
667
+ }
668
+ },
669
+ {
670
+ id: "salesforce",
671
+ title: "Salesforce",
672
+ category: "crm",
673
+ twin: planned(NO_TWIN),
674
+ signatures: {
675
+ packages: {
676
+ node: ["jsforce", "@salesforce/"],
677
+ python: ["simple-salesforce"],
678
+ java: ["force-wsc"]
679
+ },
680
+ imports: ["jsforce", "simple-salesforce"],
681
+ hosts: ["salesforce.com", "force.com"],
682
+ envPrefixes: ["SALESFORCE_", "SF_"]
683
+ }
684
+ },
685
+ {
686
+ id: "zendesk",
687
+ title: "Zendesk",
688
+ category: "support",
689
+ twin: planned(NO_TWIN),
690
+ signatures: {
691
+ packages: {
692
+ node: ["node-zendesk"],
693
+ python: ["zendesk", "zenpy"],
694
+ ruby: ["zendesk_api"]
695
+ },
696
+ imports: ["node-zendesk", "zenpy"],
697
+ hosts: ["zendesk.com"],
698
+ envPrefixes: ["ZENDESK_"]
699
+ }
700
+ },
701
+ {
702
+ id: "linear",
703
+ title: "Linear",
704
+ category: "pm",
705
+ twin: planned(NO_TWIN),
706
+ signatures: {
707
+ packages: { node: ["@linear/sdk"], python: ["linear"] },
708
+ imports: ["@linear/sdk"],
709
+ hosts: ["api.linear.app"],
710
+ envPrefixes: ["LINEAR_"]
711
+ }
712
+ },
713
+ {
714
+ id: "notion",
715
+ title: "Notion",
716
+ category: "docs",
717
+ twin: planned(NO_TWIN),
718
+ signatures: {
719
+ packages: { node: ["@notionhq/client"], python: ["notion-client"] },
720
+ imports: ["@notionhq/client", "notion-client"],
721
+ hosts: ["api.notion.com"],
722
+ envPrefixes: ["NOTION_"]
723
+ }
724
+ },
725
+ {
726
+ id: "resend",
727
+ title: "Resend",
728
+ category: "comms",
729
+ twin: planned(NO_TWIN),
730
+ signatures: {
731
+ packages: { node: ["resend"] },
732
+ imports: [`from ["']resend["']`],
733
+ hosts: ["api.resend.com"],
734
+ envPrefixes: ["RESEND_"]
735
+ }
736
+ }
737
+ ];
738
+ for (const [id, title, category, node, python, hosts] of [
739
+ ["composio", "Composio", "integration", ["@composio/core", "@composio/client"], ["composio", "composio-core"], ["backend.composio.dev"]],
740
+ ["kernel", "Kernel", "browser", ["@onkernel/sdk"], ["kernel"], ["api.onkernel.com"]],
741
+ ["e2b", "E2B", "runtime", ["e2b", "@e2b/code-interpreter"], ["e2b", "e2b-code-interpreter"], ["api.e2b.dev"]],
742
+ ["mem0", "Mem0", "ai", ["mem0ai"], ["mem0ai"], ["api.mem0.ai"]],
743
+ ["ollama", "Ollama", "ai", ["ollama"], ["ollama"], []],
744
+ ["onepassword", "1Password", "security", ["@1password/sdk"], ["onepassword-sdk"], []]
745
+ ]) PROVIDERS.push({ id, title, category, twin: planned(NO_TWIN), signatures: { packages: { node: [...node], python: [...python] }, imports: [...node, ...python], hosts: [...hosts] } });
746
+ }
747
+ });
748
+
51
749
  // src/inventory/dashboard-transfer.ts
52
750
  function discoveryTransfer(value) {
53
751
  const input = value;
@@ -91,37 +789,98 @@ var init_dashboard_transfer = __esm({
91
789
  }
92
790
  });
93
791
 
792
+ // src/inventory/project-products.ts
793
+ async function projectProducts(cwd, data, base) {
794
+ const current = new Set(base.tools.map((t) => t.id));
795
+ const known = new Set(PROVIDERS.map((p) => p.title));
796
+ const tools = data.tools.filter((t) => current.has(t.id) || !known.has(t.title) && t.category !== "access" && t.category !== "tooling");
797
+ const ids = new Set(tools.map((t) => t.id));
798
+ const workflows = data.workflows.map((w) => ({ ...w, tools: w.tools.filter((id) => ids.has(id)), steps: w.steps.filter((s) => ids.has(s.tool)) }));
799
+ const google = tools.find((t) => /Google \(Gmail/.test(t.title));
800
+ if (!google) return discoveryTransfer({ ...data, tools, workflows });
801
+ const products = [
802
+ { key: "gmail", title: "Gmail", pattern: /(?:gmail\.googleapis\.com|\/gmail\/v\d|\bgoogle\.gmail\s*\()/i },
803
+ { key: "calendar", title: "Google Calendar", pattern: /(?:\/calendar\/v\d|\bgoogle\.calendar\s*\()/i },
804
+ { key: "drive", title: "Google Drive", pattern: /(?:\/drive\/v\d|\bgoogle\.drive\s*\()/i },
805
+ { key: "docs", title: "Google Docs", pattern: /(?:docs\.googleapis\.com|\/documents\/|\bgoogle\.docs\s*\()/i },
806
+ { key: "sheets", title: "Google Sheets", pattern: /(?:sheets\.googleapis\.com|\/spreadsheets\/|\bgoogle\.sheets\s*\()/i }
807
+ ];
808
+ const files = await createFileIndex(cwd);
809
+ const paths = (await files.walk()).filter((p) => /\.(?:ts|tsx|js|mjs|py)$/.test(p) && !/(?:^|\/)(?:artifacts|tests?|testbed|docs?|fixtures?|examples?|public|assets)(?:\/|$)|\.(?:test|spec)\.|fixtures?\./i.test(p)).slice(0, 2500);
810
+ const found = /* @__PURE__ */ new Set();
811
+ for (const path of paths) {
812
+ const source = await files.read(path, 2e5);
813
+ if (!source) continue;
814
+ for (const p of products) if (p.pattern.test(source)) found.add(p.key);
815
+ }
816
+ const mapped = workflows.map((w) => {
817
+ if (!w.tools.includes(google.id)) return w;
818
+ const [path, fn] = (w.entrypoint ?? "").split("#");
819
+ return { w, path, fn };
820
+ });
821
+ const finalWorkflows = [];
822
+ let unresolved = false;
823
+ for (const value of mapped) {
824
+ if (!("w" in value)) {
825
+ finalWorkflows.push(value);
826
+ continue;
827
+ }
828
+ const { w, path, fn } = value;
829
+ const source = path ? await files.read(path, 2e5) : void 0;
830
+ const entry = source && fn ? exportedEntrypoints(source).find((e) => e.name === fn) : void 0;
831
+ const body = source && entry ? source.split("\n").slice(entry.startLine - 1, entry.endLine).join("\n") : source ?? "";
832
+ const matches = products.filter((p) => found.has(p.key) && p.pattern.test(body)).map((p) => google.id + ":" + p.key);
833
+ if (!matches.length) {
834
+ unresolved = true;
835
+ finalWorkflows.push(w);
836
+ continue;
837
+ }
838
+ finalWorkflows.push({ ...w, tools: [...new Set(w.tools.flatMap((id) => id === google.id ? matches : [id]))], steps: w.steps.flatMap((s) => s.tool === google.id ? matches.map((tool) => ({ ...s, tool })) : [s]) });
839
+ }
840
+ const productTools = products.filter((p) => found.has(p.key)).map((p) => ({ ...google, id: google.id + ":" + p.key, title: p.title }));
841
+ return discoveryTransfer({ ...data, tools: [...tools.filter((t) => t.id !== google.id || unresolved || !productTools.length).map((t) => t.id === google.id ? { ...t, title: "Google API (unspecified)" } : t), ...productTools], workflows: finalWorkflows });
842
+ }
843
+ var init_project_products = __esm({
844
+ "src/inventory/project-products.ts"() {
845
+ "use strict";
846
+ init_files();
847
+ init_entrypoints();
848
+ init_catalog();
849
+ init_dashboard_transfer();
850
+ }
851
+ });
852
+
94
853
  // src/inspect/sanitize.ts
95
854
  function stripUrlCredentials(value) {
96
855
  return value.replace(URL_CREDENTIALS, "$1");
97
856
  }
98
857
  function sanitizeOutput(value) {
99
- return stripUrlCredentials(value).replace(TOKENISH, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
858
+ return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL2, "$1***$2");
100
859
  }
101
860
  function excerpt(value, max = 200) {
102
861
  const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
103
862
  return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
104
863
  }
105
- var URL_CREDENTIALS, TOKENISH, EMAIL;
864
+ var URL_CREDENTIALS, TOKENISH2, EMAIL2;
106
865
  var init_sanitize = __esm({
107
866
  "src/inspect/sanitize.ts"() {
108
867
  "use strict";
109
868
  URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
110
- TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
111
- EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
869
+ TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
870
+ EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
112
871
  }
113
872
  });
114
873
 
115
874
  // src/commands/dashboard.ts
116
- import { cp, mkdir, readFile as readFile3, writeFile } from "fs/promises";
117
- import { basename, join as join3 } from "path";
875
+ import { cp, mkdir, readFile as readFile4, writeFile } from "fs/promises";
876
+ import { basename, join as join4 } from "path";
118
877
  import { fileURLToPath } from "url";
119
878
  function dashboardInventory(cwd, inventory) {
120
879
  return JSON.parse(sanitizeOutput(JSON.stringify({
121
880
  schema: "company-inventory/v1",
122
881
  generatedAt: inventory.generatedAt,
123
882
  name: basename(cwd),
124
- tools: inventory.tools.map((t) => ({
883
+ tools: inventory.tools.filter((t) => !t.facts?.length || !t.facts.every((f) => f.evidence.every((e) => /^(?:credential:|command:)/.test(e.ref)))).map((t) => ({
125
884
  id: t.id,
126
885
  title: t.title,
127
886
  kind: t.kind,
@@ -133,26 +892,26 @@ function dashboardInventory(cwd, inventory) {
133
892
  })));
134
893
  }
135
894
  async function writeDashboard(cwd, inventory) {
136
- const directory = join3(cwd, ".agenttwin", "dashboard");
895
+ const directory = join4(cwd, ".agenttwin", "dashboard");
137
896
  let bundle = fileURLToPath(new URL("./dashboard/", import.meta.url));
138
897
  try {
139
- await readFile3(join3(bundle, "index.html"));
898
+ await readFile4(join4(bundle, "index.html"));
140
899
  } catch {
141
900
  bundle = fileURLToPath(new URL("../../../../site/", import.meta.url));
142
901
  }
143
- await mkdir(join3(directory, "assets", "dashboard"), { recursive: true });
144
- const html = await readFile3(join3(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
902
+ await mkdir(join4(directory, "assets", "dashboard"), { recursive: true });
903
+ const html = await readFile4(join4(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
145
904
  for (const folder of ["dashboard", "icons", "logo", "fonts", "vendor"]) {
146
- await cp(join3(bundle, "assets", folder), join3(directory, "assets", folder), { recursive: true });
905
+ await cp(join4(bundle, "assets", folder), join4(directory, "assets", folder), { recursive: true });
147
906
  }
148
- await cp(join3(bundle, "assets", "brand.css"), join3(directory, "assets", "brand.css"));
907
+ await cp(join4(bundle, "assets", "brand.css"), join4(directory, "assets", "brand.css"));
149
908
  const data = await loadDashboardDiscovery(cwd, inventory);
150
909
  const payload = JSON.stringify(data).replace(/</g, "\\u003c");
151
910
  const page2 = html.replaceAll('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");
152
- await writeFile(join3(directory, "index.html"), page2, "utf8");
153
- await writeFile(join3(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
154
- await writeFile(join3(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
155
- return join3(directory, "index.html");
911
+ await writeFile(join4(directory, "index.html"), page2, "utf8");
912
+ await writeFile(join4(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
913
+ await writeFile(join4(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
914
+ return join4(directory, "index.html");
156
915
  }
157
916
  var init_dashboard = __esm({
158
917
  "src/commands/dashboard.ts"() {
@@ -172,8 +931,8 @@ __export(session_exports, {
172
931
  writeSessionTask: () => writeSessionTask
173
932
  });
174
933
  import { spawn, spawnSync } from "child_process";
175
- import { writeFile as writeFile2, readFile as readFile4 } from "fs/promises";
176
- import { join as join4 } from "path";
934
+ import { writeFile as writeFile2, readFile as readFile5 } from "fs/promises";
935
+ import { join as join5 } from "path";
177
936
  function sessionTask() {
178
937
  return `# Prepare this repository for AgentTwin
179
938
 
@@ -198,7 +957,7 @@ function installedOpenCode() {
198
957
  return spawnSync("opencode", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
199
958
  }
200
959
  async function writeSessionTask(cwd) {
201
- const path = join4(cwd, ".agenttwin/AGENT-TASK.md");
960
+ const path = join5(cwd, ".agenttwin/AGENT-TASK.md");
202
961
  await writeFile2(path, sessionTask(), "utf8");
203
962
  return path;
204
963
  }
@@ -215,9 +974,9 @@ async function loadDashboardDiscovery(cwd, inventory) {
215
974
  const base = discoveryTransfer(dashboardInventory(cwd, inventory));
216
975
  let raw;
217
976
  try {
218
- raw = await readFile4(join4(cwd, ".agenttwin/agent-discovery.json"), "utf8");
977
+ raw = await readFile5(join5(cwd, ".agenttwin/agent-discovery.json"), "utf8");
219
978
  } catch (error) {
220
- if (error.code === "ENOENT") return base;
979
+ if (error.code === "ENOENT") return projectProducts(cwd, base, base);
221
980
  throw error;
222
981
  }
223
982
  if (Buffer.byteLength(raw) > 2 * 1024 * 1024) throw new Error("Agent discovery exceeds 2 MiB");
@@ -229,11 +988,12 @@ async function loadDashboardDiscovery(cwd, inventory) {
229
988
  const result = discoveryTransfer({ ...base, tools: [...tools.values()], workflows: [...workflows.values()] });
230
989
  const ids = new Set(result.tools.map((t) => t.id));
231
990
  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.");
232
- return result;
991
+ return projectProducts(cwd, result, base);
233
992
  }
234
993
  var init_session = __esm({
235
994
  "src/commands/session.ts"() {
236
995
  "use strict";
996
+ init_project_products();
237
997
  init_dashboard_transfer();
238
998
  init_dashboard();
239
999
  }
@@ -289,8 +1049,8 @@ import { parseArgs } from "util";
289
1049
 
290
1050
  // src/commands/sync.ts
291
1051
  init_git();
292
- import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
293
- import { join as join5 } from "path";
1052
+ import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
1053
+ import { join as join6 } from "path";
294
1054
 
295
1055
  // src/runtime/receipt-sync.ts
296
1056
  init_git();
@@ -438,11 +1198,11 @@ async function runSync(cwd) {
438
1198
  const token = process.env.AGENTTWIN_TOKEN;
439
1199
  if (!token || !/^at_user_[a-f0-9]{64}$/.test(token)) throw new Error("Set AGENTTWIN_TOKEN to your individual account token.");
440
1200
  const base = apiBase(process.env.AGENTTWIN_API_URL || "https://agenttwin-api.212.56.40.125.sslip.io");
441
- const inventory = JSON.parse(await readFile5(join5(cwd, ".agenttwin/inventory.json"), "utf8"));
1201
+ const inventory = JSON.parse(await readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8"));
442
1202
  const payload = await loadDashboardDiscovery(cwd, inventory);
443
1203
  let previousHead;
444
1204
  try {
445
- previousHead = JSON.parse(await readFile5(join5(cwd, ".agenttwin/git.json"), "utf8")).head;
1205
+ previousHead = JSON.parse(await readFile6(join6(cwd, ".agenttwin/git.json"), "utf8")).head;
446
1206
  } catch {
447
1207
  }
448
1208
  const git = await captureGit(cwd, previousHead);
@@ -450,22 +1210,22 @@ async function runSync(cwd) {
450
1210
  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) });
451
1211
  const result = await response.json();
452
1212
  if (!response.ok) throw new Error(result.error || "Discovery sync failed: " + response.status);
453
- if (git) await writeFile3(join5(cwd, ".agenttwin/git.json"), JSON.stringify(git) + "\n", { mode: 384 });
1213
+ if (git) await writeFile3(join6(cwd, ".agenttwin/git.json"), JSON.stringify(git) + "\n", { mode: 384 });
454
1214
  const receipts = await syncReceipts(cwd, base, token);
455
1215
  return { receipts, synced: true, tools: payload.tools.length, workflows: payload.workflows.length, dashboard: "https://agenttwin.si/site/dashboard.html", uploaded: "discovery, Git metadata and run evidence; no application source or credentials" };
456
1216
  }
457
1217
 
458
1218
  // src/commands/prepare.ts
459
1219
  init_git();
460
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile8, writeFile as writeFile5, rm as rm2 } from "fs/promises";
461
- import { join as join8, resolve as resolve2, dirname as dirname2 } from "path";
1220
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile9, writeFile as writeFile5, rm as rm2 } from "fs/promises";
1221
+ import { join as join9, resolve as resolve2, dirname as dirname2 } from "path";
462
1222
  import { tmpdir as tmpdir2 } from "os";
463
1223
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
464
1224
 
465
1225
  // src/commands/run.ts
466
- import { mkdtemp, mkdir as mkdir2, readFile as readFile7, writeFile as writeFile4, realpath, lstat, rm, copyFile } from "fs/promises";
1226
+ import { mkdtemp, mkdir as mkdir2, readFile as readFile8, writeFile as writeFile4, realpath, lstat, rm, copyFile } from "fs/promises";
467
1227
  import { tmpdir } from "os";
468
- import { basename as basename2, dirname, isAbsolute, join as join7, relative, resolve, sep } from "path";
1228
+ import { basename as basename2, dirname, isAbsolute, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
469
1229
  import { fileURLToPath as fileURLToPath2 } from "url";
470
1230
  import { createHash, randomUUID } from "crypto";
471
1231
 
@@ -718,17 +1478,17 @@ init_git();
718
1478
 
719
1479
  // src/runtime/e2b.ts
720
1480
  import { Sandbox } from "e2b";
721
- import { readFile as readFile6 } from "fs/promises";
1481
+ import { readFile as readFile7 } from "fs/promises";
722
1482
  import { randomBytes as randomBytes2 } from "crypto";
723
1483
  import { homedir } from "os";
724
- import { join as join6 } from "path";
1484
+ import { join as join7 } from "path";
725
1485
  async function executeE2B(stage, spec, log, signal) {
726
1486
  signal?.throwIfAborted();
727
1487
  const apiKey = process.env.E2B_API_KEY;
728
1488
  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." });
729
1489
  let saved = {};
730
1490
  try {
731
- saved = JSON.parse(await readFile6(join6(homedir(), ".agenttwin/runtime.json"), "utf8"));
1491
+ saved = JSON.parse(await readFile7(join7(homedir(), ".agenttwin/runtime.json"), "utf8"));
732
1492
  } catch {
733
1493
  }
734
1494
  const template = process.env.E2B_TEMPLATE_ID ?? saved.templateId;
@@ -747,7 +1507,7 @@ async function executeE2B(stage, spec, log, signal) {
747
1507
  signal?.throwIfAborted();
748
1508
  await sandbox.commands.run("mkdir -p /input/app && chmod 755 /input /input/app", { user: "root" });
749
1509
  for (const file of ["world.json", "worker.js", ...spec.files.map((f) => `app/${f}`)]) {
750
- await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile6(join6(stage, file))).buffer, { user: "root" });
1510
+ await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile7(join7(stage, file))).buffer, { user: "root" });
751
1511
  }
752
1512
  await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
753
1513
  const runtimeEnv = {};
@@ -868,12 +1628,12 @@ async function stageApplication(root, files, destination) {
868
1628
  for (const file of files) {
869
1629
  if (isAbsolute(file) || file.includes("\\") || file.split("/").some((p) => p === ".." || /^(\.env.*|\.git|\.ssh|\.aws|\.azure|\.config|\.npmrc|\.netrc|credentials.*)$/i.test(p)) || /\.(pem|key|p12|pfx)$/i.test(file)) throw new CliError(`Unsafe application file: ${file}`, { code: EXIT.USAGE });
870
1630
  const source = await realpath(resolve(canonicalRoot, file));
871
- const rel = relative(canonicalRoot, source);
872
- 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 });
873
- const contents = await readFile7(source);
1631
+ const rel = relative2(canonicalRoot, source);
1632
+ if (rel.startsWith(`..${sep2}`) || 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 });
1633
+ const contents = await readFile8(source);
874
1634
  bytes += contents.length;
875
1635
  if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
876
- const target = join7(destination, file);
1636
+ const target = join8(destination, file);
877
1637
  await mkdir2(dirname(target), { recursive: true });
878
1638
  await writeFile4(target, contents, { mode: 420 });
879
1639
  digests[file] = createHash("sha256").update(contents).digest("hex");
@@ -884,14 +1644,14 @@ async function runWorld(cwd, opts) {
884
1644
  const file = resolve(cwd, opts.file ?? "agenttwin.world.json");
885
1645
  let spec;
886
1646
  try {
887
- spec = parseWorld(JSON.parse(await readFile7(file, "utf8")));
1647
+ spec = parseWorld(JSON.parse(await readFile8(file, "utf8")));
888
1648
  } catch (error) {
889
1649
  throw new CliError(String(error), { code: EXIT.USAGE, action: "Create agenttwin.world.json; run agenttwin example --out ./agenttwin-example for a working example." });
890
1650
  }
891
1651
  if (spec.workflowId) {
892
1652
  let inventory;
893
1653
  try {
894
- inventory = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8"));
1654
+ inventory = JSON.parse(await readFile8(join8(cwd, ".agenttwin/inventory.json"), "utf8"));
895
1655
  } catch {
896
1656
  throw new CliError("Linked workflow needs discovery inventory; run init first", { code: EXIT.BLOCKED });
897
1657
  }
@@ -904,24 +1664,24 @@ async function runWorld(cwd, opts) {
904
1664
  const runtime = opts.runtime ?? "local";
905
1665
  if (!["local", "e2b", "hosted"].includes(runtime)) throw new CliError("runtime must be local, hosted or e2b", { code: EXIT.USAGE });
906
1666
  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." });
907
- const stage = await mkdtemp(join7(tmpdir(), "agenttwin-world-"));
1667
+ const stage = await mkdtemp(join8(tmpdir(), "agenttwin-world-"));
908
1668
  try {
909
- const sourceDigests = await stageApplication(dirname(file), spec.files, join7(stage, "app"));
910
- await writeFile4(join7(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
1669
+ const sourceDigests = await stageApplication(dirname(file), spec.files, join8(stage, "app"));
1670
+ await writeFile4(join8(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
911
1671
  const git = await captureGit(cwd);
912
1672
  let project;
913
1673
  try {
914
- project = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8")).name ?? basename2(cwd);
1674
+ project = JSON.parse(await readFile8(join8(cwd, ".agenttwin/inventory.json"), "utf8")).name ?? basename2(cwd);
915
1675
  } catch {
916
1676
  }
917
1677
  if (runtime === "local") {
918
1678
  opts.log("Testing through the CLI with in-process service twins. No cloud VM or containers.");
919
- const report2 = await executeLocal(join7(stage, "app"), spec, opts.signal);
1679
+ const report2 = await executeLocal(join8(stage, "app"), spec, opts.signal);
920
1680
  const runId2 = randomUUID();
921
1681
  const receipt2 = { ...report2, runId: runId2, runtime, project, git, createdAt: (/* @__PURE__ */ new Date()).toISOString(), sourceDigests, specDigest: createHash("sha256").update(JSON.stringify(spec)).digest("hex"), provenance: "CLI-reported execution" };
922
1682
  const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId2}`);
923
1683
  await mkdir2(out2, { recursive: true });
924
- const evidencePath = join7(out2, "run.json"), reportPath = join7(out2, "report.md");
1684
+ const evidencePath = join8(out2, "run.json"), reportPath = join8(out2, "report.md");
925
1685
  await writeFile4(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
926
1686
  await writeFile4(reportPath, `# ${spec.name}: ${receipt2.status}
927
1687
 
@@ -931,8 +1691,8 @@ Git: ${git?.head ?? "unavailable"}
931
1691
  ` + receipt2.assertions.map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}${a.path}: expected ${a.count}, observed ${a.actual}`).join("\n") + "\n\nSee run.json for activities, fault and initial/final state.\n");
932
1692
  return { ...receipt2, reportPath, evidencePath };
933
1693
  }
934
- const worker = join7(dirname(fileURLToPath2(import.meta.url)), "worker.js");
935
- await copyFile(worker, join7(stage, "worker.js"));
1694
+ const worker = join8(dirname(fileURLToPath2(import.meta.url)), "worker.js");
1695
+ await copyFile(worker, join8(stage, "worker.js"));
936
1696
  if (runtime === "hosted") {
937
1697
  const receipt2 = await executeRemote(stage, spec, opts.log, { git, project });
938
1698
  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");
@@ -941,8 +1701,8 @@ Git: ${git?.head ?? "unavailable"}
941
1701
  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");
942
1702
  const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
943
1703
  await mkdir2(out2, { recursive: true });
944
- const evidencePath = join7(out2, "run.json");
945
- const reportPath = join7(out2, "report.md");
1704
+ const evidencePath = join8(out2, "run.json");
1705
+ const reportPath = join8(out2, "report.md");
946
1706
  await writeFile4(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
947
1707
  await writeFile4(reportPath, `# ${spec.name}: ${receipt2.status}
948
1708
 
@@ -979,11 +1739,11 @@ See run.json for assertions, service state, application output and isolation met
979
1739
  };
980
1740
  const out = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
981
1741
  await mkdir2(out, { recursive: true });
982
- await writeFile4(join7(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
983
- await writeFile4(join7(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
1742
+ await writeFile4(join8(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
1743
+ await writeFile4(join8(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
984
1744
  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");
985
- await writeFile4(join7(out, "report.md"), summary);
986
- return { ...receipt, reportPath: join7(out, "report.md"), evidencePath: join7(out, "run.json") };
1745
+ await writeFile4(join8(out, "report.md"), summary);
1746
+ return { ...receipt, reportPath: join8(out, "report.md"), evidencePath: join8(out, "run.json") };
987
1747
  } finally {
988
1748
  await rm(stage, { recursive: true, force: true });
989
1749
  }
@@ -1037,36 +1797,36 @@ function reviewPreparation(inventory, input) {
1037
1797
  async function runPrepare(cwd, opts) {
1038
1798
  if (!opts.file) throw new CliError("prepare requires --file preparation.json", { code: EXIT.USAGE });
1039
1799
  const file = resolve2(cwd, opts.file);
1040
- const inventory = JSON.parse(await readFile8(join8(cwd, ".agenttwin/inventory.json"), "utf8"));
1800
+ const inventory = JSON.parse(await readFile9(join9(cwd, ".agenttwin/inventory.json"), "utf8"));
1041
1801
  const discovered = await loadDashboardDiscovery(cwd, inventory);
1042
1802
  const known = new Set(inventory.workflows.map((w) => w.id));
1043
1803
  for (const w of discovered.workflows) if (!known.has(w.id)) {
1044
1804
  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 });
1045
1805
  }
1046
1806
  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.");
1047
- const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile8(file, "utf8")));
1807
+ const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile9(file, "utf8")));
1048
1808
  const directory = resolve2(cwd, opts.out ?? `.agenttwin/prepared/${spec.name}-${randomUUID2().slice(0, 8)}`);
1049
- const stage = await mkdtemp2(join8(tmpdir2(), "agenttwin-prepare-"));
1809
+ const stage = await mkdtemp2(join9(tmpdir2(), "agenttwin-prepare-"));
1050
1810
  try {
1051
1811
  const sourceDigests = await stageApplication(dirname2(file), spec.files, stage);
1052
1812
  const files = /* @__PURE__ */ Object.create(null);
1053
- for (const name of spec.files) files[name] = (await readFile8(join8(stage, name))).toString("base64");
1813
+ for (const name of spec.files) files[name] = (await readFile9(join9(stage, name))).toString("base64");
1054
1814
  const manifest = { project: discovered.name, git: await captureGit(cwd), 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() };
1055
- await writeFile5(join8(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
1056
- await writeFile5(join8(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
1057
- await writeFile5(join8(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
1058
- await mkdir3(join8(stage, ".agenttwin"), { recursive: true });
1059
- await writeFile5(join8(stage, ".agenttwin/inventory.json"), JSON.stringify({ name: discovered.name, workflows: [{ id: workflow.id }] }), { mode: 384 });
1815
+ await writeFile5(join9(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
1816
+ await writeFile5(join9(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
1817
+ await writeFile5(join9(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
1818
+ await mkdir3(join9(stage, ".agenttwin"), { recursive: true });
1819
+ await writeFile5(join9(stage, ".agenttwin/inventory.json"), JSON.stringify({ name: discovered.name, workflows: [{ id: workflow.id }] }), { mode: 384 });
1060
1820
  await mkdir3(dirname2(directory), { recursive: true });
1061
1821
  await mkdir3(directory, { mode: 448 });
1062
1822
  try {
1063
1823
  const { cp: cp2, readdir: readdir3 } = await import("fs/promises");
1064
- for (const entry of await readdir3(stage)) await cp2(join8(stage, entry), join8(directory, entry), { recursive: true, force: false, errorOnExist: true });
1824
+ for (const entry of await readdir3(stage)) await cp2(join9(stage, entry), join9(directory, entry), { recursive: true, force: false, errorOnExist: true });
1065
1825
  } catch (error) {
1066
1826
  await rm2(directory, { recursive: true, force: true });
1067
1827
  throw error;
1068
1828
  }
1069
- return { directory, worldPath: join8(directory, "agenttwin.world.json"), bundlePath: join8(directory, "agenttwin.bundle.json"), manifest };
1829
+ return { directory, worldPath: join9(directory, "agenttwin.world.json"), bundlePath: join9(directory, "agenttwin.bundle.json"), manifest };
1070
1830
  } finally {
1071
1831
  await rm2(stage, { recursive: true, force: true });
1072
1832
  }
@@ -1076,7 +1836,7 @@ async function runPrepare(cwd, opts) {
1076
1836
  import { Template } from "e2b";
1077
1837
  import { mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
1078
1838
  import { homedir as homedir2 } from "os";
1079
- import { join as join9 } from "path";
1839
+ import { join as join10 } from "path";
1080
1840
  async function runSetup(log) {
1081
1841
  if (!process.env.E2B_API_KEY) throw new CliError("Set E2B_API_KEY before creating your hosted runtime template", { code: EXIT.BLOCKED });
1082
1842
  log("Building the AgentTwin template on E2B (Node 22, PostgreSQL 15, Redis). No local containers are used.");
@@ -1084,16 +1844,16 @@ async function runSetup(log) {
1084
1844
  const result = await Template.build(template, "agenttwin-node-v2", { apiKey: process.env.E2B_API_KEY, cpuCount: 2, memoryMB: 2048, onBuildLogs: (entry) => {
1085
1845
  if (!entry.message.includes("[stdout]") && !entry.message.includes("[stderr]")) log(entry.message);
1086
1846
  } });
1087
- const directory = join9(homedir2(), ".agenttwin");
1847
+ const directory = join10(homedir2(), ".agenttwin");
1088
1848
  await mkdir4(directory, { recursive: true, mode: 448 });
1089
- const path = join9(directory, "runtime.json");
1849
+ const path = join10(directory, "runtime.json");
1090
1850
  await writeFile6(path, JSON.stringify({ provider: "e2b", templateId: result.templateId, buildId: result.buildId }, null, 2) + "\n", { mode: 384 });
1091
1851
  return { provider: "e2b", templateId: result.templateId, buildId: result.buildId, configPath: path };
1092
1852
  }
1093
1853
 
1094
1854
  // src/commands/example.ts
1095
- import { mkdir as mkdir5, writeFile as writeFile7, stat } from "fs/promises";
1096
- import { resolve as resolve3, join as join10 } from "path";
1855
+ import { mkdir as mkdir5, writeFile as writeFile7, stat as stat2 } from "fs/promises";
1856
+ import { resolve as resolve3, join as join11 } from "path";
1097
1857
  var exampleSource = `// Replace this file with your own bundled Node workflow.
1098
1858
  const safe = process.argv.includes('--safe');
1099
1859
  const crm = process.env.AGENTTWIN_CRM_URL;
@@ -1136,18 +1896,18 @@ function exampleWorld(safe = false) {
1136
1896
  }
1137
1897
  async function runExample(cwd, out) {
1138
1898
  const directory = resolve3(cwd, out ?? "agenttwin-example");
1139
- if (await stat(directory).then(() => true, () => false)) throw new CliError(`Destination exists: ${directory}`, { code: EXIT.USAGE, action: "Choose a new --out directory." });
1899
+ if (await stat2(directory).then(() => true, () => false)) throw new CliError(`Destination exists: ${directory}`, { code: EXIT.USAGE, action: "Choose a new --out directory." });
1140
1900
  await mkdir5(directory, { recursive: true });
1141
- await writeFile7(join10(directory, "workflow.mjs"), exampleSource);
1142
- await writeFile7(join10(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
1143
- await writeFile7(join10(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
1144
- await writeFile7(join10(directory, "README.md"), `# Support workflow
1901
+ await writeFile7(join11(directory, "workflow.mjs"), exampleSource);
1902
+ await writeFile7(join11(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
1903
+ await writeFile7(join11(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
1904
+ await writeFile7(join11(directory, "README.md"), `# Support workflow
1145
1905
 
1146
1906
  Run with agenttwin run --file agenttwin.world.json (expected FAIL), then --file safe.world.json (expected PASS). Runs through the CLI with in-process service twins by default. No VM or containers. Use --runtime hosted or --runtime e2b explicitly only when a cloud machine is needed.
1147
1907
 
1148
1908
  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.
1149
1909
  `);
1150
- return { directory, worldPath: join10(directory, "agenttwin.world.json") };
1910
+ return { directory, worldPath: join11(directory, "agenttwin.world.json") };
1151
1911
  }
1152
1912
 
1153
1913
  // src/commands/init.ts
@@ -1155,152 +1915,8 @@ import { readFileSync as readFileSync2 } from "fs";
1155
1915
  import { writeFile as writeFile11 } from "fs/promises";
1156
1916
  import { join as join17 } from "path";
1157
1917
 
1158
- // src/lib/files.ts
1159
- import { readdir as readdir2, readFile as readFile9, stat as stat2 } from "fs/promises";
1160
- import { join as join11, relative as relative2, sep as sep2 } from "path";
1161
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
1162
- ".git",
1163
- ".agenttwin",
1164
- "node_modules",
1165
- "dist",
1166
- "build",
1167
- "out",
1168
- "coverage",
1169
- ".next",
1170
- ".nuxt",
1171
- ".venv",
1172
- "venv",
1173
- "env",
1174
- "site-packages",
1175
- "__pycache__",
1176
- ".pytest_cache",
1177
- ".mypy_cache",
1178
- ".ruff_cache",
1179
- ".tox",
1180
- "vendor",
1181
- "target",
1182
- ".turbo",
1183
- ".cache",
1184
- ".idea",
1185
- ".vscode",
1186
- ".terraform",
1187
- "models",
1188
- "data",
1189
- "datasets",
1190
- "media",
1191
- "uploads",
1192
- "tmp",
1193
- "logs",
1194
- "gen"
1195
- ]);
1196
- var MAX_FILES = 2e4;
1197
- var MAX_BYTES = 1e6;
1198
- var WALK_BUDGET_MS = 8e3;
1199
- function shouldIgnoreDir(name) {
1200
- return IGNORED_DIRS.has(name) || name === ".git";
1201
- }
1202
- async function walkDir(root, maxFiles, budgetMs) {
1203
- const out = [];
1204
- const limits = [];
1205
- const stack = [root];
1206
- const started = Date.now();
1207
- while (stack.length > 0 && out.length < maxFiles && Date.now() - started < budgetMs) {
1208
- const dir = stack.pop();
1209
- let entries;
1210
- try {
1211
- entries = await readdir2(dir, { withFileTypes: true });
1212
- } catch {
1213
- limits.push(`Directory could not be read: ${relative2(root, dir) || "."}`);
1214
- continue;
1215
- }
1216
- entries.sort((a, b) => a.name.localeCompare(b.name));
1217
- for (const entry of entries) {
1218
- const full = join11(dir, entry.name);
1219
- if (entry.isDirectory()) {
1220
- if (!shouldIgnoreDir(entry.name)) stack.push(full);
1221
- } else if (entry.isFile()) {
1222
- out.push(relative2(root, full).split(sep2).join("/"));
1223
- if (out.length >= maxFiles) break;
1224
- }
1225
- }
1226
- }
1227
- if (out.length >= maxFiles) limits.push(`File scan limit reached (${maxFiles} files); additional files may be unscanned`);
1228
- if (stack.length && Date.now() - started >= budgetMs) limits.push(`File walk time budget reached (${budgetMs}ms); directories remain unscanned`);
1229
- return { files: out.sort(), limits };
1230
- }
1231
- var RepoFileIndex = class {
1232
- cwd;
1233
- cache = { contents: /* @__PURE__ */ new Map(), limits: [] };
1234
- bounds;
1235
- constructor(cwd, bounds = { maxFiles: MAX_FILES, budgetMs: WALK_BUDGET_MS }) {
1236
- this.cwd = cwd;
1237
- this.bounds = bounds;
1238
- }
1239
- async walk() {
1240
- if (!this.cache.files) {
1241
- const result = await walkDir(this.cwd, this.bounds.maxFiles, this.bounds.budgetMs);
1242
- this.cache.files = result.files;
1243
- this.cache.limits.push(...result.limits);
1244
- }
1245
- return this.cache.files;
1246
- }
1247
- scanLimits() {
1248
- return [...new Set(this.cache.limits)].sort();
1249
- }
1250
- async exists(path) {
1251
- try {
1252
- await stat2(join11(this.cwd, path));
1253
- return true;
1254
- } catch {
1255
- return false;
1256
- }
1257
- }
1258
- async read(path, maxBytes = MAX_BYTES) {
1259
- const cached = this.cache.contents.get(path);
1260
- if (cached !== void 0) return cached;
1261
- const full = join11(this.cwd, path);
1262
- try {
1263
- const info2 = await stat2(full);
1264
- if (!info2.isFile()) return void 0;
1265
- if (info2.size > maxBytes) {
1266
- this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
1267
- return void 0;
1268
- }
1269
- const text = await readFile9(full, "utf8");
1270
- this.cache.contents.set(path, text);
1271
- return text;
1272
- } catch {
1273
- return void 0;
1274
- }
1275
- }
1276
- async byExt(...exts) {
1277
- const wanted = new Set(exts.map((e) => e.replace(/^\./, "").toLowerCase()));
1278
- const files = await this.walk();
1279
- return files.filter((f) => {
1280
- const dot = f.lastIndexOf(".");
1281
- if (dot < 0) return false;
1282
- return wanted.has(f.slice(dot + 1).toLowerCase());
1283
- });
1284
- }
1285
- async byName(pattern) {
1286
- const files = await this.walk();
1287
- return files.filter((f) => pattern.test(f));
1288
- }
1289
- line(ref) {
1290
- const idx = ref.lastIndexOf(":");
1291
- if (idx < 0) return void 0;
1292
- const path = ref.slice(0, idx);
1293
- const num = Number(ref.slice(idx + 1));
1294
- if (!Number.isInteger(num)) return void 0;
1295
- const text = this.cache.contents.get(path);
1296
- if (text === void 0) return void 0;
1297
- const lines = text.split(/\r?\n/);
1298
- return lines[num - 1];
1299
- }
1300
- };
1301
- async function createFileIndex(cwd) {
1302
- return new RepoFileIndex(cwd);
1303
- }
1918
+ // src/discovery/engine.ts
1919
+ init_files();
1304
1920
 
1305
1921
  // src/discovery/scoring.ts
1306
1922
  var WEIGHTS = {
@@ -1856,9 +2472,9 @@ var iacDetector = {
1856
2472
  var SOURCE_EXTS = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "java", "kt", "rb", "php", "cs"];
1857
2473
  var MAX_FILES2 = 2500;
1858
2474
  var MAX_EVIDENCE = 6;
1859
- var NON_USAGE_DIR = /(^|\/)(docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated|dist|build|coverage)(\/|$)/i;
2475
+ var NON_USAGE_DIR = /(^|\/)(artifacts|testbed|docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated|dist|build|coverage)(\/|$)/i;
1860
2476
  function isNonUsagePath(path) {
1861
- return NON_USAGE_DIR.test(path);
2477
+ return NON_USAGE_DIR.test(path) || /(?:\.(?:test|spec)\.[^/]+$|(?:demo|test)?fixtures?\.[^/]+$)/i.test(path);
1862
2478
  }
1863
2479
  function safeRegex(source) {
1864
2480
  try {
@@ -2024,7 +2640,7 @@ var envVarDetector = {
2024
2640
  };
2025
2641
 
2026
2642
  // src/discovery/detectors/ci.ts
2027
- var PROVIDERS = [
2643
+ var PROVIDERS2 = [
2028
2644
  { re: /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/, id: "github-actions", title: "GitHub Actions" },
2029
2645
  { re: /(^|\/)\.gitlab-ci\.ya?ml$/, id: "gitlab-ci", title: "GitLab CI" },
2030
2646
  { re: /(^|\/)\.circleci\/config\.ya?ml$/, id: "circleci", title: "CircleCI" },
@@ -2040,7 +2656,7 @@ var ciDetector = {
2040
2656
  const all = await ctx.files.walk();
2041
2657
  const findings = [];
2042
2658
  const providerEvidence = /* @__PURE__ */ new Map();
2043
- for (const system of PROVIDERS) {
2659
+ for (const system of PROVIDERS2) {
2044
2660
  const matches = all.filter((f) => system.re.test(f));
2045
2661
  if (matches.length === 0) continue;
2046
2662
  const evidence = matches.slice(0, 3).map((ref) => ({
@@ -2182,487 +2798,8 @@ function registerDetectors() {
2182
2798
  ];
2183
2799
  }
2184
2800
 
2185
- // src/discovery/masking.ts
2186
- var EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
2187
- var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
2188
- function maskSecret(value) {
2189
- return value.replace(TOKENISH2, (match) => `${match.slice(0, 4)}***`);
2190
- }
2191
- function maskEmail(value) {
2192
- return value.replace(EMAIL2, "$1***$2");
2193
- }
2194
- function maskUrl(value) {
2195
- try {
2196
- const url = new URL(value);
2197
- return `${url.protocol}//${url.host}${url.pathname}`;
2198
- } catch {
2199
- return maskSecret(value);
2200
- }
2201
- }
2202
- function redactDetail(value) {
2203
- return maskSecret(maskEmail(value)).slice(0, 200);
2204
- }
2205
-
2206
- // src/discovery/probes/providers/stripe.ts
2207
- var stripeProbe = {
2208
- id: "stripe.readonly",
2209
- providerId: "stripe",
2210
- title: "Enumerate Stripe account configuration (read-only)",
2211
- description: "Reads products, prices and webhook endpoints. Read-only; issues no writes.",
2212
- readScope: {
2213
- endpoints: [
2214
- "GET /v1/products",
2215
- "GET /v1/prices",
2216
- "GET /v1/webhook_endpoints"
2217
- ],
2218
- methods: ["GET"]
2219
- },
2220
- credentials: [
2221
- {
2222
- envVar: "STRIPE_SECRET_KEY",
2223
- label: "Stripe restricted key (read scopes)",
2224
- required: true,
2225
- scopes: ["products:read", "prices:read", "webhook_endpoints:read"],
2226
- hint: "Prefer a restricted key (rk_...) with read scopes over a full sk_live_ key."
2227
- }
2228
- ],
2229
- async run(ctx) {
2230
- const spec = this.credentials[0];
2231
- const key = ctx.credential(spec);
2232
- if (!key) {
2233
- return {
2234
- providerId: this.providerId,
2235
- ok: false,
2236
- summary: "no credential present",
2237
- resources: [],
2238
- evidence: []
2239
- };
2240
- }
2241
- const get = async (path) => {
2242
- const response = await ctx.fetch(`https://api.stripe.com${path}`, {
2243
- method: "GET",
2244
- headers: { Authorization: `Bearer ${key}` },
2245
- signal: ctx.signal
2246
- });
2247
- if (!response.ok) {
2248
- throw new Error(`Stripe ${path} -> HTTP ${response.status}`);
2249
- }
2250
- return await response.json();
2251
- };
2252
- try {
2253
- const [products, prices, hooks] = await Promise.all([
2254
- get("/v1/products?limit=100"),
2255
- get("/v1/prices?limit=100"),
2256
- get("/v1/webhook_endpoints?limit=100")
2257
- ]);
2258
- return {
2259
- providerId: this.providerId,
2260
- ok: true,
2261
- summary: `${products.data.length} products, ${prices.data.length} prices, ${hooks.data.length} webhook endpoints`,
2262
- resources: [
2263
- {
2264
- kind: "products",
2265
- count: products.data.length,
2266
- sample: products.data.slice(0, 5).map((p) => ({
2267
- id: p.id,
2268
- name: p.name,
2269
- active: p.active
2270
- }))
2271
- },
2272
- { kind: "prices", count: prices.data.length },
2273
- {
2274
- kind: "webhook_endpoints",
2275
- count: hooks.data.length,
2276
- sample: hooks.data.slice(0, 5).map((h2) => ({
2277
- url: typeof h2.url === "string" ? maskUrl(h2.url) : void 0,
2278
- status: h2.status
2279
- }))
2280
- }
2281
- ],
2282
- evidence: [
2283
- {
2284
- ref: "probe:stripe",
2285
- pass: "probe",
2286
- signal: "probe",
2287
- detail: "account reachable via read scopes"
2288
- }
2289
- ]
2290
- };
2291
- } catch (error) {
2292
- return {
2293
- providerId: this.providerId,
2294
- ok: false,
2295
- error: { message: error instanceof Error ? error.message : String(error) },
2296
- summary: "probe failed",
2297
- resources: [],
2298
- evidence: []
2299
- };
2300
- }
2301
- }
2302
- };
2303
-
2304
- // src/discovery/probes/providers/github.ts
2305
- var githubProbe = {
2306
- id: "github.readonly",
2307
- providerId: "github",
2308
- title: "Enumerate GitHub account (read-only)",
2309
- description: "Reads the authenticated user and their repositories. Read-only.",
2310
- readScope: {
2311
- endpoints: ["GET /user", "GET /user/repos"],
2312
- methods: ["GET"]
2313
- },
2314
- credentials: [
2315
- {
2316
- envVar: "GITHUB_TOKEN",
2317
- label: "GitHub token (read:user, repo:read)",
2318
- required: true,
2319
- scopes: ["read:user", "public_repo"],
2320
- hint: "A fine-grained token with read-only repository access is sufficient."
2321
- }
2322
- ],
2323
- async run(ctx) {
2324
- const key = ctx.credential(this.credentials[0]);
2325
- if (!key) {
2326
- return {
2327
- providerId: this.providerId,
2328
- ok: false,
2329
- summary: "no credential present",
2330
- resources: [],
2331
- evidence: []
2332
- };
2333
- }
2334
- const headers = {
2335
- Authorization: `Bearer ${key}`,
2336
- Accept: "application/vnd.github+json",
2337
- "User-Agent": "agenttwin-discovery"
2338
- };
2339
- try {
2340
- const userResponse = await ctx.fetch("https://api.github.com/user", {
2341
- method: "GET",
2342
- headers,
2343
- signal: ctx.signal
2344
- });
2345
- if (!userResponse.ok) {
2346
- throw new Error(`GitHub /user -> HTTP ${userResponse.status}`);
2347
- }
2348
- const user = await userResponse.json();
2349
- const reposResponse = await ctx.fetch(
2350
- "https://api.github.com/user/repos?per_page=100&sort=updated",
2351
- { method: "GET", headers, signal: ctx.signal }
2352
- );
2353
- const repos = reposResponse.ok ? await reposResponse.json() : [];
2354
- return {
2355
- providerId: this.providerId,
2356
- ok: true,
2357
- summary: `${repos.length} repositories readable for @${user.login ?? "unknown"}`,
2358
- resources: [
2359
- {
2360
- kind: "repositories",
2361
- count: repos.length,
2362
- sample: repos.slice(0, 5).map((r) => ({
2363
- name: r.name,
2364
- private: r.private,
2365
- archived: r.archived
2366
- }))
2367
- }
2368
- ],
2369
- evidence: [
2370
- {
2371
- ref: "probe:github",
2372
- pass: "probe",
2373
- signal: "probe",
2374
- detail: "account reachable via read scopes"
2375
- }
2376
- ]
2377
- };
2378
- } catch (error) {
2379
- return {
2380
- providerId: this.providerId,
2381
- ok: false,
2382
- error: { message: error instanceof Error ? error.message : String(error) },
2383
- summary: "probe failed",
2384
- resources: [],
2385
- evidence: []
2386
- };
2387
- }
2388
- }
2389
- };
2390
-
2391
- // src/discovery/providers/catalog.ts
2392
- function planned(unknown) {
2393
- return { supported: [], verified: [], unknown, implemented: false };
2394
- }
2395
- var NO_TWIN = ["all behavior (no twin implemented)"];
2396
- var PROVIDERS2 = [
2397
- {
2398
- id: "stripe",
2399
- title: "Stripe",
2400
- category: "payments",
2401
- twin: planned(["refunds", "payouts", "disputes", "webhook ordering"]),
2402
- probe: stripeProbe,
2403
- signatures: {
2404
- packages: {
2405
- node: ["stripe"],
2406
- python: ["stripe"],
2407
- go: ["stripe-go"],
2408
- java: ["stripe-java"],
2409
- ruby: ["stripe"],
2410
- php: ["stripe/stripe-php"],
2411
- dotnet: ["Stripe.net"]
2412
- },
2413
- imports: ["new Stripe\\(", `from ["']stripe["']`, `require\\(['"]stripe['"]\\)`, "stripe-go"],
2414
- hosts: ["api.stripe.com"],
2415
- envPrefixes: ["STRIPE_"],
2416
- webhookPaths: ["/webhooks/stripe", "/stripe/webhook", "/webhook/stripe"]
2417
- },
2418
- docs: "https://stripe.com/docs/api"
2419
- },
2420
- {
2421
- id: "slack",
2422
- title: "Slack",
2423
- category: "comms",
2424
- twin: planned(NO_TWIN),
2425
- signatures: {
2426
- packages: {
2427
- node: ["@slack/web-api", "@slack/bolt", "@slack/events-api", "@slack/socket-mode"],
2428
- python: ["slack_sdk", "slack-bolt"],
2429
- go: ["slack-go"],
2430
- java: ["slack-java-sdk"],
2431
- ruby: ["slack-ruby-client"]
2432
- },
2433
- imports: ["@slack/web-api", "WebClient\\(", "slack_sdk"],
2434
- hosts: ["slack.com", "api.slack.com", "hooks.slack.com"],
2435
- envPrefixes: ["SLACK_"]
2436
- }
2437
- },
2438
- {
2439
- id: "github",
2440
- title: "GitHub",
2441
- category: "vcs",
2442
- twin: planned(NO_TWIN),
2443
- probe: githubProbe,
2444
- signatures: {
2445
- packages: {
2446
- node: ["@octokit/rest", "@octokit/core", "@octokit/auth", "octokit"],
2447
- python: ["PyGithub", "github"],
2448
- go: ["google/go-github"],
2449
- java: ["github-api"],
2450
- ruby: ["octokit"]
2451
- },
2452
- imports: ["Octokit", "@octokit/", "PyGithub"],
2453
- hosts: ["api.github.com"],
2454
- envPrefixes: ["GITHUB_", "GH_"],
2455
- webhookPaths: ["/webhooks/github", "/github/webhook"]
2456
- }
2457
- },
2458
- {
2459
- id: "aws",
2460
- title: "AWS",
2461
- category: "cloud",
2462
- twin: planned(["service coverage (LocalStack passthrough planned)"]),
2463
- envWeak: true,
2464
- signatures: {
2465
- packages: {
2466
- node: ["@aws-sdk/", "aws-sdk"],
2467
- python: ["boto3", "botocore"],
2468
- go: ["aws-sdk-go", "aws-sdk-go-v2"],
2469
- java: ["software.amazon.awssdk", "aws-java-sdk"],
2470
- ruby: ["aws-sdk-"],
2471
- php: ["aws/aws-sdk-php"],
2472
- dotnet: ["AWSSDK."]
2473
- },
2474
- imports: ["@aws-sdk/", "boto3", "aws-sdk-go"],
2475
- hosts: ["amazonaws.com"],
2476
- envPrefixes: ["AWS_"]
2477
- }
2478
- },
2479
- {
2480
- id: "google",
2481
- title: "Google (Gmail / Calendar / Workspace)",
2482
- category: "comms",
2483
- twin: planned(NO_TWIN),
2484
- envWeak: true,
2485
- signatures: {
2486
- packages: {
2487
- node: ["googleapis", "google-auth-library"],
2488
- python: ["google-api-python-client", "google-cloud", "google-auth"],
2489
- go: ["google.golang.org/api"],
2490
- java: ["google-api-client"],
2491
- ruby: ["google-apis-"]
2492
- },
2493
- imports: ["googleapis", "google-api-python-client", "google.golang.org/api"],
2494
- hosts: ["googleapis.com", "gmail.googleapis.com", "www.googleapis.com"],
2495
- envPrefixes: ["GOOGLE_", "GCP_", "GMAIL_"]
2496
- }
2497
- },
2498
- {
2499
- id: "openai",
2500
- title: "OpenAI",
2501
- category: "ai",
2502
- twin: planned(NO_TWIN),
2503
- signatures: {
2504
- packages: {
2505
- node: ["openai"],
2506
- python: ["openai"],
2507
- go: ["go-openai"],
2508
- ruby: ["ruby-openai"]
2509
- },
2510
- imports: ["OpenAI\\(", `from ["']openai["']`, "go-openai"],
2511
- hosts: ["api.openai.com"],
2512
- envPrefixes: ["OPENAI_"]
2513
- }
2514
- },
2515
- {
2516
- id: "anthropic",
2517
- title: "Anthropic",
2518
- category: "ai",
2519
- twin: planned(NO_TWIN),
2520
- signatures: {
2521
- packages: {
2522
- node: ["@anthropic-ai/sdk"],
2523
- python: ["anthropic"],
2524
- go: ["anthropic-sdk-go"]
2525
- },
2526
- imports: ["@anthropic-ai/sdk", "Anthropic\\("],
2527
- hosts: ["api.anthropic.com"],
2528
- envPrefixes: ["ANTHROPIC_"]
2529
- }
2530
- },
2531
- {
2532
- id: "shopify",
2533
- title: "Shopify",
2534
- category: "commerce",
2535
- twin: planned(NO_TWIN),
2536
- signatures: {
2537
- packages: {
2538
- node: ["@shopify/", "shopify-api"],
2539
- python: ["ShopifyAPI"],
2540
- ruby: ["shopify_api"]
2541
- },
2542
- imports: ["@shopify/", "ShopifyAPI"],
2543
- hosts: ["myshopify.com", "shopify.com"],
2544
- envPrefixes: ["SHOPIFY_"]
2545
- }
2546
- },
2547
- {
2548
- id: "twilio",
2549
- title: "Twilio",
2550
- category: "comms",
2551
- twin: planned(NO_TWIN),
2552
- signatures: {
2553
- packages: {
2554
- node: ["twilio"],
2555
- python: ["twilio"],
2556
- java: ["twilio"],
2557
- ruby: ["twilio-ruby"],
2558
- php: ["twilio/sdk"]
2559
- },
2560
- imports: ["twilio"],
2561
- hosts: ["api.twilio.com"],
2562
- envPrefixes: ["TWILIO_"]
2563
- }
2564
- },
2565
- {
2566
- id: "sendgrid",
2567
- title: "SendGrid",
2568
- category: "comms",
2569
- twin: planned(NO_TWIN),
2570
- signatures: {
2571
- packages: {
2572
- node: ["@sendgrid/mail", "@sendgrid/client"],
2573
- python: ["sendgrid"],
2574
- ruby: ["sendgrid-ruby"]
2575
- },
2576
- imports: ["@sendgrid/", "sendgrid"],
2577
- hosts: ["api.sendgrid.com"],
2578
- envPrefixes: ["SENDGRID_"]
2579
- }
2580
- },
2581
- {
2582
- id: "hubspot",
2583
- title: "HubSpot",
2584
- category: "crm",
2585
- twin: planned(NO_TWIN),
2586
- signatures: {
2587
- packages: {
2588
- node: ["@hubspot/api-client"],
2589
- python: ["hubspot-api-client"],
2590
- ruby: ["hubspot-api-client"]
2591
- },
2592
- imports: ["@hubspot/api-client", "hubspot-api-client"],
2593
- hosts: ["api.hubapi.com"],
2594
- envPrefixes: ["HUBSPOT_"]
2595
- }
2596
- },
2597
- {
2598
- id: "salesforce",
2599
- title: "Salesforce",
2600
- category: "crm",
2601
- twin: planned(NO_TWIN),
2602
- signatures: {
2603
- packages: {
2604
- node: ["jsforce", "@salesforce/"],
2605
- python: ["simple-salesforce"],
2606
- java: ["force-wsc"]
2607
- },
2608
- imports: ["jsforce", "simple-salesforce"],
2609
- hosts: ["salesforce.com", "force.com"],
2610
- envPrefixes: ["SALESFORCE_", "SF_"]
2611
- }
2612
- },
2613
- {
2614
- id: "zendesk",
2615
- title: "Zendesk",
2616
- category: "support",
2617
- twin: planned(NO_TWIN),
2618
- signatures: {
2619
- packages: {
2620
- node: ["node-zendesk"],
2621
- python: ["zendesk", "zenpy"],
2622
- ruby: ["zendesk_api"]
2623
- },
2624
- imports: ["node-zendesk", "zenpy"],
2625
- hosts: ["zendesk.com"],
2626
- envPrefixes: ["ZENDESK_"]
2627
- }
2628
- },
2629
- {
2630
- id: "linear",
2631
- title: "Linear",
2632
- category: "pm",
2633
- twin: planned(NO_TWIN),
2634
- signatures: {
2635
- packages: { node: ["@linear/sdk"], python: ["linear"] },
2636
- imports: ["@linear/sdk"],
2637
- hosts: ["api.linear.app"],
2638
- envPrefixes: ["LINEAR_"]
2639
- }
2640
- },
2641
- {
2642
- id: "notion",
2643
- title: "Notion",
2644
- category: "docs",
2645
- twin: planned(NO_TWIN),
2646
- signatures: {
2647
- packages: { node: ["@notionhq/client"], python: ["notion-client"] },
2648
- imports: ["@notionhq/client", "notion-client"],
2649
- hosts: ["api.notion.com"],
2650
- envPrefixes: ["NOTION_"]
2651
- }
2652
- },
2653
- {
2654
- id: "resend",
2655
- title: "Resend",
2656
- category: "comms",
2657
- twin: planned(NO_TWIN),
2658
- signatures: {
2659
- packages: { node: ["resend"] },
2660
- imports: [`from ["']resend["']`],
2661
- hosts: ["api.resend.com"],
2662
- envPrefixes: ["RESEND_"]
2663
- }
2664
- }
2665
- ];
2801
+ // src/discovery/engine.ts
2802
+ init_catalog();
2666
2803
 
2667
2804
  // src/discovery/registry.ts
2668
2805
  var SimpleProviderRegistry = class {
@@ -2883,6 +3020,7 @@ async function runProbes({ probes, timeoutMs = 15e3, log }) {
2883
3020
  }
2884
3021
 
2885
3022
  // src/discovery/engine.ts
3023
+ init_masking();
2886
3024
  function twinStatus(provider) {
2887
3025
  if (!provider) return "unknown";
2888
3026
  const twin = provider.twin;
@@ -2893,7 +3031,7 @@ function twinStatus(provider) {
2893
3031
  var PASS_BUDGET_MS = 2e4;
2894
3032
  async function runDiscovery(opts) {
2895
3033
  const files = await createFileIndex(opts.cwd);
2896
- const providers = new SimpleProviderRegistry(PROVIDERS2);
3034
+ const providers = new SimpleProviderRegistry(PROVIDERS);
2897
3035
  const detectors = new SimpleDetectorRegistry(registerDetectors());
2898
3036
  const collected = [];
2899
3037
  const passes = [];
@@ -3235,31 +3373,9 @@ function singularize(word) {
3235
3373
  }
3236
3374
 
3237
3375
  // src/inventory/relationships.ts
3376
+ init_entrypoints();
3238
3377
  import { createHash as createHash4 } from "crypto";
3239
3378
 
3240
- // src/inventory/entrypoints.ts
3241
- function codeOnly(source) {
3242
- return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
3243
- }
3244
- function exportedEntrypoints(source) {
3245
- const code = codeOnly(source);
3246
- 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;
3247
- const result = [];
3248
- let match;
3249
- while (match = pattern.exec(code)) {
3250
- let depth = 1, end = pattern.lastIndex;
3251
- while (end < code.length && depth) {
3252
- if (code[end] === "{") depth++;
3253
- if (code[end] === "}") depth--;
3254
- end++;
3255
- }
3256
- if (depth) continue;
3257
- result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
3258
- pattern.lastIndex = end;
3259
- }
3260
- return result;
3261
- }
3262
-
3263
3379
  // src/inventory/operations.ts
3264
3380
  var WRITE_VERBS = /* @__PURE__ */ new Set([
3265
3381
  "create",
@@ -3318,9 +3434,10 @@ function scanOperations(text, path) {
3318
3434
  }
3319
3435
 
3320
3436
  // src/inventory/relationships.ts
3437
+ init_catalog();
3321
3438
  var SOURCE_EXTS2 = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "java", "kt", "rb", "php", "cs"];
3322
3439
  var MAX_FILES3 = 2500;
3323
- var NON_USAGE_DIR2 = /(^|\/)(docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated)(\/|$)/i;
3440
+ var NON_USAGE_DIR2 = /(^|\/)(artifacts|testbed|docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated)(\/|$)/i;
3324
3441
  var PROVIDER_KINDS = /* @__PURE__ */ new Set([
3325
3442
  "payments",
3326
3443
  "comms",
@@ -3362,7 +3479,7 @@ function fileRefs(tool) {
3362
3479
  return [...refs2];
3363
3480
  }
3364
3481
  function packagesFor(providerId) {
3365
- const provider = PROVIDERS2.find((p) => p.id === providerId);
3482
+ const provider = PROVIDERS.find((p) => p.id === providerId);
3366
3483
  if (!provider) return [];
3367
3484
  const set = /* @__PURE__ */ new Set();
3368
3485
  for (const list2 of Object.values(provider.signatures.packages ?? {})) {
@@ -3489,7 +3606,7 @@ async function buildRelationships(apps, tools, files, decisions) {
3489
3606
  });
3490
3607
  }
3491
3608
  }
3492
- const sourceFiles = (await Promise.all(SOURCE_EXTS2.map((ext) => files.byExt(ext)))).flat().filter((f) => !NON_USAGE_DIR2.test(f)).slice(0, MAX_FILES3);
3609
+ const sourceFiles = (await Promise.all(SOURCE_EXTS2.map((ext) => files.byExt(ext)))).flat().filter((f) => !NON_USAGE_DIR2.test(f) && !/(?:\.(?:test|spec)\.[^/]+$|(?:demo|test)?fixtures?\.[^/]+$)/i.test(f)).slice(0, MAX_FILES3);
3493
3610
  const records = /* @__PURE__ */ new Map();
3494
3611
  const roles = /* @__PURE__ */ new Map();
3495
3612
  const fileEntities = /* @__PURE__ */ new Map();
@@ -3738,6 +3855,8 @@ async function extractScopeHints(files, inspections) {
3738
3855
  }
3739
3856
 
3740
3857
  // src/inventory/build.ts
3858
+ init_catalog();
3859
+ init_files();
3741
3860
  var CATEGORY_KIND = {
3742
3861
  payments: "payments",
3743
3862
  comms: "comms",
@@ -3748,7 +3867,7 @@ var CATEGORY_KIND = {
3748
3867
  ai: "ai"
3749
3868
  };
3750
3869
  function providerKind(id) {
3751
- const provider = PROVIDERS2.find((p) => p.id === id);
3870
+ const provider = PROVIDERS.find((p) => p.id === id);
3752
3871
  if (!provider) return "external-api";
3753
3872
  return CATEGORY_KIND[provider.category] ?? "external-api";
3754
3873
  }
@@ -3982,7 +4101,7 @@ async function buildInventory({
3982
4101
  const scanLimits = [.../* @__PURE__ */ new Set([...manifest.scanLimits ?? [], ...files.scanLimits()])];
3983
4102
  if (scanLimits.length) sources.push({ id: "files:coverage", tier: "local", status: "limited", findings: 0, detail: scanLimits.join("; ") });
3984
4103
  const unclassified = candidates.filter((u) => !classified.has(u.ref));
3985
- const withTwin = toolList.filter((t) => PROVIDERS2.some((p) => p.id === t.id && p.twin.implemented)).length;
4104
+ const withTwin = toolList.filter((t) => PROVIDERS.some((p) => p.id === t.id && p.twin.implemented)).length;
3986
4105
  const coverage = {
3987
4106
  sources,
3988
4107
  counts: {
@@ -4364,6 +4483,9 @@ async function runInspections(opts) {
4364
4483
  return results.sort((a, b) => a.id.localeCompare(b.id));
4365
4484
  }
4366
4485
 
4486
+ // src/commands/inventory.ts
4487
+ init_catalog();
4488
+
4367
4489
  // src/inventory/workflows.ts
4368
4490
  function permissionsFor(kind, entities) {
4369
4491
  const list2 = [];