@kaddo/cli 3.81.0 → 3.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -7,6 +7,10 @@ function exists(p2) {
7
7
  function readFile(p2) {
8
8
  return fs.readFileSync(p2, "utf-8");
9
9
  }
10
+ function writeFile(filePath, content) {
11
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
12
+ fs.writeFileSync(filePath, content, "utf-8");
13
+ }
10
14
  function readDir(dirPath) {
11
15
  if (!exists(dirPath)) return [];
12
16
  return fs.readdirSync(dirPath);
@@ -6583,6 +6587,7 @@ function parseWorkItemSource(frontmatter) {
6583
6587
  title: optStr(obj.title) ?? optStr(frontmatter.source_title),
6584
6588
  context: optStr(obj.context) ?? optStr(frontmatter.source_context),
6585
6589
  provider: optStr(obj.provider) ?? optStr(frontmatter.source_provider),
6590
+ integration: optStr(obj.integration) ?? optStr(frontmatter.source_integration),
6586
6591
  url: optStr(obj.url) ?? optStr(frontmatter.source_url),
6587
6592
  imported_at: optStr(obj.imported_at) ?? optStr(frontmatter.source_imported_at),
6588
6593
  synced_at: optStr(obj.synced_at) ?? optStr(frontmatter.source_synced_at),
@@ -8834,6 +8839,7 @@ function createWorkItem(dir, opts) {
8834
8839
  const id = nextWorkItemId(dir);
8835
8840
  const title = intent.split(/\r?\n/)[0].trim().slice(0, 120);
8836
8841
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8842
+ const source = opts.source ? { ...opts.source, inferred: false } : { type: "manual", inferred: false };
8837
8843
  const data = {
8838
8844
  type,
8839
8845
  id,
@@ -8841,7 +8847,7 @@ function createWorkItem(dir, opts) {
8841
8847
  status: "draft",
8842
8848
  work_type: type,
8843
8849
  created_at: today,
8844
- source: { type: "manual", inferred: false },
8850
+ source,
8845
8851
  generated_by: "kaddo-admin",
8846
8852
  affected_modules: [],
8847
8853
  summary: intent
@@ -9366,7 +9372,760 @@ function buildRefinementHandoff(dir, workItemId) {
9366
9372
  text: lines.join("\n")
9367
9373
  };
9368
9374
  }
9375
+
9376
+ // src/services/integrations.ts
9377
+ import matter8 from "gray-matter";
9378
+ import { parse as parseYaml9, stringify as stringifyYaml5 } from "yaml";
9379
+
9380
+ // ../integrations/src/errors.ts
9381
+ var RETRYABLE = /* @__PURE__ */ new Set([
9382
+ "INTEGRATION_RATE_LIMITED",
9383
+ "INTEGRATION_UNAVAILABLE",
9384
+ "INTEGRATION_TIMEOUT"
9385
+ ]);
9386
+ var IntegrationError = class extends Error {
9387
+ code;
9388
+ /** True for transient conditions (rate limit / unavailable / timeout). */
9389
+ retryable;
9390
+ /** A safe, provider-agnostic reason. Never the raw SDK message. */
9391
+ safeMessage;
9392
+ constructor(code, message) {
9393
+ super(message);
9394
+ this.name = "IntegrationError";
9395
+ this.code = code;
9396
+ this.retryable = RETRYABLE.has(code);
9397
+ this.safeMessage = message;
9398
+ }
9399
+ };
9400
+ function defaultMessageFor(code) {
9401
+ switch (code) {
9402
+ case "INTEGRATION_CONFIG_INVALID":
9403
+ return "The integration configuration is invalid.";
9404
+ case "INTEGRATION_UNAUTHORIZED":
9405
+ return "Authentication failed. Check the configured credentials.";
9406
+ case "INTEGRATION_FORBIDDEN":
9407
+ return "The configured credentials lack permission for this operation.";
9408
+ case "INTEGRATION_NOT_FOUND":
9409
+ return "The requested external resource was not found.";
9410
+ case "INTEGRATION_RATE_LIMITED":
9411
+ return "The external provider is rate limiting requests. Try again later.";
9412
+ case "INTEGRATION_UNAVAILABLE":
9413
+ return "The external provider is temporarily unavailable.";
9414
+ case "INTEGRATION_TIMEOUT":
9415
+ return "The external provider did not respond in time.";
9416
+ case "INTEGRATION_PROVIDER_ERROR":
9417
+ return "The external provider returned an error.";
9418
+ case "UNSUPPORTED_CAPABILITY":
9419
+ return "This adapter does not support the requested capability.";
9420
+ }
9421
+ }
9422
+ function integrationError(code, message) {
9423
+ return new IntegrationError(code, message ?? defaultMessageFor(code));
9424
+ }
9425
+
9426
+ // ../integrations/src/identity.ts
9427
+ function externalIdentityKey(integrationId, externalId) {
9428
+ return `${integrationId}#${externalId}`;
9429
+ }
9430
+ function externalDisplayKey(provider, externalId) {
9431
+ return `${provider}#${externalId}`;
9432
+ }
9433
+
9434
+ // ../integrations/src/config.ts
9435
+ var ENV_SUFFIX = "_env";
9436
+ var SECRET_KEY = /(token|secret|password|key|pat|apikey|api_key)/i;
9437
+ function parseCredentials(raw, id, findings) {
9438
+ const creds = {};
9439
+ if (raw == null) return creds;
9440
+ if (typeof raw !== "object" || Array.isArray(raw)) {
9441
+ findings.push({ level: "blocking", id, message: `Integration "${id}": credentials must be a mapping of secret references.` });
9442
+ return creds;
9443
+ }
9444
+ for (const [key, value] of Object.entries(raw)) {
9445
+ if (key.endsWith(ENV_SUFFIX)) {
9446
+ const name = key.slice(0, -ENV_SUFFIX.length);
9447
+ if (typeof value === "string" && value.trim()) creds[name] = { env: value.trim() };
9448
+ else findings.push({ level: "blocking", id, message: `Integration "${id}": credential "${key}" must name an environment variable.` });
9449
+ continue;
9450
+ }
9451
+ if (SECRET_KEY.test(key) && typeof value === "string") {
9452
+ findings.push({ level: "blocking", id, message: `Integration "${id}": secret "${key}" must not be stored in config. Use "${key}${ENV_SUFFIX}: <ENV_VAR_NAME>".` });
9453
+ continue;
9454
+ }
9455
+ if (value && typeof value === "object" && typeof value.env === "string") {
9456
+ creds[key] = { env: String(value.env) };
9457
+ continue;
9458
+ }
9459
+ findings.push({ level: "warning", id, message: `Integration "${id}": ignoring unrecognized credential entry "${key}".` });
9460
+ }
9461
+ return creds;
9462
+ }
9463
+ function parseSecrets(raw, id, findings) {
9464
+ const secrets = {};
9465
+ if (raw == null) return secrets;
9466
+ if (typeof raw !== "object" || Array.isArray(raw)) {
9467
+ findings.push({ level: "blocking", id, message: `Integration "${id}": secrets must be a mapping of logical references.` });
9468
+ return secrets;
9469
+ }
9470
+ for (const [key, value] of Object.entries(raw)) {
9471
+ if (typeof value === "string" && value.trim()) {
9472
+ if (value.length > 100 || /^(ghp_|sk-|xox[bpsa]-|glpat-|ey[A-Za-z0-9])/i.test(value)) {
9473
+ findings.push({ level: "blocking", id, message: `Integration "${id}": secret "${key}" appears to contain an actual credential, not a reference name.` });
9474
+ continue;
9475
+ }
9476
+ secrets[key] = value.trim();
9477
+ } else {
9478
+ findings.push({ level: "warning", id, message: `Integration "${id}": ignoring non-string secret entry "${key}".` });
9479
+ }
9480
+ }
9481
+ return secrets;
9482
+ }
9483
+ function parseIntegrationsConfig(raw, opts) {
9484
+ const findings = [];
9485
+ const integrations = [];
9486
+ const list2 = raw && typeof raw === "object" && Array.isArray(raw.integrations) ? raw.integrations : Array.isArray(raw) ? raw : [];
9487
+ const seen = /* @__PURE__ */ new Set();
9488
+ for (const entry of list2) {
9489
+ if (!entry || typeof entry !== "object") {
9490
+ findings.push({ level: "blocking", message: "Each integration must be a mapping." });
9491
+ continue;
9492
+ }
9493
+ const o = entry;
9494
+ const id = typeof o.id === "string" ? o.id.trim() : "";
9495
+ const adapter = typeof o.adapter === "string" ? o.adapter.trim() : "";
9496
+ if (!id) {
9497
+ findings.push({ level: "blocking", message: 'An integration is missing a required "id".' });
9498
+ continue;
9499
+ }
9500
+ if (seen.has(id)) {
9501
+ findings.push({ level: "blocking", id, message: `Duplicate integration id "${id}".` });
9502
+ continue;
9503
+ }
9504
+ seen.add(id);
9505
+ if (!adapter) {
9506
+ findings.push({ level: "blocking", id, message: `Integration "${id}" is missing a required "adapter".` });
9507
+ continue;
9508
+ }
9509
+ if (!opts.adapterIds.has(adapter)) {
9510
+ findings.push({ level: "blocking", id, message: `Integration "${id}" references unknown adapter "${adapter}".` });
9511
+ }
9512
+ const enabled = o.enabled === void 0 ? true : o.enabled === true || o.enabled === "true";
9513
+ const config = o.config && typeof o.config === "object" && !Array.isArray(o.config) ? o.config : {};
9514
+ const credentials = parseCredentials(o.credentials, id, findings);
9515
+ const secrets = parseSecrets(o.secrets, id, findings);
9516
+ const timeoutMs = typeof o.timeout_ms === "number" ? o.timeout_ms : typeof o.timeoutMs === "number" ? o.timeoutMs : void 0;
9517
+ integrations.push({ id, adapter, enabled, config, credentials, secrets, timeoutMs });
9518
+ }
9519
+ return { integrations, findings };
9520
+ }
9521
+ async function resolveAllCredentials(integration, resolver, env) {
9522
+ const credentials = {};
9523
+ const missing = [];
9524
+ for (const [name, ref] of Object.entries(integration.credentials)) {
9525
+ const value = env[ref.env];
9526
+ if (value && value.length > 0) credentials[name] = value;
9527
+ else missing.push(ref.env);
9528
+ }
9529
+ for (const [name, ref] of Object.entries(integration.secrets)) {
9530
+ if (credentials[name]) continue;
9531
+ const value = await resolver.resolve(ref);
9532
+ if (value !== void 0) credentials[name] = value;
9533
+ else missing.push(ref);
9534
+ }
9535
+ return { credentials, missing };
9536
+ }
9537
+ function serializeIntegrationsConfig(integrations) {
9538
+ return {
9539
+ integrations: integrations.map((i) => {
9540
+ const entry = {
9541
+ id: i.id,
9542
+ adapter: i.adapter,
9543
+ enabled: i.enabled
9544
+ };
9545
+ if (Object.keys(i.config).length > 0) entry.config = i.config;
9546
+ if (Object.keys(i.credentials).length > 0) {
9547
+ const creds = {};
9548
+ for (const [name, ref] of Object.entries(i.credentials)) {
9549
+ creds[`${name}${ENV_SUFFIX}`] = ref.env;
9550
+ }
9551
+ entry.credentials = creds;
9552
+ }
9553
+ if (Object.keys(i.secrets).length > 0) entry.secrets = i.secrets;
9554
+ if (i.timeoutMs !== void 0) entry.timeout_ms = i.timeoutMs;
9555
+ return entry;
9556
+ })
9557
+ };
9558
+ }
9559
+ function integrationConfigFromInput(input) {
9560
+ return {
9561
+ id: input.id,
9562
+ adapter: input.adapter,
9563
+ enabled: input.enabled ?? true,
9564
+ config: input.config ?? {},
9565
+ credentials: {},
9566
+ secrets: input.secrets ?? {},
9567
+ timeoutMs: input.timeoutMs
9568
+ };
9569
+ }
9570
+
9571
+ // ../integrations/src/preview.ts
9572
+ function buildImportPreview(item, opts) {
9573
+ return {
9574
+ source: {
9575
+ provider: item.provider,
9576
+ integration: opts.integrationId,
9577
+ externalId: item.externalId,
9578
+ url: item.url,
9579
+ identityKey: externalIdentityKey(opts.integrationId, item.externalId),
9580
+ displayKey: externalDisplayKey(item.provider, item.externalId)
9581
+ },
9582
+ capturedIntent: item.title,
9583
+ description: item.description,
9584
+ externalType: item.type,
9585
+ externalStatus: item.status,
9586
+ kaddoStatus: "draft",
9587
+ kaddoType: opts.type ? opts.type : null,
9588
+ writes: false
9589
+ };
9590
+ }
9591
+
9592
+ // ../integrations/src/secrets.ts
9593
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
9594
+ import { dirname as dirname2 } from "path";
9595
+ function secretRefKey(integrationId, secretName) {
9596
+ return `${integrationId}.${secretName}`;
9597
+ }
9598
+ var SECRETS_FILENAME = ".secrets.json";
9599
+ function secretsPath(projectDir) {
9600
+ return `${projectDir}/.kaddo/${SECRETS_FILENAME}`;
9601
+ }
9602
+ function loadStore(filePath) {
9603
+ if (!existsSync2(filePath)) return {};
9604
+ try {
9605
+ const raw = readFileSync2(filePath, "utf-8");
9606
+ const parsed = JSON.parse(raw);
9607
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
9608
+ return {};
9609
+ } catch {
9610
+ return {};
9611
+ }
9612
+ }
9613
+ function saveStore(filePath, store) {
9614
+ mkdirSync(dirname2(filePath), { recursive: true });
9615
+ writeFileSync(filePath, JSON.stringify(store, null, 2) + "\n", "utf-8");
9616
+ }
9617
+ function createLocalSecretProvider(projectDir) {
9618
+ const fp = secretsPath(projectDir);
9619
+ return {
9620
+ async get(key) {
9621
+ const store = loadStore(fp);
9622
+ const val = store[key];
9623
+ return val !== void 0 && val !== "" ? val : void 0;
9624
+ },
9625
+ async set(key, value) {
9626
+ const store = loadStore(fp);
9627
+ store[key] = value;
9628
+ saveStore(fp, store);
9629
+ },
9630
+ async delete(key) {
9631
+ const store = loadStore(fp);
9632
+ delete store[key];
9633
+ saveStore(fp, store);
9634
+ },
9635
+ async exists(key) {
9636
+ const store = loadStore(fp);
9637
+ return key in store && store[key] !== void 0 && store[key] !== "";
9638
+ }
9639
+ };
9640
+ }
9641
+ function createEnvSecretProvider(env = process.env) {
9642
+ return {
9643
+ async get(key) {
9644
+ const val = env[key];
9645
+ return val !== void 0 && val !== "" ? val : void 0;
9646
+ },
9647
+ async set() {
9648
+ throw new Error("Environment secret provider is read-only.");
9649
+ },
9650
+ async delete() {
9651
+ throw new Error("Environment secret provider is read-only.");
9652
+ },
9653
+ async exists(key) {
9654
+ const val = env[key];
9655
+ return val !== void 0 && val !== "";
9656
+ }
9657
+ };
9658
+ }
9659
+ function createCompositeResolver(...providers) {
9660
+ return {
9661
+ async resolve(reference) {
9662
+ for (const provider of providers) {
9663
+ const val = await provider.get(reference);
9664
+ if (val !== void 0) return val;
9665
+ }
9666
+ return void 0;
9667
+ }
9668
+ };
9669
+ }
9670
+
9671
+ // ../integrations/src/registry.ts
9672
+ var DuplicateAdapterError = class extends Error {
9673
+ constructor(id) {
9674
+ super(`An integration adapter with id "${id}" is already registered.`);
9675
+ this.name = "DuplicateAdapterError";
9676
+ }
9677
+ };
9678
+ var IntegrationRegistry = class {
9679
+ adapters = /* @__PURE__ */ new Map();
9680
+ register(adapter) {
9681
+ if (this.adapters.has(adapter.id)) throw new DuplicateAdapterError(adapter.id);
9682
+ this.adapters.set(adapter.id, adapter);
9683
+ }
9684
+ get(id) {
9685
+ return this.adapters.get(id);
9686
+ }
9687
+ has(id) {
9688
+ return this.adapters.has(id);
9689
+ }
9690
+ list() {
9691
+ return [...this.adapters.values()].sort((a, b) => a.id.localeCompare(b.id));
9692
+ }
9693
+ ids() {
9694
+ return new Set(this.adapters.keys());
9695
+ }
9696
+ };
9697
+
9698
+ // ../integrations/src/mock-adapter.ts
9699
+ var MOCK_ADAPTER_ID = "mock";
9700
+ var DEFAULT_ITEMS = [
9701
+ {
9702
+ externalId: "EXT-001",
9703
+ provider: MOCK_ADAPTER_ID,
9704
+ title: "Open public registration to everyone",
9705
+ description: "Open self-service registration to the public once the private beta ends.",
9706
+ type: "Feature",
9707
+ status: "Open",
9708
+ url: "https://example.test/mock/EXT-001",
9709
+ labels: ["registration", "beta"],
9710
+ createdAt: "2026-01-05T10:00:00.000Z",
9711
+ updatedAt: "2026-02-01T09:30:00.000Z",
9712
+ rawMetadata: { board: "delivery" }
9713
+ },
9714
+ {
9715
+ externalId: "EXT-002",
9716
+ provider: MOCK_ADAPTER_ID,
9717
+ title: "Personalize onboarding steps",
9718
+ description: "The onboarding checklist should adapt to what the user has already completed.",
9719
+ type: "Task",
9720
+ status: "To Do",
9721
+ url: "https://example.test/mock/EXT-002",
9722
+ labels: ["onboarding"],
9723
+ createdAt: "2026-01-08T12:00:00.000Z",
9724
+ updatedAt: "2026-01-20T15:00:00.000Z"
9725
+ }
9726
+ ];
9727
+ function simulationOf(context, fallback) {
9728
+ const fromConfig = context.config?.simulate;
9729
+ return typeof fromConfig === "string" ? fromConfig : fallback;
9730
+ }
9731
+ function readFailure(sim) {
9732
+ switch (sim) {
9733
+ case "unauthorized":
9734
+ throw integrationError("INTEGRATION_UNAUTHORIZED");
9735
+ case "rate-limited":
9736
+ throw integrationError("INTEGRATION_RATE_LIMITED");
9737
+ case "unavailable":
9738
+ throw integrationError("INTEGRATION_UNAVAILABLE");
9739
+ case "timeout":
9740
+ throw integrationError("INTEGRATION_TIMEOUT");
9741
+ case "available":
9742
+ break;
9743
+ }
9744
+ }
9745
+ function createMockAdapter(opts = {}) {
9746
+ const items = opts.items ?? DEFAULT_ITEMS;
9747
+ const defaultSim = opts.simulate ?? "available";
9748
+ const defaultPageSize = opts.pageSize ?? 50;
9749
+ return {
9750
+ id: MOCK_ADAPTER_ID,
9751
+ metadata: {
9752
+ id: MOCK_ADAPTER_ID,
9753
+ displayName: "Mock Work Source",
9754
+ version: "1.0.0",
9755
+ description: "Deterministic offline reference adapter for validating the integration foundation.",
9756
+ configSchema: {
9757
+ simulate: {
9758
+ type: "select",
9759
+ required: false,
9760
+ label: "Simulation mode",
9761
+ description: "Controls what the mock adapter simulates during verify/read operations.",
9762
+ options: [
9763
+ { value: "available", label: "Available" },
9764
+ { value: "unauthorized", label: "Unauthorized" },
9765
+ { value: "rate-limited", label: "Rate Limited" },
9766
+ { value: "unavailable", label: "Unavailable" },
9767
+ { value: "timeout", label: "Timeout" }
9768
+ ],
9769
+ defaultValue: "available"
9770
+ }
9771
+ },
9772
+ secretSchema: {
9773
+ token: {
9774
+ type: "string",
9775
+ required: false,
9776
+ label: "API Token",
9777
+ description: "Optional token for testing secret handling (not used by the mock adapter)."
9778
+ }
9779
+ }
9780
+ },
9781
+ capabilities: {
9782
+ workItems: { list: true, read: true, import: true, write: false, statusSync: false, comments: false, webhooks: false }
9783
+ },
9784
+ async verifyConnection(context) {
9785
+ const sim = simulationOf(context, defaultSim);
9786
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
9787
+ switch (sim) {
9788
+ case "available":
9789
+ return { status: "available", checkedAt };
9790
+ case "unauthorized":
9791
+ return { status: "unauthorized", message: "Check the configured credentials.", checkedAt };
9792
+ case "rate-limited":
9793
+ case "unavailable":
9794
+ case "timeout":
9795
+ return { status: "unavailable", message: "The mock provider is temporarily unavailable.", checkedAt };
9796
+ }
9797
+ },
9798
+ async listWorkItems(request) {
9799
+ readFailure(simulationOf(request.context, defaultSim));
9800
+ let pool = items;
9801
+ const f = request.filters;
9802
+ if (f?.status) pool = pool.filter((i) => (i.status ?? "").toLowerCase() === f.status.toLowerCase());
9803
+ if (f?.query) pool = pool.filter((i) => `${i.title} ${i.description ?? ""}`.toLowerCase().includes(f.query.toLowerCase()));
9804
+ if (f?.updatedSince) pool = pool.filter((i) => (i.updatedAt ?? "") >= f.updatedSince);
9805
+ const size = Math.max(1, request.pageSize ?? defaultPageSize);
9806
+ const start = request.cursor ? Math.max(0, Number.parseInt(request.cursor, 10) || 0) : 0;
9807
+ const slice = pool.slice(start, start + size);
9808
+ const end = start + slice.length;
9809
+ const hasMore = end < pool.length;
9810
+ return { items: slice, hasMore, ...hasMore ? { nextCursor: String(end) } : {} };
9811
+ },
9812
+ async getWorkItem(request) {
9813
+ readFailure(simulationOf(request.context, defaultSim));
9814
+ return items.find((i) => i.externalId === request.externalId) ?? null;
9815
+ }
9816
+ };
9817
+ }
9818
+
9819
+ // ../integrations/src/index.ts
9820
+ function createDefaultRegistry() {
9821
+ const registry2 = new IntegrationRegistry();
9822
+ registry2.register(createMockAdapter());
9823
+ return registry2;
9824
+ }
9825
+
9826
+ // src/services/integrations.ts
9827
+ var INTEGRATIONS_FILE = ".kaddo/integrations.yml";
9828
+ var DEFAULT_TIMEOUT_MS = 1e4;
9829
+ var registry = createDefaultRegistry();
9830
+ function integrationRegistry() {
9831
+ return registry;
9832
+ }
9833
+ var IntegrationServiceError = class extends Error {
9834
+ code;
9835
+ constructor(code, message) {
9836
+ super(message);
9837
+ this.name = "IntegrationServiceError";
9838
+ this.code = code;
9839
+ }
9840
+ };
9841
+ function loadRaw(dir) {
9842
+ const abs = join(dir, INTEGRATIONS_FILE);
9843
+ if (!exists(abs)) return { integrations: [] };
9844
+ try {
9845
+ return parseYaml9(readFile(abs)) ?? { integrations: [] };
9846
+ } catch {
9847
+ return { integrations: [] };
9848
+ }
9849
+ }
9850
+ function loadIntegrations(dir) {
9851
+ return parseIntegrationsConfig(loadRaw(dir), { adapterIds: registry.ids() });
9852
+ }
9853
+ function saveIntegrations(dir, integrations) {
9854
+ const abs = join(dir, INTEGRATIONS_FILE);
9855
+ const data = serializeIntegrationsConfig(integrations);
9856
+ writeFile(abs, stringifyYaml5(data, { lineWidth: 120 }));
9857
+ }
9858
+ function configStatus(integration, findings) {
9859
+ if (!integration.enabled) return "disabled";
9860
+ const blocking = findings.some((f) => f.id === integration.id && f.level === "blocking");
9861
+ if (blocking || !registry.has(integration.adapter)) return "invalid-config";
9862
+ return "configured";
9863
+ }
9864
+ function secretProvider(dir) {
9865
+ return createLocalSecretProvider(dir);
9866
+ }
9867
+ function secretResolver(dir, env) {
9868
+ return createCompositeResolver(createLocalSecretProvider(dir), createEnvSecretProvider(env));
9869
+ }
9870
+ function listIntegrations(dir) {
9871
+ const { integrations, findings } = loadIntegrations(dir);
9872
+ return integrations.map((integration) => {
9873
+ const adapter = registry.get(integration.adapter);
9874
+ return {
9875
+ id: integration.id,
9876
+ adapter: integration.adapter,
9877
+ enabled: integration.enabled,
9878
+ status: configStatus(integration, findings),
9879
+ displayName: adapter?.metadata.displayName ?? integration.adapter,
9880
+ capabilities: adapter?.capabilities ?? null,
9881
+ metadata: adapter?.metadata ?? null,
9882
+ credentialRefs: Object.values(integration.credentials).map((c) => c.env),
9883
+ secretRefs: Object.values(integration.secrets),
9884
+ secretStatus: {},
9885
+ findings: findings.filter((f) => f.id === integration.id)
9886
+ };
9887
+ });
9888
+ }
9889
+ function getIntegration(dir, id) {
9890
+ const { integrations, findings } = loadIntegrations(dir);
9891
+ const integration = integrations.find((i) => i.id === id);
9892
+ if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured in this project.`);
9893
+ const adapter = registry.get(integration.adapter);
9894
+ return {
9895
+ id: integration.id,
9896
+ adapter: integration.adapter,
9897
+ enabled: integration.enabled,
9898
+ status: configStatus(integration, findings),
9899
+ displayName: adapter?.metadata.displayName ?? integration.adapter,
9900
+ capabilities: adapter?.capabilities ?? null,
9901
+ metadata: adapter?.metadata ?? null,
9902
+ credentialRefs: Object.values(integration.credentials).map((c) => c.env),
9903
+ secretRefs: Object.values(integration.secrets),
9904
+ secretStatus: {},
9905
+ findings: findings.filter((f) => f.id === integration.id)
9906
+ };
9907
+ }
9908
+ async function getIntegrationSecretStatus(dir, id) {
9909
+ const { integrations } = loadIntegrations(dir);
9910
+ const integration = integrations.find((i) => i.id === id);
9911
+ if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
9912
+ const sp = secretProvider(dir);
9913
+ const status = {};
9914
+ for (const [name, ref] of Object.entries(integration.secrets)) {
9915
+ status[name] = await sp.exists(ref);
9916
+ }
9917
+ for (const [name, ref] of Object.entries(integration.credentials)) {
9918
+ const val = process.env[ref.env];
9919
+ status[name] = val !== void 0 && val !== "";
9920
+ }
9921
+ return status;
9922
+ }
9923
+ function getAvailableIntegrationTypes() {
9924
+ return registry.list().map((a) => ({
9925
+ id: a.id,
9926
+ displayName: a.metadata.displayName,
9927
+ description: a.metadata.description,
9928
+ configSchema: a.metadata.configSchema ?? {},
9929
+ secretSchema: a.metadata.secretSchema ?? {},
9930
+ capabilities: a.capabilities
9931
+ }));
9932
+ }
9933
+ function requireIntegration(dir, id) {
9934
+ const { integrations, findings } = loadIntegrations(dir);
9935
+ const integration = integrations.find((i) => i.id === id);
9936
+ if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured in this project.`);
9937
+ return { integration, findings, all: integrations };
9938
+ }
9939
+ function resolveAdapter(integration) {
9940
+ const adapter = registry.get(integration.adapter);
9941
+ if (!adapter) throw new IntegrationServiceError("ADAPTER_NOT_FOUND", `Unknown integration adapter "${integration.adapter}".`);
9942
+ return adapter;
9943
+ }
9944
+ async function buildContextWithSecrets(dir, integration, env) {
9945
+ const resolver = secretResolver(dir, env);
9946
+ const { credentials, missing } = await resolveAllCredentials(integration, resolver, env);
9947
+ return {
9948
+ context: { integrationId: integration.id, config: integration.config, credentials, timeoutMs: integration.timeoutMs ?? DEFAULT_TIMEOUT_MS },
9949
+ missing
9950
+ };
9951
+ }
9952
+ async function withTimeout(op, ms) {
9953
+ let timer;
9954
+ const timeout = new Promise((_, reject) => {
9955
+ timer = setTimeout(() => reject(integrationError("INTEGRATION_TIMEOUT")), ms);
9956
+ });
9957
+ try {
9958
+ return await Promise.race([op, timeout]);
9959
+ } finally {
9960
+ if (timer) clearTimeout(timer);
9961
+ }
9962
+ }
9963
+ function validateId(id) {
9964
+ if (!id || !id.trim()) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id is required.");
9965
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(id)) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id must be alphanumeric with dashes, dots or underscores.");
9966
+ if (id.length > 64) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id must be 64 characters or fewer.");
9967
+ }
9968
+ function createIntegration(dir, input) {
9969
+ validateId(input.id);
9970
+ if (!input.adapter || !input.adapter.trim()) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "An adapter type is required.");
9971
+ const { integrations } = loadIntegrations(dir);
9972
+ if (integrations.find((i) => i.id === input.id)) {
9973
+ throw new IntegrationServiceError("INTEGRATION_ALREADY_EXISTS", `An integration with id "${input.id}" already exists.`);
9974
+ }
9975
+ const newConfig = integrationConfigFromInput(input);
9976
+ integrations.push(newConfig);
9977
+ saveIntegrations(dir, integrations);
9978
+ return getIntegration(dir, input.id);
9979
+ }
9980
+ function updateIntegration(dir, id, input) {
9981
+ const { integrations } = loadIntegrations(dir);
9982
+ const idx = integrations.findIndex((i) => i.id === id);
9983
+ if (idx < 0) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
9984
+ const existing = integrations[idx];
9985
+ if (input.enabled !== void 0) existing.enabled = input.enabled;
9986
+ if (input.config !== void 0) existing.config = input.config;
9987
+ if (input.secrets !== void 0) existing.secrets = input.secrets;
9988
+ if (input.timeoutMs !== void 0) existing.timeoutMs = input.timeoutMs;
9989
+ integrations[idx] = existing;
9990
+ saveIntegrations(dir, integrations);
9991
+ return getIntegration(dir, id);
9992
+ }
9993
+ function deleteIntegration(dir, id) {
9994
+ const { integrations } = loadIntegrations(dir);
9995
+ const idx = integrations.findIndex((i) => i.id === id);
9996
+ if (idx < 0) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
9997
+ const removed = integrations[idx];
9998
+ integrations.splice(idx, 1);
9999
+ saveIntegrations(dir, integrations);
10000
+ const sp = secretProvider(dir);
10001
+ for (const ref of Object.values(removed.secrets)) {
10002
+ sp.delete(ref).catch(() => {
10003
+ });
10004
+ }
10005
+ }
10006
+ function enableIntegration(dir, id) {
10007
+ return updateIntegration(dir, id, { enabled: true });
10008
+ }
10009
+ function disableIntegration(dir, id) {
10010
+ return updateIntegration(dir, id, { enabled: false });
10011
+ }
10012
+ async function setIntegrationSecret(dir, id, secretName, value) {
10013
+ const { integration } = requireIntegration(dir, id);
10014
+ const refKey = secretRefKey(id, secretName);
10015
+ const sp = secretProvider(dir);
10016
+ await sp.set(refKey, value);
10017
+ if (!integration.secrets[secretName] || integration.secrets[secretName] !== refKey) {
10018
+ const { integrations } = loadIntegrations(dir);
10019
+ const idx = integrations.findIndex((i) => i.id === id);
10020
+ if (idx >= 0) {
10021
+ integrations[idx].secrets[secretName] = refKey;
10022
+ saveIntegrations(dir, integrations);
10023
+ }
10024
+ }
10025
+ }
10026
+ async function removeIntegrationSecret(dir, id, secretName) {
10027
+ const { integration } = requireIntegration(dir, id);
10028
+ const refKey = integration.secrets[secretName];
10029
+ if (refKey) {
10030
+ const sp = secretProvider(dir);
10031
+ await sp.delete(refKey);
10032
+ }
10033
+ const { integrations } = loadIntegrations(dir);
10034
+ const idx = integrations.findIndex((i) => i.id === id);
10035
+ if (idx >= 0 && integrations[idx].secrets[secretName]) {
10036
+ delete integrations[idx].secrets[secretName];
10037
+ saveIntegrations(dir, integrations);
10038
+ }
10039
+ }
10040
+ async function verifyIntegration(dir, id, env = process.env) {
10041
+ const { integration, findings } = requireIntegration(dir, id);
10042
+ const cfgStatus = configStatus(integration, findings);
10043
+ if (cfgStatus === "disabled") return { id, status: "disabled", connection: null, missingCredentials: [] };
10044
+ if (cfgStatus === "invalid-config") return { id, status: "invalid-config", connection: null, missingCredentials: [] };
10045
+ const adapter = resolveAdapter(integration);
10046
+ const { context, missing } = await buildContextWithSecrets(dir, integration, env);
10047
+ try {
10048
+ const connection = await withTimeout(adapter.verifyConnection(context), context.timeoutMs);
10049
+ return { id, status: statusOf(connection), connection, missingCredentials: missing, message: connection.message };
10050
+ } catch (err) {
10051
+ const e = err instanceof IntegrationError ? err : integrationError("INTEGRATION_PROVIDER_ERROR");
10052
+ return { id, status: e.code === "INTEGRATION_UNAUTHORIZED" ? "unauthorized" : "unavailable", connection: null, missingCredentials: missing, message: e.safeMessage };
10053
+ }
10054
+ }
10055
+ function statusOf(connection) {
10056
+ switch (connection.status) {
10057
+ case "available":
10058
+ return "available";
10059
+ case "unauthorized":
10060
+ return "unauthorized";
10061
+ case "unavailable":
10062
+ return "unavailable";
10063
+ case "invalid-config":
10064
+ return "invalid-config";
10065
+ }
10066
+ }
10067
+ async function listExternalWorkItems(dir, id, opts = {}, env = process.env) {
10068
+ const { integration } = requireIntegration(dir, id);
10069
+ const adapter = resolveAdapter(integration);
10070
+ if (!adapter.capabilities.workItems.list) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot list work items.`);
10071
+ const { context } = await buildContextWithSecrets(dir, integration, env);
10072
+ return withTimeout(adapter.listWorkItems({ context, cursor: opts.cursor, pageSize: opts.pageSize, filters: opts.filters }), context.timeoutMs);
10073
+ }
10074
+ async function getExternalWorkItem(dir, id, externalId, env = process.env) {
10075
+ const { integration } = requireIntegration(dir, id);
10076
+ const adapter = resolveAdapter(integration);
10077
+ if (!adapter.capabilities.workItems.read) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot read work items.`);
10078
+ const { context } = await buildContextWithSecrets(dir, integration, env);
10079
+ return withTimeout(adapter.getWorkItem({ context, externalId }), context.timeoutMs);
10080
+ }
10081
+ function findLinkedWorkItem(dir, integrationId, externalId) {
10082
+ for (const art of discoverWorkItems(dir)) {
10083
+ let data;
10084
+ try {
10085
+ data = matter8(readFile(art.filePath)).data;
10086
+ } catch {
10087
+ continue;
10088
+ }
10089
+ const source = parseWorkItemSource(data);
10090
+ if (source.integration === integrationId && source.id === externalId) {
10091
+ return { workItemId: String(data.id ?? ""), title: String(data.title ?? "") };
10092
+ }
10093
+ }
10094
+ return null;
10095
+ }
10096
+ async function previewImport(dir, id, externalId, opts = {}, env = process.env) {
10097
+ const item = await getExternalWorkItem(dir, id, externalId, env);
10098
+ if (!item) throw integrationError("INTEGRATION_NOT_FOUND", `External work item "${externalId}" was not found.`);
10099
+ const preview = buildImportPreview(item, { integrationId: id, type: opts.type });
10100
+ return { preview, duplicate: findLinkedWorkItem(dir, id, externalId) };
10101
+ }
10102
+ async function importExternalWorkItem(dir, id, externalId, opts, env = process.env) {
10103
+ const item = await getExternalWorkItem(dir, id, externalId, env);
10104
+ if (!item) throw integrationError("INTEGRATION_NOT_FOUND", `External work item "${externalId}" was not found.`);
10105
+ const preview = buildImportPreview(item, { integrationId: id, type: opts.type });
10106
+ const existing = findLinkedWorkItem(dir, id, externalId);
10107
+ if (existing) return { workItemId: existing.workItemId, created: false, duplicateOf: existing.workItemId, preview };
10108
+ const intent = item.description ? `${item.title}
10109
+
10110
+ ${item.description}` : item.title;
10111
+ const created = createWorkItem(dir, {
10112
+ intent,
10113
+ type: opts.type,
10114
+ source: {
10115
+ type: "external",
10116
+ provider: item.provider,
10117
+ integration: id,
10118
+ id: externalId,
10119
+ url: item.url,
10120
+ imported_at: (/* @__PURE__ */ new Date()).toISOString()
10121
+ }
10122
+ });
10123
+ return { workItemId: created.id, created: true, preview, path: created.path };
10124
+ }
9369
10125
  export {
10126
+ INTEGRATIONS_FILE,
10127
+ IntegrationError,
10128
+ IntegrationServiceError,
9370
10129
  TOPOLOGY_FILE,
9371
10130
  TopologyWriteError,
9372
10131
  WorkItemNotFoundError,
@@ -9380,13 +10139,22 @@ export {
9380
10139
  buildRefinementHandoff,
9381
10140
  buildTopologyEnrichmentHandoff,
9382
10141
  computeRefinementStatus,
10142
+ createIntegration,
9383
10143
  createWorkItem,
9384
10144
  cwd,
10145
+ deleteIntegration,
10146
+ disableIntegration,
9385
10147
  discoverKnowledge,
9386
10148
  discoverWorkItems,
10149
+ enableIntegration,
9387
10150
  exists,
10151
+ findLinkedWorkItem,
9388
10152
  findSystemPaths,
10153
+ getAvailableIntegrationTypes,
10154
+ getExternalWorkItem,
9389
10155
  getImpactCandidates,
10156
+ getIntegration,
10157
+ getIntegrationSecretStatus,
9390
10158
  getSystemMapProjection,
9391
10159
  getSystemNeighbors,
9392
10160
  getSystemNodeContext,
@@ -9395,21 +10163,30 @@ export {
9395
10163
  getWorkItemForEdit,
9396
10164
  getWorkItems,
9397
10165
  getWorkItemsSummary,
10166
+ importExternalWorkItem,
10167
+ integrationRegistry,
9398
10168
  isActiveState,
9399
10169
  isModule,
9400
10170
  join,
9401
10171
  knowledgeLayers,
9402
10172
  lifecycleCounts,
9403
10173
  lifecycleStateOf,
10174
+ listExternalWorkItems,
10175
+ listIntegrations,
9404
10176
  loadConfig,
9405
10177
  loadMappedModules,
9406
10178
  loadSystemTopology,
10179
+ previewImport,
9407
10180
  readFile,
10181
+ removeIntegrationSecret,
9408
10182
  searchSystemNodes,
10183
+ setIntegrationSecret,
9409
10184
  topologyRevision,
9410
10185
  transitionWorkItem,
10186
+ updateIntegration,
9411
10187
  updateWorkItem,
9412
10188
  validateSystemTopology,
9413
10189
  validateTopologyProposal,
9414
- validateWorkItem
10190
+ validateWorkItem,
10191
+ verifyIntegration
9415
10192
  };