choda-deck 0.2.3 → 0.3.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/README.md +3 -1
- package/dist/cli.cjs +47538 -0
- package/dist/mcp-server.cjs +2114 -1312
- package/package.json +1 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -1869,8 +1869,8 @@ var require_keyword = __commonJS({
|
|
|
1869
1869
|
var _a2;
|
|
1870
1870
|
const { gen, keyword, schema, parentSchema, $data, it } = cxt;
|
|
1871
1871
|
checkAsyncKeyword(it, def);
|
|
1872
|
-
const
|
|
1873
|
-
const validateRef = useKeyword(gen, keyword,
|
|
1872
|
+
const validate2 = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate;
|
|
1873
|
+
const validateRef = useKeyword(gen, keyword, validate2);
|
|
1874
1874
|
const valid = gen.let("valid");
|
|
1875
1875
|
cxt.block$data(valid, validateKeyword);
|
|
1876
1876
|
cxt.ok((_a2 = def.valid) !== null && _a2 !== void 0 ? _a2 : valid);
|
|
@@ -2943,28 +2943,28 @@ var require_compile = __commonJS({
|
|
|
2943
2943
|
if (this.opts.code.process)
|
|
2944
2944
|
sourceCode = this.opts.code.process(sourceCode, sch);
|
|
2945
2945
|
const makeValidate = new Function(`${names_1.default.self}`, `${names_1.default.scope}`, sourceCode);
|
|
2946
|
-
const
|
|
2947
|
-
this.scope.value(validateName, { ref:
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2946
|
+
const validate2 = makeValidate(this, this.scope.get());
|
|
2947
|
+
this.scope.value(validateName, { ref: validate2 });
|
|
2948
|
+
validate2.errors = null;
|
|
2949
|
+
validate2.schema = sch.schema;
|
|
2950
|
+
validate2.schemaEnv = sch;
|
|
2951
2951
|
if (sch.$async)
|
|
2952
|
-
|
|
2952
|
+
validate2.$async = true;
|
|
2953
2953
|
if (this.opts.code.source === true) {
|
|
2954
|
-
|
|
2954
|
+
validate2.source = { validateName, validateCode, scopeValues: gen._values };
|
|
2955
2955
|
}
|
|
2956
2956
|
if (this.opts.unevaluated) {
|
|
2957
2957
|
const { props, items } = schemaCxt;
|
|
2958
|
-
|
|
2958
|
+
validate2.evaluated = {
|
|
2959
2959
|
props: props instanceof codegen_1.Name ? void 0 : props,
|
|
2960
2960
|
items: items instanceof codegen_1.Name ? void 0 : items,
|
|
2961
2961
|
dynamicProps: props instanceof codegen_1.Name,
|
|
2962
2962
|
dynamicItems: items instanceof codegen_1.Name
|
|
2963
2963
|
};
|
|
2964
|
-
if (
|
|
2965
|
-
|
|
2964
|
+
if (validate2.source)
|
|
2965
|
+
validate2.source.evaluated = (0, codegen_1.stringify)(validate2.evaluated);
|
|
2966
2966
|
}
|
|
2967
|
-
sch.validate =
|
|
2967
|
+
sch.validate = validate2;
|
|
2968
2968
|
return sch;
|
|
2969
2969
|
} catch (e) {
|
|
2970
2970
|
delete sch.validate;
|
|
@@ -3225,8 +3225,8 @@ var require_utils = __commonJS({
|
|
|
3225
3225
|
}
|
|
3226
3226
|
return ind;
|
|
3227
3227
|
}
|
|
3228
|
-
function removeDotSegments(
|
|
3229
|
-
let input =
|
|
3228
|
+
function removeDotSegments(path9) {
|
|
3229
|
+
let input = path9;
|
|
3230
3230
|
const output = [];
|
|
3231
3231
|
let nextSlash = -1;
|
|
3232
3232
|
let len = 0;
|
|
@@ -3425,8 +3425,8 @@ var require_schemes = __commonJS({
|
|
|
3425
3425
|
wsComponent.secure = void 0;
|
|
3426
3426
|
}
|
|
3427
3427
|
if (wsComponent.resourceName) {
|
|
3428
|
-
const [
|
|
3429
|
-
wsComponent.path =
|
|
3428
|
+
const [path9, query] = wsComponent.resourceName.split("?");
|
|
3429
|
+
wsComponent.path = path9 && path9 !== "/" ? path9 : void 0;
|
|
3430
3430
|
wsComponent.query = query;
|
|
3431
3431
|
wsComponent.resourceName = void 0;
|
|
3432
3432
|
}
|
|
@@ -6490,8 +6490,8 @@ var require_formats = __commonJS({
|
|
|
6490
6490
|
"use strict";
|
|
6491
6491
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
6492
6492
|
exports2.formatNames = exports2.fastFormats = exports2.fullFormats = void 0;
|
|
6493
|
-
function fmtDef(
|
|
6494
|
-
return { validate:
|
|
6493
|
+
function fmtDef(validate2, compare) {
|
|
6494
|
+
return { validate: validate2, compare };
|
|
6495
6495
|
}
|
|
6496
6496
|
exports2.fullFormats = {
|
|
6497
6497
|
// date: http://tools.ietf.org/html/rfc3339#section-5.6
|
|
@@ -7355,51 +7355,51 @@ var require_textParsers = __commonJS({
|
|
|
7355
7355
|
result.radius = parseFloat(radius);
|
|
7356
7356
|
return result;
|
|
7357
7357
|
};
|
|
7358
|
-
var init = function(
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7358
|
+
var init = function(register22) {
|
|
7359
|
+
register22(20, parseBigInteger);
|
|
7360
|
+
register22(21, parseInteger);
|
|
7361
|
+
register22(23, parseInteger);
|
|
7362
|
+
register22(26, parseInteger);
|
|
7363
|
+
register22(700, parseFloat);
|
|
7364
|
+
register22(701, parseFloat);
|
|
7365
|
+
register22(16, parseBool);
|
|
7366
|
+
register22(1082, parseDate);
|
|
7367
|
+
register22(1114, parseDate);
|
|
7368
|
+
register22(1184, parseDate);
|
|
7369
|
+
register22(600, parsePoint);
|
|
7370
|
+
register22(651, parseStringArray);
|
|
7371
|
+
register22(718, parseCircle);
|
|
7372
|
+
register22(1e3, parseBoolArray);
|
|
7373
|
+
register22(1001, parseByteAArray);
|
|
7374
|
+
register22(1005, parseIntegerArray);
|
|
7375
|
+
register22(1007, parseIntegerArray);
|
|
7376
|
+
register22(1028, parseIntegerArray);
|
|
7377
|
+
register22(1016, parseBigIntegerArray);
|
|
7378
|
+
register22(1017, parsePointArray);
|
|
7379
|
+
register22(1021, parseFloatArray);
|
|
7380
|
+
register22(1022, parseFloatArray);
|
|
7381
|
+
register22(1231, parseFloatArray);
|
|
7382
|
+
register22(1014, parseStringArray);
|
|
7383
|
+
register22(1015, parseStringArray);
|
|
7384
|
+
register22(1008, parseStringArray);
|
|
7385
|
+
register22(1009, parseStringArray);
|
|
7386
|
+
register22(1040, parseStringArray);
|
|
7387
|
+
register22(1041, parseStringArray);
|
|
7388
|
+
register22(1115, parseDateArray);
|
|
7389
|
+
register22(1182, parseDateArray);
|
|
7390
|
+
register22(1185, parseDateArray);
|
|
7391
|
+
register22(1186, parseInterval);
|
|
7392
|
+
register22(1187, parseIntervalArray);
|
|
7393
|
+
register22(17, parseByteA);
|
|
7394
|
+
register22(114, JSON.parse.bind(JSON));
|
|
7395
|
+
register22(3802, JSON.parse.bind(JSON));
|
|
7396
|
+
register22(199, parseJsonArray);
|
|
7397
|
+
register22(3807, parseJsonArray);
|
|
7398
|
+
register22(3907, parseStringArray);
|
|
7399
|
+
register22(2951, parseStringArray);
|
|
7400
|
+
register22(791, parseStringArray);
|
|
7401
|
+
register22(1183, parseStringArray);
|
|
7402
|
+
register22(1270, parseStringArray);
|
|
7403
7403
|
};
|
|
7404
7404
|
module2.exports = {
|
|
7405
7405
|
init
|
|
@@ -7663,23 +7663,23 @@ var require_binaryParsers = __commonJS({
|
|
|
7663
7663
|
if (value === null) return null;
|
|
7664
7664
|
return parseBits(value, 8) > 0;
|
|
7665
7665
|
};
|
|
7666
|
-
var init = function(
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7666
|
+
var init = function(register22) {
|
|
7667
|
+
register22(20, parseInt64);
|
|
7668
|
+
register22(21, parseInt16);
|
|
7669
|
+
register22(23, parseInt32);
|
|
7670
|
+
register22(26, parseInt32);
|
|
7671
|
+
register22(1700, parseNumeric);
|
|
7672
|
+
register22(700, parseFloat32);
|
|
7673
|
+
register22(701, parseFloat64);
|
|
7674
|
+
register22(16, parseBool);
|
|
7675
|
+
register22(1114, parseDate.bind(null, false));
|
|
7676
|
+
register22(1184, parseDate.bind(null, true));
|
|
7677
|
+
register22(1e3, parseArray);
|
|
7678
|
+
register22(1007, parseArray);
|
|
7679
|
+
register22(1016, parseArray);
|
|
7680
|
+
register22(1008, parseArray);
|
|
7681
|
+
register22(1009, parseArray);
|
|
7682
|
+
register22(25, parseText);
|
|
7683
7683
|
};
|
|
7684
7684
|
module2.exports = {
|
|
7685
7685
|
init
|
|
@@ -8017,7 +8017,7 @@ var require_utils3 = __commonJS({
|
|
|
8017
8017
|
var nodeCrypto = require("crypto");
|
|
8018
8018
|
module2.exports = {
|
|
8019
8019
|
postgresMd5PasswordHash,
|
|
8020
|
-
randomBytes
|
|
8020
|
+
randomBytes,
|
|
8021
8021
|
deriveKey,
|
|
8022
8022
|
sha256,
|
|
8023
8023
|
hashByName,
|
|
@@ -8027,7 +8027,7 @@ var require_utils3 = __commonJS({
|
|
|
8027
8027
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
8028
8028
|
var subtleCrypto = webCrypto.subtle;
|
|
8029
8029
|
var textEncoder = new TextEncoder();
|
|
8030
|
-
function
|
|
8030
|
+
function randomBytes(length) {
|
|
8031
8031
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
8032
8032
|
}
|
|
8033
8033
|
async function md5(string4) {
|
|
@@ -10225,7 +10225,7 @@ var require_split2 = __commonJS({
|
|
|
10225
10225
|
var require_helper = __commonJS({
|
|
10226
10226
|
"node_modules/pgpass/lib/helper.js"(exports2, module2) {
|
|
10227
10227
|
"use strict";
|
|
10228
|
-
var
|
|
10228
|
+
var path9 = require("path");
|
|
10229
10229
|
var Stream = require("stream").Stream;
|
|
10230
10230
|
var split = require_split2();
|
|
10231
10231
|
var util2 = require("util");
|
|
@@ -10264,7 +10264,7 @@ var require_helper = __commonJS({
|
|
|
10264
10264
|
};
|
|
10265
10265
|
module2.exports.getFileName = function(rawEnv) {
|
|
10266
10266
|
var env = rawEnv || process.env;
|
|
10267
|
-
var file2 = env.PGPASSFILE || (isWin ?
|
|
10267
|
+
var file2 = env.PGPASSFILE || (isWin ? path9.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path9.join(env.HOME || "./", ".pgpass"));
|
|
10268
10268
|
return file2;
|
|
10269
10269
|
};
|
|
10270
10270
|
module2.exports.usePgPass = function(stats, fname) {
|
|
@@ -10396,7 +10396,7 @@ var require_helper = __commonJS({
|
|
|
10396
10396
|
var require_lib = __commonJS({
|
|
10397
10397
|
"node_modules/pgpass/lib/index.js"(exports2, module2) {
|
|
10398
10398
|
"use strict";
|
|
10399
|
-
var
|
|
10399
|
+
var path9 = require("path");
|
|
10400
10400
|
var fs9 = require("fs");
|
|
10401
10401
|
var helper = require_helper();
|
|
10402
10402
|
module2.exports = function(connInfo, cb) {
|
|
@@ -11953,8 +11953,6 @@ var require_lib2 = __commonJS({
|
|
|
11953
11953
|
|
|
11954
11954
|
// src/adapters/mcp/server-bootstrap.ts
|
|
11955
11955
|
var fs8 = __toESM(require("fs"));
|
|
11956
|
-
var path9 = __toESM(require("path"));
|
|
11957
|
-
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
11958
11956
|
|
|
11959
11957
|
// node_modules/zod/v3/helpers/util.js
|
|
11960
11958
|
var util;
|
|
@@ -12315,8 +12313,8 @@ function getErrorMap() {
|
|
|
12315
12313
|
|
|
12316
12314
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
12317
12315
|
var makeIssue = (params) => {
|
|
12318
|
-
const { data, path:
|
|
12319
|
-
const fullPath = [...
|
|
12316
|
+
const { data, path: path9, errorMaps, issueData } = params;
|
|
12317
|
+
const fullPath = [...path9, ...issueData.path || []];
|
|
12320
12318
|
const fullIssue = {
|
|
12321
12319
|
...issueData,
|
|
12322
12320
|
path: fullPath
|
|
@@ -12431,11 +12429,11 @@ var errorUtil;
|
|
|
12431
12429
|
|
|
12432
12430
|
// node_modules/zod/v3/types.js
|
|
12433
12431
|
var ParseInputLazyPath = class {
|
|
12434
|
-
constructor(parent, value,
|
|
12432
|
+
constructor(parent, value, path9, key) {
|
|
12435
12433
|
this._cachedPath = [];
|
|
12436
12434
|
this.parent = parent;
|
|
12437
12435
|
this.data = value;
|
|
12438
|
-
this._path =
|
|
12436
|
+
this._path = path9;
|
|
12439
12437
|
this._key = key;
|
|
12440
12438
|
}
|
|
12441
12439
|
get path() {
|
|
@@ -16358,10 +16356,10 @@ function mergeDefs(...defs) {
|
|
|
16358
16356
|
function cloneDef(schema) {
|
|
16359
16357
|
return mergeDefs(schema._zod.def);
|
|
16360
16358
|
}
|
|
16361
|
-
function getElementAtPath(obj,
|
|
16362
|
-
if (!
|
|
16359
|
+
function getElementAtPath(obj, path9) {
|
|
16360
|
+
if (!path9)
|
|
16363
16361
|
return obj;
|
|
16364
|
-
return
|
|
16362
|
+
return path9.reduce((acc, key) => acc?.[key], obj);
|
|
16365
16363
|
}
|
|
16366
16364
|
function promiseAllObject(promisesObj) {
|
|
16367
16365
|
const keys = Object.keys(promisesObj);
|
|
@@ -16744,11 +16742,11 @@ function aborted(x, startIndex = 0) {
|
|
|
16744
16742
|
}
|
|
16745
16743
|
return false;
|
|
16746
16744
|
}
|
|
16747
|
-
function prefixIssues(
|
|
16745
|
+
function prefixIssues(path9, issues) {
|
|
16748
16746
|
return issues.map((iss) => {
|
|
16749
16747
|
var _a2;
|
|
16750
16748
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
16751
|
-
iss.path.unshift(
|
|
16749
|
+
iss.path.unshift(path9);
|
|
16752
16750
|
return iss;
|
|
16753
16751
|
});
|
|
16754
16752
|
}
|
|
@@ -16931,7 +16929,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
16931
16929
|
}
|
|
16932
16930
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
16933
16931
|
const result = { errors: [] };
|
|
16934
|
-
const processError = (error49,
|
|
16932
|
+
const processError = (error49, path9 = []) => {
|
|
16935
16933
|
var _a2, _b;
|
|
16936
16934
|
for (const issue2 of error49.issues) {
|
|
16937
16935
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -16941,7 +16939,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
16941
16939
|
} else if (issue2.code === "invalid_element") {
|
|
16942
16940
|
processError({ issues: issue2.issues }, issue2.path);
|
|
16943
16941
|
} else {
|
|
16944
|
-
const fullpath = [...
|
|
16942
|
+
const fullpath = [...path9, ...issue2.path];
|
|
16945
16943
|
if (fullpath.length === 0) {
|
|
16946
16944
|
result.errors.push(mapper(issue2));
|
|
16947
16945
|
continue;
|
|
@@ -16973,8 +16971,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
16973
16971
|
}
|
|
16974
16972
|
function toDotPath(_path) {
|
|
16975
16973
|
const segs = [];
|
|
16976
|
-
const
|
|
16977
|
-
for (const seg of
|
|
16974
|
+
const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
16975
|
+
for (const seg of path9) {
|
|
16978
16976
|
if (typeof seg === "number")
|
|
16979
16977
|
segs.push(`[${seg}]`);
|
|
16980
16978
|
else if (typeof seg === "symbol")
|
|
@@ -18572,13 +18570,13 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
18572
18570
|
}
|
|
18573
18571
|
return propValues;
|
|
18574
18572
|
});
|
|
18575
|
-
const
|
|
18573
|
+
const isObject3 = isObject;
|
|
18576
18574
|
const catchall = def.catchall;
|
|
18577
18575
|
let value;
|
|
18578
18576
|
inst._zod.parse = (payload, ctx) => {
|
|
18579
18577
|
value ?? (value = _normalized.value);
|
|
18580
18578
|
const input = payload.value;
|
|
18581
|
-
if (!
|
|
18579
|
+
if (!isObject3(input)) {
|
|
18582
18580
|
payload.issues.push({
|
|
18583
18581
|
expected: "object",
|
|
18584
18582
|
code: "invalid_type",
|
|
@@ -18676,7 +18674,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
18676
18674
|
return (payload, ctx) => fn(shape, payload, ctx);
|
|
18677
18675
|
};
|
|
18678
18676
|
let fastpass;
|
|
18679
|
-
const
|
|
18677
|
+
const isObject3 = isObject;
|
|
18680
18678
|
const jit = !globalConfig.jitless;
|
|
18681
18679
|
const allowsEval2 = allowsEval;
|
|
18682
18680
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -18685,7 +18683,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
18685
18683
|
inst._zod.parse = (payload, ctx) => {
|
|
18686
18684
|
value ?? (value = _normalized.value);
|
|
18687
18685
|
const input = payload.value;
|
|
18688
|
-
if (!
|
|
18686
|
+
if (!isObject3(input)) {
|
|
18689
18687
|
payload.issues.push({
|
|
18690
18688
|
expected: "object",
|
|
18691
18689
|
code: "invalid_type",
|
|
@@ -29380,13 +29378,13 @@ function resolveRef(ref, ctx) {
|
|
|
29380
29378
|
if (!ref.startsWith("#")) {
|
|
29381
29379
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
29382
29380
|
}
|
|
29383
|
-
const
|
|
29384
|
-
if (
|
|
29381
|
+
const path9 = ref.slice(1).split("/").filter(Boolean);
|
|
29382
|
+
if (path9.length === 0) {
|
|
29385
29383
|
return ctx.rootSchema;
|
|
29386
29384
|
}
|
|
29387
29385
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
29388
|
-
if (
|
|
29389
|
-
const key =
|
|
29386
|
+
if (path9[0] === defsKey) {
|
|
29387
|
+
const key = path9[1];
|
|
29390
29388
|
if (!key || !ctx.defs[key]) {
|
|
29391
29389
|
throw new Error(`Reference not found: ${ref}`);
|
|
29392
29390
|
}
|
|
@@ -32894,7 +32892,7 @@ var Protocol = class {
|
|
|
32894
32892
|
const capturedTransport = this._transport;
|
|
32895
32893
|
const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
|
|
32896
32894
|
if (handler === void 0) {
|
|
32897
|
-
const
|
|
32895
|
+
const errorResponse = {
|
|
32898
32896
|
jsonrpc: "2.0",
|
|
32899
32897
|
id: request.id,
|
|
32900
32898
|
error: {
|
|
@@ -32905,11 +32903,11 @@ var Protocol = class {
|
|
|
32905
32903
|
if (relatedTaskId && this._taskMessageQueue) {
|
|
32906
32904
|
this._enqueueTaskMessage(relatedTaskId, {
|
|
32907
32905
|
type: "error",
|
|
32908
|
-
message:
|
|
32906
|
+
message: errorResponse,
|
|
32909
32907
|
timestamp: Date.now()
|
|
32910
32908
|
}, capturedTransport?.sessionId).catch((error48) => this._onerror(new Error(`Failed to enqueue error response: ${error48}`)));
|
|
32911
32909
|
} else {
|
|
32912
|
-
capturedTransport?.send(
|
|
32910
|
+
capturedTransport?.send(errorResponse).catch((error48) => this._onerror(new Error(`Failed to send an error response: ${error48}`)));
|
|
32913
32911
|
}
|
|
32914
32912
|
return;
|
|
32915
32913
|
}
|
|
@@ -32979,7 +32977,7 @@ var Protocol = class {
|
|
|
32979
32977
|
if (abortController.signal.aborted) {
|
|
32980
32978
|
return;
|
|
32981
32979
|
}
|
|
32982
|
-
const
|
|
32980
|
+
const errorResponse = {
|
|
32983
32981
|
jsonrpc: "2.0",
|
|
32984
32982
|
id: request.id,
|
|
32985
32983
|
error: {
|
|
@@ -32991,11 +32989,11 @@ var Protocol = class {
|
|
|
32991
32989
|
if (relatedTaskId && this._taskMessageQueue) {
|
|
32992
32990
|
await this._enqueueTaskMessage(relatedTaskId, {
|
|
32993
32991
|
type: "error",
|
|
32994
|
-
message:
|
|
32992
|
+
message: errorResponse,
|
|
32995
32993
|
timestamp: Date.now()
|
|
32996
32994
|
}, capturedTransport?.sessionId);
|
|
32997
32995
|
} else {
|
|
32998
|
-
await capturedTransport?.send(
|
|
32996
|
+
await capturedTransport?.send(errorResponse);
|
|
32999
32997
|
}
|
|
33000
32998
|
}).catch((error48) => this._onerror(new Error(`Failed to send response: ${error48}`))).finally(() => {
|
|
33001
32999
|
if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
|
|
@@ -35281,6 +35279,60 @@ var path3 = __toESM(require("path"));
|
|
|
35281
35279
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
35282
35280
|
var sqliteVec = __toESM(require("sqlite-vec"));
|
|
35283
35281
|
|
|
35282
|
+
// src/core/sync/syncable-tables.ts
|
|
35283
|
+
var SYNCABLE_TABLES = [
|
|
35284
|
+
"projects",
|
|
35285
|
+
"workspaces",
|
|
35286
|
+
"tasks",
|
|
35287
|
+
"inbox_items",
|
|
35288
|
+
"conversations",
|
|
35289
|
+
"conversation_messages",
|
|
35290
|
+
"conversation_actions"
|
|
35291
|
+
];
|
|
35292
|
+
var SYNC_COLUMNS = [
|
|
35293
|
+
{ name: "sync_updated_at", sqliteType: "INTEGER", pgType: "BIGINT" },
|
|
35294
|
+
{ name: "sync_deleted_at", sqliteType: "INTEGER", pgType: "BIGINT" },
|
|
35295
|
+
{ name: "sync_origin", sqliteType: "TEXT", pgType: "TEXT" }
|
|
35296
|
+
];
|
|
35297
|
+
|
|
35298
|
+
// src/core/sync/sync-source.ts
|
|
35299
|
+
function fetchSinceFromSqlite(db, since) {
|
|
35300
|
+
const deltas = [];
|
|
35301
|
+
for (const table of SYNCABLE_TABLES) {
|
|
35302
|
+
const rows = db.prepare(
|
|
35303
|
+
`SELECT * FROM ${table} WHERE sync_updated_at > ? OR sync_deleted_at > ? ORDER BY sync_updated_at`
|
|
35304
|
+
).all(since, since);
|
|
35305
|
+
if (rows.length > 0) deltas.push({ table, rows });
|
|
35306
|
+
}
|
|
35307
|
+
return deltas;
|
|
35308
|
+
}
|
|
35309
|
+
async function fetchSinceFromPg(conn, since) {
|
|
35310
|
+
const deltas = [];
|
|
35311
|
+
for (const table of SYNCABLE_TABLES) {
|
|
35312
|
+
const result = await conn.query(
|
|
35313
|
+
`SELECT * FROM ${table} WHERE sync_updated_at > $1 OR sync_deleted_at > $1 ORDER BY sync_updated_at`,
|
|
35314
|
+
[since]
|
|
35315
|
+
);
|
|
35316
|
+
if (result.rows.length > 0) {
|
|
35317
|
+
deltas.push({ table, rows: result.rows.map(normalizePgRow) });
|
|
35318
|
+
}
|
|
35319
|
+
}
|
|
35320
|
+
return deltas;
|
|
35321
|
+
}
|
|
35322
|
+
function normalizePgRow(row) {
|
|
35323
|
+
const out = {};
|
|
35324
|
+
for (const [k, v] of Object.entries(row)) {
|
|
35325
|
+
if (k === "sync_updated_at" || k === "sync_deleted_at") {
|
|
35326
|
+
out[k] = v === null || v === void 0 ? null : Number(v);
|
|
35327
|
+
} else if (v instanceof Date) {
|
|
35328
|
+
out[k] = v.toISOString();
|
|
35329
|
+
} else {
|
|
35330
|
+
out[k] = v;
|
|
35331
|
+
}
|
|
35332
|
+
}
|
|
35333
|
+
return out;
|
|
35334
|
+
}
|
|
35335
|
+
|
|
35284
35336
|
// src/core/domain/embedding/embedding-store.ts
|
|
35285
35337
|
var EmbeddingStore = class {
|
|
35286
35338
|
db;
|
|
@@ -35549,6 +35601,33 @@ var NoActiveSessionError = class extends LifecycleError {
|
|
|
35549
35601
|
this.name = "NoActiveSessionError";
|
|
35550
35602
|
}
|
|
35551
35603
|
};
|
|
35604
|
+
var InvestigationNotFoundError = class extends LifecycleError {
|
|
35605
|
+
constructor(id) {
|
|
35606
|
+
super("INVESTIGATION_NOT_FOUND", `Investigation ${id} not found`);
|
|
35607
|
+
this.name = "InvestigationNotFoundError";
|
|
35608
|
+
}
|
|
35609
|
+
};
|
|
35610
|
+
var InvestigationStatusError = class extends LifecycleError {
|
|
35611
|
+
constructor(id, current, message) {
|
|
35612
|
+
super("INVESTIGATION_INVALID_STATUS", `Investigation ${id} is ${current} \u2014 ${message}`);
|
|
35613
|
+
this.name = "InvestigationStatusError";
|
|
35614
|
+
}
|
|
35615
|
+
};
|
|
35616
|
+
var HypothesisNotFoundError = class extends LifecycleError {
|
|
35617
|
+
constructor(id) {
|
|
35618
|
+
super("HYPOTHESIS_NOT_FOUND", `Hypothesis ${id} not found`);
|
|
35619
|
+
this.name = "HypothesisNotFoundError";
|
|
35620
|
+
}
|
|
35621
|
+
};
|
|
35622
|
+
var HypothesisTransitionError = class extends LifecycleError {
|
|
35623
|
+
constructor(id, current, target) {
|
|
35624
|
+
super(
|
|
35625
|
+
"HYPOTHESIS_INVALID_TRANSITION",
|
|
35626
|
+
`Hypothesis ${id} is ${current} \u2014 cannot transition to ${target} (only testing \u2192 ruled_out | confirmed is allowed)`
|
|
35627
|
+
);
|
|
35628
|
+
this.name = "HypothesisTransitionError";
|
|
35629
|
+
}
|
|
35630
|
+
};
|
|
35552
35631
|
|
|
35553
35632
|
// src/core/domain/lifecycle/inbox-lifecycle-service.ts
|
|
35554
35633
|
var InboxLifecycleService = class {
|
|
@@ -35609,7 +35688,8 @@ var InboxLifecycleService = class {
|
|
|
35609
35688
|
this.closeLinkedConversations(id, `Converted to ${task.id}: ${input.title}`);
|
|
35610
35689
|
const final = this.tasks.get(task.id);
|
|
35611
35690
|
if (!final) throw new Error(`Task ${task.id} disappeared mid-transaction`);
|
|
35612
|
-
|
|
35691
|
+
const localizationWarning = item.workspaceId ? void 0 : `${id} had no workspaceId \u2014 converted ${task.id} is not localized to an app. Set it during research (inbox_ready) next time, or scope the task via its worker session.`;
|
|
35692
|
+
return { inboxId: id, taskId: task.id, task: final, localizationWarning };
|
|
35613
35693
|
});
|
|
35614
35694
|
return tx();
|
|
35615
35695
|
}
|
|
@@ -35876,13 +35956,15 @@ function computeLineOffsets(body, lineCount) {
|
|
|
35876
35956
|
|
|
35877
35957
|
// src/core/domain/lifecycle/session-lifecycle-service.ts
|
|
35878
35958
|
var SessionLifecycleService = class {
|
|
35879
|
-
constructor(db, sessions, contextSources, conversations, tasks, sessionEvents, recallMemoriesFn) {
|
|
35959
|
+
constructor(db, sessions, contextSources, conversations, tasks, sessionEvents, relationships, codeRefs, recallMemoriesFn) {
|
|
35880
35960
|
this.db = db;
|
|
35881
35961
|
this.sessions = sessions;
|
|
35882
35962
|
this.contextSources = contextSources;
|
|
35883
35963
|
this.conversations = conversations;
|
|
35884
35964
|
this.tasks = tasks;
|
|
35885
35965
|
this.sessionEvents = sessionEvents;
|
|
35966
|
+
this.relationships = relationships;
|
|
35967
|
+
this.codeRefs = codeRefs;
|
|
35886
35968
|
this.recallMemoriesFn = recallMemoriesFn;
|
|
35887
35969
|
}
|
|
35888
35970
|
db;
|
|
@@ -35891,6 +35973,8 @@ var SessionLifecycleService = class {
|
|
|
35891
35973
|
conversations;
|
|
35892
35974
|
tasks;
|
|
35893
35975
|
sessionEvents;
|
|
35976
|
+
relationships;
|
|
35977
|
+
codeRefs;
|
|
35894
35978
|
recallMemoriesFn;
|
|
35895
35979
|
async startSession(input) {
|
|
35896
35980
|
const tx = this.db.transaction(() => {
|
|
@@ -35913,7 +35997,8 @@ var SessionLifecycleService = class {
|
|
|
35913
35997
|
workspaceId: input.workspaceId,
|
|
35914
35998
|
taskId: input.taskId,
|
|
35915
35999
|
startedAt: now(),
|
|
35916
|
-
status: "active"
|
|
36000
|
+
status: "active",
|
|
36001
|
+
ccSessionId: input.ccSessionId
|
|
35917
36002
|
});
|
|
35918
36003
|
if (input.taskId) {
|
|
35919
36004
|
this.tasks.update(input.taskId, { status: "IN-PROGRESS" });
|
|
@@ -35979,6 +36064,19 @@ var SessionLifecycleService = class {
|
|
|
35979
36064
|
memoryCandidate: false
|
|
35980
36065
|
});
|
|
35981
36066
|
}
|
|
36067
|
+
if (session.taskId) {
|
|
36068
|
+
this.deriveTouchesFromFileEdits(id, session.taskId, session.projectId, endedAt);
|
|
36069
|
+
}
|
|
36070
|
+
const featureId = session.taskId ? this.inferFeatureForTask(session.taskId) : null;
|
|
36071
|
+
for (const decision of input.handoff.decisions ?? []) {
|
|
36072
|
+
const draft = draftGotchaFromDecision(decision, featureId);
|
|
36073
|
+
this.sessionEvents.create({
|
|
36074
|
+
sessionId: id,
|
|
36075
|
+
eventType: "observation",
|
|
36076
|
+
payloadJson: JSON.stringify({ kind: "gotcha_draft", ...draft }),
|
|
36077
|
+
memoryCandidate: true
|
|
36078
|
+
});
|
|
36079
|
+
}
|
|
35982
36080
|
const memoryCandidates = this.sessionEvents.listMemoryCandidates(id);
|
|
35983
36081
|
const selfEditPrompt = buildSelfEditPrompt(memoryCandidates);
|
|
35984
36082
|
return { session: updated, closedConversationIds, taskUpdated, memoryCandidates, selfEditPrompt };
|
|
@@ -36027,6 +36125,30 @@ var SessionLifecycleService = class {
|
|
|
36027
36125
|
});
|
|
36028
36126
|
return { session: updated };
|
|
36029
36127
|
}
|
|
36128
|
+
// TASK-998: the feature a task REALIZES (task → feature edge, TASK-992). A task
|
|
36129
|
+
// normally realizes one feature; if several, the first by edge order is used.
|
|
36130
|
+
// Returns null when the task has no REALIZES edge.
|
|
36131
|
+
inferFeatureForTask(taskId) {
|
|
36132
|
+
const edges = this.relationships.getFrom(taskId, "REALIZES");
|
|
36133
|
+
return edges.length > 0 ? edges[0].toId : null;
|
|
36134
|
+
}
|
|
36135
|
+
// INBOX-424: upsert a file-level code_ref (symbol=null — the sanctioned
|
|
36136
|
+
// convention for non-symbol refs) + a `modifies` TOUCHES edge for each DISTINCT
|
|
36137
|
+
// path the session edited. Pure-derivation: reads channel-1 events, writes the
|
|
36138
|
+
// graph projection; no AC / summary side effects.
|
|
36139
|
+
deriveTouchesFromFileEdits(sessionId, taskId, projectId, nowIso) {
|
|
36140
|
+
const paths = /* @__PURE__ */ new Set();
|
|
36141
|
+
for (const evt of this.sessionEvents.listBySession(sessionId, "observation")) {
|
|
36142
|
+
const payload = parseObservationPayload(evt.payloadJson);
|
|
36143
|
+
if (payload?.kind === "file_modified" && typeof payload.path === "string") {
|
|
36144
|
+
paths.add(payload.path);
|
|
36145
|
+
}
|
|
36146
|
+
}
|
|
36147
|
+
for (const path9 of paths) {
|
|
36148
|
+
const ref = this.codeRefs.upsert({ slug: fileRefSlug(path9), projectId, path: path9, symbol: null }, nowIso);
|
|
36149
|
+
this.codeRefs.addTouches(taskId, ref.slug, "modifies");
|
|
36150
|
+
}
|
|
36151
|
+
}
|
|
36030
36152
|
async resumeSession(id) {
|
|
36031
36153
|
const session = this.sessions.get(id);
|
|
36032
36154
|
if (!session) throw new SessionNotFoundError(id);
|
|
@@ -36089,6 +36211,9 @@ function aggregateSessionSummary(sessionEvents, tasks, sessionId, summary) {
|
|
|
36089
36211
|
acCoverage: mergedAcCoverage
|
|
36090
36212
|
};
|
|
36091
36213
|
}
|
|
36214
|
+
function fileRefSlug(path9) {
|
|
36215
|
+
return `code-ref-${path9.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}`;
|
|
36216
|
+
}
|
|
36092
36217
|
function parseObservationPayload(json2) {
|
|
36093
36218
|
if (!json2) return null;
|
|
36094
36219
|
try {
|
|
@@ -36100,11 +36225,245 @@ function parseObservationPayload(json2) {
|
|
|
36100
36225
|
}
|
|
36101
36226
|
function buildSelfEditPrompt(candidates) {
|
|
36102
36227
|
if (candidates.length === 0) return "";
|
|
36103
|
-
const
|
|
36104
|
-
|
|
36105
|
-
|
|
36228
|
+
const gotchaDrafts = candidates.filter((c) => {
|
|
36229
|
+
const p = parseObservationPayload(c.payloadJson);
|
|
36230
|
+
return p?.kind === "gotcha_draft";
|
|
36231
|
+
});
|
|
36232
|
+
const memN = candidates.length - gotchaDrafts.length;
|
|
36233
|
+
const parts = [];
|
|
36234
|
+
if (memN > 0) {
|
|
36235
|
+
const word = memN === 1 ? "event" : "events";
|
|
36236
|
+
parts.push(
|
|
36237
|
+
`Review ${memN} candidate ${word} from the session. Call memory_write for 1-3 entries worth remembering across sessions \u2014 use type='episodic' with scope='task' for task-specific learnings, or type='procedural' with scope='project' or 'workspace' for reusable patterns.`
|
|
36238
|
+
);
|
|
36239
|
+
}
|
|
36240
|
+
if (gotchaDrafts.length > 0) {
|
|
36241
|
+
const word = gotchaDrafts.length === 1 ? "gotcha draft" : "gotcha drafts";
|
|
36242
|
+
const needFeature = gotchaDrafts.some((c) => {
|
|
36243
|
+
const p = parseObservationPayload(c.payloadJson);
|
|
36244
|
+
return p?.needsFeature === true;
|
|
36245
|
+
});
|
|
36246
|
+
parts.push(
|
|
36247
|
+
`${gotchaDrafts.length} ${word} (kind='gotcha_draft') were proposed from the session's decisions. Review each, refine the structured fields (trigger / context / business_rule / resolution), and call knowledge_create(type='gotcha') for ones worth keeping.` + (needFeature ? ` Some have no affectedFeatureId (the task has no REALIZES edge) \u2014 ask the human which feature before creating those.` : "")
|
|
36248
|
+
);
|
|
36249
|
+
}
|
|
36250
|
+
parts.push("Skip entirely if nothing here is worth keeping.");
|
|
36251
|
+
return parts.join(" ");
|
|
36252
|
+
}
|
|
36253
|
+
function draftGotchaFromDecision(decision, featureId) {
|
|
36254
|
+
const text = decision.trim();
|
|
36255
|
+
const marker = text.match(/\bbecause\b|\bso that\b|[—–]| - |:/i);
|
|
36256
|
+
let businessRule = text;
|
|
36257
|
+
let resolution = "";
|
|
36258
|
+
if (marker && marker.index !== void 0 && marker.index > 0) {
|
|
36259
|
+
businessRule = text.slice(0, marker.index).replace(/[\s,;:—–-]+$/, "").trim();
|
|
36260
|
+
resolution = text.slice(marker.index + marker[0].length).trim();
|
|
36261
|
+
}
|
|
36262
|
+
return {
|
|
36263
|
+
trigger: "",
|
|
36264
|
+
context: text,
|
|
36265
|
+
businessRule,
|
|
36266
|
+
resolution,
|
|
36267
|
+
affectedFeatureId: featureId,
|
|
36268
|
+
needsFeature: featureId === null,
|
|
36269
|
+
sourceDecision: text
|
|
36270
|
+
};
|
|
36106
36271
|
}
|
|
36107
36272
|
|
|
36273
|
+
// src/core/domain/lifecycle/investigation-lifecycle-service.ts
|
|
36274
|
+
var InvestigationLifecycleService = class {
|
|
36275
|
+
constructor(db, investigations) {
|
|
36276
|
+
this.db = db;
|
|
36277
|
+
this.investigations = investigations;
|
|
36278
|
+
}
|
|
36279
|
+
db;
|
|
36280
|
+
investigations;
|
|
36281
|
+
async startInvestigation(input) {
|
|
36282
|
+
return this.investigations.insertInvestigation(input);
|
|
36283
|
+
}
|
|
36284
|
+
async getInvestigation(id) {
|
|
36285
|
+
return this.investigations.getInvestigation(id);
|
|
36286
|
+
}
|
|
36287
|
+
async addHypothesis(investigationId, description) {
|
|
36288
|
+
const tx = this.db.transaction(() => {
|
|
36289
|
+
const inv = this.investigations.getInvestigation(investigationId);
|
|
36290
|
+
if (!inv) throw new InvestigationNotFoundError(investigationId);
|
|
36291
|
+
if (inv.status === "resolved") {
|
|
36292
|
+
throw new InvestigationStatusError(investigationId, inv.status, "cannot add a hypothesis");
|
|
36293
|
+
}
|
|
36294
|
+
return this.investigations.insertHypothesis(investigationId, description);
|
|
36295
|
+
});
|
|
36296
|
+
return tx();
|
|
36297
|
+
}
|
|
36298
|
+
// Only testing → ruled_out | confirmed is legal. A terminal hypothesis (already
|
|
36299
|
+
// ruled_out/confirmed) cannot transition again, and 'testing' is never a target.
|
|
36300
|
+
async setHypothesisStatus(hypothesisId, status) {
|
|
36301
|
+
const tx = this.db.transaction(() => {
|
|
36302
|
+
const hyp = this.investigations.getHypothesis(hypothesisId);
|
|
36303
|
+
if (!hyp) throw new HypothesisNotFoundError(hypothesisId);
|
|
36304
|
+
const legal = hyp.status === "testing" && (status === "ruled_out" || status === "confirmed");
|
|
36305
|
+
if (!legal) throw new HypothesisTransitionError(hypothesisId, hyp.status, status);
|
|
36306
|
+
this.investigations.setHypothesisStatus(hypothesisId, status);
|
|
36307
|
+
return this.investigations.getHypothesis(hypothesisId);
|
|
36308
|
+
});
|
|
36309
|
+
return tx();
|
|
36310
|
+
}
|
|
36311
|
+
async addEvidence(input) {
|
|
36312
|
+
const tx = this.db.transaction(() => {
|
|
36313
|
+
const inv = this.investigations.getInvestigation(input.investigationId);
|
|
36314
|
+
if (!inv) throw new InvestigationNotFoundError(input.investigationId);
|
|
36315
|
+
if (input.hypothesisId) {
|
|
36316
|
+
const hyp = this.investigations.getHypothesis(input.hypothesisId);
|
|
36317
|
+
if (!hyp || hyp.investigationId !== input.investigationId) {
|
|
36318
|
+
throw new HypothesisNotFoundError(input.hypothesisId);
|
|
36319
|
+
}
|
|
36320
|
+
}
|
|
36321
|
+
return this.investigations.insertEvidence(input);
|
|
36322
|
+
});
|
|
36323
|
+
return tx();
|
|
36324
|
+
}
|
|
36325
|
+
async resolveInvestigation(id, input) {
|
|
36326
|
+
const tx = this.db.transaction(() => {
|
|
36327
|
+
const inv = this.investigations.getInvestigation(id);
|
|
36328
|
+
if (!inv) throw new InvestigationNotFoundError(id);
|
|
36329
|
+
if (inv.status === "resolved") {
|
|
36330
|
+
throw new InvestigationStatusError(id, inv.status, "already resolved");
|
|
36331
|
+
}
|
|
36332
|
+
const patternTag = input.patternTag ?? null;
|
|
36333
|
+
this.investigations.setInvestigationResolved(id, {
|
|
36334
|
+
rootCause: input.rootCause,
|
|
36335
|
+
fixSummary: input.fixSummary,
|
|
36336
|
+
patternTag
|
|
36337
|
+
});
|
|
36338
|
+
const investigation = this.investigations.getInvestigation(id);
|
|
36339
|
+
const knowledgeDraft = buildKnowledgeDraft(investigation);
|
|
36340
|
+
return { investigation, knowledgeDraft };
|
|
36341
|
+
});
|
|
36342
|
+
return tx();
|
|
36343
|
+
}
|
|
36344
|
+
};
|
|
36345
|
+
function buildKnowledgeDraft(inv) {
|
|
36346
|
+
const title = inv.patternTag ? `Gotcha: ${inv.patternTag}` : `Gotcha: ${inv.symptom.slice(0, 80)}`;
|
|
36347
|
+
const body = [
|
|
36348
|
+
`**Symptom:** ${inv.symptom}`,
|
|
36349
|
+
`**Root cause:** ${inv.rootCause ?? ""}`,
|
|
36350
|
+
`**Fix:** ${inv.fixSummary ?? ""}`,
|
|
36351
|
+
inv.patternTag ? `**Pattern tag:** ${inv.patternTag}` : null,
|
|
36352
|
+
``,
|
|
36353
|
+
`_Drafted from ${inv.id} (ADR-035). Review before committing via knowledge_create._`
|
|
36354
|
+
].filter((line) => line !== null).join("\n");
|
|
36355
|
+
return { type: "gotcha", title, body, patternTag: inv.patternTag };
|
|
36356
|
+
}
|
|
36357
|
+
|
|
36358
|
+
// src/core/domain/repositories/investigation-repository.ts
|
|
36359
|
+
function rowToInvestigation(row) {
|
|
36360
|
+
return {
|
|
36361
|
+
id: row.id,
|
|
36362
|
+
symptom: row.symptom,
|
|
36363
|
+
status: row.status,
|
|
36364
|
+
taskId: row.task_id || null,
|
|
36365
|
+
sessionId: row.session_id || null,
|
|
36366
|
+
rootCause: row.root_cause || null,
|
|
36367
|
+
fixSummary: row.fix_summary || null,
|
|
36368
|
+
patternTag: row.pattern_tag || null,
|
|
36369
|
+
createdAt: row.created_at,
|
|
36370
|
+
resolvedAt: row.resolved_at || null,
|
|
36371
|
+
hypotheses: [],
|
|
36372
|
+
evidence: []
|
|
36373
|
+
};
|
|
36374
|
+
}
|
|
36375
|
+
function rowToHypothesis(row) {
|
|
36376
|
+
return {
|
|
36377
|
+
id: row.id,
|
|
36378
|
+
investigationId: row.investigation_id,
|
|
36379
|
+
description: row.description,
|
|
36380
|
+
status: row.status,
|
|
36381
|
+
createdAt: row.created_at
|
|
36382
|
+
};
|
|
36383
|
+
}
|
|
36384
|
+
function rowToEvidence(row) {
|
|
36385
|
+
return {
|
|
36386
|
+
id: row.id,
|
|
36387
|
+
investigationId: row.investigation_id,
|
|
36388
|
+
hypothesisId: row.hypothesis_id || null,
|
|
36389
|
+
type: row.type,
|
|
36390
|
+
ref: row.ref,
|
|
36391
|
+
note: row.note || null,
|
|
36392
|
+
createdAt: row.created_at
|
|
36393
|
+
};
|
|
36394
|
+
}
|
|
36395
|
+
var InvestigationRepository = class {
|
|
36396
|
+
constructor(db, counters) {
|
|
36397
|
+
this.db = db;
|
|
36398
|
+
this.counters = counters;
|
|
36399
|
+
}
|
|
36400
|
+
db;
|
|
36401
|
+
counters;
|
|
36402
|
+
nextId(prefix, entity) {
|
|
36403
|
+
return `${prefix}-${String(this.counters.nextNumber(entity)).padStart(3, "0")}`;
|
|
36404
|
+
}
|
|
36405
|
+
insertInvestigation(input) {
|
|
36406
|
+
const ts = now();
|
|
36407
|
+
const id = this.nextId("INV", "investigation");
|
|
36408
|
+
this.db.prepare(
|
|
36409
|
+
`INSERT INTO investigations (id, symptom, status, task_id, session_id, created_at)
|
|
36410
|
+
VALUES (?, ?, 'exploring', ?, ?, ?)`
|
|
36411
|
+
).run(id, input.symptom, input.taskId ?? null, input.sessionId ?? null, ts);
|
|
36412
|
+
return this.getInvestigation(id);
|
|
36413
|
+
}
|
|
36414
|
+
// Nested read: investigation row + all hypotheses + all evidence (AC-5).
|
|
36415
|
+
getInvestigation(id) {
|
|
36416
|
+
const row = this.db.prepare("SELECT * FROM investigations WHERE id = ?").get(id);
|
|
36417
|
+
if (!row) return null;
|
|
36418
|
+
const investigation = rowToInvestigation(row);
|
|
36419
|
+
investigation.hypotheses = this.db.prepare("SELECT * FROM hypotheses WHERE investigation_id = ? ORDER BY created_at, rowid").all(id).map(rowToHypothesis);
|
|
36420
|
+
investigation.evidence = this.db.prepare("SELECT * FROM evidence WHERE investigation_id = ? ORDER BY created_at, rowid").all(id).map(rowToEvidence);
|
|
36421
|
+
return investigation;
|
|
36422
|
+
}
|
|
36423
|
+
setInvestigationResolved(id, fields) {
|
|
36424
|
+
this.db.prepare(
|
|
36425
|
+
`UPDATE investigations
|
|
36426
|
+
SET status = 'resolved', root_cause = ?, fix_summary = ?, pattern_tag = ?, resolved_at = ?
|
|
36427
|
+
WHERE id = ?`
|
|
36428
|
+
).run(fields.rootCause, fields.fixSummary, fields.patternTag, now(), id);
|
|
36429
|
+
}
|
|
36430
|
+
insertHypothesis(investigationId, description) {
|
|
36431
|
+
const id = this.nextId("HYP", "hypothesis");
|
|
36432
|
+
this.db.prepare(
|
|
36433
|
+
`INSERT INTO hypotheses (id, investigation_id, description, status, created_at)
|
|
36434
|
+
VALUES (?, ?, ?, 'testing', ?)`
|
|
36435
|
+
).run(id, investigationId, description, now());
|
|
36436
|
+
return this.getHypothesis(id);
|
|
36437
|
+
}
|
|
36438
|
+
getHypothesis(id) {
|
|
36439
|
+
const row = this.db.prepare("SELECT * FROM hypotheses WHERE id = ?").get(id);
|
|
36440
|
+
return row ? rowToHypothesis(row) : null;
|
|
36441
|
+
}
|
|
36442
|
+
setHypothesisStatus(id, status) {
|
|
36443
|
+
this.db.prepare("UPDATE hypotheses SET status = ? WHERE id = ?").run(status, id);
|
|
36444
|
+
}
|
|
36445
|
+
insertEvidence(input) {
|
|
36446
|
+
const id = this.nextId("EVID", "evidence");
|
|
36447
|
+
this.db.prepare(
|
|
36448
|
+
`INSERT INTO evidence (id, investigation_id, hypothesis_id, type, ref, note, created_at)
|
|
36449
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
36450
|
+
).run(
|
|
36451
|
+
id,
|
|
36452
|
+
input.investigationId,
|
|
36453
|
+
input.hypothesisId ?? null,
|
|
36454
|
+
input.type,
|
|
36455
|
+
input.ref,
|
|
36456
|
+
input.note ?? null,
|
|
36457
|
+
now()
|
|
36458
|
+
);
|
|
36459
|
+
return this.getEvidence(id);
|
|
36460
|
+
}
|
|
36461
|
+
getEvidence(id) {
|
|
36462
|
+
const row = this.db.prepare("SELECT * FROM evidence WHERE id = ?").get(id);
|
|
36463
|
+
return row ? rowToEvidence(row) : null;
|
|
36464
|
+
}
|
|
36465
|
+
};
|
|
36466
|
+
|
|
36108
36467
|
// src/core/domain/knowledge-service.ts
|
|
36109
36468
|
var fs = __toESM(require("fs"));
|
|
36110
36469
|
var path2 = __toESM(require("path"));
|
|
@@ -36142,6 +36501,17 @@ var GitOpsImpl = class {
|
|
|
36142
36501
|
if (out === "") return [];
|
|
36143
36502
|
return out.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
36144
36503
|
}
|
|
36504
|
+
commitsInWindow(cwd, sinceIso, grepTaskId) {
|
|
36505
|
+
const args = ["log", `--since=${sinceIso}`, "--format=%h %s"];
|
|
36506
|
+
if (grepTaskId) args.push(`--grep=${grepTaskId}`);
|
|
36507
|
+
try {
|
|
36508
|
+
const out = runGit(cwd, args).trim();
|
|
36509
|
+
if (out === "") return [];
|
|
36510
|
+
return out.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
36511
|
+
} catch {
|
|
36512
|
+
return [];
|
|
36513
|
+
}
|
|
36514
|
+
}
|
|
36145
36515
|
};
|
|
36146
36516
|
function runGit(cwd, args) {
|
|
36147
36517
|
try {
|
|
@@ -36158,11 +36528,23 @@ var KNOWLEDGE_TYPES = [
|
|
|
36158
36528
|
"decision",
|
|
36159
36529
|
"postmortem",
|
|
36160
36530
|
"learning",
|
|
36161
|
-
"evaluation"
|
|
36531
|
+
"evaluation",
|
|
36532
|
+
"feature",
|
|
36533
|
+
"code_ref",
|
|
36534
|
+
"gotcha"
|
|
36162
36535
|
];
|
|
36163
36536
|
var KNOWLEDGE_SCOPES = ["project", "cross"];
|
|
36537
|
+
var EFFORT_BANDS = ["S", "M", "L", "XL"];
|
|
36538
|
+
var FEATURE_STATUSES = [
|
|
36539
|
+
"planned",
|
|
36540
|
+
"in-progress",
|
|
36541
|
+
"shipped",
|
|
36542
|
+
"blocked"
|
|
36543
|
+
];
|
|
36164
36544
|
|
|
36165
36545
|
// src/core/domain/knowledge-frontmatter.ts
|
|
36546
|
+
var STRUCTURED_SCALAR_KEYS = ["anchorTaskId", "effortBand", "status", "affectedFeatureId"];
|
|
36547
|
+
var STRUCTURED_LIST_KEYS = ["realizesTasks", "inWorkspaces"];
|
|
36166
36548
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
36167
36549
|
var FrontmatterParseError = class extends Error {
|
|
36168
36550
|
constructor(message) {
|
|
@@ -36176,6 +36558,7 @@ function parseFrontmatter(raw) {
|
|
|
36176
36558
|
const fmText = m[1];
|
|
36177
36559
|
const body = m[2] ?? "";
|
|
36178
36560
|
const fm = { refs: [] };
|
|
36561
|
+
const structured = {};
|
|
36179
36562
|
const lines = fmText.split(/\r?\n/);
|
|
36180
36563
|
let i = 0;
|
|
36181
36564
|
while (i < lines.length) {
|
|
@@ -36199,11 +36582,42 @@ function parseFrontmatter(raw) {
|
|
|
36199
36582
|
const kv = line.match(/^([a-zA-Z]+):\s*(.*)$/);
|
|
36200
36583
|
if (!kv) throw new FrontmatterParseError(`unrecognized line: ${line}`);
|
|
36201
36584
|
const key = kv[1];
|
|
36202
|
-
const
|
|
36203
|
-
|
|
36585
|
+
const rawValue = kv[2].trim();
|
|
36586
|
+
if (assignStructured(structured, key, rawValue)) {
|
|
36587
|
+
i++;
|
|
36588
|
+
continue;
|
|
36589
|
+
}
|
|
36590
|
+
assignScalar(fm, key, unquote(rawValue));
|
|
36204
36591
|
i++;
|
|
36205
36592
|
}
|
|
36206
|
-
return { frontmatter: validate(fm), body };
|
|
36593
|
+
return { frontmatter: validate(fm, structured), body };
|
|
36594
|
+
}
|
|
36595
|
+
function assignStructured(s, key, rawValue) {
|
|
36596
|
+
if (STRUCTURED_SCALAR_KEYS.includes(key)) {
|
|
36597
|
+
const value = unquote(rawValue);
|
|
36598
|
+
if (key === "effortBand") s.effortBand = value;
|
|
36599
|
+
else if (key === "status") s.status = value;
|
|
36600
|
+
else if (key === "anchorTaskId") s.anchorTaskId = value;
|
|
36601
|
+
else if (key === "affectedFeatureId") s.affectedFeatureId = value;
|
|
36602
|
+
return true;
|
|
36603
|
+
}
|
|
36604
|
+
if (STRUCTURED_LIST_KEYS.includes(key)) {
|
|
36605
|
+
const list = parseInlineList(rawValue);
|
|
36606
|
+
if (key === "realizesTasks") s.realizesTasks = list;
|
|
36607
|
+
else if (key === "inWorkspaces") s.inWorkspaces = list;
|
|
36608
|
+
return true;
|
|
36609
|
+
}
|
|
36610
|
+
return false;
|
|
36611
|
+
}
|
|
36612
|
+
function parseInlineList(raw) {
|
|
36613
|
+
const trimmed = raw.trim();
|
|
36614
|
+
if (trimmed === "[]" || trimmed === "") return [];
|
|
36615
|
+
try {
|
|
36616
|
+
const parsed = JSON.parse(trimmed);
|
|
36617
|
+
if (Array.isArray(parsed)) return parsed.map((v) => String(v));
|
|
36618
|
+
} catch {
|
|
36619
|
+
}
|
|
36620
|
+
return trimmed.replace(/^\[|\]$/g, "").split(",").map((v) => unquote(v.trim())).filter((v) => v.length > 0);
|
|
36207
36621
|
}
|
|
36208
36622
|
function parseRefsBlock(lines, startIdx) {
|
|
36209
36623
|
const out = [];
|
|
@@ -36266,7 +36680,7 @@ function assignScalar(fm, key, value) {
|
|
|
36266
36680
|
throw new FrontmatterParseError(`unknown key: ${key}`);
|
|
36267
36681
|
}
|
|
36268
36682
|
}
|
|
36269
|
-
function validate(fm) {
|
|
36683
|
+
function validate(fm, structured) {
|
|
36270
36684
|
if (!fm.type || !KNOWLEDGE_TYPES.includes(fm.type)) {
|
|
36271
36685
|
throw new FrontmatterParseError(`invalid type: ${fm.type}`);
|
|
36272
36686
|
}
|
|
@@ -36277,6 +36691,13 @@ function validate(fm) {
|
|
|
36277
36691
|
if (!fm.projectId) throw new FrontmatterParseError("missing projectId");
|
|
36278
36692
|
if (!fm.createdAt) throw new FrontmatterParseError("missing createdAt");
|
|
36279
36693
|
if (!fm.lastVerifiedAt) throw new FrontmatterParseError("missing lastVerifiedAt");
|
|
36694
|
+
if (structured.effortBand && !EFFORT_BANDS.includes(structured.effortBand)) {
|
|
36695
|
+
throw new FrontmatterParseError(`invalid effortBand: ${structured.effortBand}`);
|
|
36696
|
+
}
|
|
36697
|
+
if (structured.status && !FEATURE_STATUSES.includes(structured.status)) {
|
|
36698
|
+
throw new FrontmatterParseError(`invalid status: ${structured.status}`);
|
|
36699
|
+
}
|
|
36700
|
+
const hasStructured = Object.values(structured).some((v) => v !== void 0);
|
|
36280
36701
|
return {
|
|
36281
36702
|
type: fm.type,
|
|
36282
36703
|
title: fm.title,
|
|
@@ -36285,7 +36706,8 @@ function validate(fm) {
|
|
|
36285
36706
|
scope: fm.scope,
|
|
36286
36707
|
refs: fm.refs ?? [],
|
|
36287
36708
|
createdAt: fm.createdAt,
|
|
36288
|
-
lastVerifiedAt: fm.lastVerifiedAt
|
|
36709
|
+
lastVerifiedAt: fm.lastVerifiedAt,
|
|
36710
|
+
structured: hasStructured ? structured : void 0
|
|
36289
36711
|
};
|
|
36290
36712
|
}
|
|
36291
36713
|
function serializeFrontmatter(fm, body) {
|
|
@@ -36306,6 +36728,19 @@ function serializeFrontmatter(fm, body) {
|
|
|
36306
36728
|
}
|
|
36307
36729
|
lines.push(`createdAt: ${fm.createdAt}`);
|
|
36308
36730
|
lines.push(`lastVerifiedAt: ${fm.lastVerifiedAt}`);
|
|
36731
|
+
const s = fm.structured;
|
|
36732
|
+
if (s) {
|
|
36733
|
+
if (s.anchorTaskId) lines.push(`anchorTaskId: ${s.anchorTaskId}`);
|
|
36734
|
+
if (s.realizesTasks && s.realizesTasks.length > 0) {
|
|
36735
|
+
lines.push(`realizesTasks: ${JSON.stringify(s.realizesTasks)}`);
|
|
36736
|
+
}
|
|
36737
|
+
if (s.inWorkspaces && s.inWorkspaces.length > 0) {
|
|
36738
|
+
lines.push(`inWorkspaces: ${JSON.stringify(s.inWorkspaces)}`);
|
|
36739
|
+
}
|
|
36740
|
+
if (s.effortBand) lines.push(`effortBand: ${s.effortBand}`);
|
|
36741
|
+
if (s.status) lines.push(`status: ${s.status}`);
|
|
36742
|
+
if (s.affectedFeatureId) lines.push(`affectedFeatureId: ${s.affectedFeatureId}`);
|
|
36743
|
+
}
|
|
36309
36744
|
lines.push("---");
|
|
36310
36745
|
const trimmedBody = body.replace(/^\r?\n+/, "");
|
|
36311
36746
|
return lines.join("\n") + "\n\n" + trimmedBody + (trimmedBody.endsWith("\n") ? "" : "\n");
|
|
@@ -36358,6 +36793,7 @@ var KnowledgeService = class {
|
|
|
36358
36793
|
now;
|
|
36359
36794
|
embeddingStore;
|
|
36360
36795
|
embeddingProvider;
|
|
36796
|
+
edges;
|
|
36361
36797
|
constructor(deps) {
|
|
36362
36798
|
this.knowledge = deps.knowledge;
|
|
36363
36799
|
this.projects = deps.projects;
|
|
@@ -36367,11 +36803,13 @@ var KnowledgeService = class {
|
|
|
36367
36803
|
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
36368
36804
|
this.embeddingStore = deps.embeddingStore ?? null;
|
|
36369
36805
|
this.embeddingProvider = deps.embeddingProvider ?? null;
|
|
36806
|
+
this.edges = deps.edges ?? null;
|
|
36370
36807
|
}
|
|
36371
36808
|
async createKnowledge(input) {
|
|
36372
36809
|
this.validateInput(input);
|
|
36373
36810
|
const project = await this.projects.get(input.projectId);
|
|
36374
36811
|
if (!project) throw new KnowledgeValidationError(`unknown projectId: ${input.projectId}`);
|
|
36812
|
+
await this.validateStructured(input);
|
|
36375
36813
|
const workspaceCwd = await this.resolveWorkspaceCwd(input.projectId, input.workspaceId);
|
|
36376
36814
|
const slug = input.slug ?? slugify2(input.title);
|
|
36377
36815
|
if (!slug) throw new KnowledgeValidationError("cannot derive slug from title");
|
|
@@ -36398,7 +36836,8 @@ var KnowledgeService = class {
|
|
|
36398
36836
|
scope: input.scope,
|
|
36399
36837
|
refs,
|
|
36400
36838
|
createdAt: isoDate,
|
|
36401
|
-
lastVerifiedAt: isoDate
|
|
36839
|
+
lastVerifiedAt: isoDate,
|
|
36840
|
+
structured: input.structured
|
|
36402
36841
|
};
|
|
36403
36842
|
const content = serializeFrontmatter(frontmatter, input.body);
|
|
36404
36843
|
fs.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
@@ -36422,6 +36861,7 @@ var KnowledgeService = class {
|
|
|
36422
36861
|
await this.regenerateIndexMd(input.projectId, project.cwd);
|
|
36423
36862
|
}
|
|
36424
36863
|
}
|
|
36864
|
+
await this.syncEdgesOnCreate(slug, input.type, input.structured);
|
|
36425
36865
|
this.scheduleEmbed(slug, input.body);
|
|
36426
36866
|
const staleness = this.computeStaleness(refs, stalenessCwd, input.scope);
|
|
36427
36867
|
return {
|
|
@@ -36433,6 +36873,25 @@ var KnowledgeService = class {
|
|
|
36433
36873
|
isStale: staleness.some((s) => s.commitsSince > 0)
|
|
36434
36874
|
};
|
|
36435
36875
|
}
|
|
36876
|
+
// TASK-992: derive ADR-NNN Pillar 3 edges from a new entry's structured
|
|
36877
|
+
// frontmatter so the relationships table stays current without re-running the
|
|
36878
|
+
// migration. feature → REALIZES (task→feature) + IN (feature→workspace);
|
|
36879
|
+
// gotcha → ABOUT (gotcha→feature). Edges are idempotent (INSERT OR IGNORE).
|
|
36880
|
+
// Only createKnowledge calls this — updateKnowledge cannot change structured
|
|
36881
|
+
// fields (it spreads the existing frontmatter), so there is nothing to re-sync.
|
|
36882
|
+
async syncEdgesOnCreate(slug, type, structured) {
|
|
36883
|
+
if (!this.edges || !structured) return;
|
|
36884
|
+
if (type === "feature") {
|
|
36885
|
+
for (const taskId of structured.realizesTasks ?? []) {
|
|
36886
|
+
await this.edges.add(taskId, slug, "REALIZES");
|
|
36887
|
+
}
|
|
36888
|
+
for (const ws of structured.inWorkspaces ?? []) {
|
|
36889
|
+
await this.edges.add(slug, ws, "IN");
|
|
36890
|
+
}
|
|
36891
|
+
} else if (type === "gotcha" && structured.affectedFeatureId) {
|
|
36892
|
+
await this.edges.add(slug, structured.affectedFeatureId, "ABOUT");
|
|
36893
|
+
}
|
|
36894
|
+
}
|
|
36436
36895
|
async registerExistingKnowledge(input) {
|
|
36437
36896
|
if (!fs.existsSync(input.filePath)) {
|
|
36438
36897
|
throw new KnowledgeValidationError(`file not found: ${input.filePath}`);
|
|
@@ -36634,6 +37093,25 @@ var KnowledgeService = class {
|
|
|
36634
37093
|
}));
|
|
36635
37094
|
return { slug, refs: staleness, isStale: false, lastVerifiedAt: isoDate };
|
|
36636
37095
|
}
|
|
37096
|
+
// TASK-988: structured-field validation for the first-class types. A gotcha's
|
|
37097
|
+
// affectedFeatureId must resolve to an existing `feature` entry — otherwise the
|
|
37098
|
+
// ABOUT edge dangles. Other structured fields (effortBand/status enums) are
|
|
37099
|
+
// validated in the frontmatter layer.
|
|
37100
|
+
async validateStructured(input) {
|
|
37101
|
+
const featureId = input.structured?.affectedFeatureId;
|
|
37102
|
+
if (!featureId) return;
|
|
37103
|
+
const target = await this.knowledge.get(featureId);
|
|
37104
|
+
if (!target) {
|
|
37105
|
+
throw new KnowledgeValidationError(
|
|
37106
|
+
`affectedFeatureId "${featureId}" does not resolve to a known knowledge entry`
|
|
37107
|
+
);
|
|
37108
|
+
}
|
|
37109
|
+
if (target.type !== "feature") {
|
|
37110
|
+
throw new KnowledgeValidationError(
|
|
37111
|
+
`affectedFeatureId "${featureId}" is a ${target.type}, expected a feature`
|
|
37112
|
+
);
|
|
37113
|
+
}
|
|
37114
|
+
}
|
|
36637
37115
|
validateInput(input) {
|
|
36638
37116
|
if (!KNOWLEDGE_TYPES.includes(input.type)) {
|
|
36639
37117
|
throw new KnowledgeValidationError(`invalid type: ${input.type}`);
|
|
@@ -36887,18 +37365,168 @@ var KnowledgeRepository = class {
|
|
|
36887
37365
|
}
|
|
36888
37366
|
};
|
|
36889
37367
|
|
|
37368
|
+
// src/core/domain/repositories/code-ref-repository.ts
|
|
37369
|
+
function rowToCodeRef(row) {
|
|
37370
|
+
return {
|
|
37371
|
+
slug: row.slug,
|
|
37372
|
+
projectId: row.project_id,
|
|
37373
|
+
workspaceId: row.workspace_id ?? null,
|
|
37374
|
+
path: row.path,
|
|
37375
|
+
symbol: row.symbol ?? null,
|
|
37376
|
+
lineHint: row.line_hint ?? null,
|
|
37377
|
+
commitSha: row.commit_sha ?? null,
|
|
37378
|
+
createdAt: row.created_at,
|
|
37379
|
+
lastVerifiedAt: row.last_verified_at
|
|
37380
|
+
};
|
|
37381
|
+
}
|
|
37382
|
+
var CodeRefRepository = class {
|
|
37383
|
+
constructor(db) {
|
|
37384
|
+
this.db = db;
|
|
37385
|
+
}
|
|
37386
|
+
db;
|
|
37387
|
+
// Identity = (project_id, path, COALESCE(symbol,'')). A write matching an
|
|
37388
|
+
// existing identity re-pins commit_sha / line_hint / last_verified_at on the
|
|
37389
|
+
// ORIGINAL slug instead of inserting a second row (ADR Pillar 2c). The slug
|
|
37390
|
+
// supplied on such a write is ignored in favour of the existing one.
|
|
37391
|
+
upsert(input, nowIso) {
|
|
37392
|
+
const existing = this.getByIdentity(input.projectId, input.path, input.symbol ?? null);
|
|
37393
|
+
if (existing) {
|
|
37394
|
+
this.db.prepare(
|
|
37395
|
+
`UPDATE code_refs
|
|
37396
|
+
SET commit_sha = ?, line_hint = ?, workspace_id = ?, last_verified_at = ?
|
|
37397
|
+
WHERE slug = ?`
|
|
37398
|
+
).run(
|
|
37399
|
+
input.commitSha ?? existing.commitSha,
|
|
37400
|
+
input.lineHint ?? existing.lineHint,
|
|
37401
|
+
input.workspaceId ?? existing.workspaceId,
|
|
37402
|
+
nowIso,
|
|
37403
|
+
existing.slug
|
|
37404
|
+
);
|
|
37405
|
+
return this.get(existing.slug);
|
|
37406
|
+
}
|
|
37407
|
+
this.db.prepare(
|
|
37408
|
+
`INSERT INTO code_refs
|
|
37409
|
+
(slug, project_id, workspace_id, path, symbol, line_hint, commit_sha, created_at, last_verified_at)
|
|
37410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
37411
|
+
).run(
|
|
37412
|
+
input.slug,
|
|
37413
|
+
input.projectId,
|
|
37414
|
+
input.workspaceId ?? null,
|
|
37415
|
+
input.path,
|
|
37416
|
+
input.symbol ?? null,
|
|
37417
|
+
input.lineHint ?? null,
|
|
37418
|
+
input.commitSha ?? null,
|
|
37419
|
+
nowIso,
|
|
37420
|
+
nowIso
|
|
37421
|
+
);
|
|
37422
|
+
return this.get(input.slug);
|
|
37423
|
+
}
|
|
37424
|
+
get(slug) {
|
|
37425
|
+
const row = this.db.prepare("SELECT * FROM code_refs WHERE slug = ?").get(slug);
|
|
37426
|
+
return row ? rowToCodeRef(row) : null;
|
|
37427
|
+
}
|
|
37428
|
+
getByIdentity(projectId, path9, symbol2) {
|
|
37429
|
+
const row = this.db.prepare(
|
|
37430
|
+
"SELECT * FROM code_refs WHERE project_id = ? AND path = ? AND COALESCE(symbol, '') = COALESCE(?, '')"
|
|
37431
|
+
).get(projectId, path9, symbol2);
|
|
37432
|
+
return row ? rowToCodeRef(row) : null;
|
|
37433
|
+
}
|
|
37434
|
+
// Prefix query over the dotted symbol (ADR Pillar 2c): e.g. all Domain-layer
|
|
37435
|
+
// refs via symbolPrefix = 'Ichiba.Pim.TradingCatalog.Domain.'. Falls back to
|
|
37436
|
+
// a path filter, or lists the whole project when neither is given.
|
|
37437
|
+
listByPrefix(filter) {
|
|
37438
|
+
const where = ["project_id = ?"];
|
|
37439
|
+
const params = [filter.projectId];
|
|
37440
|
+
if (filter.symbolPrefix) {
|
|
37441
|
+
where.push("symbol LIKE ? ESCAPE ?");
|
|
37442
|
+
params.push(`${escapeLike(filter.symbolPrefix)}%`, "\\");
|
|
37443
|
+
}
|
|
37444
|
+
if (filter.path) {
|
|
37445
|
+
where.push("path = ?");
|
|
37446
|
+
params.push(filter.path);
|
|
37447
|
+
}
|
|
37448
|
+
const rows = this.db.prepare(`SELECT * FROM code_refs WHERE ${where.join(" AND ")} ORDER BY symbol, path`).all(...params);
|
|
37449
|
+
return rows.map(rowToCodeRef);
|
|
37450
|
+
}
|
|
37451
|
+
delete(slug) {
|
|
37452
|
+
this.db.prepare("DELETE FROM task_code_refs WHERE code_ref_slug = ?").run(slug);
|
|
37453
|
+
this.db.prepare("DELETE FROM code_refs WHERE slug = ?").run(slug);
|
|
37454
|
+
}
|
|
37455
|
+
// ── TOUCHES edges ──────────────────────────────────────────────────────────
|
|
37456
|
+
addTouches(taskId, codeRefSlug, relation) {
|
|
37457
|
+
this.db.prepare(
|
|
37458
|
+
`INSERT INTO task_code_refs (task_id, code_ref_slug, relation)
|
|
37459
|
+
VALUES (?, ?, ?)
|
|
37460
|
+
ON CONFLICT(task_id, code_ref_slug) DO UPDATE SET relation = excluded.relation`
|
|
37461
|
+
).run(taskId, codeRefSlug, relation);
|
|
37462
|
+
}
|
|
37463
|
+
removeTouches(taskId, codeRefSlug) {
|
|
37464
|
+
this.db.prepare("DELETE FROM task_code_refs WHERE task_id = ? AND code_ref_slug = ?").run(taskId, codeRefSlug);
|
|
37465
|
+
}
|
|
37466
|
+
getTouchesForTask(taskId) {
|
|
37467
|
+
const rows = this.db.prepare("SELECT * FROM task_code_refs WHERE task_id = ? ORDER BY code_ref_slug").all(taskId);
|
|
37468
|
+
return rows.map((r) => ({
|
|
37469
|
+
taskId: r.task_id,
|
|
37470
|
+
codeRefSlug: r.code_ref_slug,
|
|
37471
|
+
relation: r.relation
|
|
37472
|
+
}));
|
|
37473
|
+
}
|
|
37474
|
+
getTouchesForCodeRef(codeRefSlug) {
|
|
37475
|
+
const rows = this.db.prepare("SELECT * FROM task_code_refs WHERE code_ref_slug = ? ORDER BY task_id").all(codeRefSlug);
|
|
37476
|
+
return rows.map((r) => ({
|
|
37477
|
+
taskId: r.task_id,
|
|
37478
|
+
codeRefSlug: r.code_ref_slug,
|
|
37479
|
+
relation: r.relation
|
|
37480
|
+
}));
|
|
37481
|
+
}
|
|
37482
|
+
};
|
|
37483
|
+
function escapeLike(s) {
|
|
37484
|
+
return s.replace(/[\\%_]/g, (m) => `\\${m}`);
|
|
37485
|
+
}
|
|
37486
|
+
|
|
37487
|
+
// src/core/sync/lamport-clock.ts
|
|
37488
|
+
function createSyncClockTables(db) {
|
|
37489
|
+
db.exec(`
|
|
37490
|
+
CREATE TABLE IF NOT EXISTS _sync_clock (
|
|
37491
|
+
id INTEGER PRIMARY KEY CHECK (id = 0),
|
|
37492
|
+
counter INTEGER NOT NULL DEFAULT 0
|
|
37493
|
+
)
|
|
37494
|
+
`);
|
|
37495
|
+
db.exec("INSERT OR IGNORE INTO _sync_clock (id, counter) VALUES (0, 0)");
|
|
37496
|
+
db.exec(`
|
|
37497
|
+
CREATE TABLE IF NOT EXISTS _sync_state (
|
|
37498
|
+
id INTEGER PRIMARY KEY CHECK (id = 0),
|
|
37499
|
+
last_pull_at INTEGER NOT NULL DEFAULT 0
|
|
37500
|
+
)
|
|
37501
|
+
`);
|
|
37502
|
+
db.exec("INSERT OR IGNORE INTO _sync_state (id, last_pull_at) VALUES (0, 0)");
|
|
37503
|
+
}
|
|
37504
|
+
|
|
36890
37505
|
// src/core/domain/repositories/schema.ts
|
|
36891
|
-
var SCHEMA_VERSION =
|
|
37506
|
+
var SCHEMA_VERSION = 6;
|
|
36892
37507
|
function initSchema(db) {
|
|
36893
37508
|
createCoreTables(db);
|
|
36894
37509
|
runLegacyMigrations(db);
|
|
36895
37510
|
createM1Tables(db);
|
|
36896
37511
|
createM2Tables(db);
|
|
36897
|
-
|
|
37512
|
+
createInvestigationTables(db);
|
|
37513
|
+
dropLegacyOAuthTables(db);
|
|
37514
|
+
addSyncColumns(db);
|
|
37515
|
+
createSyncClockTables(db);
|
|
36898
37516
|
createIndexes(db);
|
|
36899
37517
|
cleanupPoisonedTaskIds(db);
|
|
36900
37518
|
seedSchemaVersion(db);
|
|
36901
37519
|
}
|
|
37520
|
+
function addSyncColumns(db) {
|
|
37521
|
+
for (const table of SYNCABLE_TABLES) {
|
|
37522
|
+
for (const col of SYNC_COLUMNS) {
|
|
37523
|
+
try {
|
|
37524
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.sqliteType}`);
|
|
37525
|
+
} catch {
|
|
37526
|
+
}
|
|
37527
|
+
}
|
|
37528
|
+
}
|
|
37529
|
+
}
|
|
36902
37530
|
function seedSchemaVersion(db) {
|
|
36903
37531
|
db.exec(`
|
|
36904
37532
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -37030,6 +37658,10 @@ function runLegacyMigrations(db) {
|
|
|
37030
37658
|
db.exec("ALTER TABLE sessions ADD COLUMN checkpoint_at TEXT");
|
|
37031
37659
|
} catch {
|
|
37032
37660
|
}
|
|
37661
|
+
try {
|
|
37662
|
+
db.exec("ALTER TABLE sessions ADD COLUMN cc_session_id TEXT");
|
|
37663
|
+
} catch {
|
|
37664
|
+
}
|
|
37033
37665
|
try {
|
|
37034
37666
|
db.exec("ALTER TABLE workspaces ADD COLUMN archived_at TEXT");
|
|
37035
37667
|
} catch {
|
|
@@ -37077,6 +37709,34 @@ function migrateSessionsStatus(db) {
|
|
|
37077
37709
|
db.exec("DROP TABLE sessions");
|
|
37078
37710
|
db.exec("ALTER TABLE sessions_new RENAME TO sessions");
|
|
37079
37711
|
}
|
|
37712
|
+
function migrateKnowledgeTypeCheck(db) {
|
|
37713
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='knowledge_index'").get();
|
|
37714
|
+
if (!row?.sql) return;
|
|
37715
|
+
if (row.sql.includes("'feature'")) return;
|
|
37716
|
+
const cols = db.pragma("table_info(knowledge_index)").map(
|
|
37717
|
+
(c) => c.name
|
|
37718
|
+
);
|
|
37719
|
+
const colList = cols.join(", ");
|
|
37720
|
+
db.exec(`
|
|
37721
|
+
CREATE TABLE knowledge_index_new (
|
|
37722
|
+
slug TEXT PRIMARY KEY,
|
|
37723
|
+
project_id TEXT NOT NULL,
|
|
37724
|
+
scope TEXT NOT NULL CHECK (scope IN ('project','cross')),
|
|
37725
|
+
type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation','feature','code_ref','gotcha')),
|
|
37726
|
+
title TEXT NOT NULL,
|
|
37727
|
+
file_path TEXT NOT NULL,
|
|
37728
|
+
created_at TEXT NOT NULL,
|
|
37729
|
+
last_verified_at TEXT NOT NULL,
|
|
37730
|
+
embedding_provider_id TEXT,
|
|
37731
|
+
embedding_dims INTEGER,
|
|
37732
|
+
workspace_id TEXT REFERENCES workspaces(id),
|
|
37733
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
37734
|
+
)
|
|
37735
|
+
`);
|
|
37736
|
+
db.exec(`INSERT INTO knowledge_index_new (${colList}) SELECT ${colList} FROM knowledge_index`);
|
|
37737
|
+
db.exec("DROP TABLE knowledge_index");
|
|
37738
|
+
db.exec("ALTER TABLE knowledge_index_new RENAME TO knowledge_index");
|
|
37739
|
+
}
|
|
37080
37740
|
var COUNTER_SANE_MAX = 1e5;
|
|
37081
37741
|
function seedGlobalCounter(db, entityType, selectIdSql, prefixLen) {
|
|
37082
37742
|
let max = 0;
|
|
@@ -37217,6 +37877,7 @@ function createM1Tables(db) {
|
|
|
37217
37877
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37218
37878
|
checkpoint TEXT,
|
|
37219
37879
|
checkpoint_at TEXT,
|
|
37880
|
+
cc_session_id TEXT,
|
|
37220
37881
|
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
37221
37882
|
)
|
|
37222
37883
|
`);
|
|
@@ -37304,6 +37965,7 @@ function createM1Tables(db) {
|
|
|
37304
37965
|
CREATE TABLE IF NOT EXISTS inbox_items (
|
|
37305
37966
|
id TEXT PRIMARY KEY,
|
|
37306
37967
|
project_id TEXT,
|
|
37968
|
+
workspace_id TEXT,
|
|
37307
37969
|
content TEXT NOT NULL,
|
|
37308
37970
|
status TEXT NOT NULL DEFAULT 'raw',
|
|
37309
37971
|
linked_task_id TEXT,
|
|
@@ -37311,12 +37973,16 @@ function createM1Tables(db) {
|
|
|
37311
37973
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
37312
37974
|
)
|
|
37313
37975
|
`);
|
|
37976
|
+
try {
|
|
37977
|
+
db.exec("ALTER TABLE inbox_items ADD COLUMN workspace_id TEXT");
|
|
37978
|
+
} catch {
|
|
37979
|
+
}
|
|
37314
37980
|
db.exec(`
|
|
37315
37981
|
CREATE TABLE IF NOT EXISTS knowledge_index (
|
|
37316
37982
|
slug TEXT PRIMARY KEY,
|
|
37317
37983
|
project_id TEXT NOT NULL,
|
|
37318
37984
|
scope TEXT NOT NULL CHECK (scope IN ('project','cross')),
|
|
37319
|
-
type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation')),
|
|
37985
|
+
type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation','feature','code_ref','gotcha')),
|
|
37320
37986
|
title TEXT NOT NULL,
|
|
37321
37987
|
file_path TEXT NOT NULL,
|
|
37322
37988
|
created_at TEXT NOT NULL,
|
|
@@ -37338,6 +38004,37 @@ function createM1Tables(db) {
|
|
|
37338
38004
|
db.exec("ALTER TABLE knowledge_index ADD COLUMN workspace_id TEXT REFERENCES workspaces(id)");
|
|
37339
38005
|
} catch {
|
|
37340
38006
|
}
|
|
38007
|
+
migrateKnowledgeTypeCheck(db);
|
|
38008
|
+
db.exec(`
|
|
38009
|
+
CREATE TABLE IF NOT EXISTS code_refs (
|
|
38010
|
+
slug TEXT PRIMARY KEY,
|
|
38011
|
+
project_id TEXT NOT NULL,
|
|
38012
|
+
workspace_id TEXT,
|
|
38013
|
+
path TEXT NOT NULL,
|
|
38014
|
+
symbol TEXT,
|
|
38015
|
+
line_hint INTEGER,
|
|
38016
|
+
commit_sha TEXT,
|
|
38017
|
+
created_at TEXT NOT NULL,
|
|
38018
|
+
last_verified_at TEXT NOT NULL,
|
|
38019
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
38020
|
+
)
|
|
38021
|
+
`);
|
|
38022
|
+
db.exec(
|
|
38023
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_code_refs_identity ON code_refs(project_id, path, COALESCE(symbol, ''))"
|
|
38024
|
+
);
|
|
38025
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_code_refs_symbol ON code_refs(project_id, symbol)");
|
|
38026
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_code_refs_path ON code_refs(project_id, path)");
|
|
38027
|
+
db.exec(`
|
|
38028
|
+
CREATE TABLE IF NOT EXISTS task_code_refs (
|
|
38029
|
+
task_id TEXT NOT NULL,
|
|
38030
|
+
code_ref_slug TEXT NOT NULL,
|
|
38031
|
+
relation TEXT NOT NULL CHECK (relation IN ('modifies','reference')),
|
|
38032
|
+
PRIMARY KEY (task_id, code_ref_slug),
|
|
38033
|
+
FOREIGN KEY (code_ref_slug) REFERENCES code_refs(slug)
|
|
38034
|
+
)
|
|
38035
|
+
`);
|
|
38036
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_code_refs_task ON task_code_refs(task_id)");
|
|
38037
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_code_refs_slug ON task_code_refs(code_ref_slug)");
|
|
37341
38038
|
db.exec(`
|
|
37342
38039
|
CREATE TABLE IF NOT EXISTS tool_invocations (
|
|
37343
38040
|
id INTEGER PRIMARY KEY,
|
|
@@ -37377,37 +38074,52 @@ function createM2Tables(db) {
|
|
|
37377
38074
|
)
|
|
37378
38075
|
`);
|
|
37379
38076
|
}
|
|
37380
|
-
function
|
|
38077
|
+
function createInvestigationTables(db) {
|
|
37381
38078
|
db.exec(`
|
|
37382
|
-
CREATE TABLE IF NOT EXISTS
|
|
37383
|
-
|
|
37384
|
-
|
|
37385
|
-
|
|
37386
|
-
|
|
38079
|
+
CREATE TABLE IF NOT EXISTS investigations (
|
|
38080
|
+
id TEXT PRIMARY KEY,
|
|
38081
|
+
symptom TEXT NOT NULL,
|
|
38082
|
+
status TEXT NOT NULL DEFAULT 'exploring' CHECK (status IN ('exploring','confirmed','resolved')),
|
|
38083
|
+
task_id TEXT,
|
|
38084
|
+
session_id TEXT,
|
|
38085
|
+
root_cause TEXT,
|
|
38086
|
+
fix_summary TEXT,
|
|
38087
|
+
pattern_tag TEXT,
|
|
38088
|
+
created_at TEXT NOT NULL,
|
|
38089
|
+
resolved_at TEXT
|
|
37387
38090
|
)
|
|
37388
38091
|
`);
|
|
37389
38092
|
db.exec(`
|
|
37390
|
-
CREATE TABLE IF NOT EXISTS
|
|
37391
|
-
|
|
37392
|
-
|
|
37393
|
-
|
|
37394
|
-
|
|
37395
|
-
|
|
37396
|
-
|
|
37397
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38093
|
+
CREATE TABLE IF NOT EXISTS hypotheses (
|
|
38094
|
+
id TEXT PRIMARY KEY,
|
|
38095
|
+
investigation_id TEXT NOT NULL,
|
|
38096
|
+
description TEXT NOT NULL,
|
|
38097
|
+
status TEXT NOT NULL DEFAULT 'testing' CHECK (status IN ('testing','ruled_out','confirmed')),
|
|
38098
|
+
created_at TEXT NOT NULL,
|
|
38099
|
+
FOREIGN KEY (investigation_id) REFERENCES investigations(id)
|
|
37398
38100
|
)
|
|
37399
38101
|
`);
|
|
37400
38102
|
db.exec(`
|
|
37401
|
-
CREATE TABLE IF NOT EXISTS
|
|
37402
|
-
|
|
37403
|
-
|
|
37404
|
-
|
|
37405
|
-
|
|
37406
|
-
|
|
37407
|
-
|
|
37408
|
-
created_at TEXT NOT NULL
|
|
38103
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
38104
|
+
id TEXT PRIMARY KEY,
|
|
38105
|
+
investigation_id TEXT NOT NULL,
|
|
38106
|
+
hypothesis_id TEXT,
|
|
38107
|
+
type TEXT NOT NULL CHECK (type IN ('screenshot','log','network','code_snippet')),
|
|
38108
|
+
ref TEXT NOT NULL,
|
|
38109
|
+
note TEXT,
|
|
38110
|
+
created_at TEXT NOT NULL,
|
|
38111
|
+
FOREIGN KEY (investigation_id) REFERENCES investigations(id),
|
|
38112
|
+
FOREIGN KEY (hypothesis_id) REFERENCES hypotheses(id)
|
|
37409
38113
|
)
|
|
37410
38114
|
`);
|
|
38115
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_hypotheses_investigation ON hypotheses(investigation_id)");
|
|
38116
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_evidence_investigation ON evidence(investigation_id)");
|
|
38117
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_evidence_hypothesis ON evidence(hypothesis_id)");
|
|
38118
|
+
}
|
|
38119
|
+
function dropLegacyOAuthTables(db) {
|
|
38120
|
+
db.exec("DROP TABLE IF EXISTS oauth_auth_codes");
|
|
38121
|
+
db.exec("DROP TABLE IF EXISTS oauth_tokens");
|
|
38122
|
+
db.exec("DROP TABLE IF EXISTS oauth_clients");
|
|
37411
38123
|
}
|
|
37412
38124
|
function createIndexes(db) {
|
|
37413
38125
|
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
|
|
@@ -37433,6 +38145,7 @@ function createIndexes(db) {
|
|
|
37433
38145
|
);
|
|
37434
38146
|
db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_project ON inbox_items(project_id)");
|
|
37435
38147
|
db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_status ON inbox_items(project_id, status)");
|
|
38148
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_workspace ON inbox_items(workspace_id)");
|
|
37436
38149
|
db.exec(
|
|
37437
38150
|
"CREATE INDEX IF NOT EXISTS idx_conversations_owner_session ON conversations(owner_session_id)"
|
|
37438
38151
|
);
|
|
@@ -37452,16 +38165,6 @@ function createIndexes(db) {
|
|
|
37452
38165
|
db.exec(
|
|
37453
38166
|
"CREATE INDEX IF NOT EXISTS idx_agent_memories_recall ON agent_memories(importance DESC, recall_count DESC)"
|
|
37454
38167
|
);
|
|
37455
|
-
db.exec(
|
|
37456
|
-
"CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_client ON oauth_auth_codes(client_id)"
|
|
37457
|
-
);
|
|
37458
|
-
db.exec(
|
|
37459
|
-
"CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_expires ON oauth_auth_codes(expires_at)"
|
|
37460
|
-
);
|
|
37461
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_oauth_tokens_client ON oauth_tokens(client_id)");
|
|
37462
|
-
db.exec(
|
|
37463
|
-
"CREATE INDEX IF NOT EXISTS idx_oauth_tokens_access_expires ON oauth_tokens(access_expires_at)"
|
|
37464
|
-
);
|
|
37465
38168
|
}
|
|
37466
38169
|
function cleanupPoisonedTaskIds(db) {
|
|
37467
38170
|
const rows = db.prepare("SELECT id FROM tasks WHERE id GLOB 'TASK-[0-9]*'").all();
|
|
@@ -37952,6 +38655,14 @@ var RelationshipRepository = class {
|
|
|
37952
38655
|
const rows = type ? this.db.prepare("SELECT * FROM relationships WHERE from_id = ? AND type = ?").all(itemId, type) : this.db.prepare("SELECT * FROM relationships WHERE from_id = ?").all(itemId);
|
|
37953
38656
|
return rows.map(rowToRelationship);
|
|
37954
38657
|
}
|
|
38658
|
+
// Inbound counterpart to getFrom — edges pointing AT itemId. Needed for the
|
|
38659
|
+
// ADR-NNN graph queries whose answer is the source node: "which tasks REALIZE
|
|
38660
|
+
// this feature?" = getTo(featureSlug, 'REALIZES') → from_ids; "which gotchas
|
|
38661
|
+
// are ABOUT it?" = getTo(featureSlug, 'ABOUT').
|
|
38662
|
+
getTo(itemId, type) {
|
|
38663
|
+
const rows = type ? this.db.prepare("SELECT * FROM relationships WHERE to_id = ? AND type = ?").all(itemId, type) : this.db.prepare("SELECT * FROM relationships WHERE to_id = ?").all(itemId);
|
|
38664
|
+
return rows.map(rowToRelationship);
|
|
38665
|
+
}
|
|
37955
38666
|
};
|
|
37956
38667
|
|
|
37957
38668
|
// src/core/domain/repositories/session-repository.ts
|
|
@@ -37964,6 +38675,7 @@ function rowToSession(row) {
|
|
|
37964
38675
|
startedAt: row.started_at,
|
|
37965
38676
|
endedAt: row.ended_at || null,
|
|
37966
38677
|
status: row.status,
|
|
38678
|
+
ccSessionId: row.cc_session_id || null,
|
|
37967
38679
|
handoff: row.handoff_json ? JSON.parse(row.handoff_json) : null,
|
|
37968
38680
|
checkpoint: row.checkpoint ? JSON.parse(row.checkpoint) : null,
|
|
37969
38681
|
checkpointAt: row.checkpoint_at || null,
|
|
@@ -37980,8 +38692,8 @@ var SessionRepository = class {
|
|
|
37980
38692
|
const id = input.id || generateId("SESSION");
|
|
37981
38693
|
const startedAt = input.startedAt || ts;
|
|
37982
38694
|
this.db.prepare(
|
|
37983
|
-
`INSERT INTO sessions (id, project_id, workspace_id, task_id, started_at, status, handoff_json, created_at)
|
|
37984
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
38695
|
+
`INSERT INTO sessions (id, project_id, workspace_id, task_id, started_at, status, cc_session_id, handoff_json, created_at)
|
|
38696
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
37985
38697
|
).run(
|
|
37986
38698
|
id,
|
|
37987
38699
|
input.projectId,
|
|
@@ -37989,6 +38701,7 @@ var SessionRepository = class {
|
|
|
37989
38701
|
input.taskId || null,
|
|
37990
38702
|
startedAt,
|
|
37991
38703
|
input.status || "active",
|
|
38704
|
+
input.ccSessionId || null,
|
|
37992
38705
|
input.handoff ? JSON.stringify(input.handoff) : null,
|
|
37993
38706
|
ts
|
|
37994
38707
|
);
|
|
@@ -38415,6 +39128,7 @@ function rowToInbox(row) {
|
|
|
38415
39128
|
return {
|
|
38416
39129
|
id: row.id,
|
|
38417
39130
|
projectId: row.project_id || null,
|
|
39131
|
+
workspaceId: row.workspace_id || null,
|
|
38418
39132
|
content: row.content,
|
|
38419
39133
|
status: row.status,
|
|
38420
39134
|
linkedTaskId: row.linked_task_id || null,
|
|
@@ -38437,9 +39151,9 @@ var InboxRepository = class {
|
|
|
38437
39151
|
const projectId = input.projectId ?? null;
|
|
38438
39152
|
const id = this.nextInboxId();
|
|
38439
39153
|
this.db.prepare(
|
|
38440
|
-
`INSERT INTO inbox_items (id, project_id, content, status, created_at, updated_at)
|
|
38441
|
-
VALUES (?, ?, ?, 'raw', ?, ?)`
|
|
38442
|
-
).run(id, projectId, input.content, ts, ts);
|
|
39154
|
+
`INSERT INTO inbox_items (id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at)
|
|
39155
|
+
VALUES (?, ?, ?, ?, 'raw', ?, ?, ?)`
|
|
39156
|
+
).run(id, projectId, input.workspaceId ?? null, input.content, input.linkedTaskId ?? null, ts, ts);
|
|
38443
39157
|
return this.get(id);
|
|
38444
39158
|
}
|
|
38445
39159
|
update(id, input) {
|
|
@@ -38453,6 +39167,10 @@ var InboxRepository = class {
|
|
|
38453
39167
|
sets.push("status = ?");
|
|
38454
39168
|
params.push(input.status);
|
|
38455
39169
|
}
|
|
39170
|
+
if (input.workspaceId !== void 0) {
|
|
39171
|
+
sets.push("workspace_id = ?");
|
|
39172
|
+
params.push(input.workspaceId);
|
|
39173
|
+
}
|
|
38456
39174
|
if (input.linkedTaskId !== void 0) {
|
|
38457
39175
|
sets.push("linked_task_id = ?");
|
|
38458
39176
|
params.push(input.linkedTaskId);
|
|
@@ -38481,6 +39199,14 @@ var InboxRepository = class {
|
|
|
38481
39199
|
params.push(filter.projectId);
|
|
38482
39200
|
}
|
|
38483
39201
|
}
|
|
39202
|
+
if (filter.workspaceId !== void 0) {
|
|
39203
|
+
if (filter.workspaceId === null) {
|
|
39204
|
+
wheres.push("workspace_id IS NULL");
|
|
39205
|
+
} else {
|
|
39206
|
+
wheres.push("workspace_id = ?");
|
|
39207
|
+
params.push(filter.workspaceId);
|
|
39208
|
+
}
|
|
39209
|
+
}
|
|
38484
39210
|
if (filter.status) {
|
|
38485
39211
|
wheres.push("status = ?");
|
|
38486
39212
|
params.push(filter.status);
|
|
@@ -38723,11 +39449,14 @@ var SqliteTaskService = class {
|
|
|
38723
39449
|
toolInvocations;
|
|
38724
39450
|
sessionEvents;
|
|
38725
39451
|
agentMemories;
|
|
39452
|
+
investigations;
|
|
38726
39453
|
inboxLifecycle;
|
|
38727
39454
|
conversationLifecycle;
|
|
38728
39455
|
sessionLifecycle;
|
|
39456
|
+
investigationLifecycle;
|
|
38729
39457
|
knowledgeRepo;
|
|
38730
39458
|
knowledgeService;
|
|
39459
|
+
codeRefs;
|
|
38731
39460
|
embeddingStore;
|
|
38732
39461
|
embeddingProviderPromise;
|
|
38733
39462
|
embeddingReadyPromise = null;
|
|
@@ -38756,6 +39485,8 @@ var SqliteTaskService = class {
|
|
|
38756
39485
|
this.contextSources = new ContextSourceRepository(this.db);
|
|
38757
39486
|
this.conversations = new ConversationRepository(this.db);
|
|
38758
39487
|
this.inbox = new InboxRepository(this.db, this.counters);
|
|
39488
|
+
this.investigations = new InvestigationRepository(this.db, this.counters);
|
|
39489
|
+
this.investigationLifecycle = new InvestigationLifecycleService(this.db, this.investigations);
|
|
38759
39490
|
this.inboxLifecycle = new InboxLifecycleService(
|
|
38760
39491
|
this.db,
|
|
38761
39492
|
this.inbox,
|
|
@@ -38768,6 +39499,7 @@ var SqliteTaskService = class {
|
|
|
38768
39499
|
this.tasks,
|
|
38769
39500
|
this.sessions
|
|
38770
39501
|
);
|
|
39502
|
+
this.codeRefs = new CodeRefRepository(this.db);
|
|
38771
39503
|
this.sessionLifecycle = new SessionLifecycleService(
|
|
38772
39504
|
this.db,
|
|
38773
39505
|
this.sessions,
|
|
@@ -38775,6 +39507,8 @@ var SqliteTaskService = class {
|
|
|
38775
39507
|
this.conversations,
|
|
38776
39508
|
this.tasks,
|
|
38777
39509
|
this.sessionEvents,
|
|
39510
|
+
this.relationships,
|
|
39511
|
+
this.codeRefs,
|
|
38778
39512
|
(input) => this.recallMemoriesSync(input)
|
|
38779
39513
|
);
|
|
38780
39514
|
this.knowledgeRepo = new KnowledgeRepository(this.db);
|
|
@@ -38783,7 +39517,8 @@ var SqliteTaskService = class {
|
|
|
38783
39517
|
projects: this.projects,
|
|
38784
39518
|
workspaces: this.workspaces,
|
|
38785
39519
|
embeddingStore: this.embeddingStore,
|
|
38786
|
-
embeddingProvider: () => this.embeddingProviderPromise
|
|
39520
|
+
embeddingProvider: () => this.embeddingProviderPromise,
|
|
39521
|
+
edges: this.relationships
|
|
38787
39522
|
});
|
|
38788
39523
|
}
|
|
38789
39524
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -38924,6 +39659,9 @@ var SqliteTaskService = class {
|
|
|
38924
39659
|
async getRelationshipsFrom(itemId, type) {
|
|
38925
39660
|
return this.relationships.getFrom(itemId, type);
|
|
38926
39661
|
}
|
|
39662
|
+
async getRelationshipsTo(itemId, type) {
|
|
39663
|
+
return this.relationships.getTo(itemId, type);
|
|
39664
|
+
}
|
|
38927
39665
|
// ── Session operations (M1) ────────────────────────────────────────────────
|
|
38928
39666
|
async createSession(input) {
|
|
38929
39667
|
return this.sessions.create(input);
|
|
@@ -39015,6 +39753,9 @@ var SqliteTaskService = class {
|
|
|
39015
39753
|
return this.conversations.findByLink(linkedType, linkedId);
|
|
39016
39754
|
}
|
|
39017
39755
|
// ── Inbox ──────────────────────────────────────────────────────────────────
|
|
39756
|
+
async fetchSince(since) {
|
|
39757
|
+
return fetchSinceFromSqlite(this.db, since);
|
|
39758
|
+
}
|
|
39018
39759
|
async createInbox(input) {
|
|
39019
39760
|
return this.inbox.create(input);
|
|
39020
39761
|
}
|
|
@@ -39066,6 +39807,25 @@ var SqliteTaskService = class {
|
|
|
39066
39807
|
async resumeSession(id) {
|
|
39067
39808
|
return this.sessionLifecycle.resumeSession(id);
|
|
39068
39809
|
}
|
|
39810
|
+
// ── Investigation lifecycle (ADR-035, stdio-only) ──────────────────────────
|
|
39811
|
+
async startInvestigation(input) {
|
|
39812
|
+
return this.investigationLifecycle.startInvestigation(input);
|
|
39813
|
+
}
|
|
39814
|
+
async addHypothesis(investigationId, description) {
|
|
39815
|
+
return this.investigationLifecycle.addHypothesis(investigationId, description);
|
|
39816
|
+
}
|
|
39817
|
+
async setHypothesisStatus(hypothesisId, status) {
|
|
39818
|
+
return this.investigationLifecycle.setHypothesisStatus(hypothesisId, status);
|
|
39819
|
+
}
|
|
39820
|
+
async addEvidence(input) {
|
|
39821
|
+
return this.investigationLifecycle.addEvidence(input);
|
|
39822
|
+
}
|
|
39823
|
+
async resolveInvestigation(id, input) {
|
|
39824
|
+
return this.investigationLifecycle.resolveInvestigation(id, input);
|
|
39825
|
+
}
|
|
39826
|
+
async getInvestigation(id) {
|
|
39827
|
+
return this.investigationLifecycle.getInvestigation(id);
|
|
39828
|
+
}
|
|
39069
39829
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
39070
39830
|
async createKnowledge(input) {
|
|
39071
39831
|
return this.knowledgeService.createKnowledge(input);
|
|
@@ -39091,6 +39851,31 @@ var SqliteTaskService = class {
|
|
|
39091
39851
|
searchKnowledge(query, k) {
|
|
39092
39852
|
return this.knowledgeService.searchKnowledge(query, k);
|
|
39093
39853
|
}
|
|
39854
|
+
// ── Code refs + TOUCHES edges (TASK-988) ────────────────────────────────────
|
|
39855
|
+
async upsertCodeRef(input) {
|
|
39856
|
+
return this.codeRefs.upsert(input, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
39857
|
+
}
|
|
39858
|
+
async getCodeRef(slug) {
|
|
39859
|
+
return this.codeRefs.get(slug);
|
|
39860
|
+
}
|
|
39861
|
+
async listCodeRefsByPrefix(filter) {
|
|
39862
|
+
return this.codeRefs.listByPrefix(filter);
|
|
39863
|
+
}
|
|
39864
|
+
async deleteCodeRef(slug) {
|
|
39865
|
+
this.codeRefs.delete(slug);
|
|
39866
|
+
}
|
|
39867
|
+
async addTouches(taskId, codeRefSlug, relation) {
|
|
39868
|
+
this.codeRefs.addTouches(taskId, codeRefSlug, relation);
|
|
39869
|
+
}
|
|
39870
|
+
async removeTouches(taskId, codeRefSlug) {
|
|
39871
|
+
this.codeRefs.removeTouches(taskId, codeRefSlug);
|
|
39872
|
+
}
|
|
39873
|
+
async getTouchesForTask(taskId) {
|
|
39874
|
+
return this.codeRefs.getTouchesForTask(taskId);
|
|
39875
|
+
}
|
|
39876
|
+
async getTouchesForCodeRef(codeRefSlug) {
|
|
39877
|
+
return this.codeRefs.getTouchesForCodeRef(codeRefSlug);
|
|
39878
|
+
}
|
|
39094
39879
|
// ── Session Events ─────────────────────────────────────────────────────────
|
|
39095
39880
|
async createSessionEvent(input) {
|
|
39096
39881
|
return this.sessionEvents.create(input);
|
|
@@ -39185,6 +39970,15 @@ function loadVecExtension(db) {
|
|
|
39185
39970
|
}
|
|
39186
39971
|
|
|
39187
39972
|
// src/core/domain/repositories/postgres/migrations.ts
|
|
39973
|
+
function buildSyncColumnsSql() {
|
|
39974
|
+
const lines = [];
|
|
39975
|
+
for (const table of SYNCABLE_TABLES) {
|
|
39976
|
+
for (const col of SYNC_COLUMNS) {
|
|
39977
|
+
lines.push(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${col.name} ${col.pgType};`);
|
|
39978
|
+
}
|
|
39979
|
+
}
|
|
39980
|
+
return lines.join("\n");
|
|
39981
|
+
}
|
|
39188
39982
|
var MIGRATIONS = [
|
|
39189
39983
|
{
|
|
39190
39984
|
name: "001_init",
|
|
@@ -39350,6 +40144,15 @@ var MIGRATIONS = [
|
|
|
39350
40144
|
CREATE INDEX IF NOT EXISTS inbox_status_idx ON inbox_items (project_id, status);
|
|
39351
40145
|
`
|
|
39352
40146
|
},
|
|
40147
|
+
{
|
|
40148
|
+
// P6 (ADR-032 Pillar 6, TASK-993): nullable workspace scope on inbox, filled
|
|
40149
|
+
// progressively. inbox_add is remote-allowlisted, so the PG path must persist it.
|
|
40150
|
+
name: "009_inbox_workspace",
|
|
40151
|
+
sql: `
|
|
40152
|
+
ALTER TABLE inbox_items ADD COLUMN IF NOT EXISTS workspace_id TEXT;
|
|
40153
|
+
CREATE INDEX IF NOT EXISTS inbox_workspace_idx ON inbox_items (workspace_id);
|
|
40154
|
+
`
|
|
40155
|
+
},
|
|
39353
40156
|
{
|
|
39354
40157
|
// TASK-972 Phase 1 (additive) — signed_off_json column + conversation_message_reads
|
|
39355
40158
|
// side-table. PG equivalents of SQLite's INSERT OR IGNORE = ON CONFLICT DO NOTHING;
|
|
@@ -39430,6 +40233,40 @@ var MIGRATIONS = [
|
|
|
39430
40233
|
CREATE INDEX IF NOT EXISTS oauth_tokens_client_idx ON oauth_tokens (client_id);
|
|
39431
40234
|
CREATE INDEX IF NOT EXISTS oauth_tokens_access_expires_idx ON oauth_tokens (access_expires_at);
|
|
39432
40235
|
`
|
|
40236
|
+
},
|
|
40237
|
+
{
|
|
40238
|
+
// ADR-034: drop the ADR-027 self-issued OAuth store. Keycloak now issues and
|
|
40239
|
+
// stores tokens; choda-deck only validates Keycloak JWTs. 010_oauth stays in
|
|
40240
|
+
// history (already applied on existing deploys) — this migration removes it.
|
|
40241
|
+
name: "011_drop_oauth",
|
|
40242
|
+
sql: `
|
|
40243
|
+
DROP TABLE IF EXISTS oauth_auth_codes;
|
|
40244
|
+
DROP TABLE IF EXISTS oauth_tokens;
|
|
40245
|
+
DROP TABLE IF EXISTS oauth_clients;
|
|
40246
|
+
`
|
|
40247
|
+
},
|
|
40248
|
+
{
|
|
40249
|
+
// ADR-030 Phase 1 (TASK-978) — additive sync metadata (updated_at / deleted_at
|
|
40250
|
+
// / origin) on every syncable entity table. Mirrors schema.ts addSyncColumns;
|
|
40251
|
+
// both generate from src/core/sync/syncable-tables.ts. No _sync_clock/_sync_state
|
|
40252
|
+
// singletons here — the Lamport clock is a local-SQLite concern (Postgres is
|
|
40253
|
+
// canonical and stamps updated_at server-side in Phase 2+).
|
|
40254
|
+
name: "012_sync_columns",
|
|
40255
|
+
sql: buildSyncColumnsSql()
|
|
40256
|
+
},
|
|
40257
|
+
{
|
|
40258
|
+
// ADR-030 Phase 2 (TASK-978) — server-side Lamport clock for the remote
|
|
40259
|
+
// (Postgres canonical) writer. inbox_add stamps sync_updated_at from this
|
|
40260
|
+
// counter so the local pull can order remote changes. Singleton row pinned
|
|
40261
|
+
// at id = 0 (mirrors SQLite's _sync_clock).
|
|
40262
|
+
name: "013_sync_clock",
|
|
40263
|
+
sql: `
|
|
40264
|
+
CREATE TABLE IF NOT EXISTS _sync_clock (
|
|
40265
|
+
id INTEGER PRIMARY KEY CHECK (id = 0),
|
|
40266
|
+
counter BIGINT NOT NULL DEFAULT 0
|
|
40267
|
+
);
|
|
40268
|
+
INSERT INTO _sync_clock (id, counter) VALUES (0, 0) ON CONFLICT (id) DO NOTHING;
|
|
40269
|
+
`
|
|
39433
40270
|
}
|
|
39434
40271
|
];
|
|
39435
40272
|
async function migrate(conn) {
|
|
@@ -39790,11 +40627,61 @@ var PostgresConversationRepository = class {
|
|
|
39790
40627
|
}
|
|
39791
40628
|
};
|
|
39792
40629
|
|
|
40630
|
+
// node_modules/pg/esm/index.mjs
|
|
40631
|
+
var import_lib = __toESM(require_lib2(), 1);
|
|
40632
|
+
var Client = import_lib.default.Client;
|
|
40633
|
+
var Pool = import_lib.default.Pool;
|
|
40634
|
+
var Connection = import_lib.default.Connection;
|
|
40635
|
+
var types = import_lib.default.types;
|
|
40636
|
+
var Query = import_lib.default.Query;
|
|
40637
|
+
var DatabaseError = import_lib.default.DatabaseError;
|
|
40638
|
+
var escapeIdentifier = import_lib.default.escapeIdentifier;
|
|
40639
|
+
var escapeLiteral = import_lib.default.escapeLiteral;
|
|
40640
|
+
var Result = import_lib.default.Result;
|
|
40641
|
+
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
40642
|
+
var defaults = import_lib.default.defaults;
|
|
40643
|
+
|
|
40644
|
+
// src/core/domain/repositories/postgres/connection.ts
|
|
40645
|
+
async function runInTx(q, fn) {
|
|
40646
|
+
if (q.transaction) return q.transaction(fn);
|
|
40647
|
+
return fn(q);
|
|
40648
|
+
}
|
|
40649
|
+
var PgConnection = class {
|
|
40650
|
+
pool;
|
|
40651
|
+
constructor(config2) {
|
|
40652
|
+
this.pool = typeof config2 === "string" ? new Pool({ connectionString: config2 }) : new Pool(config2);
|
|
40653
|
+
}
|
|
40654
|
+
async query(text, params) {
|
|
40655
|
+
return this.pool.query(text, params);
|
|
40656
|
+
}
|
|
40657
|
+
async transaction(fn) {
|
|
40658
|
+
const client = await this.pool.connect();
|
|
40659
|
+
try {
|
|
40660
|
+
await client.query("BEGIN");
|
|
40661
|
+
const result = await fn(client);
|
|
40662
|
+
await client.query("COMMIT");
|
|
40663
|
+
return result;
|
|
40664
|
+
} catch (err) {
|
|
40665
|
+
try {
|
|
40666
|
+
await client.query("ROLLBACK");
|
|
40667
|
+
} catch {
|
|
40668
|
+
}
|
|
40669
|
+
throw err;
|
|
40670
|
+
} finally {
|
|
40671
|
+
client.release();
|
|
40672
|
+
}
|
|
40673
|
+
}
|
|
40674
|
+
async close() {
|
|
40675
|
+
await this.pool.end();
|
|
40676
|
+
}
|
|
40677
|
+
};
|
|
40678
|
+
|
|
39793
40679
|
// src/core/domain/repositories/postgres/inbox-repository.pg.ts
|
|
39794
40680
|
function mapRow5(row) {
|
|
39795
40681
|
return {
|
|
39796
40682
|
id: row.id,
|
|
39797
40683
|
projectId: row.project_id,
|
|
40684
|
+
workspaceId: row.workspace_id,
|
|
39798
40685
|
content: row.content,
|
|
39799
40686
|
status: row.status,
|
|
39800
40687
|
linkedTaskId: row.linked_task_id,
|
|
@@ -39802,7 +40689,7 @@ function mapRow5(row) {
|
|
|
39802
40689
|
updatedAt: row.updated_at
|
|
39803
40690
|
};
|
|
39804
40691
|
}
|
|
39805
|
-
var SELECT_COLS3 = "id, project_id, content, status, linked_task_id, created_at, updated_at";
|
|
40692
|
+
var SELECT_COLS3 = "id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at";
|
|
39806
40693
|
var PostgresInboxRepository = class {
|
|
39807
40694
|
constructor(conn, counters) {
|
|
39808
40695
|
this.conn = conn;
|
|
@@ -39817,11 +40704,25 @@ var PostgresInboxRepository = class {
|
|
|
39817
40704
|
async create(input) {
|
|
39818
40705
|
const ts = now();
|
|
39819
40706
|
const id = await this.nextInboxId();
|
|
39820
|
-
await this.conn
|
|
39821
|
-
|
|
39822
|
-
|
|
39823
|
-
|
|
39824
|
-
|
|
40707
|
+
await runInTx(this.conn, async (tx) => {
|
|
40708
|
+
const clk = await tx.query(
|
|
40709
|
+
"UPDATE _sync_clock SET counter = counter + 1 WHERE id = 0 RETURNING counter"
|
|
40710
|
+
);
|
|
40711
|
+
const lamport = Number(clk.rows[0].counter);
|
|
40712
|
+
await tx.query(
|
|
40713
|
+
`INSERT INTO inbox_items (id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at, sync_updated_at, sync_origin)
|
|
40714
|
+
VALUES ($1, $2, $3, $4, 'raw', $5, $6, $6, $7, 'remote')`,
|
|
40715
|
+
[
|
|
40716
|
+
id,
|
|
40717
|
+
input.projectId ?? null,
|
|
40718
|
+
input.workspaceId ?? null,
|
|
40719
|
+
input.content,
|
|
40720
|
+
input.linkedTaskId ?? null,
|
|
40721
|
+
ts,
|
|
40722
|
+
lamport
|
|
40723
|
+
]
|
|
40724
|
+
);
|
|
40725
|
+
});
|
|
39825
40726
|
const got = await this.get(id);
|
|
39826
40727
|
if (!got) throw new Error(`Inbox item disappeared after insert: ${id}`);
|
|
39827
40728
|
return got;
|
|
@@ -39846,6 +40747,14 @@ var PostgresInboxRepository = class {
|
|
|
39846
40747
|
params.push(filter.projectId);
|
|
39847
40748
|
}
|
|
39848
40749
|
}
|
|
40750
|
+
if (filter.workspaceId !== void 0) {
|
|
40751
|
+
if (filter.workspaceId === null) {
|
|
40752
|
+
wheres.push("workspace_id IS NULL");
|
|
40753
|
+
} else {
|
|
40754
|
+
wheres.push(`workspace_id = $${n++}`);
|
|
40755
|
+
params.push(filter.workspaceId);
|
|
40756
|
+
}
|
|
40757
|
+
}
|
|
39849
40758
|
if (filter.status) {
|
|
39850
40759
|
wheres.push(`status = $${n++}`);
|
|
39851
40760
|
params.push(filter.status);
|
|
@@ -39955,54 +40864,8 @@ var PostgresTaskService = class {
|
|
|
39955
40864
|
async getConversationActions(conversationId) {
|
|
39956
40865
|
return this.conversations.getActions(conversationId);
|
|
39957
40866
|
}
|
|
39958
|
-
|
|
39959
|
-
|
|
39960
|
-
// node_modules/pg/esm/index.mjs
|
|
39961
|
-
var import_lib = __toESM(require_lib2(), 1);
|
|
39962
|
-
var Client = import_lib.default.Client;
|
|
39963
|
-
var Pool = import_lib.default.Pool;
|
|
39964
|
-
var Connection = import_lib.default.Connection;
|
|
39965
|
-
var types = import_lib.default.types;
|
|
39966
|
-
var Query = import_lib.default.Query;
|
|
39967
|
-
var DatabaseError = import_lib.default.DatabaseError;
|
|
39968
|
-
var escapeIdentifier = import_lib.default.escapeIdentifier;
|
|
39969
|
-
var escapeLiteral = import_lib.default.escapeLiteral;
|
|
39970
|
-
var Result = import_lib.default.Result;
|
|
39971
|
-
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
39972
|
-
var defaults = import_lib.default.defaults;
|
|
39973
|
-
|
|
39974
|
-
// src/core/domain/repositories/postgres/connection.ts
|
|
39975
|
-
async function runInTx(q, fn) {
|
|
39976
|
-
if (q.transaction) return q.transaction(fn);
|
|
39977
|
-
return fn(q);
|
|
39978
|
-
}
|
|
39979
|
-
var PgConnection = class {
|
|
39980
|
-
pool;
|
|
39981
|
-
constructor(config2) {
|
|
39982
|
-
this.pool = typeof config2 === "string" ? new Pool({ connectionString: config2 }) : new Pool(config2);
|
|
39983
|
-
}
|
|
39984
|
-
async query(text, params) {
|
|
39985
|
-
return this.pool.query(text, params);
|
|
39986
|
-
}
|
|
39987
|
-
async transaction(fn) {
|
|
39988
|
-
const client = await this.pool.connect();
|
|
39989
|
-
try {
|
|
39990
|
-
await client.query("BEGIN");
|
|
39991
|
-
const result = await fn(client);
|
|
39992
|
-
await client.query("COMMIT");
|
|
39993
|
-
return result;
|
|
39994
|
-
} catch (err) {
|
|
39995
|
-
try {
|
|
39996
|
-
await client.query("ROLLBACK");
|
|
39997
|
-
} catch {
|
|
39998
|
-
}
|
|
39999
|
-
throw err;
|
|
40000
|
-
} finally {
|
|
40001
|
-
client.release();
|
|
40002
|
-
}
|
|
40003
|
-
}
|
|
40004
|
-
async close() {
|
|
40005
|
-
await this.pool.end();
|
|
40867
|
+
async fetchSince(since) {
|
|
40868
|
+
return fetchSinceFromPg(this.conn, since);
|
|
40006
40869
|
}
|
|
40007
40870
|
};
|
|
40008
40871
|
|
|
@@ -40035,346 +40898,6 @@ function requireBackendForTransport(backend, transport) {
|
|
|
40035
40898
|
}
|
|
40036
40899
|
}
|
|
40037
40900
|
|
|
40038
|
-
// src/core/domain/repositories/oauth-repository.ts
|
|
40039
|
-
var import_crypto = require("crypto");
|
|
40040
|
-
var OAuthRepository = class {
|
|
40041
|
-
constructor(db) {
|
|
40042
|
-
this.db = db;
|
|
40043
|
-
this.insertClient = db.prepare(
|
|
40044
|
-
`INSERT INTO oauth_clients (client_id, client_name, redirect_uris)
|
|
40045
|
-
VALUES (?, ?, ?)`
|
|
40046
|
-
);
|
|
40047
|
-
this.selectClient = db.prepare("SELECT * FROM oauth_clients WHERE client_id = ?");
|
|
40048
|
-
this.insertAuthCode = db.prepare(
|
|
40049
|
-
`INSERT INTO oauth_auth_codes
|
|
40050
|
-
(code, client_id, code_challenge, code_challenge_method, redirect_uri, expires_at)
|
|
40051
|
-
VALUES (?, ?, ?, 'S256', ?, ?)`
|
|
40052
|
-
);
|
|
40053
|
-
this.consumeAuthCodeStmt = db.prepare(
|
|
40054
|
-
"DELETE FROM oauth_auth_codes WHERE code = ? RETURNING *"
|
|
40055
|
-
);
|
|
40056
|
-
this.insertToken = db.prepare(
|
|
40057
|
-
`INSERT INTO oauth_tokens
|
|
40058
|
-
(access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
|
|
40059
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
40060
|
-
);
|
|
40061
|
-
this.selectAccessToken = db.prepare(
|
|
40062
|
-
"SELECT * FROM oauth_tokens WHERE access_token = ? AND revoked = 0"
|
|
40063
|
-
);
|
|
40064
|
-
this.selectRefreshToken = db.prepare(
|
|
40065
|
-
"SELECT * FROM oauth_tokens WHERE refresh_token = ?"
|
|
40066
|
-
);
|
|
40067
|
-
this.markRevoked = db.prepare(
|
|
40068
|
-
"UPDATE oauth_tokens SET revoked = 1 WHERE access_token = ?"
|
|
40069
|
-
);
|
|
40070
|
-
this.revokeAllForClient = db.prepare(
|
|
40071
|
-
"UPDATE oauth_tokens SET revoked = 1 WHERE client_id = ?"
|
|
40072
|
-
);
|
|
40073
|
-
}
|
|
40074
|
-
db;
|
|
40075
|
-
insertClient;
|
|
40076
|
-
selectClient;
|
|
40077
|
-
insertAuthCode;
|
|
40078
|
-
consumeAuthCodeStmt;
|
|
40079
|
-
insertToken;
|
|
40080
|
-
selectAccessToken;
|
|
40081
|
-
selectRefreshToken;
|
|
40082
|
-
markRevoked;
|
|
40083
|
-
revokeAllForClient;
|
|
40084
|
-
async registerClient(input) {
|
|
40085
|
-
const clientId = `cdck_cli_${randomToken(16)}`;
|
|
40086
|
-
this.insertClient.run(clientId, input.clientName, JSON.stringify(input.redirectUris));
|
|
40087
|
-
const row = this.selectClient.get(clientId);
|
|
40088
|
-
return rowToClient(row);
|
|
40089
|
-
}
|
|
40090
|
-
async getClient(clientId) {
|
|
40091
|
-
const row = this.selectClient.get(clientId);
|
|
40092
|
-
return row ? rowToClient(row) : null;
|
|
40093
|
-
}
|
|
40094
|
-
async createAuthCode(input) {
|
|
40095
|
-
const code = `cdck_code_${randomToken(32)}`;
|
|
40096
|
-
const expiresAt = isoFromNow(input.ttlSeconds);
|
|
40097
|
-
this.insertAuthCode.run(
|
|
40098
|
-
code,
|
|
40099
|
-
input.clientId,
|
|
40100
|
-
input.codeChallenge,
|
|
40101
|
-
input.redirectUri,
|
|
40102
|
-
expiresAt
|
|
40103
|
-
);
|
|
40104
|
-
return {
|
|
40105
|
-
code,
|
|
40106
|
-
clientId: input.clientId,
|
|
40107
|
-
codeChallenge: input.codeChallenge,
|
|
40108
|
-
redirectUri: input.redirectUri,
|
|
40109
|
-
expiresAt
|
|
40110
|
-
};
|
|
40111
|
-
}
|
|
40112
|
-
// Single-use: deletes the row even if expired. Caller must check expiresAt.
|
|
40113
|
-
async consumeAuthCode(code) {
|
|
40114
|
-
const row = this.consumeAuthCodeStmt.get(code);
|
|
40115
|
-
if (!row) return null;
|
|
40116
|
-
return {
|
|
40117
|
-
code: row.code,
|
|
40118
|
-
clientId: row.client_id,
|
|
40119
|
-
codeChallenge: row.code_challenge,
|
|
40120
|
-
redirectUri: row.redirect_uri,
|
|
40121
|
-
expiresAt: row.expires_at
|
|
40122
|
-
};
|
|
40123
|
-
}
|
|
40124
|
-
async createTokens(input) {
|
|
40125
|
-
const accessToken = `cdck_at_${randomToken(32)}`;
|
|
40126
|
-
const refreshToken = `cdck_rt_${randomToken(32)}`;
|
|
40127
|
-
const accessExpiresAt = isoFromNow(input.accessTtlSeconds);
|
|
40128
|
-
const refreshExpiresAt = isoFromNow(input.refreshTtlSeconds);
|
|
40129
|
-
this.insertToken.run(
|
|
40130
|
-
accessToken,
|
|
40131
|
-
refreshToken,
|
|
40132
|
-
input.clientId,
|
|
40133
|
-
accessExpiresAt,
|
|
40134
|
-
refreshExpiresAt
|
|
40135
|
-
);
|
|
40136
|
-
return {
|
|
40137
|
-
accessToken,
|
|
40138
|
-
refreshToken,
|
|
40139
|
-
clientId: input.clientId,
|
|
40140
|
-
accessExpiresAt,
|
|
40141
|
-
refreshExpiresAt
|
|
40142
|
-
};
|
|
40143
|
-
}
|
|
40144
|
-
// Returns the row only if not revoked and not expired. Otherwise null.
|
|
40145
|
-
async validateAccessToken(accessToken) {
|
|
40146
|
-
const row = this.selectAccessToken.get(accessToken);
|
|
40147
|
-
if (!row) return null;
|
|
40148
|
-
if (Date.parse(row.access_expires_at) <= Date.now()) return null;
|
|
40149
|
-
return rowToToken(row);
|
|
40150
|
-
}
|
|
40151
|
-
// Atomic transaction: detect replay (revoked refresh) → revoke chain;
|
|
40152
|
-
// detect expiry → invalid_grant; happy path → mark old revoked + insert new.
|
|
40153
|
-
async rotateRefresh(refreshToken, ttls) {
|
|
40154
|
-
const tx = this.db.transaction(() => {
|
|
40155
|
-
const row = this.selectRefreshToken.get(refreshToken);
|
|
40156
|
-
if (!row) return { ok: false, error: "invalid_grant" };
|
|
40157
|
-
if (row.revoked === 1) {
|
|
40158
|
-
this.revokeAllForClient.run(row.client_id);
|
|
40159
|
-
return { ok: false, error: "replay_detected" };
|
|
40160
|
-
}
|
|
40161
|
-
if (Date.parse(row.refresh_expires_at) <= Date.now()) {
|
|
40162
|
-
return { ok: false, error: "invalid_grant" };
|
|
40163
|
-
}
|
|
40164
|
-
this.markRevoked.run(row.access_token);
|
|
40165
|
-
const accessToken = `cdck_at_${randomToken(32)}`;
|
|
40166
|
-
const newRefreshToken = `cdck_rt_${randomToken(32)}`;
|
|
40167
|
-
const accessExpiresAt = isoFromNow(ttls.accessTtlSeconds);
|
|
40168
|
-
const refreshExpiresAt = isoFromNow(ttls.refreshTtlSeconds);
|
|
40169
|
-
this.insertToken.run(
|
|
40170
|
-
accessToken,
|
|
40171
|
-
newRefreshToken,
|
|
40172
|
-
row.client_id,
|
|
40173
|
-
accessExpiresAt,
|
|
40174
|
-
refreshExpiresAt
|
|
40175
|
-
);
|
|
40176
|
-
return {
|
|
40177
|
-
ok: true,
|
|
40178
|
-
tokens: {
|
|
40179
|
-
accessToken,
|
|
40180
|
-
refreshToken: newRefreshToken,
|
|
40181
|
-
clientId: row.client_id,
|
|
40182
|
-
accessExpiresAt,
|
|
40183
|
-
refreshExpiresAt
|
|
40184
|
-
}
|
|
40185
|
-
};
|
|
40186
|
-
});
|
|
40187
|
-
return tx();
|
|
40188
|
-
}
|
|
40189
|
-
};
|
|
40190
|
-
function rowToClient(row) {
|
|
40191
|
-
return {
|
|
40192
|
-
clientId: row.client_id,
|
|
40193
|
-
clientName: row.client_name,
|
|
40194
|
-
redirectUris: JSON.parse(row.redirect_uris),
|
|
40195
|
-
createdAt: row.created_at
|
|
40196
|
-
};
|
|
40197
|
-
}
|
|
40198
|
-
function rowToToken(row) {
|
|
40199
|
-
return {
|
|
40200
|
-
accessToken: row.access_token,
|
|
40201
|
-
refreshToken: row.refresh_token,
|
|
40202
|
-
clientId: row.client_id,
|
|
40203
|
-
accessExpiresAt: row.access_expires_at,
|
|
40204
|
-
refreshExpiresAt: row.refresh_expires_at
|
|
40205
|
-
};
|
|
40206
|
-
}
|
|
40207
|
-
function randomToken(bytes) {
|
|
40208
|
-
return (0, import_crypto.randomBytes)(bytes).toString("base64url");
|
|
40209
|
-
}
|
|
40210
|
-
function isoFromNow(seconds) {
|
|
40211
|
-
return new Date(Date.now() + seconds * 1e3).toISOString();
|
|
40212
|
-
}
|
|
40213
|
-
|
|
40214
|
-
// src/core/domain/repositories/postgres/oauth-repository.pg.ts
|
|
40215
|
-
var import_crypto2 = require("crypto");
|
|
40216
|
-
function rowToClient2(row) {
|
|
40217
|
-
return {
|
|
40218
|
-
clientId: row.client_id,
|
|
40219
|
-
clientName: row.client_name,
|
|
40220
|
-
redirectUris: row.redirect_uris,
|
|
40221
|
-
createdAt: row.created_at.toISOString()
|
|
40222
|
-
};
|
|
40223
|
-
}
|
|
40224
|
-
function rowToToken2(row) {
|
|
40225
|
-
return {
|
|
40226
|
-
accessToken: row.access_token,
|
|
40227
|
-
refreshToken: row.refresh_token,
|
|
40228
|
-
clientId: row.client_id,
|
|
40229
|
-
accessExpiresAt: row.access_expires_at,
|
|
40230
|
-
refreshExpiresAt: row.refresh_expires_at
|
|
40231
|
-
};
|
|
40232
|
-
}
|
|
40233
|
-
function randomToken2(bytes) {
|
|
40234
|
-
return (0, import_crypto2.randomBytes)(bytes).toString("base64url");
|
|
40235
|
-
}
|
|
40236
|
-
function isoFromNow2(seconds) {
|
|
40237
|
-
return new Date(Date.now() + seconds * 1e3).toISOString();
|
|
40238
|
-
}
|
|
40239
|
-
var PostgresOAuthRepository = class {
|
|
40240
|
-
constructor(conn) {
|
|
40241
|
-
this.conn = conn;
|
|
40242
|
-
}
|
|
40243
|
-
conn;
|
|
40244
|
-
async registerClient(input) {
|
|
40245
|
-
const clientId = `cdck_cli_${randomToken2(16)}`;
|
|
40246
|
-
const result = await this.conn.query(
|
|
40247
|
-
`INSERT INTO oauth_clients (client_id, client_name, redirect_uris)
|
|
40248
|
-
VALUES ($1, $2, $3::jsonb)
|
|
40249
|
-
RETURNING client_id, client_name, redirect_uris, created_at`,
|
|
40250
|
-
[clientId, input.clientName, JSON.stringify(input.redirectUris)]
|
|
40251
|
-
);
|
|
40252
|
-
return rowToClient2(result.rows[0]);
|
|
40253
|
-
}
|
|
40254
|
-
async getClient(clientId) {
|
|
40255
|
-
const result = await this.conn.query(
|
|
40256
|
-
`SELECT client_id, client_name, redirect_uris, created_at
|
|
40257
|
-
FROM oauth_clients WHERE client_id = $1`,
|
|
40258
|
-
[clientId]
|
|
40259
|
-
);
|
|
40260
|
-
const row = result.rows[0];
|
|
40261
|
-
return row ? rowToClient2(row) : null;
|
|
40262
|
-
}
|
|
40263
|
-
async createAuthCode(input) {
|
|
40264
|
-
const code = `cdck_code_${randomToken2(32)}`;
|
|
40265
|
-
const expiresAt = isoFromNow2(input.ttlSeconds);
|
|
40266
|
-
await this.conn.query(
|
|
40267
|
-
`INSERT INTO oauth_auth_codes
|
|
40268
|
-
(code, client_id, code_challenge, code_challenge_method, redirect_uri, expires_at)
|
|
40269
|
-
VALUES ($1, $2, $3, 'S256', $4, $5)`,
|
|
40270
|
-
[code, input.clientId, input.codeChallenge, input.redirectUri, expiresAt]
|
|
40271
|
-
);
|
|
40272
|
-
return {
|
|
40273
|
-
code,
|
|
40274
|
-
clientId: input.clientId,
|
|
40275
|
-
codeChallenge: input.codeChallenge,
|
|
40276
|
-
redirectUri: input.redirectUri,
|
|
40277
|
-
expiresAt
|
|
40278
|
-
};
|
|
40279
|
-
}
|
|
40280
|
-
// Single-use: DELETE...RETURNING removes the row even if expired.
|
|
40281
|
-
// Caller is expected to check expiresAt.
|
|
40282
|
-
async consumeAuthCode(code) {
|
|
40283
|
-
const result = await this.conn.query(
|
|
40284
|
-
`DELETE FROM oauth_auth_codes WHERE code = $1
|
|
40285
|
-
RETURNING code, client_id, code_challenge, redirect_uri, expires_at`,
|
|
40286
|
-
[code]
|
|
40287
|
-
);
|
|
40288
|
-
const row = result.rows[0];
|
|
40289
|
-
if (!row) return null;
|
|
40290
|
-
return {
|
|
40291
|
-
code: row.code,
|
|
40292
|
-
clientId: row.client_id,
|
|
40293
|
-
codeChallenge: row.code_challenge,
|
|
40294
|
-
redirectUri: row.redirect_uri,
|
|
40295
|
-
expiresAt: row.expires_at
|
|
40296
|
-
};
|
|
40297
|
-
}
|
|
40298
|
-
async createTokens(input) {
|
|
40299
|
-
const accessToken = `cdck_at_${randomToken2(32)}`;
|
|
40300
|
-
const refreshToken = `cdck_rt_${randomToken2(32)}`;
|
|
40301
|
-
const accessExpiresAt = isoFromNow2(input.accessTtlSeconds);
|
|
40302
|
-
const refreshExpiresAt = isoFromNow2(input.refreshTtlSeconds);
|
|
40303
|
-
await this.conn.query(
|
|
40304
|
-
`INSERT INTO oauth_tokens
|
|
40305
|
-
(access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
|
|
40306
|
-
VALUES ($1, $2, $3, $4, $5)`,
|
|
40307
|
-
[accessToken, refreshToken, input.clientId, accessExpiresAt, refreshExpiresAt]
|
|
40308
|
-
);
|
|
40309
|
-
return {
|
|
40310
|
-
accessToken,
|
|
40311
|
-
refreshToken,
|
|
40312
|
-
clientId: input.clientId,
|
|
40313
|
-
accessExpiresAt,
|
|
40314
|
-
refreshExpiresAt
|
|
40315
|
-
};
|
|
40316
|
-
}
|
|
40317
|
-
// Returns the row only if not revoked AND not expired. Otherwise null.
|
|
40318
|
-
async validateAccessToken(accessToken) {
|
|
40319
|
-
const result = await this.conn.query(
|
|
40320
|
-
`SELECT access_token, refresh_token, client_id, access_expires_at,
|
|
40321
|
-
refresh_expires_at, revoked
|
|
40322
|
-
FROM oauth_tokens WHERE access_token = $1 AND revoked = FALSE`,
|
|
40323
|
-
[accessToken]
|
|
40324
|
-
);
|
|
40325
|
-
const row = result.rows[0];
|
|
40326
|
-
if (!row) return null;
|
|
40327
|
-
if (Date.parse(row.access_expires_at) <= Date.now()) return null;
|
|
40328
|
-
return rowToToken2(row);
|
|
40329
|
-
}
|
|
40330
|
-
// Atomic: detect replay (revoked refresh) → revoke chain;
|
|
40331
|
-
// detect expiry → invalid_grant; happy path → mark old revoked + insert new.
|
|
40332
|
-
async rotateRefresh(refreshToken, ttls) {
|
|
40333
|
-
return runInTx(this.conn, async (tx) => {
|
|
40334
|
-
const lookup = await tx.query(
|
|
40335
|
-
`SELECT access_token, refresh_token, client_id, access_expires_at,
|
|
40336
|
-
refresh_expires_at, revoked
|
|
40337
|
-
FROM oauth_tokens WHERE refresh_token = $1`,
|
|
40338
|
-
[refreshToken]
|
|
40339
|
-
);
|
|
40340
|
-
const row = lookup.rows[0];
|
|
40341
|
-
if (!row) return { ok: false, error: "invalid_grant" };
|
|
40342
|
-
if (row.revoked) {
|
|
40343
|
-
await tx.query("UPDATE oauth_tokens SET revoked = TRUE WHERE client_id = $1", [
|
|
40344
|
-
row.client_id
|
|
40345
|
-
]);
|
|
40346
|
-
return { ok: false, error: "replay_detected" };
|
|
40347
|
-
}
|
|
40348
|
-
if (Date.parse(row.refresh_expires_at) <= Date.now()) {
|
|
40349
|
-
return { ok: false, error: "invalid_grant" };
|
|
40350
|
-
}
|
|
40351
|
-
await tx.query("UPDATE oauth_tokens SET revoked = TRUE WHERE access_token = $1", [
|
|
40352
|
-
row.access_token
|
|
40353
|
-
]);
|
|
40354
|
-
const accessToken = `cdck_at_${randomToken2(32)}`;
|
|
40355
|
-
const newRefreshToken = `cdck_rt_${randomToken2(32)}`;
|
|
40356
|
-
const accessExpiresAt = isoFromNow2(ttls.accessTtlSeconds);
|
|
40357
|
-
const refreshExpiresAt = isoFromNow2(ttls.refreshTtlSeconds);
|
|
40358
|
-
await tx.query(
|
|
40359
|
-
`INSERT INTO oauth_tokens
|
|
40360
|
-
(access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
|
|
40361
|
-
VALUES ($1, $2, $3, $4, $5)`,
|
|
40362
|
-
[accessToken, newRefreshToken, row.client_id, accessExpiresAt, refreshExpiresAt]
|
|
40363
|
-
);
|
|
40364
|
-
return {
|
|
40365
|
-
ok: true,
|
|
40366
|
-
tokens: {
|
|
40367
|
-
accessToken,
|
|
40368
|
-
refreshToken: newRefreshToken,
|
|
40369
|
-
clientId: row.client_id,
|
|
40370
|
-
accessExpiresAt,
|
|
40371
|
-
refreshExpiresAt
|
|
40372
|
-
}
|
|
40373
|
-
};
|
|
40374
|
-
});
|
|
40375
|
-
}
|
|
40376
|
-
};
|
|
40377
|
-
|
|
40378
40901
|
// src/core/paths.ts
|
|
40379
40902
|
var path4 = __toESM(require("path"));
|
|
40380
40903
|
function resolveDataPaths(electronDataDir) {
|
|
@@ -40466,14 +40989,14 @@ function createInstrumentedServer(server, sink, toolAllowlist) {
|
|
|
40466
40989
|
|
|
40467
40990
|
// src/adapters/mcp/http-transport.ts
|
|
40468
40991
|
var import_http = require("http");
|
|
40469
|
-
var
|
|
40470
|
-
var
|
|
40992
|
+
var import_buffer = require("buffer");
|
|
40993
|
+
var import_crypto2 = require("crypto");
|
|
40471
40994
|
|
|
40472
40995
|
// node_modules/@hono/node-server/dist/index.mjs
|
|
40473
40996
|
var import_http2 = require("http2");
|
|
40474
40997
|
var import_http22 = require("http2");
|
|
40475
40998
|
var import_stream = require("stream");
|
|
40476
|
-
var
|
|
40999
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
40477
41000
|
var RequestError = class extends Error {
|
|
40478
41001
|
constructor(message, options) {
|
|
40479
41002
|
super(message, options);
|
|
@@ -40812,7 +41335,7 @@ var buildOutgoingHttpHeaders = (headers) => {
|
|
|
40812
41335
|
};
|
|
40813
41336
|
var X_ALREADY_SENT = "x-hono-already-sent";
|
|
40814
41337
|
if (typeof global.crypto === "undefined") {
|
|
40815
|
-
global.crypto =
|
|
41338
|
+
global.crypto = import_crypto.default;
|
|
40816
41339
|
}
|
|
40817
41340
|
var outgoingEnded = /* @__PURE__ */ Symbol("outgoingEnded");
|
|
40818
41341
|
var incomingDraining = /* @__PURE__ */ Symbol("incomingDraining");
|
|
@@ -41795,8 +42318,8 @@ var StreamableHTTPServerTransport = class {
|
|
|
41795
42318
|
};
|
|
41796
42319
|
|
|
41797
42320
|
// src/adapters/mcp/oauth/discovery.ts
|
|
41798
|
-
function authServerMetadata(
|
|
41799
|
-
const base = stripTrailingSlash(
|
|
42321
|
+
function authServerMetadata(origin) {
|
|
42322
|
+
const base = stripTrailingSlash(origin);
|
|
41800
42323
|
return {
|
|
41801
42324
|
issuer: base,
|
|
41802
42325
|
authorization_endpoint: `${base}/authorize`,
|
|
@@ -41808,8 +42331,8 @@ function authServerMetadata(issuer) {
|
|
|
41808
42331
|
token_endpoint_auth_methods_supported: ["none"]
|
|
41809
42332
|
};
|
|
41810
42333
|
}
|
|
41811
|
-
function protectedResourceMetadata(
|
|
41812
|
-
const base = stripTrailingSlash(
|
|
42334
|
+
function protectedResourceMetadata(origin) {
|
|
42335
|
+
const base = stripTrailingSlash(origin);
|
|
41813
42336
|
return {
|
|
41814
42337
|
resource: `${base}/mcp`,
|
|
41815
42338
|
authorization_servers: [base],
|
|
@@ -41820,335 +42343,67 @@ function stripTrailingSlash(s) {
|
|
|
41820
42343
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
41821
42344
|
}
|
|
41822
42345
|
|
|
41823
|
-
// src/adapters/mcp/oauth/
|
|
41824
|
-
|
|
41825
|
-
|
|
41826
|
-
|
|
41827
|
-
|
|
41828
|
-
|
|
41829
|
-
|
|
41830
|
-
|
|
41831
|
-
|
|
41832
|
-
|
|
41833
|
-
|
|
41834
|
-
|
|
41835
|
-
|
|
41836
|
-
|
|
41837
|
-
|
|
41838
|
-
|
|
41839
|
-
|
|
41840
|
-
|
|
41841
|
-
|
|
42346
|
+
// src/adapters/mcp/oauth/keycloak-proxy.ts
|
|
42347
|
+
var FORWARDED_AUTHORIZE_PARAMS = [
|
|
42348
|
+
"response_type",
|
|
42349
|
+
"client_id",
|
|
42350
|
+
"redirect_uri",
|
|
42351
|
+
"state",
|
|
42352
|
+
"scope",
|
|
42353
|
+
"code_challenge",
|
|
42354
|
+
"code_challenge_method",
|
|
42355
|
+
"nonce",
|
|
42356
|
+
"resource"
|
|
42357
|
+
];
|
|
42358
|
+
function handleAuthorizeRedirect(cfg, params) {
|
|
42359
|
+
const forwarded = new URLSearchParams();
|
|
42360
|
+
for (const key of FORWARDED_AUTHORIZE_PARAMS) {
|
|
42361
|
+
const value = params.get(key);
|
|
42362
|
+
if (value !== null) forwarded.set(key, value);
|
|
42363
|
+
}
|
|
42364
|
+
if (!forwarded.has("client_id")) forwarded.set("client_id", cfg.clientId);
|
|
42365
|
+
if (!forwarded.has("response_type")) forwarded.set("response_type", "code");
|
|
42366
|
+
return { location: `${cfg.authorizationEndpoint}?${forwarded.toString()}` };
|
|
42367
|
+
}
|
|
42368
|
+
async function handleTokenProxy(cfg, form, fetchImpl = fetch) {
|
|
42369
|
+
const upstream = new URLSearchParams(form);
|
|
42370
|
+
if (!upstream.has("client_id")) upstream.set("client_id", cfg.clientId);
|
|
42371
|
+
if (cfg.clientSecret && !upstream.has("client_secret")) {
|
|
42372
|
+
upstream.set("client_secret", cfg.clientSecret);
|
|
41842
42373
|
}
|
|
41843
|
-
const clientName = typeof req.client_name === "string" ? req.client_name : "mcp-client";
|
|
41844
|
-
const client = await repo.registerClient({
|
|
41845
|
-
clientName,
|
|
41846
|
-
redirectUris: req.redirect_uris
|
|
41847
|
-
});
|
|
41848
|
-
return {
|
|
41849
|
-
status: 201,
|
|
41850
|
-
body: {
|
|
41851
|
-
client_id: client.clientId,
|
|
41852
|
-
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
41853
|
-
client_name: client.clientName,
|
|
41854
|
-
redirect_uris: client.redirectUris,
|
|
41855
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
41856
|
-
response_types: ["code"],
|
|
41857
|
-
token_endpoint_auth_method: "none"
|
|
41858
|
-
}
|
|
41859
|
-
};
|
|
41860
|
-
}
|
|
41861
|
-
function errorResponse(status, error48, errorDescription) {
|
|
41862
|
-
return {
|
|
41863
|
-
status,
|
|
41864
|
-
body: { error: error48, error_description: errorDescription }
|
|
41865
|
-
};
|
|
41866
|
-
}
|
|
41867
|
-
function isHttpUrl(s) {
|
|
41868
42374
|
try {
|
|
41869
|
-
const
|
|
41870
|
-
|
|
42375
|
+
const res = await fetchImpl(cfg.tokenEndpoint, {
|
|
42376
|
+
method: "POST",
|
|
42377
|
+
headers: {
|
|
42378
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
42379
|
+
accept: "application/json"
|
|
42380
|
+
},
|
|
42381
|
+
body: upstream.toString()
|
|
42382
|
+
});
|
|
42383
|
+
const body = await res.json().catch(() => ({}));
|
|
42384
|
+
return { status: res.status, body };
|
|
41871
42385
|
} catch {
|
|
41872
|
-
return false;
|
|
41873
|
-
}
|
|
41874
|
-
}
|
|
41875
|
-
|
|
41876
|
-
// src/adapters/mcp/oauth/authorize.ts
|
|
41877
|
-
var import_crypto4 = require("crypto");
|
|
41878
|
-
var import_buffer = require("buffer");
|
|
41879
|
-
|
|
41880
|
-
// src/adapters/mcp/oauth/consent-template.ts
|
|
41881
|
-
function renderConsentScreen(params) {
|
|
41882
|
-
const errorBlock = params.errorMessage ? `<p class="err">${escapeHtml(params.errorMessage)}</p>` : "";
|
|
41883
|
-
const stateField = params.state === null ? "" : `<input type="hidden" name="state" value="${escapeHtml(params.state)}" />`;
|
|
41884
|
-
return `<!doctype html>
|
|
41885
|
-
<html lang="en">
|
|
41886
|
-
<head>
|
|
41887
|
-
<meta charset="utf-8" />
|
|
41888
|
-
<title>Authorize ${escapeHtml(params.clientName)}</title>
|
|
41889
|
-
<style>
|
|
41890
|
-
body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; color: #111; }
|
|
41891
|
-
h1 { font-size: 1.25rem; }
|
|
41892
|
-
.client { background: #f5f5f5; padding: 0.75rem 1rem; border-radius: 4px; margin: 1rem 0; font-family: ui-monospace, monospace; font-size: 0.875rem; }
|
|
41893
|
-
label { display: block; margin-top: 1rem; font-size: 0.9rem; }
|
|
41894
|
-
input[type=password] { width: 100%; padding: 0.5rem; font-size: 1rem; box-sizing: border-box; }
|
|
41895
|
-
button { margin-top: 1rem; padding: 0.5rem 1.25rem; font-size: 1rem; cursor: pointer; }
|
|
41896
|
-
.err { color: #b00020; margin-top: 1rem; font-size: 0.9rem; }
|
|
41897
|
-
.meta { color: #666; font-size: 0.8rem; margin-top: 2rem; }
|
|
41898
|
-
</style>
|
|
41899
|
-
</head>
|
|
41900
|
-
<body>
|
|
41901
|
-
<h1>Authorize access</h1>
|
|
41902
|
-
<p>An application wants to connect to your choda-deck MCP server.</p>
|
|
41903
|
-
<div class="client">
|
|
41904
|
-
<div><strong>${escapeHtml(params.clientName)}</strong></div>
|
|
41905
|
-
<div>${escapeHtml(params.clientId)}</div>
|
|
41906
|
-
<div>Redirect: ${escapeHtml(params.redirectUri)}</div>
|
|
41907
|
-
</div>
|
|
41908
|
-
${errorBlock}
|
|
41909
|
-
<form method="POST" action="/authorize">
|
|
41910
|
-
<input type="hidden" name="response_type" value="${escapeHtml(params.responseType)}" />
|
|
41911
|
-
<input type="hidden" name="client_id" value="${escapeHtml(params.clientId)}" />
|
|
41912
|
-
<input type="hidden" name="redirect_uri" value="${escapeHtml(params.redirectUri)}" />
|
|
41913
|
-
<input type="hidden" name="code_challenge" value="${escapeHtml(params.codeChallenge)}" />
|
|
41914
|
-
<input type="hidden" name="code_challenge_method" value="${escapeHtml(params.codeChallengeMethod)}" />
|
|
41915
|
-
${stateField}
|
|
41916
|
-
<label for="password">Consent password</label>
|
|
41917
|
-
<input id="password" name="consent_password" type="password" autocomplete="off" autofocus required />
|
|
41918
|
-
<button type="submit">Approve</button>
|
|
41919
|
-
</form>
|
|
41920
|
-
<p class="meta">choda-deck OAuth 2.0 + DCR (ADR-027)</p>
|
|
41921
|
-
</body>
|
|
41922
|
-
</html>`;
|
|
41923
|
-
}
|
|
41924
|
-
function escapeHtml(s) {
|
|
41925
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
41926
|
-
}
|
|
41927
|
-
|
|
41928
|
-
// src/adapters/mcp/oauth/authorize.ts
|
|
41929
|
-
var AUTH_CODE_TTL_SECONDS = 60;
|
|
41930
|
-
var CHALLENGE_MIN_LEN = 43;
|
|
41931
|
-
var CHALLENGE_MAX_LEN = 128;
|
|
41932
|
-
async function handleAuthorizeGet(repo, query) {
|
|
41933
|
-
const params = parseParams(query);
|
|
41934
|
-
const v = await validate2(repo, params);
|
|
41935
|
-
if (v.kind !== "ok") return v.result;
|
|
41936
|
-
return renderForm(params, v.client.clientName);
|
|
41937
|
-
}
|
|
41938
|
-
async function handleAuthorizePost(repo, form, consentPasswordHashHex) {
|
|
41939
|
-
const params = parseParams(form);
|
|
41940
|
-
const v = await validate2(repo, params);
|
|
41941
|
-
if (v.kind !== "ok") return v.result;
|
|
41942
|
-
const submitted = form.get("consent_password") ?? "";
|
|
41943
|
-
if (!verifyConsentPassword(submitted, consentPasswordHashHex)) {
|
|
41944
42386
|
return {
|
|
41945
|
-
|
|
41946
|
-
|
|
41947
|
-
html: renderConsentScreen({
|
|
41948
|
-
clientName: v.client.clientName,
|
|
41949
|
-
clientId: params.clientId,
|
|
41950
|
-
redirectUri: params.redirectUri,
|
|
41951
|
-
state: params.state,
|
|
41952
|
-
codeChallenge: params.codeChallenge,
|
|
41953
|
-
codeChallengeMethod: "S256",
|
|
41954
|
-
responseType: "code",
|
|
41955
|
-
errorMessage: "Incorrect consent password."
|
|
41956
|
-
})
|
|
42387
|
+
status: 502,
|
|
42388
|
+
body: { error: "server_error", error_description: "authorization server unreachable" }
|
|
41957
42389
|
};
|
|
41958
42390
|
}
|
|
41959
|
-
const ac = await repo.createAuthCode({
|
|
41960
|
-
clientId: params.clientId,
|
|
41961
|
-
codeChallenge: params.codeChallenge,
|
|
41962
|
-
redirectUri: params.redirectUri,
|
|
41963
|
-
ttlSeconds: AUTH_CODE_TTL_SECONDS
|
|
41964
|
-
});
|
|
41965
|
-
const url2 = new URL(params.redirectUri);
|
|
41966
|
-
url2.searchParams.set("code", ac.code);
|
|
41967
|
-
if (params.state !== null) url2.searchParams.set("state", params.state);
|
|
41968
|
-
return { kind: "redirect", status: 302, location: url2.toString() };
|
|
41969
|
-
}
|
|
41970
|
-
function parseParams(p) {
|
|
41971
|
-
return {
|
|
41972
|
-
responseType: p.get("response_type"),
|
|
41973
|
-
clientId: p.get("client_id"),
|
|
41974
|
-
redirectUri: p.get("redirect_uri"),
|
|
41975
|
-
state: p.get("state"),
|
|
41976
|
-
codeChallenge: p.get("code_challenge"),
|
|
41977
|
-
codeChallengeMethod: p.get("code_challenge_method")
|
|
41978
|
-
};
|
|
41979
|
-
}
|
|
41980
|
-
async function validate2(repo, p) {
|
|
41981
|
-
if (!p.clientId) {
|
|
41982
|
-
return { kind: "fatal", result: htmlError(400, "Missing client_id.") };
|
|
41983
|
-
}
|
|
41984
|
-
const client = await repo.getClient(p.clientId);
|
|
41985
|
-
if (!client) {
|
|
41986
|
-
return { kind: "fatal", result: htmlError(400, "Unknown client_id.") };
|
|
41987
|
-
}
|
|
41988
|
-
if (!p.redirectUri || !client.redirectUris.includes(p.redirectUri)) {
|
|
41989
|
-
return {
|
|
41990
|
-
kind: "fatal",
|
|
41991
|
-
result: htmlError(400, "Missing or unregistered redirect_uri.")
|
|
41992
|
-
};
|
|
41993
|
-
}
|
|
41994
|
-
if (p.responseType !== "code") {
|
|
41995
|
-
return {
|
|
41996
|
-
kind: "redirectError",
|
|
41997
|
-
result: redirectWithError(p.redirectUri, p.state, "unsupported_response_type")
|
|
41998
|
-
};
|
|
41999
|
-
}
|
|
42000
|
-
if (p.codeChallengeMethod !== "S256") {
|
|
42001
|
-
return {
|
|
42002
|
-
kind: "redirectError",
|
|
42003
|
-
result: redirectWithError(
|
|
42004
|
-
p.redirectUri,
|
|
42005
|
-
p.state,
|
|
42006
|
-
"invalid_request",
|
|
42007
|
-
"code_challenge_method must be S256"
|
|
42008
|
-
)
|
|
42009
|
-
};
|
|
42010
|
-
}
|
|
42011
|
-
if (!p.codeChallenge || p.codeChallenge.length < CHALLENGE_MIN_LEN || p.codeChallenge.length > CHALLENGE_MAX_LEN) {
|
|
42012
|
-
return {
|
|
42013
|
-
kind: "redirectError",
|
|
42014
|
-
result: redirectWithError(
|
|
42015
|
-
p.redirectUri,
|
|
42016
|
-
p.state,
|
|
42017
|
-
"invalid_request",
|
|
42018
|
-
"code_challenge missing or wrong length"
|
|
42019
|
-
)
|
|
42020
|
-
};
|
|
42021
|
-
}
|
|
42022
|
-
return { kind: "ok", client };
|
|
42023
|
-
}
|
|
42024
|
-
function renderForm(p, clientName) {
|
|
42025
|
-
return {
|
|
42026
|
-
kind: "html",
|
|
42027
|
-
status: 200,
|
|
42028
|
-
html: renderConsentScreen({
|
|
42029
|
-
clientName,
|
|
42030
|
-
clientId: p.clientId,
|
|
42031
|
-
redirectUri: p.redirectUri,
|
|
42032
|
-
state: p.state,
|
|
42033
|
-
codeChallenge: p.codeChallenge,
|
|
42034
|
-
codeChallengeMethod: "S256",
|
|
42035
|
-
responseType: "code"
|
|
42036
|
-
})
|
|
42037
|
-
};
|
|
42038
|
-
}
|
|
42039
|
-
function htmlError(status, message) {
|
|
42040
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"/><title>OAuth error</title></head><body style="font-family:system-ui;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1>Authorization error</h1><p>${escapeHtml2(message)}</p></body></html>`;
|
|
42041
|
-
return { kind: "html", status, html };
|
|
42042
|
-
}
|
|
42043
|
-
function redirectWithError(redirectUri, state, error48, errorDescription) {
|
|
42044
|
-
const url2 = new URL(redirectUri);
|
|
42045
|
-
url2.searchParams.set("error", error48);
|
|
42046
|
-
if (errorDescription) url2.searchParams.set("error_description", errorDescription);
|
|
42047
|
-
if (state !== null) url2.searchParams.set("state", state);
|
|
42048
|
-
return { kind: "redirect", status: 302, location: url2.toString() };
|
|
42049
|
-
}
|
|
42050
|
-
function verifyConsentPassword(submitted, expectedHashHex) {
|
|
42051
|
-
if (submitted.length === 0 || expectedHashHex.length === 0) return false;
|
|
42052
|
-
const submittedHash = (0, import_crypto4.createHash)("sha256").update(submitted, "utf8").digest();
|
|
42053
|
-
let expected;
|
|
42054
|
-
try {
|
|
42055
|
-
expected = import_buffer.Buffer.from(expectedHashHex, "hex");
|
|
42056
|
-
} catch {
|
|
42057
|
-
return false;
|
|
42058
|
-
}
|
|
42059
|
-
if (submittedHash.length !== expected.length) return false;
|
|
42060
|
-
return (0, import_crypto4.timingSafeEqual)(submittedHash, expected);
|
|
42061
|
-
}
|
|
42062
|
-
function escapeHtml2(s) {
|
|
42063
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
42064
|
-
}
|
|
42065
|
-
|
|
42066
|
-
// src/adapters/mcp/oauth/pkce.ts
|
|
42067
|
-
var import_crypto5 = require("crypto");
|
|
42068
|
-
var import_buffer2 = require("buffer");
|
|
42069
|
-
function verifyPkceS256(verifier, challenge) {
|
|
42070
|
-
if (verifier.length < 43 || verifier.length > 128) return false;
|
|
42071
|
-
const computed = computeChallengeS256(verifier);
|
|
42072
|
-
const a = import_buffer2.Buffer.from(computed, "utf8");
|
|
42073
|
-
const b = import_buffer2.Buffer.from(challenge, "utf8");
|
|
42074
|
-
if (a.length !== b.length) return false;
|
|
42075
|
-
return (0, import_crypto5.timingSafeEqual)(a, b);
|
|
42076
|
-
}
|
|
42077
|
-
function computeChallengeS256(verifier) {
|
|
42078
|
-
return (0, import_crypto5.createHash)("sha256").update(verifier, "utf8").digest("base64url");
|
|
42079
|
-
}
|
|
42080
|
-
|
|
42081
|
-
// src/adapters/mcp/oauth/token.ts
|
|
42082
|
-
var ACCESS_TTL_SECONDS = 60 * 60;
|
|
42083
|
-
var REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
42084
|
-
async function handleToken(repo, form) {
|
|
42085
|
-
const grantType = form.get("grant_type");
|
|
42086
|
-
if (grantType === "authorization_code") return handleCodeGrant(repo, form);
|
|
42087
|
-
if (grantType === "refresh_token") return handleRefreshGrant(repo, form);
|
|
42088
|
-
return errorResponse2(
|
|
42089
|
-
400,
|
|
42090
|
-
"unsupported_grant_type",
|
|
42091
|
-
"only authorization_code and refresh_token are supported"
|
|
42092
|
-
);
|
|
42093
|
-
}
|
|
42094
|
-
async function handleCodeGrant(repo, form) {
|
|
42095
|
-
const code = form.get("code");
|
|
42096
|
-
const redirectUri = form.get("redirect_uri");
|
|
42097
|
-
const clientId = form.get("client_id");
|
|
42098
|
-
const codeVerifier = form.get("code_verifier");
|
|
42099
|
-
if (!code || !redirectUri || !clientId || !codeVerifier) {
|
|
42100
|
-
return errorResponse2(400, "invalid_request", "missing required parameter");
|
|
42101
|
-
}
|
|
42102
|
-
const consumed = await repo.consumeAuthCode(code);
|
|
42103
|
-
if (!consumed) return errorResponse2(400, "invalid_grant", "code unknown or already used");
|
|
42104
|
-
if (Date.parse(consumed.expiresAt) <= Date.now()) {
|
|
42105
|
-
return errorResponse2(400, "invalid_grant", "code expired");
|
|
42106
|
-
}
|
|
42107
|
-
if (consumed.clientId !== clientId) {
|
|
42108
|
-
return errorResponse2(400, "invalid_grant", "client_id does not match code");
|
|
42109
|
-
}
|
|
42110
|
-
if (consumed.redirectUri !== redirectUri) {
|
|
42111
|
-
return errorResponse2(400, "invalid_grant", "redirect_uri does not match code");
|
|
42112
|
-
}
|
|
42113
|
-
if (!verifyPkceS256(codeVerifier, consumed.codeChallenge)) {
|
|
42114
|
-
return errorResponse2(400, "invalid_grant", "PKCE verifier does not match challenge");
|
|
42115
|
-
}
|
|
42116
|
-
const tokens = await repo.createTokens({
|
|
42117
|
-
clientId,
|
|
42118
|
-
accessTtlSeconds: ACCESS_TTL_SECONDS,
|
|
42119
|
-
refreshTtlSeconds: REFRESH_TTL_SECONDS
|
|
42120
|
-
});
|
|
42121
|
-
return tokenSuccess(tokens);
|
|
42122
|
-
}
|
|
42123
|
-
async function handleRefreshGrant(repo, form) {
|
|
42124
|
-
const refreshToken = form.get("refresh_token");
|
|
42125
|
-
if (!refreshToken) return errorResponse2(400, "invalid_request", "missing refresh_token");
|
|
42126
|
-
const result = await repo.rotateRefresh(refreshToken, {
|
|
42127
|
-
accessTtlSeconds: ACCESS_TTL_SECONDS,
|
|
42128
|
-
refreshTtlSeconds: REFRESH_TTL_SECONDS
|
|
42129
|
-
});
|
|
42130
|
-
if (!result.ok) {
|
|
42131
|
-
const description = result.error === "replay_detected" ? "refresh token replayed; chain revoked" : "refresh token unknown or expired";
|
|
42132
|
-
return errorResponse2(400, "invalid_grant", description);
|
|
42133
|
-
}
|
|
42134
|
-
return tokenSuccess(result.tokens);
|
|
42135
42391
|
}
|
|
42136
|
-
function
|
|
42392
|
+
function handleRegisterStatic(cfg, parsed) {
|
|
42393
|
+
const redirectUris = isObject2(parsed) && Array.isArray(parsed.redirect_uris) ? parsed.redirect_uris.filter((u) => typeof u === "string") : [];
|
|
42137
42394
|
return {
|
|
42138
|
-
status:
|
|
42395
|
+
status: 201,
|
|
42139
42396
|
body: {
|
|
42140
|
-
|
|
42141
|
-
|
|
42142
|
-
|
|
42143
|
-
|
|
42397
|
+
client_id: cfg.clientId,
|
|
42398
|
+
token_endpoint_auth_method: cfg.clientSecret ? "client_secret_post" : "none",
|
|
42399
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
42400
|
+
response_types: ["code"],
|
|
42401
|
+
redirect_uris: redirectUris
|
|
42144
42402
|
}
|
|
42145
42403
|
};
|
|
42146
42404
|
}
|
|
42147
|
-
function
|
|
42148
|
-
return
|
|
42149
|
-
status,
|
|
42150
|
-
body: { error: error48, error_description: errorDescription }
|
|
42151
|
-
};
|
|
42405
|
+
function isObject2(v) {
|
|
42406
|
+
return typeof v === "object" && v !== null;
|
|
42152
42407
|
}
|
|
42153
42408
|
|
|
42154
42409
|
// src/adapters/mcp/http-transport.ts
|
|
@@ -42160,10 +42415,10 @@ var BodyTooLargeError = class extends Error {
|
|
|
42160
42415
|
}
|
|
42161
42416
|
};
|
|
42162
42417
|
async function startHttpTransport(serverFactory, opts) {
|
|
42163
|
-
const tokenBuf =
|
|
42418
|
+
const tokenBuf = import_buffer.Buffer.from(opts.token, "utf8");
|
|
42164
42419
|
const oauth = opts.oauth;
|
|
42165
42420
|
const httpServer = (0, import_http.createServer)((req, res) => {
|
|
42166
|
-
handle(req, res, serverFactory, tokenBuf, oauth).catch((err) => {
|
|
42421
|
+
handle(req, res, serverFactory, tokenBuf, oauth, opts.syncSource).catch((err) => {
|
|
42167
42422
|
console.error("[choda-deck] http handler error", err);
|
|
42168
42423
|
if (!res.headersSent) {
|
|
42169
42424
|
res.writeHead(500);
|
|
@@ -42184,10 +42439,11 @@ async function startHttpTransport(serverFactory, opts) {
|
|
|
42184
42439
|
address: bound,
|
|
42185
42440
|
close: () => new Promise((resolve3, reject) => {
|
|
42186
42441
|
httpServer.close((err) => err ? reject(err) : resolve3());
|
|
42442
|
+
httpServer.closeIdleConnections();
|
|
42187
42443
|
})
|
|
42188
42444
|
};
|
|
42189
42445
|
}
|
|
42190
|
-
async function handle(req, res, serverFactory, tokenBuf, oauth) {
|
|
42446
|
+
async function handle(req, res, serverFactory, tokenBuf, oauth, syncSource) {
|
|
42191
42447
|
const parsedUrl = new URL(req.url ?? "/", "http://placeholder");
|
|
42192
42448
|
const pathname = parsedUrl.pathname;
|
|
42193
42449
|
const method = req.method ?? "GET";
|
|
@@ -42197,19 +42453,42 @@ async function handle(req, res, serverFactory, tokenBuf, oauth) {
|
|
|
42197
42453
|
if (oauth && await tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)) {
|
|
42198
42454
|
return;
|
|
42199
42455
|
}
|
|
42456
|
+
if (pathname === "/sync/since" && method === "GET") {
|
|
42457
|
+
return handleSyncSince(req, res, tokenBuf, oauth, syncSource, parsedUrl);
|
|
42458
|
+
}
|
|
42200
42459
|
if (pathname === "/mcp" && method === "POST") {
|
|
42201
42460
|
return handleMcp(req, res, serverFactory, tokenBuf, oauth);
|
|
42202
42461
|
}
|
|
42203
42462
|
res.writeHead(404);
|
|
42204
42463
|
res.end();
|
|
42205
42464
|
}
|
|
42465
|
+
async function handleSyncSince(req, res, tokenBuf, oauth, syncSource, parsedUrl) {
|
|
42466
|
+
if (!syncSource) {
|
|
42467
|
+
res.writeHead(404);
|
|
42468
|
+
res.end();
|
|
42469
|
+
return;
|
|
42470
|
+
}
|
|
42471
|
+
if (!await isAuthorized(req.headers.authorization ?? "", tokenBuf, oauth)) {
|
|
42472
|
+
sendUnauthorized(res, oauth);
|
|
42473
|
+
return;
|
|
42474
|
+
}
|
|
42475
|
+
const sinceRaw = parsedUrl.searchParams.get("since");
|
|
42476
|
+
const since = Number.parseInt(sinceRaw ?? "0", 10);
|
|
42477
|
+
if (!Number.isFinite(since) || since < 0) {
|
|
42478
|
+
res.writeHead(400);
|
|
42479
|
+
res.end();
|
|
42480
|
+
return;
|
|
42481
|
+
}
|
|
42482
|
+
const deltas = await syncSource.fetchSince(since);
|
|
42483
|
+
sendJson(res, 200, { since, deltas });
|
|
42484
|
+
}
|
|
42206
42485
|
async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth) {
|
|
42207
42486
|
if (pathname === "/.well-known/oauth-authorization-server" && method === "GET") {
|
|
42208
|
-
sendJson(res, 200, authServerMetadata(oauth.
|
|
42487
|
+
sendJson(res, 200, authServerMetadata(oauth.origin));
|
|
42209
42488
|
return true;
|
|
42210
42489
|
}
|
|
42211
42490
|
if (pathname === "/.well-known/oauth-protected-resource" && method === "GET") {
|
|
42212
|
-
sendJson(res, 200, protectedResourceMetadata(oauth.
|
|
42491
|
+
sendJson(res, 200, protectedResourceMetadata(oauth.origin));
|
|
42213
42492
|
return true;
|
|
42214
42493
|
}
|
|
42215
42494
|
if (pathname === "/register" && method === "POST") {
|
|
@@ -42220,26 +42499,14 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
|
|
|
42220
42499
|
writeBodyError(res, err);
|
|
42221
42500
|
return true;
|
|
42222
42501
|
}
|
|
42223
|
-
const result =
|
|
42502
|
+
const result = handleRegisterStatic(oauth.keycloak, parsed);
|
|
42224
42503
|
sendJson(res, result.status, result.body);
|
|
42225
42504
|
return true;
|
|
42226
42505
|
}
|
|
42227
42506
|
if (pathname === "/authorize" && method === "GET") {
|
|
42228
|
-
|
|
42229
|
-
|
|
42230
|
-
|
|
42231
|
-
if (pathname === "/authorize" && method === "POST") {
|
|
42232
|
-
let form;
|
|
42233
|
-
try {
|
|
42234
|
-
form = await readForm(req);
|
|
42235
|
-
} catch (err) {
|
|
42236
|
-
writeBodyError(res, err);
|
|
42237
|
-
return true;
|
|
42238
|
-
}
|
|
42239
|
-
sendAuthorizeResult(
|
|
42240
|
-
res,
|
|
42241
|
-
await handleAuthorizePost(oauth.repo, form, oauth.consentPasswordHashHex)
|
|
42242
|
-
);
|
|
42507
|
+
const { location } = handleAuthorizeRedirect(oauth.keycloak, parsedUrl.searchParams);
|
|
42508
|
+
res.writeHead(302, { location });
|
|
42509
|
+
res.end();
|
|
42243
42510
|
return true;
|
|
42244
42511
|
}
|
|
42245
42512
|
if (pathname === "/token" && method === "POST") {
|
|
@@ -42250,7 +42517,7 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
|
|
|
42250
42517
|
writeBodyError(res, err);
|
|
42251
42518
|
return true;
|
|
42252
42519
|
}
|
|
42253
|
-
const result = await
|
|
42520
|
+
const result = await handleTokenProxy(oauth.keycloak, form);
|
|
42254
42521
|
res.setHeader("Cache-Control", "no-store");
|
|
42255
42522
|
sendJson(res, result.status, result.body);
|
|
42256
42523
|
return true;
|
|
@@ -42258,15 +42525,8 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
|
|
|
42258
42525
|
return false;
|
|
42259
42526
|
}
|
|
42260
42527
|
async function handleMcp(req, res, serverFactory, tokenBuf, oauth) {
|
|
42261
|
-
|
|
42262
|
-
|
|
42263
|
-
if (!authorized) {
|
|
42264
|
-
if (oauth) {
|
|
42265
|
-
const resourceMetadataUrl = `${oauth.issuer}/.well-known/oauth-protected-resource`;
|
|
42266
|
-
res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
42267
|
-
}
|
|
42268
|
-
res.writeHead(401);
|
|
42269
|
-
res.end();
|
|
42528
|
+
if (!await isAuthorized(req.headers.authorization ?? "", tokenBuf, oauth)) {
|
|
42529
|
+
sendUnauthorized(res, oauth);
|
|
42270
42530
|
return;
|
|
42271
42531
|
}
|
|
42272
42532
|
const contentType = (req.headers["content-type"] ?? "").toLowerCase();
|
|
@@ -42309,36 +42569,39 @@ async function handleMcp(req, res, serverFactory, tokenBuf, oauth) {
|
|
|
42309
42569
|
}
|
|
42310
42570
|
}
|
|
42311
42571
|
}
|
|
42572
|
+
async function isAuthorized(authHeader, tokenBuf, oauth) {
|
|
42573
|
+
return oauth ? verifyOAuthBearer(authHeader, oauth.verifier) : verifyBearer(authHeader, tokenBuf);
|
|
42574
|
+
}
|
|
42575
|
+
function sendUnauthorized(res, oauth) {
|
|
42576
|
+
if (oauth) {
|
|
42577
|
+
const resourceMetadataUrl = `${oauth.origin}/.well-known/oauth-protected-resource`;
|
|
42578
|
+
res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
42579
|
+
}
|
|
42580
|
+
res.writeHead(401);
|
|
42581
|
+
res.end();
|
|
42582
|
+
}
|
|
42312
42583
|
function verifyBearer(authHeader, tokenBuf) {
|
|
42313
42584
|
const prefix = "Bearer ";
|
|
42314
42585
|
if (!authHeader.startsWith(prefix)) return false;
|
|
42315
|
-
const provided =
|
|
42586
|
+
const provided = import_buffer.Buffer.from(authHeader.slice(prefix.length), "utf8");
|
|
42316
42587
|
if (provided.length !== tokenBuf.length) return false;
|
|
42317
|
-
return (0,
|
|
42588
|
+
return (0, import_crypto2.timingSafeEqual)(provided, tokenBuf);
|
|
42318
42589
|
}
|
|
42319
|
-
async function verifyOAuthBearer(authHeader,
|
|
42590
|
+
async function verifyOAuthBearer(authHeader, verifier) {
|
|
42320
42591
|
const prefix = "Bearer ";
|
|
42321
42592
|
if (!authHeader.startsWith(prefix)) return false;
|
|
42322
42593
|
const token = authHeader.slice(prefix.length);
|
|
42323
|
-
|
|
42594
|
+
const claims = await verifier.verify(token);
|
|
42595
|
+
return claims !== null;
|
|
42324
42596
|
}
|
|
42325
42597
|
function sendJson(res, status, body) {
|
|
42326
42598
|
const payload = JSON.stringify(body);
|
|
42327
42599
|
res.writeHead(status, {
|
|
42328
42600
|
"content-type": "application/json",
|
|
42329
|
-
"content-length":
|
|
42601
|
+
"content-length": import_buffer.Buffer.byteLength(payload).toString()
|
|
42330
42602
|
});
|
|
42331
42603
|
res.end(payload);
|
|
42332
42604
|
}
|
|
42333
|
-
function sendAuthorizeResult(res, result) {
|
|
42334
|
-
if (result.kind === "redirect") {
|
|
42335
|
-
res.writeHead(302, { location: result.location });
|
|
42336
|
-
res.end();
|
|
42337
|
-
return;
|
|
42338
|
-
}
|
|
42339
|
-
res.writeHead(result.status, { "content-type": "text/html; charset=utf-8" });
|
|
42340
|
-
res.end(result.html);
|
|
42341
|
-
}
|
|
42342
42605
|
async function readJson(req) {
|
|
42343
42606
|
const raw = await readBody(req);
|
|
42344
42607
|
return JSON.parse(raw.toString("utf8"));
|
|
@@ -42379,7 +42642,7 @@ function readBody(req) {
|
|
|
42379
42642
|
chunks.push(chunk);
|
|
42380
42643
|
}
|
|
42381
42644
|
});
|
|
42382
|
-
req.on("end", () => settle(() => resolve3(
|
|
42645
|
+
req.on("end", () => settle(() => resolve3(import_buffer.Buffer.concat(chunks))));
|
|
42383
42646
|
req.on("error", (err) => settle(() => reject(err)));
|
|
42384
42647
|
});
|
|
42385
42648
|
}
|
|
@@ -42401,293 +42664,119 @@ function listen(server, port, bind) {
|
|
|
42401
42664
|
});
|
|
42402
42665
|
}
|
|
42403
42666
|
|
|
42404
|
-
// src/adapters/mcp/
|
|
42405
|
-
|
|
42406
|
-
|
|
42407
|
-
|
|
42408
|
-
|
|
42409
|
-
|
|
42410
|
-
|
|
42411
|
-
|
|
42412
|
-
|
|
42413
|
-
|
|
42414
|
-
|
|
42415
|
-
|
|
42416
|
-
|
|
42417
|
-
|
|
42418
|
-
|
|
42419
|
-
|
|
42420
|
-
|
|
42421
|
-
|
|
42422
|
-
|
|
42423
|
-
|
|
42424
|
-
|
|
42425
|
-
|
|
42426
|
-
|
|
42427
|
-
|
|
42428
|
-
|
|
42429
|
-
|
|
42430
|
-
|
|
42431
|
-
|
|
42432
|
-
|
|
42433
|
-
|
|
42434
|
-
|
|
42435
|
-
|
|
42436
|
-
|
|
42437
|
-
|
|
42438
|
-
|
|
42439
|
-
|
|
42440
|
-
|
|
42441
|
-
|
|
42442
|
-
|
|
42443
|
-
|
|
42444
|
-
|
|
42445
|
-
|
|
42446
|
-
|
|
42447
|
-
|
|
42448
|
-
|
|
42449
|
-
|
|
42450
|
-
|
|
42451
|
-
|
|
42452
|
-
|
|
42453
|
-
|
|
42454
|
-
|
|
42455
|
-
|
|
42456
|
-
|
|
42457
|
-
|
|
42458
|
-
|
|
42459
|
-
|
|
42460
|
-
|
|
42461
|
-
|
|
42462
|
-
|
|
42463
|
-
|
|
42464
|
-
|
|
42465
|
-
|
|
42466
|
-
|
|
42467
|
-
|
|
42468
|
-
|
|
42469
|
-
|
|
42470
|
-
|
|
42471
|
-
|
|
42472
|
-
|
|
42473
|
-
"bug",
|
|
42474
|
-
"feat",
|
|
42475
|
-
"fix",
|
|
42476
|
-
"test",
|
|
42477
|
-
"metrics",
|
|
42478
|
-
"chore",
|
|
42479
|
-
"docs"
|
|
42480
|
-
]);
|
|
42481
|
-
async function buildGraphifyContext(task, svc) {
|
|
42482
|
-
const graphPath = await findGraphPath(task.projectId, svc);
|
|
42483
|
-
if (!graphPath) {
|
|
42484
|
-
return {
|
|
42485
|
-
status: "no-graph",
|
|
42486
|
-
message: "No graphify-out/graph.json in project workspaces. Run `/graphify <workspace>` to enable graph-driven context."
|
|
42487
|
-
};
|
|
42488
|
-
}
|
|
42489
|
-
const keywords = extractKeywords(task);
|
|
42490
|
-
const filePointerPaths = extractFilePointers(task.body ?? "");
|
|
42491
|
-
if (keywords.length === 0 && filePointerPaths.length === 0) {
|
|
42492
|
-
return {
|
|
42493
|
-
status: "no-matches",
|
|
42494
|
-
message: "Task title/AC/labels/file-pointers produced no usable signal."
|
|
42495
|
-
};
|
|
42496
|
-
}
|
|
42497
|
-
const data = JSON.parse(fs3.readFileSync(graphPath, "utf8"));
|
|
42498
|
-
const nodeIndex = new Map(data.nodes.map((n) => [n.id, n]));
|
|
42499
|
-
const adj = buildAdjacency(data.links);
|
|
42500
|
-
const startNodes = findStartNodes(data.nodes, keywords, filePointerPaths, 3);
|
|
42501
|
-
if (startNodes.length === 0) {
|
|
42502
|
-
return {
|
|
42503
|
-
status: "no-matches",
|
|
42504
|
-
message: filePointerPaths.length > 0 ? `File pointers not in graph: ${filePointerPaths.join(", ")}; keywords: ${keywords.join(", ")}` : `No graph nodes matched keywords: ${keywords.join(", ")}`
|
|
42505
|
-
};
|
|
42667
|
+
// src/adapters/mcp/oauth/jwt-verifier.ts
|
|
42668
|
+
var import_crypto3 = require("crypto");
|
|
42669
|
+
var REFRESH_THROTTLE_MS = 1e4;
|
|
42670
|
+
function createKeycloakVerifier(config2, loadKeys) {
|
|
42671
|
+
return new KeycloakJwtVerifier(config2, loadKeys ?? defaultJwksLoader(config2.jwksUri));
|
|
42672
|
+
}
|
|
42673
|
+
var KeycloakJwtVerifier = class {
|
|
42674
|
+
constructor(config2, loadKeys) {
|
|
42675
|
+
this.config = config2;
|
|
42676
|
+
this.loadKeys = loadKeys;
|
|
42677
|
+
}
|
|
42678
|
+
config;
|
|
42679
|
+
loadKeys;
|
|
42680
|
+
keys = /* @__PURE__ */ new Map();
|
|
42681
|
+
lastFetchMs = 0;
|
|
42682
|
+
inflight = null;
|
|
42683
|
+
async verify(token) {
|
|
42684
|
+
const parts = token.split(".");
|
|
42685
|
+
if (parts.length !== 3) return null;
|
|
42686
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
42687
|
+
const header = decodeJson(headerB64);
|
|
42688
|
+
if (!header || header.alg !== "RS256" || !header.kid) return null;
|
|
42689
|
+
const key = await this.resolveKey(header.kid);
|
|
42690
|
+
if (!key) return null;
|
|
42691
|
+
if (!verifySignature(`${headerB64}.${payloadB64}`, signatureB64, key)) return null;
|
|
42692
|
+
const claims = decodeJson(payloadB64);
|
|
42693
|
+
if (!claims) return null;
|
|
42694
|
+
if (!this.claimsValid(claims)) return null;
|
|
42695
|
+
return claims;
|
|
42696
|
+
}
|
|
42697
|
+
claimsValid(claims) {
|
|
42698
|
+
if (claims.iss !== this.config.issuer) return false;
|
|
42699
|
+
if (typeof claims.exp !== "number" || claims.exp * 1e3 <= Date.now()) return false;
|
|
42700
|
+
return audienceMatches(claims, this.config.audience);
|
|
42701
|
+
}
|
|
42702
|
+
// Cache by kid; on an unknown kid (key rollover) refetch once, throttled so a
|
|
42703
|
+
// bogus-kid flood can't hammer Keycloak.
|
|
42704
|
+
async resolveKey(kid) {
|
|
42705
|
+
const cached2 = this.keys.get(kid);
|
|
42706
|
+
if (cached2) return cached2;
|
|
42707
|
+
if (Date.now() - this.lastFetchMs < REFRESH_THROTTLE_MS) return null;
|
|
42708
|
+
await this.refresh();
|
|
42709
|
+
return this.keys.get(kid) ?? null;
|
|
42710
|
+
}
|
|
42711
|
+
async refresh() {
|
|
42712
|
+
if (this.inflight) return this.inflight;
|
|
42713
|
+
this.inflight = (async () => {
|
|
42714
|
+
try {
|
|
42715
|
+
const jwks = await this.loadKeys();
|
|
42716
|
+
const next = /* @__PURE__ */ new Map();
|
|
42717
|
+
for (const jwk of jwks) {
|
|
42718
|
+
if (jwk.kty !== "RSA" || !jwk.kid || !jwk.n || !jwk.e) continue;
|
|
42719
|
+
try {
|
|
42720
|
+
next.set(
|
|
42721
|
+
jwk.kid,
|
|
42722
|
+
(0, import_crypto3.createPublicKey)({ key: jwk, format: "jwk" })
|
|
42723
|
+
);
|
|
42724
|
+
} catch {
|
|
42725
|
+
}
|
|
42726
|
+
}
|
|
42727
|
+
this.keys = next;
|
|
42728
|
+
this.lastFetchMs = Date.now();
|
|
42729
|
+
} catch {
|
|
42730
|
+
this.lastFetchMs = Date.now();
|
|
42731
|
+
} finally {
|
|
42732
|
+
this.inflight = null;
|
|
42733
|
+
}
|
|
42734
|
+
})();
|
|
42735
|
+
return this.inflight;
|
|
42506
42736
|
}
|
|
42507
|
-
|
|
42508
|
-
|
|
42509
|
-
|
|
42510
|
-
|
|
42511
|
-
|
|
42512
|
-
|
|
42513
|
-
);
|
|
42514
|
-
const affected_files = extractAffectedFiles(subgraph, nodeIndex, MAX_AFFECTED_FILES);
|
|
42515
|
-
const god_nodes = identifyGodNodes(
|
|
42516
|
-
subgraph,
|
|
42517
|
-
adj,
|
|
42518
|
-
nodeIndex,
|
|
42519
|
-
GOD_NODE_DEGREE_THRESHOLD,
|
|
42520
|
-
MAX_GOD_NODES
|
|
42521
|
-
);
|
|
42522
|
-
const affected_communities = identifyAffectedCommunities(subgraph, nodeIndex);
|
|
42523
|
-
const staleness = computeStaleness(graphPath);
|
|
42524
|
-
return {
|
|
42525
|
-
affected_files,
|
|
42526
|
-
god_nodes,
|
|
42527
|
-
affected_communities,
|
|
42528
|
-
keywords_used: keywords,
|
|
42529
|
-
...staleness
|
|
42530
|
-
};
|
|
42737
|
+
};
|
|
42738
|
+
function audienceMatches(claims, expected) {
|
|
42739
|
+
const aud = claims.aud;
|
|
42740
|
+
if (typeof aud === "string" && aud === expected) return true;
|
|
42741
|
+
if (Array.isArray(aud) && aud.includes(expected)) return true;
|
|
42742
|
+
return claims.azp === expected;
|
|
42531
42743
|
}
|
|
42532
|
-
|
|
42533
|
-
|
|
42534
|
-
|
|
42535
|
-
|
|
42536
|
-
|
|
42744
|
+
function verifySignature(signingInput, signatureB64, key) {
|
|
42745
|
+
try {
|
|
42746
|
+
const verifier = (0, import_crypto3.createVerify)("RSA-SHA256");
|
|
42747
|
+
verifier.update(signingInput);
|
|
42748
|
+
verifier.end();
|
|
42749
|
+
return verifier.verify(key, base64UrlToBuffer(signatureB64));
|
|
42750
|
+
} catch {
|
|
42751
|
+
return false;
|
|
42537
42752
|
}
|
|
42538
|
-
|
|
42539
|
-
|
|
42540
|
-
|
|
42541
|
-
|
|
42753
|
+
}
|
|
42754
|
+
function decodeJson(segment) {
|
|
42755
|
+
try {
|
|
42756
|
+
return JSON.parse(base64UrlToBuffer(segment).toString("utf8"));
|
|
42757
|
+
} catch {
|
|
42758
|
+
return null;
|
|
42542
42759
|
}
|
|
42543
|
-
return null;
|
|
42544
42760
|
}
|
|
42545
|
-
function
|
|
42546
|
-
const
|
|
42547
|
-
|
|
42548
|
-
|
|
42549
|
-
|
|
42550
|
-
|
|
42551
|
-
|
|
42552
|
-
|
|
42553
|
-
|
|
42554
|
-
|
|
42555
|
-
deduped.add(cleaned);
|
|
42556
|
-
}
|
|
42557
|
-
return Array.from(deduped);
|
|
42558
|
-
}
|
|
42559
|
-
function extractFilePointers(body) {
|
|
42560
|
-
const match = body.match(/(?:^|\n)##\s*File Pointers\s*\n[\s\S]*?(?=\n##\s|$)/i);
|
|
42561
|
-
if (!match) return [];
|
|
42562
|
-
const paths = [];
|
|
42563
|
-
for (const line of splitLines(match[0])) {
|
|
42564
|
-
if (/\(NEW\)/i.test(line)) continue;
|
|
42565
|
-
const pathMatch = line.match(/^\s*-\s+`([^`]+)`/);
|
|
42566
|
-
if (!pathMatch) continue;
|
|
42567
|
-
paths.push(pathMatch[1].replace(/\\/g, "/"));
|
|
42568
|
-
}
|
|
42569
|
-
return paths;
|
|
42570
|
-
}
|
|
42571
|
-
function cleanToken(t) {
|
|
42572
|
-
return t.replace(/^[^a-z0-9_-]+|[^a-z0-9_-]+$/gi, "");
|
|
42573
|
-
}
|
|
42574
|
-
function extractAcceptanceSection(body) {
|
|
42575
|
-
const match = body.match(/(?:^|\n)##\s*Acceptance\s*\n[\s\S]*?(?=\n##\s|$)/i);
|
|
42576
|
-
if (!match) return "";
|
|
42577
|
-
return match[0].slice(0, 500);
|
|
42578
|
-
}
|
|
42579
|
-
function findStartNodes(nodes, keywords, filePointerPaths, topK) {
|
|
42580
|
-
if (filePointerPaths.length > 0) {
|
|
42581
|
-
const fileMatches = findStartNodesByFile(nodes, filePointerPaths);
|
|
42582
|
-
if (fileMatches.length > 0) return fileMatches;
|
|
42583
|
-
}
|
|
42584
|
-
return findStartNodesByKeyword(nodes, keywords, topK);
|
|
42585
|
-
}
|
|
42586
|
-
function findStartNodesByFile(nodes, paths) {
|
|
42587
|
-
const targets = new Set(paths.map((p) => p.replace(/\\/g, "/")));
|
|
42588
|
-
const matches = [];
|
|
42589
|
-
for (const n of nodes) {
|
|
42590
|
-
if (!n.source_file) continue;
|
|
42591
|
-
const normalized = n.source_file.replace(/\\/g, "/");
|
|
42592
|
-
if (targets.has(normalized)) {
|
|
42593
|
-
matches.push({ id: n.id, score: 100 });
|
|
42594
|
-
}
|
|
42595
|
-
}
|
|
42596
|
-
matches.sort((a, b) => {
|
|
42597
|
-
if (a.id < b.id) return -1;
|
|
42598
|
-
if (a.id > b.id) return 1;
|
|
42599
|
-
return 0;
|
|
42600
|
-
});
|
|
42601
|
-
return matches;
|
|
42602
|
-
}
|
|
42603
|
-
function findStartNodesByKeyword(nodes, keywords, topK) {
|
|
42604
|
-
const scored = [];
|
|
42605
|
-
for (const n of nodes) {
|
|
42606
|
-
const label = (n.label ?? "").toLowerCase();
|
|
42607
|
-
let score = 0;
|
|
42608
|
-
for (const k of keywords) {
|
|
42609
|
-
if (label.includes(k)) score += 1;
|
|
42610
|
-
}
|
|
42611
|
-
if (score > 0) scored.push({ id: n.id, score });
|
|
42612
|
-
}
|
|
42613
|
-
scored.sort((a, b) => {
|
|
42614
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
42615
|
-
if (b.id > a.id) return 1;
|
|
42616
|
-
if (b.id < a.id) return -1;
|
|
42617
|
-
return 0;
|
|
42618
|
-
});
|
|
42619
|
-
return scored.slice(0, topK);
|
|
42620
|
-
}
|
|
42621
|
-
function buildAdjacency(links) {
|
|
42622
|
-
const adj = /* @__PURE__ */ new Map();
|
|
42623
|
-
const push = (from, to, link) => {
|
|
42624
|
-
if (!adj.has(from)) adj.set(from, []);
|
|
42625
|
-
adj.get(from).push({ neighbor: to, link });
|
|
42626
|
-
};
|
|
42627
|
-
for (const l of links) {
|
|
42628
|
-
push(l.source, l.target, l);
|
|
42629
|
-
push(l.target, l.source, l);
|
|
42630
|
-
}
|
|
42631
|
-
return adj;
|
|
42632
|
-
}
|
|
42633
|
-
function bfs(adj, startNodes, depth, relationFilter, confidenceMin) {
|
|
42634
|
-
const visited = new Set(startNodes);
|
|
42635
|
-
let frontier = new Set(startNodes);
|
|
42636
|
-
for (let d = 0; d < depth; d += 1) {
|
|
42637
|
-
const next = /* @__PURE__ */ new Set();
|
|
42638
|
-
for (const n of frontier) {
|
|
42639
|
-
const neighbors = adj.get(n) ?? [];
|
|
42640
|
-
for (const { neighbor, link } of neighbors) {
|
|
42641
|
-
if (link.relation && !relationFilter.has(link.relation)) continue;
|
|
42642
|
-
if ((link.confidence_score ?? 1) < confidenceMin) continue;
|
|
42643
|
-
if (!visited.has(neighbor)) next.add(neighbor);
|
|
42644
|
-
}
|
|
42645
|
-
}
|
|
42646
|
-
for (const n of next) visited.add(n);
|
|
42647
|
-
frontier = next;
|
|
42648
|
-
}
|
|
42649
|
-
return visited;
|
|
42650
|
-
}
|
|
42651
|
-
function extractAffectedFiles(subgraph, nodeIndex, max) {
|
|
42652
|
-
const hits = /* @__PURE__ */ new Map();
|
|
42653
|
-
for (const id of subgraph) {
|
|
42654
|
-
const n = nodeIndex.get(id);
|
|
42655
|
-
if (!n?.source_file) continue;
|
|
42656
|
-
hits.set(n.source_file, (hits.get(n.source_file) ?? 0) + 1);
|
|
42657
|
-
}
|
|
42658
|
-
return Array.from(hits.entries()).map(([p, h]) => ({ path: p, hits: h })).sort((a, b) => b.hits - a.hits).slice(0, max);
|
|
42659
|
-
}
|
|
42660
|
-
function identifyGodNodes(subgraph, adj, nodeIndex, threshold, max) {
|
|
42661
|
-
const result = [];
|
|
42662
|
-
for (const id of subgraph) {
|
|
42663
|
-
const deg = (adj.get(id) ?? []).length;
|
|
42664
|
-
if (deg >= threshold) {
|
|
42665
|
-
result.push({ id, label: nodeIndex.get(id)?.label ?? id, degree: deg });
|
|
42666
|
-
}
|
|
42667
|
-
}
|
|
42668
|
-
return result.sort((a, b) => b.degree - a.degree).slice(0, max);
|
|
42669
|
-
}
|
|
42670
|
-
function identifyAffectedCommunities(subgraph, nodeIndex) {
|
|
42671
|
-
const counts = /* @__PURE__ */ new Map();
|
|
42672
|
-
for (const id of subgraph) {
|
|
42673
|
-
const n = nodeIndex.get(id);
|
|
42674
|
-
if (typeof n?.community !== "number") continue;
|
|
42675
|
-
counts.set(n.community, (counts.get(n.community) ?? 0) + 1);
|
|
42676
|
-
}
|
|
42677
|
-
return Array.from(counts.entries()).map(([cid, count]) => ({ id: cid, label: null, nodeCount: count })).sort((a, b) => b.nodeCount - a.nodeCount);
|
|
42678
|
-
}
|
|
42679
|
-
function computeStaleness(graphPath) {
|
|
42680
|
-
const stat = fs3.statSync(graphPath);
|
|
42681
|
-
const mtimeMs = stat.mtimeMs;
|
|
42682
|
-
const ageMs = Date.now() - mtimeMs;
|
|
42683
|
-
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
42684
|
-
return {
|
|
42685
|
-
graph_mtime_iso: new Date(mtimeMs).toISOString(),
|
|
42686
|
-
graph_age_days: Math.round(ageDays * 10) / 10,
|
|
42687
|
-
graph_is_stale: ageDays > STALE_DAYS
|
|
42761
|
+
function base64UrlToBuffer(input) {
|
|
42762
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
42763
|
+
return Buffer.from(padded, "base64");
|
|
42764
|
+
}
|
|
42765
|
+
function defaultJwksLoader(jwksUri) {
|
|
42766
|
+
return async () => {
|
|
42767
|
+
const res = await fetch(jwksUri, { headers: { accept: "application/json" } });
|
|
42768
|
+
if (!res.ok) throw new Error(`JWKS fetch ${res.status}`);
|
|
42769
|
+
const body = await res.json();
|
|
42770
|
+
return body.keys ?? [];
|
|
42688
42771
|
};
|
|
42689
42772
|
}
|
|
42690
42773
|
|
|
42774
|
+
// src/adapters/mcp/mcp-tools/types.ts
|
|
42775
|
+
function textResponse(payload) {
|
|
42776
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
42777
|
+
return { content: [{ type: "text", text }] };
|
|
42778
|
+
}
|
|
42779
|
+
|
|
42691
42780
|
// src/adapters/mcp/mcp-tools/task-tools.ts
|
|
42692
42781
|
function defaultBody(id, title) {
|
|
42693
42782
|
return `# ${id}: ${title}
|
|
@@ -42738,7 +42827,6 @@ var register = (server, svc) => {
|
|
|
42738
42827
|
}))
|
|
42739
42828
|
}))
|
|
42740
42829
|
);
|
|
42741
|
-
const graphify_context = await buildGraphifyContext(task, svc);
|
|
42742
42830
|
return textResponse({
|
|
42743
42831
|
task,
|
|
42744
42832
|
dependencies: deps,
|
|
@@ -42746,8 +42834,7 @@ var register = (server, svc) => {
|
|
|
42746
42834
|
tags,
|
|
42747
42835
|
relationships: rels,
|
|
42748
42836
|
conversations,
|
|
42749
|
-
body: task.body
|
|
42750
|
-
graphify_context
|
|
42837
|
+
body: task.body
|
|
42751
42838
|
});
|
|
42752
42839
|
}
|
|
42753
42840
|
);
|
|
@@ -42869,13 +42956,13 @@ var EMPTY_RULES = {
|
|
|
42869
42956
|
sessionEnd: "",
|
|
42870
42957
|
conversationRead: ""
|
|
42871
42958
|
};
|
|
42872
|
-
function loadMcpRules(
|
|
42873
|
-
if (!(0, import_fs.existsSync)(
|
|
42874
|
-
console.warn(`[mcp-rules] file not found at ${
|
|
42959
|
+
function loadMcpRules(path9 = rulesPath()) {
|
|
42960
|
+
if (!(0, import_fs.existsSync)(path9)) {
|
|
42961
|
+
console.warn(`[mcp-rules] file not found at ${path9} \u2014 returning empty rules`);
|
|
42875
42962
|
return { ...EMPTY_RULES };
|
|
42876
42963
|
}
|
|
42877
42964
|
try {
|
|
42878
|
-
const content = (0, import_fs.readFileSync)(
|
|
42965
|
+
const content = (0, import_fs.readFileSync)(path9, "utf-8");
|
|
42879
42966
|
const sessionStart = parseSection(content, "On session_start");
|
|
42880
42967
|
const sessionCheckpoint = parseSection(content, "On session_checkpoint");
|
|
42881
42968
|
const sessionResume = parseSection(content, "On session_resume");
|
|
@@ -42888,11 +42975,11 @@ function loadMcpRules(path10 = rulesPath()) {
|
|
|
42888
42975
|
["On session_end", sessionEnd],
|
|
42889
42976
|
["On conversation_read", conversationRead]
|
|
42890
42977
|
]) {
|
|
42891
|
-
if (!value) console.warn(`[mcp-rules] "## ${name}" section missing in ${
|
|
42978
|
+
if (!value) console.warn(`[mcp-rules] "## ${name}" section missing in ${path9}`);
|
|
42892
42979
|
}
|
|
42893
42980
|
return { sessionStart, sessionCheckpoint, sessionResume, sessionEnd, conversationRead };
|
|
42894
42981
|
} catch (err) {
|
|
42895
|
-
console.error(`[mcp-rules] failed to read ${
|
|
42982
|
+
console.error(`[mcp-rules] failed to read ${path9}:`, err);
|
|
42896
42983
|
return { ...EMPTY_RULES };
|
|
42897
42984
|
}
|
|
42898
42985
|
}
|
|
@@ -43121,8 +43208,8 @@ var register2 = (server, svc) => {
|
|
|
43121
43208
|
};
|
|
43122
43209
|
|
|
43123
43210
|
// src/adapters/mcp/mcp-tools/project-context-builder.ts
|
|
43124
|
-
var
|
|
43125
|
-
var
|
|
43211
|
+
var fs3 = __toESM(require("fs"));
|
|
43212
|
+
var path5 = __toESM(require("path"));
|
|
43126
43213
|
|
|
43127
43214
|
// src/core/domain/inbox-triage-policy.ts
|
|
43128
43215
|
var STALE_RAW_DAYS = 3;
|
|
@@ -43225,10 +43312,10 @@ function loadRecentDecisions(sources, contentRoot, depth) {
|
|
|
43225
43312
|
}).filter((d) => d.excerpt.length > 0);
|
|
43226
43313
|
}
|
|
43227
43314
|
function readMarkdown(contentRoot, sourcePath, depth) {
|
|
43228
|
-
const absolute =
|
|
43229
|
-
if (!
|
|
43315
|
+
const absolute = path5.isAbsolute(sourcePath) ? sourcePath : path5.join(contentRoot, sourcePath);
|
|
43316
|
+
if (!fs3.existsSync(absolute)) return null;
|
|
43230
43317
|
try {
|
|
43231
|
-
const raw =
|
|
43318
|
+
const raw = fs3.readFileSync(absolute, "utf-8");
|
|
43232
43319
|
const stripped = stripFrontmatter(raw);
|
|
43233
43320
|
return depth === "summary" ? summarize(stripped) : stripped;
|
|
43234
43321
|
} catch {
|
|
@@ -43299,7 +43386,7 @@ var register3 = (server, svc) => {
|
|
|
43299
43386
|
};
|
|
43300
43387
|
|
|
43301
43388
|
// src/adapters/mcp/mcp-tools/workspace-tools.ts
|
|
43302
|
-
var
|
|
43389
|
+
var fs4 = __toESM(require("node:fs"));
|
|
43303
43390
|
var register4 = (server, svc) => {
|
|
43304
43391
|
server.registerTool(
|
|
43305
43392
|
"workspace_list",
|
|
@@ -43337,7 +43424,7 @@ var register4 = (server, svc) => {
|
|
|
43337
43424
|
);
|
|
43338
43425
|
}
|
|
43339
43426
|
const ws = await svc.addWorkspace(projectId, id, label, cwd);
|
|
43340
|
-
const cwdExists =
|
|
43427
|
+
const cwdExists = fs4.existsSync(cwd);
|
|
43341
43428
|
return textResponse({ workspace: ws, warning: cwdExists ? null : `cwd ${cwd} not found on disk` });
|
|
43342
43429
|
}
|
|
43343
43430
|
);
|
|
@@ -43373,18 +43460,18 @@ async function archiveOrError(svc, projectId, workspaceId) {
|
|
|
43373
43460
|
}
|
|
43374
43461
|
|
|
43375
43462
|
// src/adapters/mcp/mcp-tools/workspace-resolver.ts
|
|
43376
|
-
var
|
|
43463
|
+
var path6 = __toESM(require("path"));
|
|
43377
43464
|
var isWindows = process.platform === "win32";
|
|
43378
43465
|
function normalize2(p) {
|
|
43379
|
-
const resolved =
|
|
43466
|
+
const resolved = path6.resolve(p).replace(/[\\/]+$/, "");
|
|
43380
43467
|
return isWindows ? resolved.toLowerCase().replace(/\//g, "\\") : resolved;
|
|
43381
43468
|
}
|
|
43382
43469
|
function isDescendantOrEqual(parent, child) {
|
|
43383
43470
|
if (parent === child) return true;
|
|
43384
|
-
const rel =
|
|
43471
|
+
const rel = path6.relative(parent, child);
|
|
43385
43472
|
if (rel === "") return true;
|
|
43386
43473
|
if (rel.startsWith("..")) return false;
|
|
43387
|
-
return !
|
|
43474
|
+
return !path6.isAbsolute(rel);
|
|
43388
43475
|
}
|
|
43389
43476
|
function resolveWorkspaceId(input) {
|
|
43390
43477
|
const { explicitWorkspaceId, cwd, workspaces } = input;
|
|
@@ -43512,6 +43599,72 @@ function collectFilesByCommit(cwd, commitLines, git) {
|
|
|
43512
43599
|
return map2;
|
|
43513
43600
|
}
|
|
43514
43601
|
|
|
43602
|
+
// src/core/domain/session-transcript.ts
|
|
43603
|
+
var fs5 = __toESM(require("fs"));
|
|
43604
|
+
var path7 = __toESM(require("path"));
|
|
43605
|
+
var os = __toESM(require("os"));
|
|
43606
|
+
function isTextBlock(b) {
|
|
43607
|
+
return typeof b === "object" && b !== null && b.type === "text" && typeof b.text === "string";
|
|
43608
|
+
}
|
|
43609
|
+
function cwdToProjectSlug(cwd) {
|
|
43610
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
43611
|
+
}
|
|
43612
|
+
function parseTranscript(content) {
|
|
43613
|
+
const rows = [];
|
|
43614
|
+
for (const line of splitLines(content)) {
|
|
43615
|
+
if (!line.trim()) continue;
|
|
43616
|
+
try {
|
|
43617
|
+
rows.push(JSON.parse(line));
|
|
43618
|
+
} catch {
|
|
43619
|
+
}
|
|
43620
|
+
}
|
|
43621
|
+
return rows;
|
|
43622
|
+
}
|
|
43623
|
+
function extractResumePoint(rows) {
|
|
43624
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
43625
|
+
const r = rows[i];
|
|
43626
|
+
if (r.type !== "assistant") continue;
|
|
43627
|
+
const content = r.message?.content;
|
|
43628
|
+
if (!Array.isArray(content)) continue;
|
|
43629
|
+
const text = content.filter(isTextBlock).map((b) => b.text).join("\n").trim();
|
|
43630
|
+
if (text) return text;
|
|
43631
|
+
}
|
|
43632
|
+
return null;
|
|
43633
|
+
}
|
|
43634
|
+
var TranscriptOpsImpl = class {
|
|
43635
|
+
readResumePoint(opts) {
|
|
43636
|
+
try {
|
|
43637
|
+
const base = path7.join(
|
|
43638
|
+
opts.homeDir ?? os.homedir(),
|
|
43639
|
+
".claude",
|
|
43640
|
+
"projects",
|
|
43641
|
+
cwdToProjectSlug(opts.cwd)
|
|
43642
|
+
);
|
|
43643
|
+
if (!fs5.existsSync(base)) return null;
|
|
43644
|
+
if (opts.ccSessionId) {
|
|
43645
|
+
const file2 = path7.join(base, `${opts.ccSessionId}.jsonl`);
|
|
43646
|
+
if (fs5.existsSync(file2)) {
|
|
43647
|
+
return extractResumePoint(parseTranscript(fs5.readFileSync(file2, "utf8")));
|
|
43648
|
+
}
|
|
43649
|
+
}
|
|
43650
|
+
const sinceMs = Date.parse(opts.startedAt);
|
|
43651
|
+
const candidates = fs5.readdirSync(base).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
43652
|
+
const full = path7.join(base, f);
|
|
43653
|
+
let mtimeMs = 0;
|
|
43654
|
+
try {
|
|
43655
|
+
mtimeMs = fs5.statSync(full).mtimeMs;
|
|
43656
|
+
} catch {
|
|
43657
|
+
}
|
|
43658
|
+
return { full, mtimeMs };
|
|
43659
|
+
}).filter((c) => c.mtimeMs > 0 && (Number.isNaN(sinceMs) || c.mtimeMs >= sinceMs)).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
43660
|
+
if (candidates.length === 0) return null;
|
|
43661
|
+
return extractResumePoint(parseTranscript(fs5.readFileSync(candidates[0].full, "utf8")));
|
|
43662
|
+
} catch {
|
|
43663
|
+
return null;
|
|
43664
|
+
}
|
|
43665
|
+
}
|
|
43666
|
+
};
|
|
43667
|
+
|
|
43515
43668
|
// src/adapters/mcp/mcp-tools/session-tools.ts
|
|
43516
43669
|
var sessionSummarySchema = external_exports3.object({
|
|
43517
43670
|
summary: external_exports3.string(),
|
|
@@ -43544,9 +43697,13 @@ var sessionSummarySchema = external_exports3.object({
|
|
|
43544
43697
|
});
|
|
43545
43698
|
var handoffInputSchema = {
|
|
43546
43699
|
sessionId: external_exports3.string(),
|
|
43547
|
-
commits: external_exports3.array(external_exports3.string()).optional()
|
|
43700
|
+
commits: external_exports3.array(external_exports3.string()).optional().describe(
|
|
43701
|
+
'Format: "<short-sha> <subject>". Optional (ADR-031) \u2014 when omitted, the server derives commits from the session time window (filtered by the bound task id). Any value you provide wins; derivation only fills the gap.'
|
|
43702
|
+
),
|
|
43548
43703
|
decisions: external_exports3.array(external_exports3.string()).optional(),
|
|
43549
|
-
resumePoint: external_exports3.string()
|
|
43704
|
+
resumePoint: external_exports3.string().optional().describe(
|
|
43705
|
+
"One sentence: where you stopped + what to pick up next. Optional (ADR-031) \u2014 when omitted, the server derives it from the last text-bearing assistant turn in the session transcript. Best-effort; any value you provide wins."
|
|
43706
|
+
),
|
|
43550
43707
|
looseEnds: external_exports3.array(external_exports3.string()).optional(),
|
|
43551
43708
|
notes: external_exports3.string().optional(),
|
|
43552
43709
|
testResults: external_exports3.object({
|
|
@@ -43567,7 +43724,7 @@ async function tryLifecycle2(fn) {
|
|
|
43567
43724
|
throw e;
|
|
43568
43725
|
}
|
|
43569
43726
|
}
|
|
43570
|
-
var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
43727
|
+
var register5 = (server, svc, git = new GitOpsImpl(), transcript = new TranscriptOpsImpl()) => {
|
|
43571
43728
|
server.registerTool(
|
|
43572
43729
|
"session_start",
|
|
43573
43730
|
{
|
|
@@ -43578,10 +43735,13 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
43578
43735
|
workspaceId: external_exports3.string().optional().describe("Workspace ID (e.g. workflow-engine)"),
|
|
43579
43736
|
cwd: external_exports3.string().optional().describe(
|
|
43580
43737
|
"Current working directory \u2014 used to auto-detect workspaceId when not passed explicitly"
|
|
43738
|
+
),
|
|
43739
|
+
ccSessionId: external_exports3.string().optional().describe(
|
|
43740
|
+
"Claude Code session UUID (the transcript .jsonl filename under ~/.claude/projects/). Pass it so session_end can derive resumePoint from the transcript (ADR-031). Optional \u2014 omitted falls back to heuristic correlation."
|
|
43581
43741
|
)
|
|
43582
43742
|
}
|
|
43583
43743
|
},
|
|
43584
|
-
async ({ projectId, taskId, workspaceId, cwd }) => tryLifecycle2(async () => {
|
|
43744
|
+
async ({ projectId, taskId, workspaceId, cwd, ccSessionId }) => tryLifecycle2(async () => {
|
|
43585
43745
|
const project = await svc.getProject(projectId);
|
|
43586
43746
|
if (!project) throw new Error(`Project ${projectId} not found`);
|
|
43587
43747
|
const resolvedWorkspaceId = resolveWorkspaceId({
|
|
@@ -43592,7 +43752,8 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
43592
43752
|
const { session, contextSources, existingActiveSessions, recalledMemories } = await svc.startSession({
|
|
43593
43753
|
projectId,
|
|
43594
43754
|
taskId,
|
|
43595
|
-
workspaceId: resolvedWorkspaceId
|
|
43755
|
+
workspaceId: resolvedWorkspaceId,
|
|
43756
|
+
ccSessionId
|
|
43596
43757
|
});
|
|
43597
43758
|
const lastSession = await loadLastSession(svc, projectId, resolvedWorkspaceId);
|
|
43598
43759
|
const bundle = await buildProjectContext(svc, projectId, "summary");
|
|
@@ -43706,10 +43867,12 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
43706
43867
|
inputSchema: handoffInputSchema
|
|
43707
43868
|
},
|
|
43708
43869
|
async (input) => tryLifecycle2(async () => {
|
|
43870
|
+
const commits = await resolveCommits(svc, git, input.sessionId, input.commits);
|
|
43871
|
+
const resumePoint = await resolveResumePoint(svc, transcript, input.sessionId, input.resumePoint);
|
|
43709
43872
|
const handoff = {
|
|
43710
|
-
commits
|
|
43873
|
+
commits,
|
|
43711
43874
|
decisions: input.decisions,
|
|
43712
|
-
resumePoint
|
|
43875
|
+
resumePoint,
|
|
43713
43876
|
looseEnds: input.looseEnds,
|
|
43714
43877
|
tasksUpdated: [],
|
|
43715
43878
|
testResults: input.testResults
|
|
@@ -43732,7 +43895,58 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
43732
43895
|
};
|
|
43733
43896
|
})
|
|
43734
43897
|
);
|
|
43898
|
+
server.registerTool(
|
|
43899
|
+
"session_cancel",
|
|
43900
|
+
{
|
|
43901
|
+
description: "Cancel (retire) an active session WITHOUT marking its task DONE. Use for an empty or abandoned session \u2014 an orphaned session_start where no real work followed, or a session you want to drop without completing the task. Marks the session completed with a failureReason, closes any linked conversations, and intentionally leaves the bound task status untouched (stays IN-PROGRESS for human review). Distinct from session_end, which marks the task DONE.",
|
|
43902
|
+
inputSchema: {
|
|
43903
|
+
sessionId: external_exports3.string(),
|
|
43904
|
+
reason: external_exports3.string().optional().describe(
|
|
43905
|
+
'Why the session is being cancelled (e.g. "empty session \u2014 no work recorded"). Defaults to "cancelled \u2014 no work recorded".'
|
|
43906
|
+
)
|
|
43907
|
+
}
|
|
43908
|
+
},
|
|
43909
|
+
async ({ sessionId, reason }) => tryLifecycle2(async () => {
|
|
43910
|
+
const result = await svc.abandonSession(
|
|
43911
|
+
sessionId,
|
|
43912
|
+
reason ?? "cancelled \u2014 no work recorded"
|
|
43913
|
+
);
|
|
43914
|
+
return {
|
|
43915
|
+
sessionId: result.session.id,
|
|
43916
|
+
status: result.session.status,
|
|
43917
|
+
endedAt: result.session.endedAt,
|
|
43918
|
+
taskId: result.session.taskId,
|
|
43919
|
+
taskUntouched: true,
|
|
43920
|
+
closedConversationIds: result.closedConversationIds
|
|
43921
|
+
};
|
|
43922
|
+
})
|
|
43923
|
+
);
|
|
43735
43924
|
};
|
|
43925
|
+
async function resolveCommits(svc, git, sessionId, provided) {
|
|
43926
|
+
if (provided && provided.length > 0) return provided;
|
|
43927
|
+
const session = await svc.getSession(sessionId);
|
|
43928
|
+
if (!session) return provided;
|
|
43929
|
+
const project = await svc.getProject(session.projectId);
|
|
43930
|
+
const cwd = project?.cwd;
|
|
43931
|
+
if (!cwd) return provided;
|
|
43932
|
+
const derived = git.commitsInWindow(cwd, session.startedAt, session.taskId ?? void 0);
|
|
43933
|
+
return derived.length > 0 ? derived : provided;
|
|
43934
|
+
}
|
|
43935
|
+
async function resolveResumePoint(svc, transcript, sessionId, provided) {
|
|
43936
|
+
if (provided && provided.trim()) return provided;
|
|
43937
|
+
const session = await svc.getSession(sessionId);
|
|
43938
|
+
if (!session) return provided;
|
|
43939
|
+
const project = await svc.getProject(session.projectId);
|
|
43940
|
+
const cwd = project?.cwd;
|
|
43941
|
+
if (!cwd) return provided;
|
|
43942
|
+
const derived = transcript.readResumePoint({
|
|
43943
|
+
cwd,
|
|
43944
|
+
ccSessionId: session.ccSessionId,
|
|
43945
|
+
startedAt: session.startedAt,
|
|
43946
|
+
endedAt: session.endedAt
|
|
43947
|
+
});
|
|
43948
|
+
return derived ?? provided;
|
|
43949
|
+
}
|
|
43736
43950
|
async function buildSuggestedKnowledge(svc, git, projectId, handoff) {
|
|
43737
43951
|
const project = await svc.getProject(projectId);
|
|
43738
43952
|
const cwd = project?.cwd ?? "";
|
|
@@ -43793,7 +44007,8 @@ async function createLooseEndInboxes(svc, looseEnds, session) {
|
|
|
43793
44007
|
projectId: session.projectId,
|
|
43794
44008
|
content: `${content}
|
|
43795
44009
|
|
|
43796
|
-
\u2014 from session ${tag}
|
|
44010
|
+
\u2014 from session ${tag}`,
|
|
44011
|
+
...session.taskId ? { linkedTaskId: session.taskId } : {}
|
|
43797
44012
|
});
|
|
43798
44013
|
ids.push(item.id);
|
|
43799
44014
|
}
|
|
@@ -43816,10 +44031,13 @@ var register6 = (server, svc) => {
|
|
|
43816
44031
|
description: "Add a raw idea to inbox. Returns INBOX-NNN with status=raw.",
|
|
43817
44032
|
inputSchema: {
|
|
43818
44033
|
projectId: external_exports3.string().describe("Project ID (required)"),
|
|
43819
|
-
content: external_exports3.string().describe("Raw idea content")
|
|
44034
|
+
content: external_exports3.string().describe("Raw idea content"),
|
|
44035
|
+
workspaceId: external_exports3.string().optional().describe(
|
|
44036
|
+
"App/workspace scope, if already clear. Omit to leave domain-level (null) \u2014 research fills it later via inbox_ready."
|
|
44037
|
+
)
|
|
43820
44038
|
}
|
|
43821
44039
|
},
|
|
43822
|
-
async ({ projectId, content }) => textResponse(await svc.createInbox({ projectId, content }))
|
|
44040
|
+
async ({ projectId, content, workspaceId }) => textResponse(await svc.createInbox({ projectId, content, workspaceId }))
|
|
43823
44041
|
);
|
|
43824
44042
|
server.registerTool(
|
|
43825
44043
|
"inbox_update",
|
|
@@ -43903,16 +44121,24 @@ var register6 = (server, svc) => {
|
|
|
43903
44121
|
server.registerTool(
|
|
43904
44122
|
"inbox_ready",
|
|
43905
44123
|
{
|
|
43906
|
-
description: "Mark research complete. Transitions researching \u2192 ready.",
|
|
43907
|
-
inputSchema: {
|
|
44124
|
+
description: "Mark research complete. Transitions researching \u2192 ready. Pass workspaceId to localize the item to the app research identified (ADR-032 Pillar 6).",
|
|
44125
|
+
inputSchema: {
|
|
44126
|
+
id: external_exports3.string().describe("Inbox item ID"),
|
|
44127
|
+
workspaceId: external_exports3.string().optional().describe("App/workspace identified during research \u2014 localizes the item before convert.")
|
|
44128
|
+
}
|
|
43908
44129
|
},
|
|
43909
|
-
async ({ id }) => {
|
|
44130
|
+
async ({ id, workspaceId }) => {
|
|
43910
44131
|
const item = await svc.getInbox(id);
|
|
43911
44132
|
if (!item) return textResponse(`Inbox ${id} not found`);
|
|
43912
44133
|
if (item.status !== "researching") {
|
|
43913
44134
|
return textResponse(`Inbox ${id} is ${item.status}, not researching`);
|
|
43914
44135
|
}
|
|
43915
|
-
return textResponse(
|
|
44136
|
+
return textResponse(
|
|
44137
|
+
await svc.updateInbox(
|
|
44138
|
+
id,
|
|
44139
|
+
workspaceId !== void 0 ? { status: "ready", workspaceId } : { status: "ready" }
|
|
44140
|
+
)
|
|
44141
|
+
);
|
|
43916
44142
|
}
|
|
43917
44143
|
);
|
|
43918
44144
|
server.registerTool(
|
|
@@ -43942,6 +44168,90 @@ var register6 = (server, svc) => {
|
|
|
43942
44168
|
);
|
|
43943
44169
|
};
|
|
43944
44170
|
|
|
44171
|
+
// src/adapters/mcp/mcp-tools/investigation-tools.ts
|
|
44172
|
+
async function tryLifecycle4(fn) {
|
|
44173
|
+
try {
|
|
44174
|
+
return textResponse(await fn());
|
|
44175
|
+
} catch (e) {
|
|
44176
|
+
if (e instanceof LifecycleError) return textResponse(e.message);
|
|
44177
|
+
throw e;
|
|
44178
|
+
}
|
|
44179
|
+
}
|
|
44180
|
+
var register7 = (server, svc) => {
|
|
44181
|
+
server.registerTool(
|
|
44182
|
+
"investigation_start",
|
|
44183
|
+
{
|
|
44184
|
+
description: "Start a debugging investigation (ADR-035). Creates a container for nonlinear trace state (hypotheses + evidence), status=exploring. Optionally bind to a task/session.",
|
|
44185
|
+
inputSchema: {
|
|
44186
|
+
symptom: external_exports3.string().describe('The observed symptom, e.g. "Test button does not work"'),
|
|
44187
|
+
taskId: external_exports3.string().optional().describe("Optional task to bind to (standalone allowed)"),
|
|
44188
|
+
sessionId: external_exports3.string().optional().describe("Optional session to bind to")
|
|
44189
|
+
}
|
|
44190
|
+
},
|
|
44191
|
+
async ({ symptom, taskId, sessionId }) => tryLifecycle4(() => svc.startInvestigation({ symptom, taskId, sessionId }))
|
|
44192
|
+
);
|
|
44193
|
+
server.registerTool(
|
|
44194
|
+
"investigation_add_hypothesis",
|
|
44195
|
+
{
|
|
44196
|
+
description: "Add a hypothesis to an investigation (status=testing). Blocked once the investigation is resolved.",
|
|
44197
|
+
inputSchema: {
|
|
44198
|
+
investigationId: external_exports3.string().describe("Investigation ID (e.g. INV-001)"),
|
|
44199
|
+
description: external_exports3.string().describe("What you think might be the cause")
|
|
44200
|
+
}
|
|
44201
|
+
},
|
|
44202
|
+
async ({ investigationId, description }) => tryLifecycle4(() => svc.addHypothesis(investigationId, description))
|
|
44203
|
+
);
|
|
44204
|
+
server.registerTool(
|
|
44205
|
+
"investigation_set_hypothesis_status",
|
|
44206
|
+
{
|
|
44207
|
+
description: "Transition a hypothesis: testing \u2192 ruled_out | confirmed. Ruled-out hypotheses persist (dead ends are kept). A terminal hypothesis cannot transition again.",
|
|
44208
|
+
inputSchema: {
|
|
44209
|
+
hypothesisId: external_exports3.string().describe("Hypothesis ID (e.g. HYP-001)"),
|
|
44210
|
+
status: external_exports3.enum(["ruled_out", "confirmed"]).describe("New status")
|
|
44211
|
+
}
|
|
44212
|
+
},
|
|
44213
|
+
async ({ hypothesisId, status }) => tryLifecycle4(() => svc.setHypothesisStatus(hypothesisId, status))
|
|
44214
|
+
);
|
|
44215
|
+
server.registerTool(
|
|
44216
|
+
"investigation_add_evidence",
|
|
44217
|
+
{
|
|
44218
|
+
description: "Attach typed evidence to an investigation, or to a specific hypothesis within it.",
|
|
44219
|
+
inputSchema: {
|
|
44220
|
+
investigationId: external_exports3.string().describe("Investigation ID"),
|
|
44221
|
+
type: external_exports3.enum(["screenshot", "log", "network", "code_snippet"]).describe("Evidence type"),
|
|
44222
|
+
ref: external_exports3.string().describe("Path / URL / locator for the evidence"),
|
|
44223
|
+
hypothesisId: external_exports3.string().optional().describe("Attach to this hypothesis instead of the investigation as a whole"),
|
|
44224
|
+
note: external_exports3.string().optional().describe("Optional note about the evidence")
|
|
44225
|
+
}
|
|
44226
|
+
},
|
|
44227
|
+
async ({ investigationId, type, ref, hypothesisId, note }) => tryLifecycle4(() => svc.addEvidence({ investigationId, type, ref, hypothesisId, note }))
|
|
44228
|
+
);
|
|
44229
|
+
server.registerTool(
|
|
44230
|
+
"investigation_resolve",
|
|
44231
|
+
{
|
|
44232
|
+
description: "Resolve an investigation (status=resolved). Returns a human-gated knowledge_create(gotcha) draft built from pattern_tag + root_cause + fix \u2014 NOT auto-written. Errors if already resolved.",
|
|
44233
|
+
inputSchema: {
|
|
44234
|
+
id: external_exports3.string().describe("Investigation ID"),
|
|
44235
|
+
rootCause: external_exports3.string().describe("The confirmed root cause"),
|
|
44236
|
+
fixSummary: external_exports3.string().describe("How it was fixed"),
|
|
44237
|
+
patternTag: external_exports3.string().optional().describe('Reusable pattern tag, e.g. "expression-no-upstream-data"')
|
|
44238
|
+
}
|
|
44239
|
+
},
|
|
44240
|
+
async ({ id, rootCause, fixSummary, patternTag }) => tryLifecycle4(() => svc.resolveInvestigation(id, { rootCause, fixSummary, patternTag }))
|
|
44241
|
+
);
|
|
44242
|
+
server.registerTool(
|
|
44243
|
+
"investigation_get",
|
|
44244
|
+
{
|
|
44245
|
+
description: "Get full investigation state \u2014 symptom, status, all hypotheses (incl. ruled_out), all evidence, root_cause/fix. Reconstructs a trace without prior context.",
|
|
44246
|
+
inputSchema: { id: external_exports3.string().describe("Investigation ID") }
|
|
44247
|
+
},
|
|
44248
|
+
async ({ id }) => {
|
|
44249
|
+
const inv = await svc.getInvestigation(id);
|
|
44250
|
+
return textResponse(inv ?? `Investigation ${id} not found`);
|
|
44251
|
+
}
|
|
44252
|
+
);
|
|
44253
|
+
};
|
|
44254
|
+
|
|
43945
44255
|
// src/adapters/mcp/mcp-tools/backup-tools.ts
|
|
43946
44256
|
var import_fs3 = require("fs");
|
|
43947
44257
|
var import_path3 = require("path");
|
|
@@ -44004,7 +44314,7 @@ function runBackup(db, userData, now2 = /* @__PURE__ */ new Date()) {
|
|
|
44004
44314
|
}
|
|
44005
44315
|
|
|
44006
44316
|
// src/adapters/mcp/mcp-tools/backup-tools.ts
|
|
44007
|
-
function
|
|
44317
|
+
function register8(server, svc, dataDir, dbPath) {
|
|
44008
44318
|
server.registerTool(
|
|
44009
44319
|
"backup_list",
|
|
44010
44320
|
{
|
|
@@ -44051,13 +44361,24 @@ function register7(server, svc, dataDir, dbPath) {
|
|
|
44051
44361
|
}
|
|
44052
44362
|
|
|
44053
44363
|
// src/adapters/mcp/mcp-tools/knowledge-tools.ts
|
|
44054
|
-
var TYPE_ENUM = [
|
|
44364
|
+
var TYPE_ENUM = [
|
|
44365
|
+
"spike",
|
|
44366
|
+
"decision",
|
|
44367
|
+
"postmortem",
|
|
44368
|
+
"learning",
|
|
44369
|
+
"evaluation",
|
|
44370
|
+
"feature",
|
|
44371
|
+
"code_ref",
|
|
44372
|
+
"gotcha"
|
|
44373
|
+
];
|
|
44055
44374
|
var SCOPE_ENUM = ["project", "cross"];
|
|
44056
|
-
var
|
|
44375
|
+
var EFFORT_BAND_ENUM = ["S", "M", "L", "XL"];
|
|
44376
|
+
var FEATURE_STATUS_ENUM = ["planned", "in-progress", "shipped", "blocked"];
|
|
44377
|
+
var register9 = (server, svc) => {
|
|
44057
44378
|
server.registerTool(
|
|
44058
44379
|
"knowledge_create",
|
|
44059
44380
|
{
|
|
44060
|
-
description: "Create a knowledge entry (
|
|
44381
|
+
description: "Create a knowledge entry. Original types (spike/decision/postmortem/learning/evaluation) capture findings/ADRs. First-class graph types (TASK-988): feature (a user-perceivable outcome \u2014 set structured.anchorTaskId/realizesTasks/inWorkspaces/effortBand/status), code_ref (a code anchor note \u2014 pair with the code_ref_upsert tool for the queryable identity row), gotcha (a concern \u2014 set structured.affectedFeatureId pointing at an existing feature slug). Project-scope writes to <projectCwd>/docs/knowledge/<slug>.md and tracks staleness against refs[]. When workspaceId is provided, the file is written to <workspaceCwd>/docs/knowledge/<slug>.md instead (workspace must belong to projectId). Cross-scope writes to vault/30-Knowledge/<slug>.md (no staleness).",
|
|
44061
44382
|
inputSchema: {
|
|
44062
44383
|
projectId: external_exports3.string().describe("Project ID"),
|
|
44063
44384
|
workspaceId: external_exports3.string().optional().describe(
|
|
@@ -44073,11 +44394,19 @@ var register8 = (server, svc) => {
|
|
|
44073
44394
|
commitSha: external_exports3.string().optional().describe("Pin SHA. Omit to auto-capture current HEAD.")
|
|
44074
44395
|
})
|
|
44075
44396
|
).optional().describe("Code references for staleness tracking. Empty/omitted = no tracking."),
|
|
44076
|
-
slug: external_exports3.string().optional().describe("Override auto-derived slug")
|
|
44397
|
+
slug: external_exports3.string().optional().describe("Override auto-derived slug"),
|
|
44398
|
+
structured: external_exports3.object({
|
|
44399
|
+
anchorTaskId: external_exports3.string().optional().describe("feature: epic-shape task it was promoted from"),
|
|
44400
|
+
realizesTasks: external_exports3.array(external_exports3.string()).optional().describe("feature: task ids that implement/modify it"),
|
|
44401
|
+
inWorkspaces: external_exports3.array(external_exports3.string()).optional().describe("feature: workspaces it touches"),
|
|
44402
|
+
effortBand: external_exports3.enum(EFFORT_BAND_ENUM).optional().describe("feature: S|M|L|XL band (NOT a day count)"),
|
|
44403
|
+
status: external_exports3.enum(FEATURE_STATUS_ENUM).optional().describe("feature: planned|in-progress|shipped|blocked"),
|
|
44404
|
+
affectedFeatureId: external_exports3.string().optional().describe("gotcha: slug of the feature this concern is about (must exist)")
|
|
44405
|
+
}).optional().describe("Structured fields for feature / gotcha first-class types (TASK-988).")
|
|
44077
44406
|
}
|
|
44078
44407
|
},
|
|
44079
|
-
async ({ projectId, workspaceId, type, scope, title, body, refs, slug }) => textResponse(
|
|
44080
|
-
svc.createKnowledge({
|
|
44408
|
+
async ({ projectId, workspaceId, type, scope, title, body, refs, slug, structured }) => textResponse(
|
|
44409
|
+
await svc.createKnowledge({
|
|
44081
44410
|
projectId,
|
|
44082
44411
|
workspaceId,
|
|
44083
44412
|
type,
|
|
@@ -44085,7 +44414,8 @@ var register8 = (server, svc) => {
|
|
|
44085
44414
|
title,
|
|
44086
44415
|
body,
|
|
44087
44416
|
refs: refs ?? [],
|
|
44088
|
-
slug
|
|
44417
|
+
slug,
|
|
44418
|
+
structured
|
|
44089
44419
|
})
|
|
44090
44420
|
)
|
|
44091
44421
|
);
|
|
@@ -44101,7 +44431,7 @@ var register8 = (server, svc) => {
|
|
|
44101
44431
|
)
|
|
44102
44432
|
}
|
|
44103
44433
|
},
|
|
44104
|
-
async ({ filePath, projectId, workspaceId }) => textResponse(svc.registerExistingKnowledge({ filePath, projectId, workspaceId }))
|
|
44434
|
+
async ({ filePath, projectId, workspaceId }) => textResponse(await svc.registerExistingKnowledge({ filePath, projectId, workspaceId }))
|
|
44105
44435
|
);
|
|
44106
44436
|
server.registerTool(
|
|
44107
44437
|
"knowledge_get",
|
|
@@ -44112,7 +44442,7 @@ var register8 = (server, svc) => {
|
|
|
44112
44442
|
}
|
|
44113
44443
|
},
|
|
44114
44444
|
async ({ slug }) => {
|
|
44115
|
-
const entry = svc.getKnowledge(slug);
|
|
44445
|
+
const entry = await svc.getKnowledge(slug);
|
|
44116
44446
|
if (!entry) return textResponse(`Knowledge ${slug} not found`);
|
|
44117
44447
|
return textResponse(entry);
|
|
44118
44448
|
}
|
|
@@ -44131,7 +44461,7 @@ var register8 = (server, svc) => {
|
|
|
44131
44461
|
}
|
|
44132
44462
|
},
|
|
44133
44463
|
async ({ projectId, workspaceId, scope, type }) => textResponse(
|
|
44134
|
-
svc.listKnowledge({
|
|
44464
|
+
await svc.listKnowledge({
|
|
44135
44465
|
projectId,
|
|
44136
44466
|
workspaceId: workspaceId === "" ? null : workspaceId,
|
|
44137
44467
|
scope,
|
|
@@ -44154,7 +44484,7 @@ var register8 = (server, svc) => {
|
|
|
44154
44484
|
).optional().describe("Replace refs[]. Omit to re-pin existing refs to HEAD.")
|
|
44155
44485
|
}
|
|
44156
44486
|
},
|
|
44157
|
-
async ({ slug, body, refs }) => textResponse(svc.updateKnowledge({ slug, body, refs }))
|
|
44487
|
+
async ({ slug, body, refs }) => textResponse(await svc.updateKnowledge({ slug, body, refs }))
|
|
44158
44488
|
);
|
|
44159
44489
|
server.registerTool(
|
|
44160
44490
|
"knowledge_verify",
|
|
@@ -44164,7 +44494,7 @@ var register8 = (server, svc) => {
|
|
|
44164
44494
|
slug: external_exports3.string().describe("Slug of the entry")
|
|
44165
44495
|
}
|
|
44166
44496
|
},
|
|
44167
|
-
async ({ slug }) => textResponse(svc.verifyKnowledge(slug))
|
|
44497
|
+
async ({ slug }) => textResponse(await svc.verifyKnowledge(slug))
|
|
44168
44498
|
);
|
|
44169
44499
|
server.registerTool(
|
|
44170
44500
|
"knowledge_delete",
|
|
@@ -44174,7 +44504,7 @@ var register8 = (server, svc) => {
|
|
|
44174
44504
|
slug: external_exports3.string().describe("Slug of the entry to delete")
|
|
44175
44505
|
}
|
|
44176
44506
|
},
|
|
44177
|
-
async ({ slug }) => textResponse(svc.deleteKnowledge(slug))
|
|
44507
|
+
async ({ slug }) => textResponse(await svc.deleteKnowledge(slug))
|
|
44178
44508
|
);
|
|
44179
44509
|
server.registerTool(
|
|
44180
44510
|
"knowledge_search",
|
|
@@ -44189,6 +44519,472 @@ var register8 = (server, svc) => {
|
|
|
44189
44519
|
);
|
|
44190
44520
|
};
|
|
44191
44521
|
|
|
44522
|
+
// src/adapters/mcp/mcp-tools/code-ref-tools.ts
|
|
44523
|
+
var RELATION_ENUM = ["modifies", "reference"];
|
|
44524
|
+
var register10 = (server, svc) => {
|
|
44525
|
+
server.registerTool(
|
|
44526
|
+
"code_ref_upsert",
|
|
44527
|
+
{
|
|
44528
|
+
description: "Create or re-pin a code_ref identity row. Identity is (projectId, path, symbol) \u2014 symbol is NULL for file-level refs (.tsx/.md/migrations). A write matching an existing identity UPDATEs commit_sha / line_hint on the original slug instead of inserting a duplicate (ADR Pillar 2c). Returns the stored row.",
|
|
44529
|
+
inputSchema: {
|
|
44530
|
+
slug: external_exports3.string().describe("Slug for a NEW row (ignored when the identity already exists)"),
|
|
44531
|
+
projectId: external_exports3.string().describe("Project ID"),
|
|
44532
|
+
workspaceId: external_exports3.string().optional().describe("Workspace (app) the anchor lives in"),
|
|
44533
|
+
path: external_exports3.string().describe("Repo-relative file path"),
|
|
44534
|
+
symbol: external_exports3.string().optional().describe("Full dotted symbol (Namespace.Class.Method). Omit for file-level refs."),
|
|
44535
|
+
lineHint: external_exports3.number().int().optional().describe("Optional line hint \u2014 not trustworthy over time"),
|
|
44536
|
+
commitSha: external_exports3.string().optional().describe("Commit SHA captured at write time")
|
|
44537
|
+
}
|
|
44538
|
+
},
|
|
44539
|
+
async ({ slug, projectId, workspaceId, path: path9, symbol: symbol2, lineHint, commitSha }) => textResponse(
|
|
44540
|
+
await svc.upsertCodeRef({ slug, projectId, workspaceId, path: path9, symbol: symbol2, lineHint, commitSha })
|
|
44541
|
+
)
|
|
44542
|
+
);
|
|
44543
|
+
server.registerTool(
|
|
44544
|
+
"code_ref_prefix",
|
|
44545
|
+
{
|
|
44546
|
+
description: 'Query code_ref rows by dotted-symbol prefix (e.g. all Domain-layer anchors via symbolPrefix="Ichiba.Pim.TradingCatalog.Domain."), by exact path ("who anchors this file"), or list a whole project. Returns identity rows, not the .md notes.',
|
|
44547
|
+
inputSchema: {
|
|
44548
|
+
projectId: external_exports3.string().describe("Project ID"),
|
|
44549
|
+
symbolPrefix: external_exports3.string().optional().describe("Match symbols starting with this prefix"),
|
|
44550
|
+
path: external_exports3.string().optional().describe("Exact repo-relative path filter")
|
|
44551
|
+
}
|
|
44552
|
+
},
|
|
44553
|
+
async ({ projectId, symbolPrefix, path: path9 }) => textResponse(await svc.listCodeRefsByPrefix({ projectId, symbolPrefix, path: path9 }))
|
|
44554
|
+
);
|
|
44555
|
+
server.registerTool(
|
|
44556
|
+
"code_ref_delete",
|
|
44557
|
+
{
|
|
44558
|
+
description: "Delete a code_ref identity row by slug. Also removes its TOUCHES edges. The matching .md note (if any) is managed separately via knowledge_delete.",
|
|
44559
|
+
inputSchema: {
|
|
44560
|
+
slug: external_exports3.string().describe("Slug of the code_ref row to delete")
|
|
44561
|
+
}
|
|
44562
|
+
},
|
|
44563
|
+
async ({ slug }) => {
|
|
44564
|
+
await svc.deleteCodeRef(slug);
|
|
44565
|
+
return textResponse({ slug, deleted: true });
|
|
44566
|
+
}
|
|
44567
|
+
);
|
|
44568
|
+
server.registerTool(
|
|
44569
|
+
"touches_add",
|
|
44570
|
+
{
|
|
44571
|
+
description: 'Add (or update) a TOUCHES edge: task \u2192 code_ref with a required relation. "modifies" = the task edits the anchor; "reference" = the task reads it as a pattern. Re-adding the same (task, code_ref) pair overwrites the relation.',
|
|
44572
|
+
inputSchema: {
|
|
44573
|
+
taskId: external_exports3.string().describe("Task ID"),
|
|
44574
|
+
codeRefSlug: external_exports3.string().describe("Slug of an existing code_ref row"),
|
|
44575
|
+
relation: external_exports3.enum(RELATION_ENUM).describe("modifies | reference")
|
|
44576
|
+
}
|
|
44577
|
+
},
|
|
44578
|
+
async ({ taskId, codeRefSlug, relation }) => {
|
|
44579
|
+
await svc.addTouches(taskId, codeRefSlug, relation);
|
|
44580
|
+
return textResponse({ taskId, codeRefSlug, relation });
|
|
44581
|
+
}
|
|
44582
|
+
);
|
|
44583
|
+
server.registerTool(
|
|
44584
|
+
"touches_remove",
|
|
44585
|
+
{
|
|
44586
|
+
description: "Remove a TOUCHES edge between a task and a code_ref.",
|
|
44587
|
+
inputSchema: {
|
|
44588
|
+
taskId: external_exports3.string().describe("Task ID"),
|
|
44589
|
+
codeRefSlug: external_exports3.string().describe("Slug of the code_ref row")
|
|
44590
|
+
}
|
|
44591
|
+
},
|
|
44592
|
+
async ({ taskId, codeRefSlug }) => {
|
|
44593
|
+
await svc.removeTouches(taskId, codeRefSlug);
|
|
44594
|
+
return textResponse({ taskId, codeRefSlug, removed: true });
|
|
44595
|
+
}
|
|
44596
|
+
);
|
|
44597
|
+
server.registerTool(
|
|
44598
|
+
"task_touches",
|
|
44599
|
+
{
|
|
44600
|
+
description: "List the TOUCHES edges for a task \u2014 every code_ref it modifies or references, with the relation. Use before starting a task to see which code anchors it will edit vs. read.",
|
|
44601
|
+
inputSchema: {
|
|
44602
|
+
taskId: external_exports3.string().describe("Task ID")
|
|
44603
|
+
}
|
|
44604
|
+
},
|
|
44605
|
+
async ({ taskId }) => textResponse(await svc.getTouchesForTask(taskId))
|
|
44606
|
+
);
|
|
44607
|
+
};
|
|
44608
|
+
|
|
44609
|
+
// src/adapters/mcp/mcp-tools/graph-tools.ts
|
|
44610
|
+
var EDGE_TYPE_ENUM = [
|
|
44611
|
+
"DEPENDS_ON",
|
|
44612
|
+
"IMPLEMENTS",
|
|
44613
|
+
"USES_TECH",
|
|
44614
|
+
"DECIDED_BY",
|
|
44615
|
+
"REALIZES",
|
|
44616
|
+
"ABOUT",
|
|
44617
|
+
"PINS",
|
|
44618
|
+
"IN",
|
|
44619
|
+
"INTEGRATES_WITH"
|
|
44620
|
+
];
|
|
44621
|
+
var register11 = (server, svc) => {
|
|
44622
|
+
server.registerTool(
|
|
44623
|
+
"graph_edges",
|
|
44624
|
+
{
|
|
44625
|
+
description: 'Query first-class knowledge-graph edges on any node (task ID, feature/gotcha slug, workspace ID, code_ref slug). direction "out" = edges FROM the node, "in" = edges pointing AT it, "both" = either. Examples: which tasks realize a feature \u2192 {nodeId:"feature-x", type:"REALIZES", direction:"in"}; which workspaces a feature is in \u2192 {nodeId:"feature-x", type:"IN", direction:"out"}; which gotchas are about it \u2192 {nodeId:"feature-x", type:"ABOUT", direction:"in"}.',
|
|
44626
|
+
inputSchema: {
|
|
44627
|
+
nodeId: external_exports3.string().describe("Node identity: TASK-NNN, knowledge slug, workspace ID, or code_ref slug"),
|
|
44628
|
+
type: external_exports3.enum(EDGE_TYPE_ENUM).optional().describe("Filter to one edge type. Omit to return all edge types on the node."),
|
|
44629
|
+
direction: external_exports3.enum(["out", "in", "both"]).optional().describe("out = from node, in = to node, both = either (default)")
|
|
44630
|
+
}
|
|
44631
|
+
},
|
|
44632
|
+
async ({ nodeId, type, direction }) => {
|
|
44633
|
+
const dir = direction ?? "both";
|
|
44634
|
+
const t = type;
|
|
44635
|
+
let edges;
|
|
44636
|
+
if (dir === "out") {
|
|
44637
|
+
edges = await svc.getRelationshipsFrom(nodeId, t);
|
|
44638
|
+
} else if (dir === "in") {
|
|
44639
|
+
edges = await svc.getRelationshipsTo(nodeId, t);
|
|
44640
|
+
} else {
|
|
44641
|
+
const all = await svc.getRelationships(nodeId);
|
|
44642
|
+
edges = t ? all.filter((e) => e.type === t) : all;
|
|
44643
|
+
}
|
|
44644
|
+
return textResponse(edges);
|
|
44645
|
+
}
|
|
44646
|
+
);
|
|
44647
|
+
};
|
|
44648
|
+
|
|
44649
|
+
// src/core/domain/services/feature-projection.ts
|
|
44650
|
+
var RoleBleedError = class extends Error {
|
|
44651
|
+
constructor(message) {
|
|
44652
|
+
super(message);
|
|
44653
|
+
this.name = "RoleBleedError";
|
|
44654
|
+
}
|
|
44655
|
+
};
|
|
44656
|
+
var DAY_NUMBER_RE = /\b\d+(?:\s*[-–]\s*\d+)?\s*(?:d|days?|hrs?|hours?|h|wks?|weeks?)\b/i;
|
|
44657
|
+
var FILE_PATH_RE = /\b[\w/\\.-]+\.(?:cs|ts|tsx|js|mjs|sql|md)\b/i;
|
|
44658
|
+
var DOTTED_SYMBOL_RE = /\b[A-Z][A-Za-z0-9]+(?:\.[A-Z][A-Za-z0-9]+){2,}\b/;
|
|
44659
|
+
var SQL_RE = /\b(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|jsonb)\b/;
|
|
44660
|
+
var DEPLOY_VERB = "deploy(?:ed|ment)?|releas(?:e|ed|es)|ship(?:ped|s)?|rollout|roll(?:ed)? ?out|launch(?:ed)?|go[- ]?live";
|
|
44661
|
+
var ISO_DATE = "\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2})?";
|
|
44662
|
+
var DEPLOY_DATE_RE = new RegExp(
|
|
44663
|
+
`(?:\\b(?:${DEPLOY_VERB})\\b[^.\\n]{0,30}?${ISO_DATE})|(?:${ISO_DATE}[^.\\n]{0,20}?\\b(?:${DEPLOY_VERB})\\b)`,
|
|
44664
|
+
"i"
|
|
44665
|
+
);
|
|
44666
|
+
function assertNoNumberOfDays(label, ...texts) {
|
|
44667
|
+
for (const text of texts) {
|
|
44668
|
+
if (!text) continue;
|
|
44669
|
+
const m = text.match(DAY_NUMBER_RE);
|
|
44670
|
+
if (m) {
|
|
44671
|
+
throw new RoleBleedError(
|
|
44672
|
+
`${label}: effort-band fidelity violation (M4) \u2014 found time quantity "${m[0]}"`
|
|
44673
|
+
);
|
|
44674
|
+
}
|
|
44675
|
+
}
|
|
44676
|
+
}
|
|
44677
|
+
function assertNoCodeBleed(label, ...texts) {
|
|
44678
|
+
for (const text of texts) {
|
|
44679
|
+
if (!text) continue;
|
|
44680
|
+
const path9 = text.match(FILE_PATH_RE);
|
|
44681
|
+
if (path9) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 file path "${path9[0]}"`);
|
|
44682
|
+
const sym = text.match(DOTTED_SYMBOL_RE);
|
|
44683
|
+
if (sym) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 dotted symbol "${sym[0]}"`);
|
|
44684
|
+
const sql = text.match(SQL_RE);
|
|
44685
|
+
if (sql) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 SQL token "${sql[0]}"`);
|
|
44686
|
+
}
|
|
44687
|
+
}
|
|
44688
|
+
function assertHasCodeRefs(label, view, expectRefs) {
|
|
44689
|
+
if (expectRefs && view.codeRefs.length === 0) {
|
|
44690
|
+
throw new RoleBleedError(
|
|
44691
|
+
`${label}: role bleed (M3) \u2014 dev answer missing code_refs though REALIZES tasks TOUCH code`
|
|
44692
|
+
);
|
|
44693
|
+
}
|
|
44694
|
+
}
|
|
44695
|
+
function assertNoSymbolBleed(label, ...texts) {
|
|
44696
|
+
for (const text of texts) {
|
|
44697
|
+
if (!text) continue;
|
|
44698
|
+
const sym = text.match(DOTTED_SYMBOL_RE);
|
|
44699
|
+
if (sym) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 dotted symbol "${sym[0]}"`);
|
|
44700
|
+
}
|
|
44701
|
+
}
|
|
44702
|
+
function assertNoDeploymentDate(label, ...texts) {
|
|
44703
|
+
for (const text of texts) {
|
|
44704
|
+
if (!text) continue;
|
|
44705
|
+
const m = text.match(DEPLOY_DATE_RE);
|
|
44706
|
+
if (m) {
|
|
44707
|
+
throw new RoleBleedError(`${label}: role bleed (M3) \u2014 deployment date "${m[0]}"`);
|
|
44708
|
+
}
|
|
44709
|
+
}
|
|
44710
|
+
}
|
|
44711
|
+
function sectionFor(input, ...names) {
|
|
44712
|
+
for (const name of names) {
|
|
44713
|
+
const text = input.sections[name];
|
|
44714
|
+
if (text && text.trim().length > 0) return text.trim();
|
|
44715
|
+
}
|
|
44716
|
+
return null;
|
|
44717
|
+
}
|
|
44718
|
+
function deriveEffortBand(signal) {
|
|
44719
|
+
if (signal.length === 0) return null;
|
|
44720
|
+
const taskCount = signal.length;
|
|
44721
|
+
const baseIndex = taskCount >= 7 ? 3 : taskCount >= 4 ? 2 : taskCount >= 2 ? 1 : 0;
|
|
44722
|
+
const totalAc = signal.reduce((sum, t) => sum + t.acItemCount, 0);
|
|
44723
|
+
const hasEpic = signal.some((t) => t.labels.includes("epic"));
|
|
44724
|
+
const maxBlocked = signal.reduce((max, t) => Math.max(max, t.blockedByCount), 0);
|
|
44725
|
+
const reasons = [`${taskCount} realized task${taskCount === 1 ? "" : "s"} (base ${EFFORT_BANDS[baseIndex]})`];
|
|
44726
|
+
let index = baseIndex;
|
|
44727
|
+
if (hasEpic) {
|
|
44728
|
+
index += 1;
|
|
44729
|
+
reasons.push("+1 epic task");
|
|
44730
|
+
}
|
|
44731
|
+
if (totalAc >= 15) {
|
|
44732
|
+
index += 1;
|
|
44733
|
+
reasons.push(`+1 heavy spec (${totalAc} AC items)`);
|
|
44734
|
+
}
|
|
44735
|
+
if (maxBlocked >= 2) {
|
|
44736
|
+
index += 1;
|
|
44737
|
+
reasons.push(`+1 blocked work (${maxBlocked} blockers)`);
|
|
44738
|
+
}
|
|
44739
|
+
index = Math.min(index, EFFORT_BANDS.length - 1);
|
|
44740
|
+
return { band: EFFORT_BANDS[index], reasoning: reasons.join("; ") };
|
|
44741
|
+
}
|
|
44742
|
+
function firstLine(text) {
|
|
44743
|
+
for (const line of text.split("\n")) {
|
|
44744
|
+
const trimmed = line.trim();
|
|
44745
|
+
if (trimmed.length > 0) return trimmed;
|
|
44746
|
+
}
|
|
44747
|
+
return text.trim();
|
|
44748
|
+
}
|
|
44749
|
+
function deriveCeoBlockers(input) {
|
|
44750
|
+
if (input.status === "shipped") return [];
|
|
44751
|
+
const blocking = sectionFor(input, "currently blocking");
|
|
44752
|
+
if (!blocking) return [];
|
|
44753
|
+
return [{ slug: "currently-blocking", title: firstLine(blocking) }];
|
|
44754
|
+
}
|
|
44755
|
+
function projectCeo(input) {
|
|
44756
|
+
const authored = input.effortBand ?? null;
|
|
44757
|
+
const derived = authored ? null : deriveEffortBand(input.effortSignal);
|
|
44758
|
+
return {
|
|
44759
|
+
description: sectionFor(input, "description"),
|
|
44760
|
+
apps: input.workspaces,
|
|
44761
|
+
teams: null,
|
|
44762
|
+
effortBand: authored ?? derived?.band ?? null,
|
|
44763
|
+
effortBandSource: authored ? "authored" : derived ? "derived" : null,
|
|
44764
|
+
effortBandReasoning: derived?.reasoning ?? null,
|
|
44765
|
+
status: input.status ?? null,
|
|
44766
|
+
blockers: deriveCeoBlockers(input),
|
|
44767
|
+
// Gotchas are concerns, not blockers. Titles only — bodies carry symbols/SQL.
|
|
44768
|
+
concerns: input.gotchas.map((g) => ({ slug: g.slug, title: g.title }))
|
|
44769
|
+
};
|
|
44770
|
+
}
|
|
44771
|
+
function projectDev(input) {
|
|
44772
|
+
const blocking = sectionFor(input, "currently blocking");
|
|
44773
|
+
const status = input.status ?? null;
|
|
44774
|
+
return {
|
|
44775
|
+
module: sectionFor(input, "description"),
|
|
44776
|
+
codeRefs: input.codeRefs,
|
|
44777
|
+
gotchas: input.gotchas,
|
|
44778
|
+
relevantDecisions: input.gotchas.map((g) => g.slug),
|
|
44779
|
+
breakingChangeNote: status === "blocked" && blocking ? `Feature is blocked: ${blocking}` : blocking
|
|
44780
|
+
};
|
|
44781
|
+
}
|
|
44782
|
+
function deriveEdgeCase(g) {
|
|
44783
|
+
const detail = [g.trigger, g.context].filter((s) => s && s.trim().length > 0).join(" \u2014 ");
|
|
44784
|
+
return detail ? `${g.title}: ${detail}` : g.title;
|
|
44785
|
+
}
|
|
44786
|
+
function projectTester(input) {
|
|
44787
|
+
return {
|
|
44788
|
+
acceptanceCriteria: input.realizesTasks,
|
|
44789
|
+
edgeCases: input.gotchas.map(deriveEdgeCase),
|
|
44790
|
+
regressionScope: input.realizesTasks.filter((t) => t.status === "DONE").map((t) => t.title)
|
|
44791
|
+
};
|
|
44792
|
+
}
|
|
44793
|
+
function computeHonesty(input, role) {
|
|
44794
|
+
const used = [];
|
|
44795
|
+
const lacked = [];
|
|
44796
|
+
if (sectionFor(input, "description")) used.push("description");
|
|
44797
|
+
if (input.workspaces.length > 0) used.push("workspaces");
|
|
44798
|
+
else lacked.push("workspaces");
|
|
44799
|
+
if (input.gotchas.length > 0) used.push("gotchas");
|
|
44800
|
+
else lacked.push("recall");
|
|
44801
|
+
lacked.push("team-boundaries");
|
|
44802
|
+
if (role === "ceo-po") {
|
|
44803
|
+
if (input.effortBand) used.push("effort-band (authored)");
|
|
44804
|
+
else if (input.effortSignal.length > 0) used.push("effort-band (derived)");
|
|
44805
|
+
else lacked.push("effort-band");
|
|
44806
|
+
}
|
|
44807
|
+
if (role === "dev") {
|
|
44808
|
+
if (input.codeRefs.length > 0) used.push("code-refs");
|
|
44809
|
+
else if (input.realizesTasksHaveTouches) lacked.push("code-refs");
|
|
44810
|
+
}
|
|
44811
|
+
if (role === "tester") {
|
|
44812
|
+
if (input.realizesTasks.some((t) => t.acItems.length > 0)) used.push("acceptance-criteria");
|
|
44813
|
+
else lacked.push("acceptance-criteria");
|
|
44814
|
+
if (input.gotchas.length > 0) used.push("edge-cases");
|
|
44815
|
+
else lacked.push("edge-cases");
|
|
44816
|
+
if (input.realizesTasks.some((t) => t.status === "DONE")) used.push("regression-scope");
|
|
44817
|
+
else lacked.push("regression-scope");
|
|
44818
|
+
}
|
|
44819
|
+
if (input.isStale) lacked.push("stale-refs");
|
|
44820
|
+
return { used, lacked };
|
|
44821
|
+
}
|
|
44822
|
+
function projectFeature(input, role) {
|
|
44823
|
+
const honesty = computeHonesty(input, role);
|
|
44824
|
+
if (role === "ceo-po") {
|
|
44825
|
+
const view2 = projectCeo(input);
|
|
44826
|
+
const ceoTitles = [...view2.blockers, ...view2.concerns].map((b) => b.title);
|
|
44827
|
+
assertNoCodeBleed("ceo-po", view2.description, view2.effortBandReasoning, ...ceoTitles);
|
|
44828
|
+
assertNoNumberOfDays("ceo-po", view2.description, view2.effortBand, view2.effortBandReasoning, ...ceoTitles);
|
|
44829
|
+
return { featureId: input.featureId, role, view: view2, recall: input.gotchas, honesty };
|
|
44830
|
+
}
|
|
44831
|
+
if (role === "tester") {
|
|
44832
|
+
const view2 = projectTester(input);
|
|
44833
|
+
const derived = [...view2.edgeCases, ...view2.regressionScope, ...view2.acceptanceCriteria.map((t) => t.title)];
|
|
44834
|
+
assertNoSymbolBleed("tester", ...derived);
|
|
44835
|
+
assertNoDeploymentDate("tester", ...derived);
|
|
44836
|
+
return { featureId: input.featureId, role, view: view2, recall: input.gotchas, honesty };
|
|
44837
|
+
}
|
|
44838
|
+
const view = projectDev(input);
|
|
44839
|
+
assertHasCodeRefs("dev", view, input.realizesTasksHaveTouches);
|
|
44840
|
+
return { featureId: input.featureId, role, view, recall: input.gotchas, honesty };
|
|
44841
|
+
}
|
|
44842
|
+
|
|
44843
|
+
// src/adapters/mcp/mcp-tools/feature-projection-builder.ts
|
|
44844
|
+
var FeatureNotFoundError = class extends Error {
|
|
44845
|
+
constructor(featureId) {
|
|
44846
|
+
super(`Feature ${featureId} not found or not a feature node`);
|
|
44847
|
+
this.name = "FeatureNotFoundError";
|
|
44848
|
+
}
|
|
44849
|
+
};
|
|
44850
|
+
function parseSections(body) {
|
|
44851
|
+
const sections = {};
|
|
44852
|
+
let current = null;
|
|
44853
|
+
let buffer = [];
|
|
44854
|
+
const flush = () => {
|
|
44855
|
+
if (current) sections[current] = buffer.join("\n").trim();
|
|
44856
|
+
};
|
|
44857
|
+
for (const line of splitLines(body)) {
|
|
44858
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
44859
|
+
if (heading) {
|
|
44860
|
+
flush();
|
|
44861
|
+
current = heading[1].toLowerCase();
|
|
44862
|
+
buffer = [];
|
|
44863
|
+
} else if (current) {
|
|
44864
|
+
buffer.push(line);
|
|
44865
|
+
}
|
|
44866
|
+
}
|
|
44867
|
+
flush();
|
|
44868
|
+
return sections;
|
|
44869
|
+
}
|
|
44870
|
+
async function gatherGotchas(svc, featureId) {
|
|
44871
|
+
const aboutEdges = await svc.getRelationshipsTo(featureId, "ABOUT");
|
|
44872
|
+
const gotchas = [];
|
|
44873
|
+
for (const edge of aboutEdges) {
|
|
44874
|
+
const entry = await svc.getKnowledge(edge.fromId);
|
|
44875
|
+
if (!entry) {
|
|
44876
|
+
gotchas.push({ slug: edge.fromId, title: edge.fromId });
|
|
44877
|
+
continue;
|
|
44878
|
+
}
|
|
44879
|
+
const sections = parseSections(entry.body);
|
|
44880
|
+
gotchas.push({
|
|
44881
|
+
slug: entry.slug,
|
|
44882
|
+
title: entry.frontmatter.title,
|
|
44883
|
+
trigger: sections["trigger"],
|
|
44884
|
+
context: sections["context"],
|
|
44885
|
+
resolution: sections["resolution"]
|
|
44886
|
+
});
|
|
44887
|
+
}
|
|
44888
|
+
return gotchas;
|
|
44889
|
+
}
|
|
44890
|
+
async function gatherRealizesTasks(svc, taskIds) {
|
|
44891
|
+
const tasks = [];
|
|
44892
|
+
for (const taskId of taskIds) {
|
|
44893
|
+
const task = await svc.getTask(taskId);
|
|
44894
|
+
if (!task) continue;
|
|
44895
|
+
tasks.push({
|
|
44896
|
+
taskId,
|
|
44897
|
+
title: task.title,
|
|
44898
|
+
status: task.status,
|
|
44899
|
+
acItems: findAcItems(task.body ?? "").map((i) => i.text)
|
|
44900
|
+
});
|
|
44901
|
+
}
|
|
44902
|
+
return tasks;
|
|
44903
|
+
}
|
|
44904
|
+
async function gatherEffortSignal(svc, taskIds) {
|
|
44905
|
+
const signal = [];
|
|
44906
|
+
for (const taskId of taskIds) {
|
|
44907
|
+
const task = await svc.getTask(taskId);
|
|
44908
|
+
if (!task) continue;
|
|
44909
|
+
signal.push({
|
|
44910
|
+
taskId,
|
|
44911
|
+
labels: task.labels,
|
|
44912
|
+
acItemCount: findAcItems(task.body ?? "").length,
|
|
44913
|
+
blockedByCount: task.blockedBy.length
|
|
44914
|
+
});
|
|
44915
|
+
}
|
|
44916
|
+
return signal;
|
|
44917
|
+
}
|
|
44918
|
+
async function gatherCodeRefs(svc, taskIds) {
|
|
44919
|
+
const pointers = [];
|
|
44920
|
+
let hasTouches = false;
|
|
44921
|
+
for (const taskId of taskIds) {
|
|
44922
|
+
const edges = await svc.getTouchesForTask(taskId);
|
|
44923
|
+
if (edges.length > 0) hasTouches = true;
|
|
44924
|
+
for (const edge of edges) {
|
|
44925
|
+
const ref = await svc.getCodeRef(edge.codeRefSlug);
|
|
44926
|
+
pointers.push({
|
|
44927
|
+
taskId,
|
|
44928
|
+
slug: edge.codeRefSlug,
|
|
44929
|
+
path: ref?.path ?? edge.codeRefSlug,
|
|
44930
|
+
symbol: ref?.symbol ?? null,
|
|
44931
|
+
relation: edge.relation
|
|
44932
|
+
});
|
|
44933
|
+
}
|
|
44934
|
+
}
|
|
44935
|
+
return { pointers, hasTouches };
|
|
44936
|
+
}
|
|
44937
|
+
async function buildFeatureProjection(svc, featureId, role) {
|
|
44938
|
+
const entry = await svc.getKnowledge(featureId);
|
|
44939
|
+
if (!entry || entry.frontmatter.type !== "feature") throw new FeatureNotFoundError(featureId);
|
|
44940
|
+
const structured = entry.frontmatter.structured ?? {};
|
|
44941
|
+
const inEdges = await svc.getRelationshipsFrom(featureId, "IN");
|
|
44942
|
+
const realizesEdges = await svc.getRelationshipsTo(featureId, "REALIZES");
|
|
44943
|
+
const realizesTaskIds = realizesEdges.map((e) => e.fromId);
|
|
44944
|
+
const gotchas = await gatherGotchas(svc, featureId);
|
|
44945
|
+
const dev = role === "dev" ? await gatherCodeRefs(svc, realizesTaskIds) : null;
|
|
44946
|
+
const realizesTasks = role === "tester" ? await gatherRealizesTasks(svc, realizesTaskIds) : [];
|
|
44947
|
+
const effortSignal = role === "ceo-po" && !structured.effortBand ? await gatherEffortSignal(svc, realizesTaskIds) : [];
|
|
44948
|
+
const input = {
|
|
44949
|
+
featureId,
|
|
44950
|
+
title: entry.frontmatter.title,
|
|
44951
|
+
status: structured.status,
|
|
44952
|
+
effortBand: structured.effortBand,
|
|
44953
|
+
sections: parseSections(entry.body),
|
|
44954
|
+
workspaces: inEdges.map((e) => e.toId),
|
|
44955
|
+
realizesTaskIds,
|
|
44956
|
+
effortSignal,
|
|
44957
|
+
gotchas,
|
|
44958
|
+
codeRefs: dev?.pointers ?? [],
|
|
44959
|
+
realizesTasksHaveTouches: dev?.hasTouches ?? false,
|
|
44960
|
+
realizesTasks,
|
|
44961
|
+
isStale: entry.isStale
|
|
44962
|
+
};
|
|
44963
|
+
return projectFeature(input, role);
|
|
44964
|
+
}
|
|
44965
|
+
|
|
44966
|
+
// src/adapters/mcp/mcp-tools/feature-projection-tools.ts
|
|
44967
|
+
var register12 = (server, svc) => {
|
|
44968
|
+
server.registerTool(
|
|
44969
|
+
"feature_projection",
|
|
44970
|
+
{
|
|
44971
|
+
description: 'Project a feature knowledge node to a role-appropriate answer. role="ceo-po" \u2192 business description + apps + effort BAND (never number-of-days) + blockers; role="dev" \u2192 module + code_ref pointers with modifies/reference relation + gotchas recalled before the first question; role="tester" \u2192 acceptance criteria collated per realized task + edge cases derived from gotcha trigger/context + regression scope (shipped tasks that must not break). Each bundle includes an honesty section (what the projection used vs. lacked). featureId is the feature slug (e.g. feature-crawler-list-ui-enhancements).',
|
|
44972
|
+
inputSchema: {
|
|
44973
|
+
featureId: external_exports3.string().describe("Feature knowledge slug"),
|
|
44974
|
+
role: external_exports3.enum(["ceo-po", "dev", "tester"]).describe("Asker role: ceo-po | dev | tester")
|
|
44975
|
+
}
|
|
44976
|
+
},
|
|
44977
|
+
async ({ featureId, role }) => {
|
|
44978
|
+
try {
|
|
44979
|
+
return textResponse(await buildFeatureProjection(svc, featureId, role));
|
|
44980
|
+
} catch (err) {
|
|
44981
|
+
if (err instanceof FeatureNotFoundError) return textResponse(err.message);
|
|
44982
|
+
throw err;
|
|
44983
|
+
}
|
|
44984
|
+
}
|
|
44985
|
+
);
|
|
44986
|
+
};
|
|
44987
|
+
|
|
44192
44988
|
// src/core/domain/stats-service.ts
|
|
44193
44989
|
var FLOOR_CALLS = 5;
|
|
44194
44990
|
var BROKEN_ERROR_RATE = 0.2;
|
|
@@ -44257,7 +45053,7 @@ function classify(t, mvpThreshold) {
|
|
|
44257
45053
|
}
|
|
44258
45054
|
|
|
44259
45055
|
// src/adapters/mcp/mcp-tools/stats-tools.ts
|
|
44260
|
-
var
|
|
45056
|
+
var register13 = (server, svc) => {
|
|
44261
45057
|
server.registerTool(
|
|
44262
45058
|
"stats_report",
|
|
44263
45059
|
{
|
|
@@ -44283,7 +45079,7 @@ var register9 = (server, svc) => {
|
|
|
44283
45079
|
// src/adapters/mcp/mcp-tools/cleanup-tools.ts
|
|
44284
45080
|
var fs6 = __toESM(require("node:fs"));
|
|
44285
45081
|
var KNOWLEDGE_ACTION_ENUM = ["delete", "leave"];
|
|
44286
|
-
var
|
|
45082
|
+
var register14 = (server, svc) => {
|
|
44287
45083
|
server.registerTool(
|
|
44288
45084
|
"cleanup_worktree_orphans",
|
|
44289
45085
|
{
|
|
@@ -44372,7 +45168,7 @@ function dirSizeBytes(dirPath) {
|
|
|
44372
45168
|
}
|
|
44373
45169
|
return total;
|
|
44374
45170
|
}
|
|
44375
|
-
var
|
|
45171
|
+
var register15 = (server, artifactsDir) => {
|
|
44376
45172
|
server.registerTool(
|
|
44377
45173
|
"cleanup_artifacts",
|
|
44378
45174
|
{
|
|
@@ -44457,7 +45253,7 @@ var EVENT_TYPE_ENUM = [
|
|
|
44457
45253
|
"memory_write",
|
|
44458
45254
|
"memory_recall"
|
|
44459
45255
|
];
|
|
44460
|
-
var
|
|
45256
|
+
var register16 = (server, svc) => {
|
|
44461
45257
|
server.registerTool(
|
|
44462
45258
|
"session_event_add",
|
|
44463
45259
|
{
|
|
@@ -44490,7 +45286,7 @@ var EVENT_TYPE_ENUM2 = [
|
|
|
44490
45286
|
"memory_write",
|
|
44491
45287
|
"memory_recall"
|
|
44492
45288
|
];
|
|
44493
|
-
var
|
|
45289
|
+
var register17 = (server, svc) => {
|
|
44494
45290
|
server.registerTool(
|
|
44495
45291
|
"session_event_list",
|
|
44496
45292
|
{
|
|
@@ -44511,7 +45307,7 @@ var register13 = (server, svc) => {
|
|
|
44511
45307
|
// src/adapters/mcp/mcp-tools/memory-write.ts
|
|
44512
45308
|
var SCOPE_TYPE_ENUM = ["user", "project", "workspace", "task"];
|
|
44513
45309
|
var MEMORY_TYPE_ENUM = ["episodic", "procedural"];
|
|
44514
|
-
var
|
|
45310
|
+
var register18 = (server, svc) => {
|
|
44515
45311
|
server.registerTool(
|
|
44516
45312
|
"memory_write",
|
|
44517
45313
|
{
|
|
@@ -44544,7 +45340,7 @@ var register14 = (server, svc) => {
|
|
|
44544
45340
|
};
|
|
44545
45341
|
|
|
44546
45342
|
// src/adapters/mcp/mcp-tools/memory-recall.ts
|
|
44547
|
-
var
|
|
45343
|
+
var register19 = (server, svc) => {
|
|
44548
45344
|
server.registerTool(
|
|
44549
45345
|
"memory_recall",
|
|
44550
45346
|
{
|
|
@@ -44569,7 +45365,7 @@ var register15 = (server, svc) => {
|
|
|
44569
45365
|
};
|
|
44570
45366
|
|
|
44571
45367
|
// src/adapters/mcp/mcp-tools/memory-promote-to-knowledge.ts
|
|
44572
|
-
var
|
|
45368
|
+
var register20 = (server, svc) => {
|
|
44573
45369
|
server.registerTool(
|
|
44574
45370
|
"memory_promote_to_knowledge",
|
|
44575
45371
|
{
|
|
@@ -44607,7 +45403,7 @@ var register16 = (server, svc) => {
|
|
|
44607
45403
|
};
|
|
44608
45404
|
|
|
44609
45405
|
// src/adapters/mcp/mcp-tools/ac-check.ts
|
|
44610
|
-
var
|
|
45406
|
+
var register21 = (server, svc) => {
|
|
44611
45407
|
server.registerTool(
|
|
44612
45408
|
"ac_check",
|
|
44613
45409
|
{
|
|
@@ -44667,17 +45463,21 @@ function buildMcpServer(deps, toolAllowlist, sink) {
|
|
|
44667
45463
|
register4(instrumented, deps.svc);
|
|
44668
45464
|
register5(instrumented, deps.svc);
|
|
44669
45465
|
register6(instrumented, deps.svc);
|
|
44670
|
-
register7(instrumented, deps.svc
|
|
44671
|
-
register8(instrumented, deps.svc);
|
|
45466
|
+
register7(instrumented, deps.svc);
|
|
45467
|
+
register8(instrumented, deps.svc, deps.dataDir, deps.dbPath);
|
|
44672
45468
|
register9(instrumented, deps.svc);
|
|
44673
45469
|
register10(instrumented, deps.svc);
|
|
44674
|
-
register11(instrumented, deps.
|
|
45470
|
+
register11(instrumented, deps.svc);
|
|
44675
45471
|
register12(instrumented, deps.svc);
|
|
44676
45472
|
register13(instrumented, deps.svc);
|
|
44677
45473
|
register14(instrumented, deps.svc);
|
|
44678
|
-
register15(instrumented, deps.
|
|
45474
|
+
register15(instrumented, deps.artifactsDir);
|
|
44679
45475
|
register16(instrumented, deps.svc);
|
|
44680
45476
|
register17(instrumented, deps.svc);
|
|
45477
|
+
register18(instrumented, deps.svc);
|
|
45478
|
+
register19(instrumented, deps.svc);
|
|
45479
|
+
register20(instrumented, deps.svc);
|
|
45480
|
+
register21(instrumented, deps.svc);
|
|
44681
45481
|
return { server, toolCount: instrumented.registeredToolNames.length };
|
|
44682
45482
|
}
|
|
44683
45483
|
async function startMcpServer() {
|
|
@@ -44699,11 +45499,11 @@ async function startMcpServer() {
|
|
|
44699
45499
|
dbPath: dataPaths.dbPath
|
|
44700
45500
|
};
|
|
44701
45501
|
if (mode === "http") {
|
|
44702
|
-
const oauth =
|
|
45502
|
+
const oauth = buildOAuthConfig();
|
|
44703
45503
|
const token = process.env.MCP_HTTP_TOKEN ?? "";
|
|
44704
45504
|
if (!oauth && token.length === 0) {
|
|
44705
45505
|
process.stderr.write(
|
|
44706
|
-
"[choda-deck] MCP_TRANSPORT=http requires MCP_HTTP_TOKEN (or MCP_OAUTH_MODE=1 +
|
|
45506
|
+
"[choda-deck] MCP_TRANSPORT=http requires MCP_HTTP_TOKEN (or MCP_OAUTH_MODE=1 + Keycloak config) \u2014 refusing to expose unauthenticated\n"
|
|
44707
45507
|
);
|
|
44708
45508
|
process.exit(2);
|
|
44709
45509
|
}
|
|
@@ -44720,7 +45520,9 @@ async function startMcpServer() {
|
|
|
44720
45520
|
port,
|
|
44721
45521
|
bind,
|
|
44722
45522
|
token,
|
|
44723
|
-
oauth
|
|
45523
|
+
oauth,
|
|
45524
|
+
// ADR-030 Phase 2 — read-only pull source (GET /sync/since).
|
|
45525
|
+
syncSource: { fetchSince: (since) => svc.fetchSince(since) }
|
|
44724
45526
|
}
|
|
44725
45527
|
);
|
|
44726
45528
|
return;
|
|
@@ -44730,44 +45532,44 @@ async function startMcpServer() {
|
|
|
44730
45532
|
const transport = new StdioServerTransport();
|
|
44731
45533
|
await server.connect(transport);
|
|
44732
45534
|
}
|
|
44733
|
-
|
|
45535
|
+
function buildOAuthConfig() {
|
|
44734
45536
|
if (process.env.MCP_OAUTH_MODE !== "1") return void 0;
|
|
44735
|
-
const
|
|
44736
|
-
|
|
44737
|
-
|
|
44738
|
-
|
|
44739
|
-
|
|
44740
|
-
|
|
44741
|
-
|
|
44742
|
-
|
|
44743
|
-
|
|
44744
|
-
|
|
44745
|
-
|
|
44746
|
-
|
|
44747
|
-
|
|
44748
|
-
|
|
44749
|
-
|
|
44750
|
-
|
|
44751
|
-
|
|
44752
|
-
|
|
44753
|
-
|
|
44754
|
-
|
|
44755
|
-
|
|
45537
|
+
const origin = requireEnv("MCP_OAUTH_ISSUER", "public origin e.g. https://mcp.choda.dev").replace(
|
|
45538
|
+
/\/$/,
|
|
45539
|
+
""
|
|
45540
|
+
);
|
|
45541
|
+
const realmIssuer = requireEnv(
|
|
45542
|
+
"MCP_OIDC_ISSUER",
|
|
45543
|
+
"Keycloak realm issuer e.g. https://id.choda.dev/realms/choda"
|
|
45544
|
+
).replace(/\/$/, "");
|
|
45545
|
+
const clientId = requireEnv("MCP_OIDC_CLIENT_ID", "pinned Keycloak public client id");
|
|
45546
|
+
const audience = process.env.MCP_OIDC_AUDIENCE ?? clientId;
|
|
45547
|
+
const secretFile = process.env.MCP_OIDC_CLIENT_SECRET_FILE;
|
|
45548
|
+
const clientSecret = secretFile && fs8.existsSync(secretFile) ? fs8.readFileSync(secretFile, "utf8").trim() : void 0;
|
|
45549
|
+
const verifier = createKeycloakVerifier({
|
|
45550
|
+
issuer: realmIssuer,
|
|
45551
|
+
audience,
|
|
45552
|
+
jwksUri: `${realmIssuer}/protocol/openid-connect/certs`
|
|
45553
|
+
});
|
|
45554
|
+
return {
|
|
45555
|
+
origin,
|
|
45556
|
+
keycloak: {
|
|
45557
|
+
authorizationEndpoint: `${realmIssuer}/protocol/openid-connect/auth`,
|
|
45558
|
+
tokenEndpoint: `${realmIssuer}/protocol/openid-connect/token`,
|
|
45559
|
+
clientId,
|
|
45560
|
+
clientSecret
|
|
45561
|
+
},
|
|
45562
|
+
verifier
|
|
45563
|
+
};
|
|
45564
|
+
}
|
|
45565
|
+
function requireEnv(name, hint) {
|
|
45566
|
+
const value = (process.env[name] ?? "").trim();
|
|
45567
|
+
if (value.length === 0) {
|
|
45568
|
+
process.stderr.write(`[choda-deck] MCP_OAUTH_MODE=1 requires ${name} (${hint})
|
|
45569
|
+
`);
|
|
44756
45570
|
process.exit(2);
|
|
44757
45571
|
}
|
|
44758
|
-
|
|
44759
|
-
return { repo, issuer, consentPasswordHashHex: hash2 };
|
|
44760
|
-
}
|
|
44761
|
-
function buildSqliteOAuthRepo(dbPath) {
|
|
44762
|
-
const oauthDb = new import_better_sqlite32.default(dbPath);
|
|
44763
|
-
oauthDb.pragma("foreign_keys = ON");
|
|
44764
|
-
return new OAuthRepository(oauthDb);
|
|
44765
|
-
}
|
|
44766
|
-
async function buildPostgresOAuthRepo(connectionString) {
|
|
44767
|
-
const poolMax = Number.parseInt(process.env.CHODA_PG_POOL_SIZE ?? "10", 10);
|
|
44768
|
-
const conn = new PgConnection({ connectionString, max: poolMax });
|
|
44769
|
-
await migrate(conn);
|
|
44770
|
-
return new PostgresOAuthRepository(conn);
|
|
45572
|
+
return value;
|
|
44771
45573
|
}
|
|
44772
45574
|
|
|
44773
45575
|
// src/adapters/mcp/server.ts
|