@kaddo/cli 3.80.0 → 3.82.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/admin-dist/assets/index-DIfpc20n.js +44 -0
- package/dist/admin-dist/assets/index-DNkI-1xG.css +2 -0
- package/dist/admin-dist/index.html +2 -2
- package/dist/admin-server/index.js +109 -3
- package/dist/core.js +520 -17
- package/dist/index.js +839 -57
- package/package.json +1 -1
- package/dist/admin-dist/assets/index-BPQdYfAp.css +0 -2
- package/dist/admin-dist/assets/index-CmcfS42m.js +0 -44
package/dist/core.js
CHANGED
|
@@ -6583,6 +6583,7 @@ function parseWorkItemSource(frontmatter) {
|
|
|
6583
6583
|
title: optStr(obj.title) ?? optStr(frontmatter.source_title),
|
|
6584
6584
|
context: optStr(obj.context) ?? optStr(frontmatter.source_context),
|
|
6585
6585
|
provider: optStr(obj.provider) ?? optStr(frontmatter.source_provider),
|
|
6586
|
+
integration: optStr(obj.integration) ?? optStr(frontmatter.source_integration),
|
|
6586
6587
|
url: optStr(obj.url) ?? optStr(frontmatter.source_url),
|
|
6587
6588
|
imported_at: optStr(obj.imported_at) ?? optStr(frontmatter.source_imported_at),
|
|
6588
6589
|
synced_at: optStr(obj.synced_at) ?? optStr(frontmatter.source_synced_at),
|
|
@@ -8122,6 +8123,25 @@ function getWorkItem(dir, workItemId) {
|
|
|
8122
8123
|
};
|
|
8123
8124
|
return { ...detail, refinement: computeRefinementStatus(detail) };
|
|
8124
8125
|
}
|
|
8126
|
+
function parseGraphReason(v) {
|
|
8127
|
+
if (!v || typeof v !== "object") return null;
|
|
8128
|
+
const o = v;
|
|
8129
|
+
const relationship = typeof o.relationship === "string" && o.relationship.trim() ? o.relationship.trim() : null;
|
|
8130
|
+
const path4 = Array.isArray(o.path) ? o.path.map(String).filter(Boolean) : [];
|
|
8131
|
+
return relationship || path4.length ? { relationship, path: path4 } : null;
|
|
8132
|
+
}
|
|
8133
|
+
function parseExplain(o) {
|
|
8134
|
+
const refs = Array.isArray(o.evidence) ? o.evidence : Array.isArray(o.evidence_refs) ? o.evidence_refs : [];
|
|
8135
|
+
return {
|
|
8136
|
+
reason: typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : null,
|
|
8137
|
+
graphReason: parseGraphReason(o.graph_reason),
|
|
8138
|
+
evidenceRefs: refs.map(String).filter(Boolean),
|
|
8139
|
+
evidenceSummary: typeof o.evidence_summary === "string" && o.evidence_summary.trim() ? o.evidence_summary.trim() : null
|
|
8140
|
+
};
|
|
8141
|
+
}
|
|
8142
|
+
function emptyExplain() {
|
|
8143
|
+
return { reason: null, graphReason: null, evidenceRefs: [], evidenceSummary: null };
|
|
8144
|
+
}
|
|
8125
8145
|
function parseSystemImpact(dir, fm) {
|
|
8126
8146
|
const topology = loadSystemTopology(dir);
|
|
8127
8147
|
const byId = new Map(topology.entities.map((e) => [e.id, e]));
|
|
@@ -8129,10 +8149,19 @@ function parseSystemImpact(dir, fm) {
|
|
|
8129
8149
|
const e = byId.get(id);
|
|
8130
8150
|
return { id, nodeId: `sys:${id}`, label: e?.label ?? id, kind: e?.kind ?? "unknown", moduleId: e?.moduleId ?? null };
|
|
8131
8151
|
};
|
|
8132
|
-
const affected = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map(
|
|
8133
|
-
|
|
8152
|
+
const affected = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map((raw) => {
|
|
8153
|
+
if (typeof raw === "string") return raw ? { ...resolve(raw), ...emptyExplain() } : null;
|
|
8154
|
+
if (raw && typeof raw === "object") {
|
|
8155
|
+
const o = raw;
|
|
8156
|
+
const id = String(o.id ?? "");
|
|
8157
|
+
return id ? { ...resolve(id), ...parseExplain(o) } : null;
|
|
8158
|
+
}
|
|
8159
|
+
return null;
|
|
8160
|
+
}).filter((e) => e != null) : [];
|
|
8161
|
+
const reviewed = Array.isArray(fm.reviewed_system_entities) ? fm.reviewed_system_entities.filter((r) => Boolean(r) && typeof r === "object").map((r) => ({ ...resolve(String(r.id ?? "")), ...parseExplain(r), status: String(r.status ?? "unknown") })).filter((r) => r.id) : [];
|
|
8134
8162
|
const graphRevision = typeof fm.graph_revision === "string" && fm.graph_revision.trim() ? fm.graph_revision.trim() : null;
|
|
8135
|
-
|
|
8163
|
+
const graphCoverage = topology.entities.length === 0 ? "unavailable" : topology.relationships.length > 0 ? "available" : "partial";
|
|
8164
|
+
return { affectedSystemEntities: affected, reviewedSystemEntities: reviewed, graphRevision, graphCoverage };
|
|
8136
8165
|
}
|
|
8137
8166
|
function readBody(filePath) {
|
|
8138
8167
|
try {
|
|
@@ -8806,6 +8835,7 @@ function createWorkItem(dir, opts) {
|
|
|
8806
8835
|
const id = nextWorkItemId(dir);
|
|
8807
8836
|
const title = intent.split(/\r?\n/)[0].trim().slice(0, 120);
|
|
8808
8837
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8838
|
+
const source = opts.source ? { ...opts.source, inferred: false } : { type: "manual", inferred: false };
|
|
8809
8839
|
const data = {
|
|
8810
8840
|
type,
|
|
8811
8841
|
id,
|
|
@@ -8813,7 +8843,7 @@ function createWorkItem(dir, opts) {
|
|
|
8813
8843
|
status: "draft",
|
|
8814
8844
|
work_type: type,
|
|
8815
8845
|
created_at: today,
|
|
8816
|
-
source
|
|
8846
|
+
source,
|
|
8817
8847
|
generated_by: "kaddo-admin",
|
|
8818
8848
|
affected_modules: [],
|
|
8819
8849
|
summary: intent
|
|
@@ -8890,7 +8920,8 @@ function validateWorkItem(dir, id) {
|
|
|
8890
8920
|
const topology = loadSystemTopology(dir);
|
|
8891
8921
|
const entityById = new Map(topology.entities.map((e) => [e.id, e]));
|
|
8892
8922
|
const fm = data;
|
|
8893
|
-
const
|
|
8923
|
+
const idOf = (raw2) => typeof raw2 === "string" ? raw2 : raw2 && typeof raw2 === "object" ? String(raw2.id ?? "") : "";
|
|
8924
|
+
const affectedEntities = Array.isArray(fm.affected_system_entities) ? fm.affected_system_entities.map(idOf).filter(Boolean) : [];
|
|
8894
8925
|
for (const eid of affectedEntities) {
|
|
8895
8926
|
const e = entityById.get(eid);
|
|
8896
8927
|
if (!e) {
|
|
@@ -9276,11 +9307,12 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
9276
9307
|
const lines = [
|
|
9277
9308
|
`Refine Work Item ${wi.id} \u2014 "${wi.title}" \u2014 in project "${projectName}" using Kaddo.`,
|
|
9278
9309
|
"",
|
|
9279
|
-
|
|
9280
|
-
|
|
9310
|
+
`Use the canonical ${RECOMMENDED_AGENT} and the ${RECOMMENDED_SKILL} skill (via Kaddo MCP or skills),`,
|
|
9311
|
+
"with access to this repository.",
|
|
9281
9312
|
"",
|
|
9282
|
-
"Inspect the actual implementation
|
|
9283
|
-
"the Work Item title. Read the current behavior in the code first,
|
|
9313
|
+
"Inspect the actual implementation and the relevant mapped modules before defining the final scope \u2014",
|
|
9314
|
+
"do not guess affected modules from the Work Item title. Read the current behavior in the code first,",
|
|
9315
|
+
"then classify."
|
|
9284
9316
|
];
|
|
9285
9317
|
if (multirepo) {
|
|
9286
9318
|
lines.push(
|
|
@@ -9293,15 +9325,23 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
9293
9325
|
if (topology !== "unavailable") {
|
|
9294
9326
|
lines.push(
|
|
9295
9327
|
"",
|
|
9296
|
-
`The semantic system Graph is ${topology}.
|
|
9297
|
-
"
|
|
9298
|
-
"
|
|
9299
|
-
"
|
|
9300
|
-
"
|
|
9301
|
-
"
|
|
9328
|
+
`The semantic system Graph is ${topology}. When semantic system topology is available:`,
|
|
9329
|
+
"1. identify the relevant system entry points;",
|
|
9330
|
+
"2. query the Kaddo Graph (search / neighbors / paths) for connected entities;",
|
|
9331
|
+
"3. treat Graph results as IMPACT CANDIDATES, not confirmed scope;",
|
|
9332
|
+
"4. inspect the actual implementation in the repository for each relevant candidate;",
|
|
9333
|
+
"5. classify each candidate as affected, reviewed-not-affected or unknown;",
|
|
9334
|
+
"6. preserve the evidence and reasons behind each classification.",
|
|
9335
|
+
"",
|
|
9336
|
+
"A missing Graph relationship does not mean no impact, especially when Graph coverage is partial \u2014",
|
|
9337
|
+
"keep inspecting the repository beyond the Graph candidates when the task requires it."
|
|
9302
9338
|
);
|
|
9303
9339
|
} else {
|
|
9304
|
-
lines.push(
|
|
9340
|
+
lines.push(
|
|
9341
|
+
"",
|
|
9342
|
+
"The semantic system Graph is unavailable. Continue repository-driven refinement normally, using the",
|
|
9343
|
+
"repository, Knowledge and mapped modules \u2014 the Graph is enrichment, not a prerequisite."
|
|
9344
|
+
);
|
|
9305
9345
|
}
|
|
9306
9346
|
lines.push(
|
|
9307
9347
|
"",
|
|
@@ -9328,7 +9368,462 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
9328
9368
|
text: lines.join("\n")
|
|
9329
9369
|
};
|
|
9330
9370
|
}
|
|
9371
|
+
|
|
9372
|
+
// src/services/integrations.ts
|
|
9373
|
+
import matter8 from "gray-matter";
|
|
9374
|
+
import { parse as parseYaml9 } from "yaml";
|
|
9375
|
+
|
|
9376
|
+
// ../integrations/src/errors.ts
|
|
9377
|
+
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
9378
|
+
"INTEGRATION_RATE_LIMITED",
|
|
9379
|
+
"INTEGRATION_UNAVAILABLE",
|
|
9380
|
+
"INTEGRATION_TIMEOUT"
|
|
9381
|
+
]);
|
|
9382
|
+
var IntegrationError = class extends Error {
|
|
9383
|
+
code;
|
|
9384
|
+
/** True for transient conditions (rate limit / unavailable / timeout). */
|
|
9385
|
+
retryable;
|
|
9386
|
+
/** A safe, provider-agnostic reason. Never the raw SDK message. */
|
|
9387
|
+
safeMessage;
|
|
9388
|
+
constructor(code, message) {
|
|
9389
|
+
super(message);
|
|
9390
|
+
this.name = "IntegrationError";
|
|
9391
|
+
this.code = code;
|
|
9392
|
+
this.retryable = RETRYABLE.has(code);
|
|
9393
|
+
this.safeMessage = message;
|
|
9394
|
+
}
|
|
9395
|
+
};
|
|
9396
|
+
function defaultMessageFor(code) {
|
|
9397
|
+
switch (code) {
|
|
9398
|
+
case "INTEGRATION_CONFIG_INVALID":
|
|
9399
|
+
return "The integration configuration is invalid.";
|
|
9400
|
+
case "INTEGRATION_UNAUTHORIZED":
|
|
9401
|
+
return "Authentication failed. Check the configured credentials.";
|
|
9402
|
+
case "INTEGRATION_FORBIDDEN":
|
|
9403
|
+
return "The configured credentials lack permission for this operation.";
|
|
9404
|
+
case "INTEGRATION_NOT_FOUND":
|
|
9405
|
+
return "The requested external resource was not found.";
|
|
9406
|
+
case "INTEGRATION_RATE_LIMITED":
|
|
9407
|
+
return "The external provider is rate limiting requests. Try again later.";
|
|
9408
|
+
case "INTEGRATION_UNAVAILABLE":
|
|
9409
|
+
return "The external provider is temporarily unavailable.";
|
|
9410
|
+
case "INTEGRATION_TIMEOUT":
|
|
9411
|
+
return "The external provider did not respond in time.";
|
|
9412
|
+
case "INTEGRATION_PROVIDER_ERROR":
|
|
9413
|
+
return "The external provider returned an error.";
|
|
9414
|
+
case "UNSUPPORTED_CAPABILITY":
|
|
9415
|
+
return "This adapter does not support the requested capability.";
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
function integrationError(code, message) {
|
|
9419
|
+
return new IntegrationError(code, message ?? defaultMessageFor(code));
|
|
9420
|
+
}
|
|
9421
|
+
|
|
9422
|
+
// ../integrations/src/identity.ts
|
|
9423
|
+
function externalIdentityKey(integrationId, externalId) {
|
|
9424
|
+
return `${integrationId}#${externalId}`;
|
|
9425
|
+
}
|
|
9426
|
+
function externalDisplayKey(provider, externalId) {
|
|
9427
|
+
return `${provider}#${externalId}`;
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
// ../integrations/src/config.ts
|
|
9431
|
+
var ENV_SUFFIX = "_env";
|
|
9432
|
+
var SECRET_KEY = /(token|secret|password|key|pat|apikey|api_key)/i;
|
|
9433
|
+
function parseCredentials(raw, id, findings) {
|
|
9434
|
+
const creds = {};
|
|
9435
|
+
if (raw == null) return creds;
|
|
9436
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
9437
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}": credentials must be a mapping of secret references.` });
|
|
9438
|
+
return creds;
|
|
9439
|
+
}
|
|
9440
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
9441
|
+
if (key.endsWith(ENV_SUFFIX)) {
|
|
9442
|
+
const name = key.slice(0, -ENV_SUFFIX.length);
|
|
9443
|
+
if (typeof value === "string" && value.trim()) creds[name] = { env: value.trim() };
|
|
9444
|
+
else findings.push({ level: "blocking", id, message: `Integration "${id}": credential "${key}" must name an environment variable.` });
|
|
9445
|
+
continue;
|
|
9446
|
+
}
|
|
9447
|
+
if (SECRET_KEY.test(key) && typeof value === "string") {
|
|
9448
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}": secret "${key}" must not be stored in config. Use "${key}${ENV_SUFFIX}: <ENV_VAR_NAME>".` });
|
|
9449
|
+
continue;
|
|
9450
|
+
}
|
|
9451
|
+
if (value && typeof value === "object" && typeof value.env === "string") {
|
|
9452
|
+
creds[key] = { env: String(value.env) };
|
|
9453
|
+
continue;
|
|
9454
|
+
}
|
|
9455
|
+
findings.push({ level: "warning", id, message: `Integration "${id}": ignoring unrecognized credential entry "${key}".` });
|
|
9456
|
+
}
|
|
9457
|
+
return creds;
|
|
9458
|
+
}
|
|
9459
|
+
function parseIntegrationsConfig(raw, opts) {
|
|
9460
|
+
const findings = [];
|
|
9461
|
+
const integrations = [];
|
|
9462
|
+
const list2 = raw && typeof raw === "object" && Array.isArray(raw.integrations) ? raw.integrations : Array.isArray(raw) ? raw : [];
|
|
9463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9464
|
+
for (const entry of list2) {
|
|
9465
|
+
if (!entry || typeof entry !== "object") {
|
|
9466
|
+
findings.push({ level: "blocking", message: "Each integration must be a mapping." });
|
|
9467
|
+
continue;
|
|
9468
|
+
}
|
|
9469
|
+
const o = entry;
|
|
9470
|
+
const id = typeof o.id === "string" ? o.id.trim() : "";
|
|
9471
|
+
const adapter = typeof o.adapter === "string" ? o.adapter.trim() : "";
|
|
9472
|
+
if (!id) {
|
|
9473
|
+
findings.push({ level: "blocking", message: 'An integration is missing a required "id".' });
|
|
9474
|
+
continue;
|
|
9475
|
+
}
|
|
9476
|
+
if (seen.has(id)) {
|
|
9477
|
+
findings.push({ level: "blocking", id, message: `Duplicate integration id "${id}".` });
|
|
9478
|
+
continue;
|
|
9479
|
+
}
|
|
9480
|
+
seen.add(id);
|
|
9481
|
+
if (!adapter) {
|
|
9482
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}" is missing a required "adapter".` });
|
|
9483
|
+
continue;
|
|
9484
|
+
}
|
|
9485
|
+
if (!opts.adapterIds.has(adapter)) {
|
|
9486
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}" references unknown adapter "${adapter}".` });
|
|
9487
|
+
}
|
|
9488
|
+
const enabled = o.enabled === void 0 ? true : o.enabled === true || o.enabled === "true";
|
|
9489
|
+
const config = o.config && typeof o.config === "object" && !Array.isArray(o.config) ? o.config : {};
|
|
9490
|
+
const credentials = parseCredentials(o.credentials, id, findings);
|
|
9491
|
+
const timeoutMs = typeof o.timeout_ms === "number" ? o.timeout_ms : typeof o.timeoutMs === "number" ? o.timeoutMs : void 0;
|
|
9492
|
+
integrations.push({ id, adapter, enabled, config, credentials, timeoutMs });
|
|
9493
|
+
}
|
|
9494
|
+
return { integrations, findings };
|
|
9495
|
+
}
|
|
9496
|
+
function resolveCredentials(integration, env) {
|
|
9497
|
+
const credentials = {};
|
|
9498
|
+
const missing = [];
|
|
9499
|
+
for (const [name, ref] of Object.entries(integration.credentials)) {
|
|
9500
|
+
const value = env[ref.env];
|
|
9501
|
+
if (value && value.length > 0) credentials[name] = value;
|
|
9502
|
+
else missing.push(ref.env);
|
|
9503
|
+
}
|
|
9504
|
+
return { credentials, missing };
|
|
9505
|
+
}
|
|
9506
|
+
|
|
9507
|
+
// ../integrations/src/preview.ts
|
|
9508
|
+
function buildImportPreview(item, opts) {
|
|
9509
|
+
return {
|
|
9510
|
+
source: {
|
|
9511
|
+
provider: item.provider,
|
|
9512
|
+
integration: opts.integrationId,
|
|
9513
|
+
externalId: item.externalId,
|
|
9514
|
+
url: item.url,
|
|
9515
|
+
identityKey: externalIdentityKey(opts.integrationId, item.externalId),
|
|
9516
|
+
displayKey: externalDisplayKey(item.provider, item.externalId)
|
|
9517
|
+
},
|
|
9518
|
+
capturedIntent: item.title,
|
|
9519
|
+
description: item.description,
|
|
9520
|
+
externalType: item.type,
|
|
9521
|
+
externalStatus: item.status,
|
|
9522
|
+
kaddoStatus: "draft",
|
|
9523
|
+
kaddoType: opts.type ? opts.type : null,
|
|
9524
|
+
writes: false
|
|
9525
|
+
};
|
|
9526
|
+
}
|
|
9527
|
+
|
|
9528
|
+
// ../integrations/src/registry.ts
|
|
9529
|
+
var DuplicateAdapterError = class extends Error {
|
|
9530
|
+
constructor(id) {
|
|
9531
|
+
super(`An integration adapter with id "${id}" is already registered.`);
|
|
9532
|
+
this.name = "DuplicateAdapterError";
|
|
9533
|
+
}
|
|
9534
|
+
};
|
|
9535
|
+
var IntegrationRegistry = class {
|
|
9536
|
+
adapters = /* @__PURE__ */ new Map();
|
|
9537
|
+
register(adapter) {
|
|
9538
|
+
if (this.adapters.has(adapter.id)) throw new DuplicateAdapterError(adapter.id);
|
|
9539
|
+
this.adapters.set(adapter.id, adapter);
|
|
9540
|
+
}
|
|
9541
|
+
get(id) {
|
|
9542
|
+
return this.adapters.get(id);
|
|
9543
|
+
}
|
|
9544
|
+
has(id) {
|
|
9545
|
+
return this.adapters.has(id);
|
|
9546
|
+
}
|
|
9547
|
+
list() {
|
|
9548
|
+
return [...this.adapters.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
9549
|
+
}
|
|
9550
|
+
ids() {
|
|
9551
|
+
return new Set(this.adapters.keys());
|
|
9552
|
+
}
|
|
9553
|
+
};
|
|
9554
|
+
|
|
9555
|
+
// ../integrations/src/mock-adapter.ts
|
|
9556
|
+
var MOCK_ADAPTER_ID = "mock";
|
|
9557
|
+
var DEFAULT_ITEMS = [
|
|
9558
|
+
{
|
|
9559
|
+
externalId: "EXT-001",
|
|
9560
|
+
provider: MOCK_ADAPTER_ID,
|
|
9561
|
+
title: "Open public registration to everyone",
|
|
9562
|
+
description: "Open self-service registration to the public once the private beta ends.",
|
|
9563
|
+
type: "Feature",
|
|
9564
|
+
status: "Open",
|
|
9565
|
+
url: "https://example.test/mock/EXT-001",
|
|
9566
|
+
labels: ["registration", "beta"],
|
|
9567
|
+
createdAt: "2026-01-05T10:00:00.000Z",
|
|
9568
|
+
updatedAt: "2026-02-01T09:30:00.000Z",
|
|
9569
|
+
rawMetadata: { board: "delivery" }
|
|
9570
|
+
},
|
|
9571
|
+
{
|
|
9572
|
+
externalId: "EXT-002",
|
|
9573
|
+
provider: MOCK_ADAPTER_ID,
|
|
9574
|
+
title: "Personalize onboarding steps",
|
|
9575
|
+
description: "The onboarding checklist should adapt to what the user has already completed.",
|
|
9576
|
+
type: "Task",
|
|
9577
|
+
status: "To Do",
|
|
9578
|
+
url: "https://example.test/mock/EXT-002",
|
|
9579
|
+
labels: ["onboarding"],
|
|
9580
|
+
createdAt: "2026-01-08T12:00:00.000Z",
|
|
9581
|
+
updatedAt: "2026-01-20T15:00:00.000Z"
|
|
9582
|
+
}
|
|
9583
|
+
];
|
|
9584
|
+
function simulationOf(context, fallback) {
|
|
9585
|
+
const fromConfig = context.config?.simulate;
|
|
9586
|
+
return typeof fromConfig === "string" ? fromConfig : fallback;
|
|
9587
|
+
}
|
|
9588
|
+
function readFailure(sim) {
|
|
9589
|
+
switch (sim) {
|
|
9590
|
+
case "unauthorized":
|
|
9591
|
+
throw integrationError("INTEGRATION_UNAUTHORIZED");
|
|
9592
|
+
case "rate-limited":
|
|
9593
|
+
throw integrationError("INTEGRATION_RATE_LIMITED");
|
|
9594
|
+
case "unavailable":
|
|
9595
|
+
throw integrationError("INTEGRATION_UNAVAILABLE");
|
|
9596
|
+
case "timeout":
|
|
9597
|
+
throw integrationError("INTEGRATION_TIMEOUT");
|
|
9598
|
+
case "available":
|
|
9599
|
+
break;
|
|
9600
|
+
}
|
|
9601
|
+
}
|
|
9602
|
+
function createMockAdapter(opts = {}) {
|
|
9603
|
+
const items = opts.items ?? DEFAULT_ITEMS;
|
|
9604
|
+
const defaultSim = opts.simulate ?? "available";
|
|
9605
|
+
const defaultPageSize = opts.pageSize ?? 50;
|
|
9606
|
+
return {
|
|
9607
|
+
id: MOCK_ADAPTER_ID,
|
|
9608
|
+
metadata: {
|
|
9609
|
+
id: MOCK_ADAPTER_ID,
|
|
9610
|
+
displayName: "Mock Work Source",
|
|
9611
|
+
version: "1.0.0",
|
|
9612
|
+
description: "Deterministic offline reference adapter for validating the integration foundation."
|
|
9613
|
+
},
|
|
9614
|
+
capabilities: {
|
|
9615
|
+
workItems: { list: true, read: true, import: true, write: false, statusSync: false, comments: false, webhooks: false }
|
|
9616
|
+
},
|
|
9617
|
+
async verifyConnection(context) {
|
|
9618
|
+
const sim = simulationOf(context, defaultSim);
|
|
9619
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9620
|
+
switch (sim) {
|
|
9621
|
+
case "available":
|
|
9622
|
+
return { status: "available", checkedAt };
|
|
9623
|
+
case "unauthorized":
|
|
9624
|
+
return { status: "unauthorized", message: "Check the configured credentials.", checkedAt };
|
|
9625
|
+
case "rate-limited":
|
|
9626
|
+
case "unavailable":
|
|
9627
|
+
case "timeout":
|
|
9628
|
+
return { status: "unavailable", message: "The mock provider is temporarily unavailable.", checkedAt };
|
|
9629
|
+
}
|
|
9630
|
+
},
|
|
9631
|
+
async listWorkItems(request) {
|
|
9632
|
+
readFailure(simulationOf(request.context, defaultSim));
|
|
9633
|
+
let pool = items;
|
|
9634
|
+
const f = request.filters;
|
|
9635
|
+
if (f?.status) pool = pool.filter((i) => (i.status ?? "").toLowerCase() === f.status.toLowerCase());
|
|
9636
|
+
if (f?.query) pool = pool.filter((i) => `${i.title} ${i.description ?? ""}`.toLowerCase().includes(f.query.toLowerCase()));
|
|
9637
|
+
if (f?.updatedSince) pool = pool.filter((i) => (i.updatedAt ?? "") >= f.updatedSince);
|
|
9638
|
+
const size = Math.max(1, request.pageSize ?? defaultPageSize);
|
|
9639
|
+
const start = request.cursor ? Math.max(0, Number.parseInt(request.cursor, 10) || 0) : 0;
|
|
9640
|
+
const slice = pool.slice(start, start + size);
|
|
9641
|
+
const end = start + slice.length;
|
|
9642
|
+
const hasMore = end < pool.length;
|
|
9643
|
+
return { items: slice, hasMore, ...hasMore ? { nextCursor: String(end) } : {} };
|
|
9644
|
+
},
|
|
9645
|
+
async getWorkItem(request) {
|
|
9646
|
+
readFailure(simulationOf(request.context, defaultSim));
|
|
9647
|
+
return items.find((i) => i.externalId === request.externalId) ?? null;
|
|
9648
|
+
}
|
|
9649
|
+
};
|
|
9650
|
+
}
|
|
9651
|
+
|
|
9652
|
+
// ../integrations/src/index.ts
|
|
9653
|
+
function createDefaultRegistry() {
|
|
9654
|
+
const registry2 = new IntegrationRegistry();
|
|
9655
|
+
registry2.register(createMockAdapter());
|
|
9656
|
+
return registry2;
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9659
|
+
// src/services/integrations.ts
|
|
9660
|
+
var INTEGRATIONS_FILE = ".kaddo/integrations.yml";
|
|
9661
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
9662
|
+
var registry = createDefaultRegistry();
|
|
9663
|
+
function integrationRegistry() {
|
|
9664
|
+
return registry;
|
|
9665
|
+
}
|
|
9666
|
+
var IntegrationServiceError = class extends Error {
|
|
9667
|
+
code;
|
|
9668
|
+
constructor(code, message) {
|
|
9669
|
+
super(message);
|
|
9670
|
+
this.name = "IntegrationServiceError";
|
|
9671
|
+
this.code = code;
|
|
9672
|
+
}
|
|
9673
|
+
};
|
|
9674
|
+
function loadRaw(dir) {
|
|
9675
|
+
const abs = join(dir, INTEGRATIONS_FILE);
|
|
9676
|
+
if (!exists(abs)) return { integrations: [] };
|
|
9677
|
+
try {
|
|
9678
|
+
return parseYaml9(readFile(abs)) ?? { integrations: [] };
|
|
9679
|
+
} catch {
|
|
9680
|
+
return { integrations: [] };
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
function loadIntegrations(dir) {
|
|
9684
|
+
return parseIntegrationsConfig(loadRaw(dir), { adapterIds: registry.ids() });
|
|
9685
|
+
}
|
|
9686
|
+
function configStatus(integration, findings) {
|
|
9687
|
+
if (!integration.enabled) return "disabled";
|
|
9688
|
+
const blocking = findings.some((f) => f.id === integration.id && f.level === "blocking");
|
|
9689
|
+
if (blocking || !registry.has(integration.adapter)) return "invalid-config";
|
|
9690
|
+
return "configured";
|
|
9691
|
+
}
|
|
9692
|
+
function listIntegrations(dir) {
|
|
9693
|
+
const { integrations, findings } = loadIntegrations(dir);
|
|
9694
|
+
return integrations.map((integration) => {
|
|
9695
|
+
const adapter = registry.get(integration.adapter);
|
|
9696
|
+
return {
|
|
9697
|
+
id: integration.id,
|
|
9698
|
+
adapter: integration.adapter,
|
|
9699
|
+
enabled: integration.enabled,
|
|
9700
|
+
status: configStatus(integration, findings),
|
|
9701
|
+
displayName: adapter?.metadata.displayName ?? integration.adapter,
|
|
9702
|
+
capabilities: adapter?.capabilities ?? null,
|
|
9703
|
+
metadata: adapter?.metadata ?? null,
|
|
9704
|
+
credentialRefs: Object.values(integration.credentials).map((c) => c.env),
|
|
9705
|
+
findings: findings.filter((f) => f.id === integration.id)
|
|
9706
|
+
};
|
|
9707
|
+
});
|
|
9708
|
+
}
|
|
9709
|
+
function requireIntegration(dir, id) {
|
|
9710
|
+
const { integrations, findings } = loadIntegrations(dir);
|
|
9711
|
+
const integration = integrations.find((i) => i.id === id);
|
|
9712
|
+
if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured in this project.`);
|
|
9713
|
+
return { integration, findings };
|
|
9714
|
+
}
|
|
9715
|
+
function resolveAdapter(integration) {
|
|
9716
|
+
const adapter = registry.get(integration.adapter);
|
|
9717
|
+
if (!adapter) throw new IntegrationServiceError("ADAPTER_NOT_FOUND", `Unknown integration adapter "${integration.adapter}".`);
|
|
9718
|
+
return adapter;
|
|
9719
|
+
}
|
|
9720
|
+
function buildContext(integration, env) {
|
|
9721
|
+
const { credentials, missing } = resolveCredentials(integration, env);
|
|
9722
|
+
return {
|
|
9723
|
+
context: { integrationId: integration.id, config: integration.config, credentials, timeoutMs: integration.timeoutMs ?? DEFAULT_TIMEOUT_MS },
|
|
9724
|
+
missing
|
|
9725
|
+
};
|
|
9726
|
+
}
|
|
9727
|
+
async function withTimeout(op, ms) {
|
|
9728
|
+
let timer;
|
|
9729
|
+
const timeout = new Promise((_, reject) => {
|
|
9730
|
+
timer = setTimeout(() => reject(integrationError("INTEGRATION_TIMEOUT")), ms);
|
|
9731
|
+
});
|
|
9732
|
+
try {
|
|
9733
|
+
return await Promise.race([op, timeout]);
|
|
9734
|
+
} finally {
|
|
9735
|
+
if (timer) clearTimeout(timer);
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
async function verifyIntegration(dir, id, env = process.env) {
|
|
9739
|
+
const { integration, findings } = requireIntegration(dir, id);
|
|
9740
|
+
const cfgStatus = configStatus(integration, findings);
|
|
9741
|
+
if (cfgStatus === "disabled") return { id, status: "disabled", connection: null, missingCredentials: [] };
|
|
9742
|
+
if (cfgStatus === "invalid-config") return { id, status: "invalid-config", connection: null, missingCredentials: [] };
|
|
9743
|
+
const adapter = resolveAdapter(integration);
|
|
9744
|
+
const { context, missing } = buildContext(integration, env);
|
|
9745
|
+
try {
|
|
9746
|
+
const connection = await withTimeout(adapter.verifyConnection(context), context.timeoutMs);
|
|
9747
|
+
return { id, status: statusOf(connection), connection, missingCredentials: missing, message: connection.message };
|
|
9748
|
+
} catch (err) {
|
|
9749
|
+
const e = err instanceof IntegrationError ? err : integrationError("INTEGRATION_PROVIDER_ERROR");
|
|
9750
|
+
return { id, status: e.code === "INTEGRATION_UNAUTHORIZED" ? "unauthorized" : "unavailable", connection: null, missingCredentials: missing, message: e.safeMessage };
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
function statusOf(connection) {
|
|
9754
|
+
switch (connection.status) {
|
|
9755
|
+
case "available":
|
|
9756
|
+
return "available";
|
|
9757
|
+
case "unauthorized":
|
|
9758
|
+
return "unauthorized";
|
|
9759
|
+
case "unavailable":
|
|
9760
|
+
return "unavailable";
|
|
9761
|
+
case "invalid-config":
|
|
9762
|
+
return "invalid-config";
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9765
|
+
async function listExternalWorkItems(dir, id, opts = {}, env = process.env) {
|
|
9766
|
+
const { integration } = requireIntegration(dir, id);
|
|
9767
|
+
const adapter = resolveAdapter(integration);
|
|
9768
|
+
if (!adapter.capabilities.workItems.list) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot list work items.`);
|
|
9769
|
+
const { context } = buildContext(integration, env);
|
|
9770
|
+
return withTimeout(adapter.listWorkItems({ context, cursor: opts.cursor, pageSize: opts.pageSize, filters: opts.filters }), context.timeoutMs);
|
|
9771
|
+
}
|
|
9772
|
+
async function getExternalWorkItem(dir, id, externalId, env = process.env) {
|
|
9773
|
+
const { integration } = requireIntegration(dir, id);
|
|
9774
|
+
const adapter = resolveAdapter(integration);
|
|
9775
|
+
if (!adapter.capabilities.workItems.read) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot read work items.`);
|
|
9776
|
+
const { context } = buildContext(integration, env);
|
|
9777
|
+
return withTimeout(adapter.getWorkItem({ context, externalId }), context.timeoutMs);
|
|
9778
|
+
}
|
|
9779
|
+
function findLinkedWorkItem(dir, integrationId, externalId) {
|
|
9780
|
+
for (const art of discoverWorkItems(dir)) {
|
|
9781
|
+
let data;
|
|
9782
|
+
try {
|
|
9783
|
+
data = matter8(readFile(art.filePath)).data;
|
|
9784
|
+
} catch {
|
|
9785
|
+
continue;
|
|
9786
|
+
}
|
|
9787
|
+
const source = parseWorkItemSource(data);
|
|
9788
|
+
if (source.integration === integrationId && source.id === externalId) {
|
|
9789
|
+
return { workItemId: String(data.id ?? ""), title: String(data.title ?? "") };
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
return null;
|
|
9793
|
+
}
|
|
9794
|
+
async function previewImport(dir, id, externalId, opts = {}, env = process.env) {
|
|
9795
|
+
const item = await getExternalWorkItem(dir, id, externalId, env);
|
|
9796
|
+
if (!item) throw integrationError("INTEGRATION_NOT_FOUND", `External work item "${externalId}" was not found.`);
|
|
9797
|
+
const preview = buildImportPreview(item, { integrationId: id, type: opts.type });
|
|
9798
|
+
return { preview, duplicate: findLinkedWorkItem(dir, id, externalId) };
|
|
9799
|
+
}
|
|
9800
|
+
async function importExternalWorkItem(dir, id, externalId, opts, env = process.env) {
|
|
9801
|
+
const item = await getExternalWorkItem(dir, id, externalId, env);
|
|
9802
|
+
if (!item) throw integrationError("INTEGRATION_NOT_FOUND", `External work item "${externalId}" was not found.`);
|
|
9803
|
+
const preview = buildImportPreview(item, { integrationId: id, type: opts.type });
|
|
9804
|
+
const existing = findLinkedWorkItem(dir, id, externalId);
|
|
9805
|
+
if (existing) return { workItemId: existing.workItemId, created: false, duplicateOf: existing.workItemId, preview };
|
|
9806
|
+
const intent = item.description ? `${item.title}
|
|
9807
|
+
|
|
9808
|
+
${item.description}` : item.title;
|
|
9809
|
+
const created = createWorkItem(dir, {
|
|
9810
|
+
intent,
|
|
9811
|
+
type: opts.type,
|
|
9812
|
+
source: {
|
|
9813
|
+
type: "external",
|
|
9814
|
+
provider: item.provider,
|
|
9815
|
+
integration: id,
|
|
9816
|
+
id: externalId,
|
|
9817
|
+
url: item.url,
|
|
9818
|
+
imported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9819
|
+
}
|
|
9820
|
+
});
|
|
9821
|
+
return { workItemId: created.id, created: true, preview, path: created.path };
|
|
9822
|
+
}
|
|
9331
9823
|
export {
|
|
9824
|
+
INTEGRATIONS_FILE,
|
|
9825
|
+
IntegrationError,
|
|
9826
|
+
IntegrationServiceError,
|
|
9332
9827
|
TOPOLOGY_FILE,
|
|
9333
9828
|
TopologyWriteError,
|
|
9334
9829
|
WorkItemNotFoundError,
|
|
@@ -9347,7 +9842,9 @@ export {
|
|
|
9347
9842
|
discoverKnowledge,
|
|
9348
9843
|
discoverWorkItems,
|
|
9349
9844
|
exists,
|
|
9845
|
+
findLinkedWorkItem,
|
|
9350
9846
|
findSystemPaths,
|
|
9847
|
+
getExternalWorkItem,
|
|
9351
9848
|
getImpactCandidates,
|
|
9352
9849
|
getSystemMapProjection,
|
|
9353
9850
|
getSystemNeighbors,
|
|
@@ -9357,15 +9854,20 @@ export {
|
|
|
9357
9854
|
getWorkItemForEdit,
|
|
9358
9855
|
getWorkItems,
|
|
9359
9856
|
getWorkItemsSummary,
|
|
9857
|
+
importExternalWorkItem,
|
|
9858
|
+
integrationRegistry,
|
|
9360
9859
|
isActiveState,
|
|
9361
9860
|
isModule,
|
|
9362
9861
|
join,
|
|
9363
9862
|
knowledgeLayers,
|
|
9364
9863
|
lifecycleCounts,
|
|
9365
9864
|
lifecycleStateOf,
|
|
9865
|
+
listExternalWorkItems,
|
|
9866
|
+
listIntegrations,
|
|
9366
9867
|
loadConfig,
|
|
9367
9868
|
loadMappedModules,
|
|
9368
9869
|
loadSystemTopology,
|
|
9870
|
+
previewImport,
|
|
9369
9871
|
readFile,
|
|
9370
9872
|
searchSystemNodes,
|
|
9371
9873
|
topologyRevision,
|
|
@@ -9373,5 +9875,6 @@ export {
|
|
|
9373
9875
|
updateWorkItem,
|
|
9374
9876
|
validateSystemTopology,
|
|
9375
9877
|
validateTopologyProposal,
|
|
9376
|
-
validateWorkItem
|
|
9878
|
+
validateWorkItem,
|
|
9879
|
+
verifyIntegration
|
|
9377
9880
|
};
|