@kaddo/cli 3.81.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.
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>admin</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DIfpc20n.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DNkI-1xG.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -70,6 +70,14 @@ import {
|
|
|
70
70
|
buildRefinementHandoff as coreBuildRefinementHandoff,
|
|
71
71
|
getSystemMapProjection as coreGetSystemMapProjection,
|
|
72
72
|
buildTopologyEnrichmentHandoff as coreBuildTopologyHandoff,
|
|
73
|
+
listIntegrations as coreListIntegrations,
|
|
74
|
+
verifyIntegration as coreVerifyIntegration,
|
|
75
|
+
listExternalWorkItems as coreListExternalWorkItems,
|
|
76
|
+
getExternalWorkItem as coreGetExternalWorkItem,
|
|
77
|
+
previewImport as corePreviewImport,
|
|
78
|
+
importExternalWorkItem as coreImportExternalWorkItem,
|
|
79
|
+
IntegrationError,
|
|
80
|
+
IntegrationServiceError,
|
|
73
81
|
WorkItemWriteError,
|
|
74
82
|
exists,
|
|
75
83
|
join,
|
|
@@ -335,6 +343,58 @@ var CoreError = class extends Error {
|
|
|
335
343
|
}
|
|
336
344
|
code;
|
|
337
345
|
};
|
|
346
|
+
function mapIntegrationError(err) {
|
|
347
|
+
if (err instanceof IntegrationError) throw new CoreError(err.code, err.safeMessage);
|
|
348
|
+
if (err instanceof IntegrationServiceError) throw new CoreError(err.code, err.message);
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
function assertExternalId(externalId) {
|
|
352
|
+
if (!externalId || externalId.includes("/") || externalId.includes("\\") || externalId.includes("..")) {
|
|
353
|
+
throw new CoreError("INVALID_EXTERNAL_ID", "Invalid external work item identifier.");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function getIntegrations(dir) {
|
|
357
|
+
return coreListIntegrations(dir);
|
|
358
|
+
}
|
|
359
|
+
async function getIntegrationStatus(dir, id) {
|
|
360
|
+
try {
|
|
361
|
+
return await coreVerifyIntegration(dir, id);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
mapIntegrationError(err);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function getExternalWorkItems(dir, id, opts) {
|
|
367
|
+
try {
|
|
368
|
+
return await coreListExternalWorkItems(dir, id, { cursor: opts.cursor, pageSize: opts.pageSize, filters: { status: opts.status, query: opts.query } });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
mapIntegrationError(err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function getExternalWorkItemDetail(dir, id, externalId) {
|
|
374
|
+
assertExternalId(externalId);
|
|
375
|
+
try {
|
|
376
|
+
return await coreGetExternalWorkItem(dir, id, externalId);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
mapIntegrationError(err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function previewIntegrationImport(dir, id, externalId, opts) {
|
|
382
|
+
assertExternalId(externalId);
|
|
383
|
+
try {
|
|
384
|
+
return await corePreviewImport(dir, id, externalId, { type: opts.type });
|
|
385
|
+
} catch (err) {
|
|
386
|
+
mapIntegrationError(err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function importIntegrationWorkItem(dir, id, externalId, opts) {
|
|
390
|
+
assertExternalId(externalId);
|
|
391
|
+
try {
|
|
392
|
+
return await coreImportExternalWorkItem(dir, id, externalId, { type: opts.type });
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err instanceof WorkItemWriteError) throw new CoreError(err.code, err.message);
|
|
395
|
+
mapIntegrationError(err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
338
398
|
|
|
339
399
|
// src/contracts/schemas.ts
|
|
340
400
|
import { z } from "zod";
|
|
@@ -827,6 +887,44 @@ async function createAdminServer(opts) {
|
|
|
827
887
|
app.get("/api/v1/admin/findings", coreRoute(getFindings));
|
|
828
888
|
app.get("/api/v1/admin/system", coreRoute(getSystemMap));
|
|
829
889
|
app.get("/api/v1/admin/system/topology-handoff", coreRoute(getTopologyHandoff));
|
|
890
|
+
const asyncCore = async (reply, fn) => {
|
|
891
|
+
try {
|
|
892
|
+
return await fn();
|
|
893
|
+
} catch (err) {
|
|
894
|
+
if (err instanceof CoreError) return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
895
|
+
throw err;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
app.get("/api/v1/admin/integrations", coreRoute(getIntegrations));
|
|
899
|
+
app.get(
|
|
900
|
+
"/api/v1/admin/integrations/:id/status",
|
|
901
|
+
async (request, reply) => asyncCore(reply, () => getIntegrationStatus(projectDir, request.params.id))
|
|
902
|
+
);
|
|
903
|
+
app.get(
|
|
904
|
+
"/api/v1/admin/integrations/:id/work-items",
|
|
905
|
+
async (request, reply) => asyncCore(reply, () => getExternalWorkItems(projectDir, request.params.id, {
|
|
906
|
+
cursor: request.query.cursor,
|
|
907
|
+
pageSize: request.query.pageSize ? Number.parseInt(request.query.pageSize, 10) : void 0,
|
|
908
|
+
status: request.query.status,
|
|
909
|
+
query: request.query.query
|
|
910
|
+
}))
|
|
911
|
+
);
|
|
912
|
+
app.get(
|
|
913
|
+
"/api/v1/admin/integrations/:id/work-items/:externalId",
|
|
914
|
+
async (request, reply) => asyncCore(reply, () => getExternalWorkItemDetail(projectDir, request.params.id, request.params.externalId))
|
|
915
|
+
);
|
|
916
|
+
app.get(
|
|
917
|
+
"/api/v1/admin/integrations/:id/work-items/:externalId/import-preview",
|
|
918
|
+
async (request, reply) => asyncCore(reply, () => previewIntegrationImport(projectDir, request.params.id, request.params.externalId, { type: request.query.type }))
|
|
919
|
+
);
|
|
920
|
+
app.post(
|
|
921
|
+
"/api/v1/admin/integrations/:id/work-items/:externalId/import",
|
|
922
|
+
async (request, reply) => {
|
|
923
|
+
const type = request.body?.type;
|
|
924
|
+
if (!type) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "A Kaddo Work Item type is required to import." } });
|
|
925
|
+
return asyncCore(reply, () => importIntegrationWorkItem(projectDir, request.params.id, request.params.externalId, { type }));
|
|
926
|
+
}
|
|
927
|
+
);
|
|
830
928
|
app.get("/api/v1/admin/knowledge/inventory", coreRoute(getKnowledgeInventory));
|
|
831
929
|
app.get("/api/v1/admin/knowledge/artifact/:artifactId", async (request) => {
|
|
832
930
|
try {
|
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),
|
|
@@ -8834,6 +8835,7 @@ function createWorkItem(dir, opts) {
|
|
|
8834
8835
|
const id = nextWorkItemId(dir);
|
|
8835
8836
|
const title = intent.split(/\r?\n/)[0].trim().slice(0, 120);
|
|
8836
8837
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8838
|
+
const source = opts.source ? { ...opts.source, inferred: false } : { type: "manual", inferred: false };
|
|
8837
8839
|
const data = {
|
|
8838
8840
|
type,
|
|
8839
8841
|
id,
|
|
@@ -8841,7 +8843,7 @@ function createWorkItem(dir, opts) {
|
|
|
8841
8843
|
status: "draft",
|
|
8842
8844
|
work_type: type,
|
|
8843
8845
|
created_at: today,
|
|
8844
|
-
source
|
|
8846
|
+
source,
|
|
8845
8847
|
generated_by: "kaddo-admin",
|
|
8846
8848
|
affected_modules: [],
|
|
8847
8849
|
summary: intent
|
|
@@ -9366,7 +9368,462 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
9366
9368
|
text: lines.join("\n")
|
|
9367
9369
|
};
|
|
9368
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
|
+
}
|
|
9369
9823
|
export {
|
|
9824
|
+
INTEGRATIONS_FILE,
|
|
9825
|
+
IntegrationError,
|
|
9826
|
+
IntegrationServiceError,
|
|
9370
9827
|
TOPOLOGY_FILE,
|
|
9371
9828
|
TopologyWriteError,
|
|
9372
9829
|
WorkItemNotFoundError,
|
|
@@ -9385,7 +9842,9 @@ export {
|
|
|
9385
9842
|
discoverKnowledge,
|
|
9386
9843
|
discoverWorkItems,
|
|
9387
9844
|
exists,
|
|
9845
|
+
findLinkedWorkItem,
|
|
9388
9846
|
findSystemPaths,
|
|
9847
|
+
getExternalWorkItem,
|
|
9389
9848
|
getImpactCandidates,
|
|
9390
9849
|
getSystemMapProjection,
|
|
9391
9850
|
getSystemNeighbors,
|
|
@@ -9395,15 +9854,20 @@ export {
|
|
|
9395
9854
|
getWorkItemForEdit,
|
|
9396
9855
|
getWorkItems,
|
|
9397
9856
|
getWorkItemsSummary,
|
|
9857
|
+
importExternalWorkItem,
|
|
9858
|
+
integrationRegistry,
|
|
9398
9859
|
isActiveState,
|
|
9399
9860
|
isModule,
|
|
9400
9861
|
join,
|
|
9401
9862
|
knowledgeLayers,
|
|
9402
9863
|
lifecycleCounts,
|
|
9403
9864
|
lifecycleStateOf,
|
|
9865
|
+
listExternalWorkItems,
|
|
9866
|
+
listIntegrations,
|
|
9404
9867
|
loadConfig,
|
|
9405
9868
|
loadMappedModules,
|
|
9406
9869
|
loadSystemTopology,
|
|
9870
|
+
previewImport,
|
|
9407
9871
|
readFile,
|
|
9408
9872
|
searchSystemNodes,
|
|
9409
9873
|
topologyRevision,
|
|
@@ -9411,5 +9875,6 @@ export {
|
|
|
9411
9875
|
updateWorkItem,
|
|
9412
9876
|
validateSystemTopology,
|
|
9413
9877
|
validateTopologyProposal,
|
|
9414
|
-
validateWorkItem
|
|
9878
|
+
validateWorkItem,
|
|
9879
|
+
verifyIntegration
|
|
9415
9880
|
};
|