akm-cli 0.7.5 → 0.8.0-rc2
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/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { computeGraphBoost } from "./graph-boost";
|
|
2
|
+
const TYPE_BOOST = {
|
|
3
|
+
skill: 0.4,
|
|
4
|
+
command: 0.35,
|
|
5
|
+
workflow: 0.35,
|
|
6
|
+
agent: 0.3,
|
|
7
|
+
script: 0.2,
|
|
8
|
+
knowledge: 0.22,
|
|
9
|
+
memory: -0.02,
|
|
10
|
+
};
|
|
11
|
+
const MAX_BOOST_SUM = 3.0;
|
|
12
|
+
const UTILITY_WEIGHT = 0.5;
|
|
13
|
+
const UTILITY_MAX_BOOST = 1.5;
|
|
14
|
+
const RECENCY_DECAY_DAYS = 30;
|
|
15
|
+
function beliefStateBoost(item) {
|
|
16
|
+
const entry = item.entry;
|
|
17
|
+
if (entry.type !== "memory")
|
|
18
|
+
return 0;
|
|
19
|
+
if (entry.beliefState === "contradicted")
|
|
20
|
+
return -0.45;
|
|
21
|
+
if (entry.beliefState === "superseded")
|
|
22
|
+
return -0.25;
|
|
23
|
+
if (entry.beliefState === "archived")
|
|
24
|
+
return -0.6;
|
|
25
|
+
if (entry.beliefState === "active")
|
|
26
|
+
return 0.06;
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const exactNameRankingContributor = {
|
|
30
|
+
name: "exact-name-ranking",
|
|
31
|
+
appliesTo: () => true,
|
|
32
|
+
adjust(item, ctx) {
|
|
33
|
+
const entry = item.entry;
|
|
34
|
+
const nameLower = entry.name.toLowerCase();
|
|
35
|
+
const rawNameBase = nameLower.split("/").pop() ?? nameLower;
|
|
36
|
+
const nameBase = entry.type === "memory" && rawNameBase.endsWith(".derived")
|
|
37
|
+
? rawNameBase.slice(0, -".derived".length)
|
|
38
|
+
: rawNameBase;
|
|
39
|
+
if (nameBase === ctx.queryLower || nameLower === ctx.queryLower) {
|
|
40
|
+
return 2.0;
|
|
41
|
+
}
|
|
42
|
+
if (nameBase.includes(ctx.queryLower) || ctx.queryLower.includes(nameBase)) {
|
|
43
|
+
return 1.0;
|
|
44
|
+
}
|
|
45
|
+
const nameTokens = nameBase.split(/[-_\s]+/).filter(Boolean);
|
|
46
|
+
const matchCount = ctx.queryTokens.filter((qt) => nameTokens.some((nt) => nt === qt || nt.includes(qt))).length;
|
|
47
|
+
return matchCount > 0 ? Math.min(0.9, matchCount * 0.3) : 0;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const typeRankingContributor = {
|
|
51
|
+
name: "type-ranking",
|
|
52
|
+
appliesTo: () => true,
|
|
53
|
+
adjust(item) {
|
|
54
|
+
return TYPE_BOOST[item.entry.type] ?? 0;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const memoryRankingContributor = {
|
|
58
|
+
name: "memory-ranking",
|
|
59
|
+
appliesTo(item) {
|
|
60
|
+
return item.entry.type === "memory";
|
|
61
|
+
},
|
|
62
|
+
adjust(item) {
|
|
63
|
+
const derivedBoost = item.entry.name.toLowerCase().endsWith(".derived") ? 0.12 : -0.08;
|
|
64
|
+
return derivedBoost + beliefStateBoost(item);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const tagRankingContributor = {
|
|
68
|
+
name: "tag-ranking",
|
|
69
|
+
appliesTo(item) {
|
|
70
|
+
return Array.isArray(item.entry.tags) && item.entry.tags.length > 0;
|
|
71
|
+
},
|
|
72
|
+
adjust(item, ctx) {
|
|
73
|
+
let tagBoost = 0;
|
|
74
|
+
for (const tag of item.entry.tags ?? []) {
|
|
75
|
+
if (ctx.queryTokens.some((token) => tag.toLowerCase() === token))
|
|
76
|
+
tagBoost += 0.15;
|
|
77
|
+
}
|
|
78
|
+
return Math.min(0.3, tagBoost);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const searchHintRankingContributor = {
|
|
82
|
+
name: "search-hint-ranking",
|
|
83
|
+
appliesTo(item) {
|
|
84
|
+
return Array.isArray(item.entry.searchHints) && item.entry.searchHints.length > 0;
|
|
85
|
+
},
|
|
86
|
+
adjust(item, ctx) {
|
|
87
|
+
let hintBoost = 0;
|
|
88
|
+
for (const hint of item.entry.searchHints ?? []) {
|
|
89
|
+
const hintLower = hint.toLowerCase();
|
|
90
|
+
for (const token of ctx.queryTokens) {
|
|
91
|
+
if (hintLower.includes(token)) {
|
|
92
|
+
hintBoost += 0.12;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return Math.min(0.24, hintBoost);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const aliasRankingContributor = {
|
|
101
|
+
name: "alias-ranking",
|
|
102
|
+
appliesTo(item) {
|
|
103
|
+
return Array.isArray(item.entry.aliases) && item.entry.aliases.length > 0;
|
|
104
|
+
},
|
|
105
|
+
adjust(item, ctx) {
|
|
106
|
+
let boost = 0;
|
|
107
|
+
for (const alias of item.entry.aliases ?? []) {
|
|
108
|
+
const aliasLower = alias.toLowerCase();
|
|
109
|
+
if (aliasLower === ctx.queryLower) {
|
|
110
|
+
boost += 1.5;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (ctx.queryTokens.some((token) => aliasLower.includes(token)))
|
|
114
|
+
boost += 0.3;
|
|
115
|
+
}
|
|
116
|
+
return boost;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const descriptionRankingContributor = {
|
|
120
|
+
name: "description-ranking",
|
|
121
|
+
appliesTo(item) {
|
|
122
|
+
return typeof item.entry.description === "string" && item.entry.description.length > 0;
|
|
123
|
+
},
|
|
124
|
+
adjust(item, ctx) {
|
|
125
|
+
const descLower = item.entry.description?.toLowerCase() ?? "";
|
|
126
|
+
const descMatchCount = ctx.queryTokens.filter((token) => descLower.includes(token)).length;
|
|
127
|
+
if (descMatchCount === ctx.queryTokens.length && ctx.queryTokens.length > 1)
|
|
128
|
+
return 0.25;
|
|
129
|
+
if (descMatchCount > 0)
|
|
130
|
+
return 0.1;
|
|
131
|
+
return 0;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const metadataRankingContributor = {
|
|
135
|
+
name: "metadata-ranking",
|
|
136
|
+
appliesTo: () => true,
|
|
137
|
+
adjust(item) {
|
|
138
|
+
let boost = item.entry.quality === "curated" ? 0.05 : 0;
|
|
139
|
+
if (typeof item.entry.confidence === "number") {
|
|
140
|
+
boost += Math.min(0.05, Math.max(0, item.entry.confidence) * 0.05);
|
|
141
|
+
}
|
|
142
|
+
return boost;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const graphRankingContributor = {
|
|
146
|
+
name: "graph-ranking",
|
|
147
|
+
appliesTo(_item, ctx) {
|
|
148
|
+
return ctx.graphContext !== null;
|
|
149
|
+
},
|
|
150
|
+
adjust(item, ctx) {
|
|
151
|
+
return ctx.graphContext ? computeGraphBoost(ctx.graphContext, item.filePath) : 0;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const utilityRankingContributor = {
|
|
155
|
+
name: "utility-ranking",
|
|
156
|
+
appliesTo(item, ctx) {
|
|
157
|
+
const utilScore = ctx.utilityScores.get(item.id);
|
|
158
|
+
return Boolean(utilScore && utilScore.utility > 0);
|
|
159
|
+
},
|
|
160
|
+
apply(item, ctx) {
|
|
161
|
+
const utilScore = ctx.utilityScores.get(item.id);
|
|
162
|
+
if (!utilScore || utilScore.utility <= 0)
|
|
163
|
+
return;
|
|
164
|
+
let recencyFactor = 1;
|
|
165
|
+
if (utilScore.lastUsedAt) {
|
|
166
|
+
const lastUsedMs = new Date(utilScore.lastUsedAt).getTime();
|
|
167
|
+
const daysSinceLastUse = Number.isNaN(lastUsedMs)
|
|
168
|
+
? Infinity
|
|
169
|
+
: Math.max(0, (Date.now() - lastUsedMs) / (1000 * 60 * 60 * 24));
|
|
170
|
+
recencyFactor = Math.exp(-daysSinceLastUse / RECENCY_DECAY_DAYS);
|
|
171
|
+
}
|
|
172
|
+
const rawBoost = 1 + utilScore.utility * recencyFactor * UTILITY_WEIGHT;
|
|
173
|
+
item.score *= Math.min(rawBoost, UTILITY_MAX_BOOST);
|
|
174
|
+
item.utilityBoosted = true;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
export const defaultRankingContributors = [
|
|
178
|
+
exactNameRankingContributor,
|
|
179
|
+
typeRankingContributor,
|
|
180
|
+
memoryRankingContributor,
|
|
181
|
+
tagRankingContributor,
|
|
182
|
+
searchHintRankingContributor,
|
|
183
|
+
aliasRankingContributor,
|
|
184
|
+
descriptionRankingContributor,
|
|
185
|
+
metadataRankingContributor,
|
|
186
|
+
graphRankingContributor,
|
|
187
|
+
];
|
|
188
|
+
export const defaultUtilityRankingContributors = [utilityRankingContributor];
|
|
189
|
+
export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
|
|
190
|
+
let boostSum = 0;
|
|
191
|
+
for (const contributor of contributors) {
|
|
192
|
+
if (!contributor.appliesTo(item, ctx))
|
|
193
|
+
continue;
|
|
194
|
+
boostSum += contributor.adjust(item, ctx);
|
|
195
|
+
}
|
|
196
|
+
item.score *= 1 + Math.min(boostSum, MAX_BOOST_SUM);
|
|
197
|
+
}
|
|
198
|
+
export function applyUtilityContributors(item, ctx, contributors = defaultUtilityRankingContributors) {
|
|
199
|
+
for (const contributor of contributors) {
|
|
200
|
+
if (!contributor.appliesTo(item, ctx))
|
|
201
|
+
continue;
|
|
202
|
+
contributor.apply(item, ctx);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getUtilityScoresByIds } from "./db";
|
|
2
|
+
import { applyScoreContributors, applyUtilityContributors } from "./ranking-contributors";
|
|
3
|
+
export function normalizeFtsScores(results) {
|
|
4
|
+
const ftsScoreMap = new Map();
|
|
5
|
+
if (results.length === 0)
|
|
6
|
+
return ftsScoreMap;
|
|
7
|
+
const bestBm25 = results[0].bm25Score;
|
|
8
|
+
const worstBm25 = results[results.length - 1].bm25Score;
|
|
9
|
+
const range = bestBm25 - worstBm25;
|
|
10
|
+
for (const result of results) {
|
|
11
|
+
const normalized = range !== 0 ? (result.bm25Score - worstBm25) / range : 1.0;
|
|
12
|
+
const ftsScore = 0.3 + normalized * 0.7;
|
|
13
|
+
ftsScoreMap.set(result.id, { score: ftsScore, result });
|
|
14
|
+
}
|
|
15
|
+
return ftsScoreMap;
|
|
16
|
+
}
|
|
17
|
+
export function combineSearchScores(options) {
|
|
18
|
+
const FTS_WEIGHT = 0.7;
|
|
19
|
+
const VEC_WEIGHT = 0.3;
|
|
20
|
+
const scored = [];
|
|
21
|
+
const seenIds = new Set();
|
|
22
|
+
for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
|
|
23
|
+
seenIds.add(id);
|
|
24
|
+
const embedScore = options.embedScoreMap.get(id);
|
|
25
|
+
const combinedScore = embedScore !== undefined ? ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT : ftsScore;
|
|
26
|
+
scored.push({
|
|
27
|
+
id,
|
|
28
|
+
entry: result.entry,
|
|
29
|
+
filePath: result.filePath,
|
|
30
|
+
score: combinedScore,
|
|
31
|
+
rankingMode: embedScore !== undefined ? "hybrid" : "fts",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const [id, cosine] of options.embedScoreMap) {
|
|
35
|
+
if (seenIds.has(id))
|
|
36
|
+
continue;
|
|
37
|
+
const found = options.getEntryById(id);
|
|
38
|
+
if (!found)
|
|
39
|
+
continue;
|
|
40
|
+
if (options.typeFilter && found.entry.type !== options.typeFilter)
|
|
41
|
+
continue;
|
|
42
|
+
scored.push({
|
|
43
|
+
id,
|
|
44
|
+
entry: found.entry,
|
|
45
|
+
filePath: found.filePath,
|
|
46
|
+
score: cosine * VEC_WEIGHT,
|
|
47
|
+
rankingMode: "semantic",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return scored;
|
|
51
|
+
}
|
|
52
|
+
export function applyRankingRules(options) {
|
|
53
|
+
const queryTokens = options.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
54
|
+
const queryLower = options.query.toLowerCase().trim();
|
|
55
|
+
const rankingContext = {
|
|
56
|
+
db: options.db,
|
|
57
|
+
query: options.query,
|
|
58
|
+
queryLower,
|
|
59
|
+
queryTokens,
|
|
60
|
+
graphContext: options.graphContext,
|
|
61
|
+
};
|
|
62
|
+
for (const item of options.items) {
|
|
63
|
+
applyScoreContributors(item, rankingContext);
|
|
64
|
+
}
|
|
65
|
+
const utilScoresMap = getUtilityScoresByIds(options.db, options.items.map((item) => item.id));
|
|
66
|
+
const utilityContext = {
|
|
67
|
+
...rankingContext,
|
|
68
|
+
utilityScores: utilScoresMap,
|
|
69
|
+
};
|
|
70
|
+
for (const item of options.items) {
|
|
71
|
+
applyUtilityContributors(item, utilityContext);
|
|
72
|
+
}
|
|
73
|
+
return options.items;
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getRenderer } from "./file-context";
|
|
2
|
+
const rendererSearchHitEnricher = {
|
|
3
|
+
name: "renderer-search-hit-enricher",
|
|
4
|
+
appliesTo(ctx) {
|
|
5
|
+
return ctx.rendererRegistry.rendererNameFor(ctx.type) !== undefined;
|
|
6
|
+
},
|
|
7
|
+
async enrich(hit, ctx) {
|
|
8
|
+
const rendererName = ctx.rendererRegistry.rendererNameFor(ctx.type);
|
|
9
|
+
if (!rendererName)
|
|
10
|
+
return;
|
|
11
|
+
const renderer = await getRenderer(rendererName);
|
|
12
|
+
renderer?.enrichSearchHit?.(hit, ctx.stashDir);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export const defaultSearchHitEnrichers = [rendererSearchHitEnricher];
|
|
16
|
+
export async function enrichSearchHit(hit, ctx, enrichers = defaultSearchHitEnrichers) {
|
|
17
|
+
for (const enricher of enrichers) {
|
|
18
|
+
if (!enricher.appliesTo(ctx))
|
|
19
|
+
continue;
|
|
20
|
+
await enricher.enrich(hit, ctx);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveStashDir } from "../core/common";
|
|
4
|
-
import { loadConfig } from "../core/config";
|
|
4
|
+
import { getSources, loadConfig } from "../core/config";
|
|
5
5
|
import { resolveSourceProviderFactory } from "../sources/provider-factory";
|
|
6
6
|
// Eager side-effect imports so all built-in source providers self-register
|
|
7
7
|
// before resolveEntryContentDir() runs.
|
|
@@ -21,7 +21,7 @@ const GIT_STASH_TYPES = new Set(["git"]);
|
|
|
21
21
|
* 1. The primary stash directory (the entry marked `primary: true`, or the
|
|
22
22
|
* legacy top-level `stashDir`). Always emitted, even when the directory
|
|
23
23
|
* does not yet exist on disk, so callers can use it as the clone target.
|
|
24
|
-
* 2. Each entry in `config.sources
|
|
24
|
+
* 2. Each entry in `config.sources[]` (in declared order), excluding the
|
|
25
25
|
* one already emitted as the primary.
|
|
26
26
|
* 3. Each entry in `config.installed[]` (registry-managed stashes).
|
|
27
27
|
*
|
|
@@ -32,9 +32,10 @@ const GIT_STASH_TYPES = new Set(["git"]);
|
|
|
32
32
|
export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
33
33
|
const stashDir = overrideStashDir ?? resolveStashDir();
|
|
34
34
|
const config = existingConfig ?? loadConfig();
|
|
35
|
-
|
|
35
|
+
// Primary stash is always writable.
|
|
36
|
+
const sources = [{ path: stashDir, writable: true }];
|
|
36
37
|
const seen = new Set([path.resolve(stashDir)]);
|
|
37
|
-
const addSource = (dir, registryId, wikiName) => {
|
|
38
|
+
const addSource = (dir, registryId, wikiName, writable) => {
|
|
38
39
|
const resolved = path.resolve(dir);
|
|
39
40
|
if (seen.has(resolved))
|
|
40
41
|
return;
|
|
@@ -47,13 +48,14 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
47
48
|
path: resolved,
|
|
48
49
|
...(registryId ? { registryId } : {}),
|
|
49
50
|
...(wikiName ? { wikiName } : {}),
|
|
51
|
+
...(writable ? { writable: true } : {}),
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
};
|
|
53
55
|
// (1) + (2) Single pass over declared stashes — primary first if present,
|
|
54
56
|
// then the rest in declared order. The primary's directory is already
|
|
55
57
|
// injected as `sources[0]` above, so we only need to dedupe the source set.
|
|
56
|
-
const stashes = config
|
|
58
|
+
const stashes = getSources(config);
|
|
57
59
|
const primaryIdx = stashes.findIndex((entry) => entry.primary === true);
|
|
58
60
|
const ordered = [];
|
|
59
61
|
if (primaryIdx >= 0) {
|
|
@@ -72,11 +74,12 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
72
74
|
const dir = resolveEntryContentDir(entry);
|
|
73
75
|
if (dir == null)
|
|
74
76
|
continue;
|
|
75
|
-
addSource(dir, entry.name, entry.wikiName);
|
|
77
|
+
addSource(dir, entry.name, entry.wikiName, entry.writable === true);
|
|
76
78
|
}
|
|
77
79
|
// (3) Installed stashes (registry-managed). Always last.
|
|
80
|
+
// Only installed entries explicitly marked writable: true are considered writable.
|
|
78
81
|
for (const entry of config.installed ?? []) {
|
|
79
|
-
addSource(entry.stashRoot, entry.id, entry.wikiName);
|
|
82
|
+
addSource(entry.stashRoot, entry.id, entry.wikiName, entry.writable === true);
|
|
80
83
|
}
|
|
81
84
|
return sources;
|
|
82
85
|
}
|
|
@@ -132,6 +135,19 @@ function resolveEntryContentDir(entry) {
|
|
|
132
135
|
export function resolveAllStashDirs(overrideStashDir) {
|
|
133
136
|
return resolveSourceEntries(overrideStashDir).map((s) => s.path);
|
|
134
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Return the resolved absolute paths of all writable stash sources.
|
|
140
|
+
*
|
|
141
|
+
* The primary stash is always writable. Filesystem/git sources that have
|
|
142
|
+
* `writable: true` in config are also included. Registry-cached sources
|
|
143
|
+
* (installed without `writable: true`) are excluded because they are
|
|
144
|
+
* overwritten on `akm update` and must never be mutated.
|
|
145
|
+
*/
|
|
146
|
+
export function getWritableStashDirs(overrideStashDir, existingConfig) {
|
|
147
|
+
return resolveSourceEntries(overrideStashDir, existingConfig)
|
|
148
|
+
.filter((s) => s.writable === true)
|
|
149
|
+
.map((s) => s.path);
|
|
150
|
+
}
|
|
135
151
|
/**
|
|
136
152
|
* Find which source a file path belongs to.
|
|
137
153
|
*/
|
|
@@ -226,8 +242,7 @@ function isValidDirectory(dir) {
|
|
|
226
242
|
export async function ensureSourceCaches(config, options) {
|
|
227
243
|
const cfg = config ?? loadConfig();
|
|
228
244
|
const force = options?.force === true;
|
|
229
|
-
|
|
230
|
-
const entries = cfg.sources ?? cfg.stashes ?? [];
|
|
245
|
+
const entries = getSources(cfg);
|
|
231
246
|
for (const entry of entries) {
|
|
232
247
|
if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
|
|
233
248
|
continue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import { writeFileAtomic } from "../core/common";
|
|
3
3
|
import { getCacheDir, getSemanticStatusPath } from "../core/paths";
|
|
4
4
|
import { DEFAULT_LOCAL_MODEL } from "../llm/embedder";
|
|
5
5
|
export function deriveSemanticProviderFingerprint(embedding) {
|
|
@@ -41,21 +41,7 @@ export function readSemanticStatus() {
|
|
|
41
41
|
export function writeSemanticStatus(status) {
|
|
42
42
|
const dir = getCacheDir();
|
|
43
43
|
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
-
|
|
45
|
-
const tmpPath = path.join(dir, `semantic-status.json.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`);
|
|
46
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
|
|
47
|
-
try {
|
|
48
|
-
fs.renameSync(tmpPath, filePath);
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
try {
|
|
52
|
-
fs.unlinkSync(tmpPath);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
/* ignore cleanup failure */
|
|
56
|
-
}
|
|
57
|
-
throw err;
|
|
58
|
-
}
|
|
44
|
+
writeFileAtomic(getSemanticStatusPath(), `${JSON.stringify(status, null, 2)}\n`);
|
|
59
45
|
}
|
|
60
46
|
export function clearSemanticStatus() {
|
|
61
47
|
try {
|
package/dist/indexer/walker.js
CHANGED
|
@@ -136,6 +136,31 @@ function isInsideGitRepo(dir) {
|
|
|
136
136
|
}
|
|
137
137
|
return false;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Recursively yield every `.md` file under `root`.
|
|
141
|
+
*
|
|
142
|
+
* Shared by graph-extraction and memory-inference so the generator logic
|
|
143
|
+
* lives in exactly one place. Silently skips directories that cannot be
|
|
144
|
+
* read (e.g. permission errors).
|
|
145
|
+
*/
|
|
146
|
+
export function* walkMarkdownFiles(root) {
|
|
147
|
+
let entries;
|
|
148
|
+
try {
|
|
149
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const full = path.join(root, entry.name);
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
yield* walkMarkdownFiles(full);
|
|
158
|
+
}
|
|
159
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
160
|
+
yield full;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
139
164
|
/** Manual walk for non-git directories. */
|
|
140
165
|
function walkStashManual(stashRoot) {
|
|
141
166
|
const results = [];
|
|
@@ -27,14 +27,28 @@ import { ConfigError } from "../../core/errors";
|
|
|
27
27
|
import { warn } from "../../core/warn";
|
|
28
28
|
import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles";
|
|
29
29
|
/** Keys recognised at the top level of an `agent` config block. */
|
|
30
|
-
const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles"]);
|
|
30
|
+
const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles", "processes"]);
|
|
31
31
|
/** Keys recognised on a profile entry. */
|
|
32
|
-
const KNOWN_PROFILE_KEYS = new Set([
|
|
32
|
+
const KNOWN_PROFILE_KEYS = new Set([
|
|
33
|
+
"bin",
|
|
34
|
+
"args",
|
|
35
|
+
"stdio",
|
|
36
|
+
"env",
|
|
37
|
+
"envPassthrough",
|
|
38
|
+
"timeoutMs",
|
|
39
|
+
"parseOutput",
|
|
40
|
+
"sdkMode",
|
|
41
|
+
"model",
|
|
42
|
+
"endpoint",
|
|
43
|
+
"apiKey",
|
|
44
|
+
]);
|
|
33
45
|
/**
|
|
34
46
|
* Default hard timeout for an agent CLI. Spec §12.2 calls for a hard
|
|
35
47
|
* timeout; 60s matches the example value in `docs/configuration.md`.
|
|
36
48
|
*/
|
|
37
49
|
export const DEFAULT_AGENT_TIMEOUT_MS = 60_000;
|
|
50
|
+
/** Keys recognised on a `processes[<name>]` object entry. */
|
|
51
|
+
const KNOWN_PROCESS_ENTRY_KEYS = new Set(["profile", "timeoutMs"]);
|
|
38
52
|
/**
|
|
39
53
|
* Parse a raw value (typically `rawConfig.agent` from `JSON.parse`) into a
|
|
40
54
|
* normalised {@link AgentConfig}. Returns `undefined` when the value is not
|
|
@@ -83,6 +97,11 @@ export function parseAgentConfig(value) {
|
|
|
83
97
|
if (profiles)
|
|
84
98
|
out.profiles = profiles;
|
|
85
99
|
}
|
|
100
|
+
if ("processes" in raw) {
|
|
101
|
+
const processes = parseProcessesMap(raw.processes);
|
|
102
|
+
if (processes)
|
|
103
|
+
out.processes = processes;
|
|
104
|
+
}
|
|
86
105
|
return out;
|
|
87
106
|
}
|
|
88
107
|
function parseAgentProfilesMap(value) {
|
|
@@ -98,6 +117,124 @@ function parseAgentProfilesMap(value) {
|
|
|
98
117
|
}
|
|
99
118
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
100
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Parse one entry in `agent.processes`. Accepts a string (profile name) or an
|
|
122
|
+
* object with optional `profile` and `timeoutMs` fields. Returns `undefined`
|
|
123
|
+
* and emits a warning for entries that are neither valid strings nor valid
|
|
124
|
+
* objects (warn-and-ignore).
|
|
125
|
+
*/
|
|
126
|
+
export function parseProcessEntry(value, name) {
|
|
127
|
+
if (typeof value === "string") {
|
|
128
|
+
if (!value.trim()) {
|
|
129
|
+
warn(`[akm] Ignoring agent.processes."${name}": string value must be non-empty (a profile name).`);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return value.trim();
|
|
133
|
+
}
|
|
134
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
135
|
+
warn(`[akm] Ignoring agent.processes."${name}": expected a string (profile name) or an object with optional "profile" and "timeoutMs".`);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const raw = value;
|
|
139
|
+
// Warn on unknown keys (warn-and-ignore contract).
|
|
140
|
+
for (const key of Object.keys(raw)) {
|
|
141
|
+
if (!KNOWN_PROCESS_ENTRY_KEYS.has(key)) {
|
|
142
|
+
warn(`[akm] Ignoring unknown agent.processes."${name}" key: "${key}"`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const out = {};
|
|
146
|
+
if ("profile" in raw) {
|
|
147
|
+
if (typeof raw.profile === "string" && raw.profile.trim()) {
|
|
148
|
+
out.profile = raw.profile.trim();
|
|
149
|
+
}
|
|
150
|
+
else if (raw.profile !== undefined) {
|
|
151
|
+
warn(`[akm] Ignoring agent.processes."${name}".profile: expected a non-empty string.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ("timeoutMs" in raw) {
|
|
155
|
+
if (raw.timeoutMs === null) {
|
|
156
|
+
// null = unlimited — explicit, valid.
|
|
157
|
+
out.timeoutMs = null;
|
|
158
|
+
}
|
|
159
|
+
else if (typeof raw.timeoutMs === "number" &&
|
|
160
|
+
Number.isFinite(raw.timeoutMs) &&
|
|
161
|
+
Number.isInteger(raw.timeoutMs) &&
|
|
162
|
+
raw.timeoutMs > 0) {
|
|
163
|
+
out.timeoutMs = raw.timeoutMs;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
warn(`[akm] Ignoring agent.processes."${name}".timeoutMs: expected a positive integer (milliseconds) or null (unlimited).`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parse the `agent.processes` map. Returns `undefined` when the value is not
|
|
173
|
+
* a valid object; per-entry validation errors are warn-and-ignored (per spec §9.2).
|
|
174
|
+
*/
|
|
175
|
+
export function parseProcessesMap(value) {
|
|
176
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
177
|
+
warn("[akm] Ignoring agent.processes: expected an object.");
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const out = {};
|
|
181
|
+
for (const [name, raw] of Object.entries(value)) {
|
|
182
|
+
const parsed = parseProcessEntry(raw, name);
|
|
183
|
+
if (parsed !== undefined)
|
|
184
|
+
out[name] = parsed;
|
|
185
|
+
}
|
|
186
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the agent profile and effective timeout for a named process.
|
|
190
|
+
*
|
|
191
|
+
* Resolution order:
|
|
192
|
+
* 1. `config.processes[processName]` — if a string, that is the profile name;
|
|
193
|
+
* if an object, extract `profile` (and optionally `timeoutMs`).
|
|
194
|
+
* 2. Profile name falls back to `config.default` when not specified in the
|
|
195
|
+
* process entry.
|
|
196
|
+
* 3. `timeoutMs` falls back: `process.timeoutMs` (null = unlimited) →
|
|
197
|
+
* profile.timeoutMs → agent.timeoutMs → DEFAULT_AGENT_TIMEOUT_MS.
|
|
198
|
+
*
|
|
199
|
+
* Returns `{ profile, timeoutMs }` where `timeoutMs` is `undefined` when the
|
|
200
|
+
* resolved timeout is `null` (unlimited) or when no timeout is set at any
|
|
201
|
+
* layer (callers treat `undefined` as the DEFAULT_AGENT_TIMEOUT_MS default).
|
|
202
|
+
*
|
|
203
|
+
* Throws {@link ConfigError} (via {@link requireAgentProfile}) when the agent
|
|
204
|
+
* block is missing or the resolved profile cannot be used.
|
|
205
|
+
*/
|
|
206
|
+
export function resolveProcessAgentProfile(processName, agentConfig) {
|
|
207
|
+
let profileName;
|
|
208
|
+
let processTimeoutMs; // null = unlimited from config
|
|
209
|
+
const processEntry = agentConfig?.processes?.[processName];
|
|
210
|
+
if (processEntry !== undefined) {
|
|
211
|
+
if (typeof processEntry === "string") {
|
|
212
|
+
profileName = processEntry;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
profileName = processEntry.profile;
|
|
216
|
+
processTimeoutMs = processEntry.timeoutMs;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Profile name falls back to agent.default when not set in the process entry.
|
|
220
|
+
const resolvedProfile = requireAgentProfile(agentConfig, profileName);
|
|
221
|
+
// Timeout resolution: process entry → profile → agent-level → undefined (caller applies DEFAULT).
|
|
222
|
+
let resolvedTimeoutMs;
|
|
223
|
+
if (processTimeoutMs === null) {
|
|
224
|
+
// null = explicit "unlimited" — surface as undefined so callers omit the timer.
|
|
225
|
+
resolvedTimeoutMs = undefined;
|
|
226
|
+
}
|
|
227
|
+
else if (processTimeoutMs !== undefined) {
|
|
228
|
+
resolvedTimeoutMs = processTimeoutMs;
|
|
229
|
+
}
|
|
230
|
+
else if (resolvedProfile.timeoutMs !== undefined) {
|
|
231
|
+
resolvedTimeoutMs = resolvedProfile.timeoutMs;
|
|
232
|
+
}
|
|
233
|
+
else if (agentConfig?.timeoutMs !== undefined) {
|
|
234
|
+
resolvedTimeoutMs = agentConfig.timeoutMs;
|
|
235
|
+
}
|
|
236
|
+
return { profile: resolvedProfile, timeoutMs: resolvedTimeoutMs };
|
|
237
|
+
}
|
|
101
238
|
function parseAgentProfileConfig(name, value) {
|
|
102
239
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
103
240
|
warn(`[akm] Ignoring agent.profiles."${name}": expected an object.`);
|
|
@@ -171,6 +308,30 @@ function parseAgentProfileConfig(name, value) {
|
|
|
171
308
|
else if (raw.parseOutput !== undefined) {
|
|
172
309
|
warn(`[akm] Ignoring agent.profiles."${name}".parseOutput: expected "text" or "json".`);
|
|
173
310
|
}
|
|
311
|
+
if (raw.sdkMode === true || raw.sdkMode === false) {
|
|
312
|
+
out.sdkMode = raw.sdkMode;
|
|
313
|
+
}
|
|
314
|
+
else if (raw.sdkMode !== undefined) {
|
|
315
|
+
warn(`[akm] Ignoring agent.profiles."${name}".sdkMode: expected a boolean.`);
|
|
316
|
+
}
|
|
317
|
+
if (typeof raw.model === "string" && raw.model.trim()) {
|
|
318
|
+
out.model = raw.model.trim();
|
|
319
|
+
}
|
|
320
|
+
else if (raw.model !== undefined) {
|
|
321
|
+
warn(`[akm] Ignoring agent.profiles."${name}".model: expected a non-empty string.`);
|
|
322
|
+
}
|
|
323
|
+
if (typeof raw.endpoint === "string" && raw.endpoint.trim()) {
|
|
324
|
+
out.endpoint = raw.endpoint.trim();
|
|
325
|
+
}
|
|
326
|
+
else if (raw.endpoint !== undefined) {
|
|
327
|
+
warn(`[akm] Ignoring agent.profiles."${name}".endpoint: expected a non-empty string.`);
|
|
328
|
+
}
|
|
329
|
+
if (typeof raw.apiKey === "string" && raw.apiKey.trim()) {
|
|
330
|
+
out.apiKey = raw.apiKey.trim();
|
|
331
|
+
}
|
|
332
|
+
else if (raw.apiKey !== undefined) {
|
|
333
|
+
warn(`[akm] Ignoring agent.profiles."${name}".apiKey: expected a non-empty string.`);
|
|
334
|
+
}
|
|
174
335
|
return out;
|
|
175
336
|
}
|
|
176
337
|
/**
|
|
@@ -184,7 +345,7 @@ function parseAgentProfileConfig(name, value) {
|
|
|
184
345
|
*/
|
|
185
346
|
export function resolveAgentProfile(name, overrides) {
|
|
186
347
|
const builtin = getBuiltinAgentProfile(name);
|
|
187
|
-
if (!builtin && !overrides?.bin)
|
|
348
|
+
if (!builtin && !overrides?.bin && overrides?.sdkMode !== true)
|
|
188
349
|
return undefined;
|
|
189
350
|
const base = builtin ??
|
|
190
351
|
{
|
|
@@ -208,6 +369,10 @@ export function resolveAgentProfile(name, overrides) {
|
|
|
208
369
|
: base.envPassthrough,
|
|
209
370
|
timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
|
|
210
371
|
parseOutput: overrides.parseOutput ?? base.parseOutput,
|
|
372
|
+
sdkMode: overrides.sdkMode ?? base.sdkMode,
|
|
373
|
+
model: overrides.model ?? base.model,
|
|
374
|
+
endpoint: overrides.endpoint ?? base.endpoint,
|
|
375
|
+
apiKey: overrides.apiKey ?? base.apiKey,
|
|
211
376
|
};
|
|
212
377
|
return merged;
|
|
213
378
|
}
|
|
@@ -274,6 +439,13 @@ export function requireAgentProfile(agent, requested) {
|
|
|
274
439
|
if (!profile) {
|
|
275
440
|
throw new ConfigError(`agent profile "${name}" is not built-in and has no \`bin\` override.`, "INVALID_CONFIG_FILE", `Define agent.profiles."${name}".bin in config.json, or pick one of: ${listAgentProfileNames(agent).join(", ")}.`);
|
|
276
441
|
}
|
|
442
|
+
// Apply the top-level agent.timeoutMs as the effective default for this
|
|
443
|
+
// profile when the profile itself has no timeout override. This makes
|
|
444
|
+
// `agent.timeoutMs` the universal fallback without requiring every
|
|
445
|
+
// profile definition in config.json to repeat it.
|
|
446
|
+
if (profile.timeoutMs === undefined && agent?.timeoutMs !== undefined) {
|
|
447
|
+
return { ...profile, timeoutMs: agent.timeoutMs };
|
|
448
|
+
}
|
|
277
449
|
return profile;
|
|
278
450
|
}
|
|
279
451
|
/**
|