@kal-elsam/kairo-runtime 0.12.0 → 0.13.1
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/CHANGELOG.md +1179 -0
- package/LICENSE +21 -0
- package/README.md +63 -1193
- package/package.json +5 -3
- package/scripts/cockpit-smoke.mjs +2 -1
- package/src/cli.js +15 -139
- package/src/global/cli-help.js +180 -0
- package/src/global/ink/cockpit-control-center.js +4 -1
- package/src/global/ink/obsidian-vault-display.js +37 -0
- package/src/global/ink/ux/live-overview.js +72 -57
- package/src/global/ink/ux/overview-needs.js +160 -0
- package/src/global/observability/build-companion-snapshot.js +23 -2
- package/src/global/observability/index.js +39 -0
- package/src/global/observability/obsidian-knowledge-preview.js +214 -0
- package/src/global/observability/obsidian-knowledge-views.js +227 -0
- package/src/global/observability/obsidian-publisher.js +181 -0
- package/src/global/observability/obsidian-status.js +76 -0
- package/src/global/observability/obsidian-vault.js +259 -0
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Live semantic Overview for Cockpit HOME — product cover.
|
|
3
3
|
* Nav owns the sole focus mark — this panel never renders `>`.
|
|
4
4
|
* ASCII wordmark only here (wide/compact); minimal is textual.
|
|
5
|
+
*
|
|
6
|
+
* Rule: show purpose + one next step + a few plain-language needs.
|
|
7
|
+
* Machine/system noise stays out of the first screen (Details only).
|
|
5
8
|
*/
|
|
6
9
|
import React from "react";
|
|
7
10
|
import { Box, Text } from "ink";
|
|
8
|
-
import {
|
|
11
|
+
import { DASHBOARD_PURPOSE } from "../../dashboard-guidance.js";
|
|
9
12
|
import { LAYOUT_MODES } from "../layout.js";
|
|
10
13
|
import { COCKPIT_COLORS } from "../theme.js";
|
|
11
14
|
import {
|
|
@@ -14,86 +17,95 @@ import {
|
|
|
14
17
|
wordmarkLines
|
|
15
18
|
} from "../brand/wordmark.js";
|
|
16
19
|
import { ActionList, Callout, Details } from "./semantic.js";
|
|
20
|
+
import {
|
|
21
|
+
humanizeDestination,
|
|
22
|
+
humanizeHealthTitle,
|
|
23
|
+
humanizePrimary,
|
|
24
|
+
mapHealthTone,
|
|
25
|
+
partitionCompanionLines
|
|
26
|
+
} from "./overview-needs.js";
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
usage: "Usage",
|
|
26
|
-
profile: "Settings"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function mapHealthTone(kind) {
|
|
30
|
-
switch (kind) {
|
|
31
|
-
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
32
|
-
return "danger";
|
|
33
|
-
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
34
|
-
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
35
|
-
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
36
|
-
return "warn";
|
|
37
|
-
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
38
|
-
return "ready";
|
|
39
|
-
default:
|
|
40
|
-
return "warn";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function humanizeDestination(destination) {
|
|
45
|
-
if (!destination) return null;
|
|
46
|
-
return DESTINATION_LABELS[destination] ?? null;
|
|
47
|
-
}
|
|
28
|
+
export {
|
|
29
|
+
humanizeCompanionNeed,
|
|
30
|
+
humanizeHealthTitle,
|
|
31
|
+
humanizePrimary,
|
|
32
|
+
mapHealthTone,
|
|
33
|
+
partitionCompanionLines
|
|
34
|
+
} from "./overview-needs.js";
|
|
48
35
|
|
|
49
|
-
/** Safe Details lines
|
|
50
|
-
export function buildOverviewDetails(model = {}) {
|
|
36
|
+
/** Safe Details lines — leftovers + raw signals; never invent paths/IDs. */
|
|
37
|
+
export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
51
38
|
const lines = [];
|
|
52
39
|
const next = model.nextAction ?? model.cta ?? {};
|
|
53
40
|
const dest = humanizeDestination(next.destination);
|
|
54
|
-
if (dest) lines.push(`
|
|
55
|
-
if (typeof model.alerts?.count === "number") {
|
|
41
|
+
if (dest) lines.push(`Opens · ${dest}`);
|
|
42
|
+
if (typeof model.alerts?.count === "number" && model.alerts.count > 0) {
|
|
56
43
|
lines.push(`Open alerts · ${model.alerts.count}`);
|
|
57
44
|
}
|
|
45
|
+
const tokens = model.tokens?.headline;
|
|
46
|
+
if (typeof tokens === "string" && tokens && !/unavailable/i.test(tokens)) {
|
|
47
|
+
lines.push(`Tokens · ${tokens}`);
|
|
48
|
+
}
|
|
58
49
|
const secondary = model.companionNextAction;
|
|
59
50
|
if (secondary?.title && secondary.kind !== "idle") {
|
|
60
51
|
lines.push(`Companion · ${secondary.title}`);
|
|
61
52
|
}
|
|
62
|
-
|
|
53
|
+
for (const line of companionRest) {
|
|
54
|
+
if (typeof line === "string" && line.trim()) lines.push(line);
|
|
55
|
+
}
|
|
56
|
+
if (lines.length === 0) return ["Nothing else to show right now."];
|
|
63
57
|
return lines;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
/**
|
|
67
61
|
* Pure adapter: buildControlCenterModel → semantic overview props.
|
|
68
|
-
*
|
|
69
|
-
* Primary action is always governance CTA — companion is secondary metrics/details only.
|
|
62
|
+
* First screen = purpose + next step + plain needs. Machine noise → Details.
|
|
70
63
|
*/
|
|
71
64
|
export function adaptControlCenterToOverview(model = {}) {
|
|
72
65
|
const status = model.status ?? model.health ?? {};
|
|
73
66
|
const next = model.nextAction ?? model.cta ?? {};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const { needs, rest } = partitionCompanionLines(model.companion?.lines ?? []);
|
|
68
|
+
const primary = humanizePrimary(next);
|
|
69
|
+
|
|
70
|
+
const activity = model.activity?.headline;
|
|
71
|
+
const metrics = [];
|
|
72
|
+
const hasUsefulActivity = typeof activity === "string"
|
|
73
|
+
&& activity
|
|
74
|
+
&& activity !== "Idle"
|
|
75
|
+
&& activity !== "No activity yet";
|
|
76
|
+
if (hasUsefulActivity) {
|
|
77
|
+
metrics.push({ id: "activity", label: `Last activity · ${activity.replace(/^Last ·\s*/, "")}` });
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < needs.length; i += 1) {
|
|
80
|
+
metrics.push({ id: `need-${i}`, label: needs[i] });
|
|
81
|
+
}
|
|
82
|
+
if ((model.alerts?.count ?? 0) > 0) {
|
|
83
|
+
metrics.push({
|
|
84
|
+
id: "alerts",
|
|
85
|
+
label: `Alerts · ${model.alerts.headline ?? `${model.alerts.count} open`}`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (rest.length > 0) {
|
|
89
|
+
metrics.push({
|
|
90
|
+
id: "more",
|
|
91
|
+
label: `${rest.length} more in Details · Space`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (metrics.length === 0) {
|
|
95
|
+
metrics.push({ id: "quiet", label: "Nothing else needs you right now" });
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
return {
|
|
79
99
|
title: model.title ?? "Overview",
|
|
100
|
+
purpose: DASHBOARD_PURPOSE,
|
|
80
101
|
callout: {
|
|
81
102
|
tone: mapHealthTone(status.kind),
|
|
82
|
-
title: status.label
|
|
103
|
+
title: humanizeHealthTitle(status.kind, status.label),
|
|
83
104
|
body: status.summaryLine ?? ""
|
|
84
105
|
},
|
|
85
|
-
primary
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hint: next.enterHint ?? null
|
|
89
|
-
},
|
|
90
|
-
metrics: [
|
|
91
|
-
{ id: "activity", label: `Activity · ${model.activity?.headline ?? "Idle"}` },
|
|
92
|
-
{ id: "alerts", label: `Alerts · ${model.alerts?.headline ?? "Alert data unavailable"}` },
|
|
93
|
-
{ id: "tokens", label: `Tokens · ${model.tokens?.headline ?? "Data unavailable"}` },
|
|
94
|
-
...companionMetrics
|
|
95
|
-
],
|
|
96
|
-
details: buildOverviewDetails(model)
|
|
106
|
+
primary,
|
|
107
|
+
metrics,
|
|
108
|
+
details: buildOverviewDetails(model, rest)
|
|
97
109
|
};
|
|
98
110
|
}
|
|
99
111
|
|
|
@@ -150,11 +162,14 @@ export function SemanticOverviewPanel({
|
|
|
150
162
|
|
|
151
163
|
return React.createElement(Box, { flexDirection: "column" },
|
|
152
164
|
hero,
|
|
165
|
+
React.createElement(Text, {
|
|
166
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
167
|
+
}, view.purpose),
|
|
153
168
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
154
169
|
React.createElement(Text, {
|
|
155
170
|
bold: true,
|
|
156
171
|
color: colorEnabled ? COCKPIT_COLORS.interactive : undefined
|
|
157
|
-
}, view.primary.label),
|
|
172
|
+
}, `→ ${view.primary.label}`),
|
|
158
173
|
view.primary.detail
|
|
159
174
|
? React.createElement(Text, null, view.primary.detail)
|
|
160
175
|
: null,
|
|
@@ -175,7 +190,7 @@ export function SemanticOverviewPanel({
|
|
|
175
190
|
),
|
|
176
191
|
React.createElement(Details, {
|
|
177
192
|
open: detailsOpen,
|
|
178
|
-
summary: "
|
|
193
|
+
summary: "More info",
|
|
179
194
|
lines: view.details,
|
|
180
195
|
colorEnabled,
|
|
181
196
|
focused: false,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language Overview needs — no machine noise on the first screen.
|
|
3
|
+
*/
|
|
4
|
+
import { CONTROL_PLANE_HEALTH } from "../../control-plane-snapshot.js";
|
|
5
|
+
import { formatCliCommand } from "../../brand/cli.js";
|
|
6
|
+
|
|
7
|
+
export const OVERVIEW_NEED_LIMIT = 3;
|
|
8
|
+
|
|
9
|
+
/** Prefixes that never belong on the first screen (machine / internals). */
|
|
10
|
+
export const DETAILS_ONLY_PREFIXES = [
|
|
11
|
+
"System",
|
|
12
|
+
"Advisor",
|
|
13
|
+
"Soft links",
|
|
14
|
+
"Gentle",
|
|
15
|
+
"Graphify",
|
|
16
|
+
"Hermes"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const DESTINATION_LABELS = {
|
|
20
|
+
setup: "Setup",
|
|
21
|
+
changes: "Governance",
|
|
22
|
+
ides: "Agents",
|
|
23
|
+
runs: "Orchestration",
|
|
24
|
+
"control-center": "Overview",
|
|
25
|
+
activity: "Activity",
|
|
26
|
+
usage: "Usage",
|
|
27
|
+
profile: "Settings"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function mapHealthTone(kind) {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
33
|
+
return "danger";
|
|
34
|
+
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
35
|
+
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
36
|
+
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
37
|
+
return "warn";
|
|
38
|
+
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
39
|
+
return "ready";
|
|
40
|
+
default:
|
|
41
|
+
return "warn";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Plain-language status title — never shouty ALL-CAPS jargon alone. */
|
|
46
|
+
export function humanizeHealthTitle(kind, fallback = "Unknown") {
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
49
|
+
return "Needs setup";
|
|
50
|
+
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
51
|
+
return "Needs attention";
|
|
52
|
+
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
53
|
+
return "Something failed";
|
|
54
|
+
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
55
|
+
return "Ready · with notes";
|
|
56
|
+
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
57
|
+
return "Ready";
|
|
58
|
+
default:
|
|
59
|
+
return typeof fallback === "string" && fallback ? fallback : "Unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function humanizeDestination(destination) {
|
|
64
|
+
if (!destination) return null;
|
|
65
|
+
return DESTINATION_LABELS[destination] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Turn a raw companion line into a user-facing need, or null to skip.
|
|
70
|
+
* System/machine lines are never returned here.
|
|
71
|
+
*/
|
|
72
|
+
export function humanizeCompanionNeed(line) {
|
|
73
|
+
if (typeof line !== "string" || line.trim().length === 0) return null;
|
|
74
|
+
if (/^\s/.test(line)) return null;
|
|
75
|
+
|
|
76
|
+
for (const prefix of DETAILS_ONLY_PREFIXES) {
|
|
77
|
+
if (line.startsWith(prefix)) return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (line.startsWith("Obsidian · unconfigured")) {
|
|
81
|
+
return "Obsidian not connected · open Settings to choose your vault";
|
|
82
|
+
}
|
|
83
|
+
if (line.startsWith("Obsidian · missing")) {
|
|
84
|
+
return "Obsidian vault folder missing · check Settings";
|
|
85
|
+
}
|
|
86
|
+
if (line.startsWith("Obsidian · unavailable") || line.startsWith("Obsidian · error")) {
|
|
87
|
+
return "Obsidian unavailable · check Settings";
|
|
88
|
+
}
|
|
89
|
+
if (/^Updates · \d+\s+available/i.test(line)) {
|
|
90
|
+
return `Update available · run ${formatCliCommand("updates check")}`;
|
|
91
|
+
}
|
|
92
|
+
if (line.startsWith("Engram · conflict")) {
|
|
93
|
+
return "Memory conflict · open Settings → Engram";
|
|
94
|
+
}
|
|
95
|
+
if (line.startsWith("Engram · missing") || line.startsWith("Engram · error")) {
|
|
96
|
+
return "Memory not ready · open Settings → Engram";
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Partition companion lines: plain needs for Overview, raw rest for Details. */
|
|
102
|
+
export function partitionCompanionLines(lines = []) {
|
|
103
|
+
const needs = [];
|
|
104
|
+
const rest = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (typeof line !== "string" || line.trim().length === 0) continue;
|
|
107
|
+
const need = humanizeCompanionNeed(line);
|
|
108
|
+
if (need && needs.length < OVERVIEW_NEED_LIMIT) {
|
|
109
|
+
needs.push(need);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
rest.push(line);
|
|
113
|
+
}
|
|
114
|
+
return { needs, rest };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function humanizePrimary(next = {}) {
|
|
118
|
+
const kind = next.kind;
|
|
119
|
+
const detail = typeof next.actionDetail === "string" && next.actionDetail.trim()
|
|
120
|
+
? next.actionDetail.trim()
|
|
121
|
+
: null;
|
|
122
|
+
switch (kind) {
|
|
123
|
+
case "setup":
|
|
124
|
+
return {
|
|
125
|
+
label: "Start setup",
|
|
126
|
+
detail: detail ?? `Run ${formatCliCommand("setup")} to configure agents for this project.`,
|
|
127
|
+
hint: "Enter → open Setup"
|
|
128
|
+
};
|
|
129
|
+
case "repair":
|
|
130
|
+
return {
|
|
131
|
+
label: "Fix drift",
|
|
132
|
+
detail: detail ?? `Run ${formatCliCommand("sync")} to repair managed content.`,
|
|
133
|
+
hint: "Enter → preview in Governance"
|
|
134
|
+
};
|
|
135
|
+
case "verify":
|
|
136
|
+
return {
|
|
137
|
+
label: "Check what failed",
|
|
138
|
+
detail: detail ?? `Run ${formatCliCommand("doctor")} for details.`,
|
|
139
|
+
hint: "Enter → reload"
|
|
140
|
+
};
|
|
141
|
+
case "review":
|
|
142
|
+
return {
|
|
143
|
+
label: "Review notes",
|
|
144
|
+
detail: detail ?? "Open Governance to inspect remaining notes.",
|
|
145
|
+
hint: "Enter → open"
|
|
146
|
+
};
|
|
147
|
+
case "idle":
|
|
148
|
+
return {
|
|
149
|
+
label: "All clear",
|
|
150
|
+
detail: detail ?? "No action needed right now.",
|
|
151
|
+
hint: null
|
|
152
|
+
};
|
|
153
|
+
default:
|
|
154
|
+
return {
|
|
155
|
+
label: next.actionTitle ?? "Review control plane",
|
|
156
|
+
detail,
|
|
157
|
+
hint: next.enterHint ?? "Enter →"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -7,6 +7,11 @@ import { loadHermesActivity as defaultHermesActivity } from "./hermes-activity.j
|
|
|
7
7
|
import { loadSystemResources as defaultSystemResources } from "./system-resources.js";
|
|
8
8
|
import { recommendSystemResources } from "./resource-advisor.js";
|
|
9
9
|
import { loadEcosystemUpdates as defaultEcosystemUpdates } from "./ecosystem-updates.js";
|
|
10
|
+
import {
|
|
11
|
+
emptyObsidianVaultStatus,
|
|
12
|
+
loadObsidianVaultStatus as defaultObsidianVault,
|
|
13
|
+
summarizeObsidianVaultStatus
|
|
14
|
+
} from "./obsidian-status.js";
|
|
10
15
|
import { getObservabilityProbe, registerObservabilityProbe } from "./probe-registry.js";
|
|
11
16
|
|
|
12
17
|
export const SOFT_LINK_WINDOW_MS = 60 * 60 * 1000;
|
|
@@ -179,7 +184,8 @@ function emptyCompanion(error = null) {
|
|
|
179
184
|
graphify: { state: "error", error: null, diagnostics: [], graphStatus: null },
|
|
180
185
|
hermes: { activity: emptyHermesActivity() },
|
|
181
186
|
system: { resources: emptySystemResources(), advice: { recommendations: [], deepScan: false } },
|
|
182
|
-
ecosystem: { updates: emptyEcosystemUpdates() }
|
|
187
|
+
ecosystem: { updates: emptyEcosystemUpdates() },
|
|
188
|
+
obsidian: { vault: emptyObsidianVaultStatus() }
|
|
183
189
|
},
|
|
184
190
|
engram: { status: "error", binary: null },
|
|
185
191
|
links: [], alertsCount: null,
|
|
@@ -199,6 +205,7 @@ export async function buildCompanionSnapshot({
|
|
|
199
205
|
loadHermesActivity = defaultHermesActivity,
|
|
200
206
|
loadSystemResources = defaultSystemResources,
|
|
201
207
|
loadEcosystemUpdates = defaultEcosystemUpdates,
|
|
208
|
+
loadObsidianVaultStatus = defaultObsidianVault,
|
|
202
209
|
resourceDeepScan = false,
|
|
203
210
|
observabilityContext = {}
|
|
204
211
|
} = {}) {
|
|
@@ -249,6 +256,19 @@ export async function buildCompanionSnapshot({
|
|
|
249
256
|
ecosystemUpdates = emptyEcosystemUpdates();
|
|
250
257
|
}
|
|
251
258
|
|
|
259
|
+
let obsidianVault = emptyObsidianVaultStatus();
|
|
260
|
+
try {
|
|
261
|
+
obsidianVault = summarizeObsidianVaultStatus(
|
|
262
|
+
await loadObsidianVaultStatus({
|
|
263
|
+
vaultPath: observabilityContext?.obsidianVaultPath ?? observabilityContext?.vaultPath ?? null,
|
|
264
|
+
lastPublishAt: observabilityContext?.obsidianLastPublishAt ?? null,
|
|
265
|
+
pendingProposals: observabilityContext?.obsidianPendingProposals ?? 0
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
obsidianVault = emptyObsidianVaultStatus();
|
|
270
|
+
}
|
|
271
|
+
|
|
252
272
|
const reviewList = Array.isArray(reviews)
|
|
253
273
|
? reviews
|
|
254
274
|
: (typeof loadReviews === "function" ? await loadReviews() : []);
|
|
@@ -259,7 +279,8 @@ export async function buildCompanionSnapshot({
|
|
|
259
279
|
...summarizeCompanionProbes(obs?.probes ?? []),
|
|
260
280
|
hermes: { activity: hermesActivity },
|
|
261
281
|
system: { resources: systemResources, advice: systemAdvice },
|
|
262
|
-
ecosystem: { updates: ecosystemUpdates }
|
|
282
|
+
ecosystem: { updates: ecosystemUpdates },
|
|
283
|
+
obsidian: { vault: obsidianVault }
|
|
263
284
|
};
|
|
264
285
|
const links = [];
|
|
265
286
|
for (const review of reviewList ?? []) {
|
|
@@ -68,6 +68,45 @@ export {
|
|
|
68
68
|
parseHermesUpdateCheck,
|
|
69
69
|
loadEcosystemUpdates
|
|
70
70
|
} from "./ecosystem-updates.js";
|
|
71
|
+
export {
|
|
72
|
+
KAIRO_VAULT_SUBDIR,
|
|
73
|
+
EXCLUDED_DIR_NAMES,
|
|
74
|
+
normalizeVaultPath,
|
|
75
|
+
isExcludedDirName,
|
|
76
|
+
isSecretBasename,
|
|
77
|
+
isAllowedKairoNoteName,
|
|
78
|
+
assertInsideKairoRoot,
|
|
79
|
+
resolveKairoNotePath,
|
|
80
|
+
inspectObsidianVault
|
|
81
|
+
} from "./obsidian-vault.js";
|
|
82
|
+
export {
|
|
83
|
+
formatKnowledgeFrontmatter,
|
|
84
|
+
renderDecisionMarkdown,
|
|
85
|
+
renderArchitectureMarkdown,
|
|
86
|
+
buildObsidianKnowledgePreview,
|
|
87
|
+
loadObsidianKnowledgePreview
|
|
88
|
+
} from "./obsidian-knowledge-preview.js";
|
|
89
|
+
export {
|
|
90
|
+
KAIRO_MANAGED_FRONTMATTER,
|
|
91
|
+
BACKUP_DIR_NAME,
|
|
92
|
+
hasConsent,
|
|
93
|
+
classifyNoteWrite,
|
|
94
|
+
planObsidianPublish,
|
|
95
|
+
publishObsidianProposals
|
|
96
|
+
} from "./obsidian-publisher.js";
|
|
97
|
+
export {
|
|
98
|
+
KAIRO_VIEW_KINDS,
|
|
99
|
+
parseKnowledgeFrontmatter,
|
|
100
|
+
extractWikilinks,
|
|
101
|
+
buildObsidianKnowledgeViews,
|
|
102
|
+
buildKnowledgeIndexProposals,
|
|
103
|
+
loadObsidianKnowledgeViews
|
|
104
|
+
} from "./obsidian-knowledge-views.js";
|
|
105
|
+
export {
|
|
106
|
+
emptyObsidianVaultStatus,
|
|
107
|
+
summarizeObsidianVaultStatus,
|
|
108
|
+
loadObsidianVaultStatus
|
|
109
|
+
} from "./obsidian-status.js";
|
|
71
110
|
export {
|
|
72
111
|
SOFT_LINK_WINDOW_MS,
|
|
73
112
|
parseCompanionTimestamp,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { resolveKairoNotePath } from "./obsidian-vault.js";
|
|
2
|
+
|
|
3
|
+
const MAX_PROPOSALS = 40;
|
|
4
|
+
const TITLE_MAX = 80;
|
|
5
|
+
|
|
6
|
+
function envelope(partial = {}) {
|
|
7
|
+
return {
|
|
8
|
+
state: "error",
|
|
9
|
+
proposals: [],
|
|
10
|
+
diagnostics: [],
|
|
11
|
+
error: null,
|
|
12
|
+
generatedAt: null,
|
|
13
|
+
...partial
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scrubTitle(raw, fallback = "untitled") {
|
|
18
|
+
const text = String(raw ?? "")
|
|
19
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
20
|
+
.replace(/[\[\]#|\\/]+/g, " ")
|
|
21
|
+
.trim()
|
|
22
|
+
.slice(0, TITLE_MAX);
|
|
23
|
+
return text || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(title) {
|
|
27
|
+
return scrubTitle(title, "note")
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "")
|
|
31
|
+
.slice(0, 48) || "note";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Stable YAML-ish frontmatter — no nested objects; values are scalars only. */
|
|
35
|
+
export function formatKnowledgeFrontmatter(fields = {}) {
|
|
36
|
+
const lines = ["---"];
|
|
37
|
+
for (const key of Object.keys(fields).sort()) {
|
|
38
|
+
const value = fields[key];
|
|
39
|
+
if (value == null || value === "") continue;
|
|
40
|
+
const safe = String(value).replace(/[\r\n]+/g, " ").replace(/"/g, "'");
|
|
41
|
+
lines.push(`${key}: "${safe}"`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("---", "");
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderDecisionMarkdown(entry, { generatedAt } = {}) {
|
|
48
|
+
const title = scrubTitle(entry?.title ?? entry?.id, "decision");
|
|
49
|
+
const id = scrubTitle(entry?.id ?? slugify(title), slugify(title));
|
|
50
|
+
const body = String(entry?.body ?? entry?.content ?? "").trim();
|
|
51
|
+
const fm = formatKnowledgeFrontmatter({
|
|
52
|
+
kairo_kind: "decision",
|
|
53
|
+
kairo_id: id,
|
|
54
|
+
source: "engram-export",
|
|
55
|
+
generated_at: generatedAt ?? null,
|
|
56
|
+
title
|
|
57
|
+
});
|
|
58
|
+
const wiki = `[[decisions/${slugify(title)}]]`;
|
|
59
|
+
return {
|
|
60
|
+
relativePath: `decisions/${slugify(title)}.md`,
|
|
61
|
+
title,
|
|
62
|
+
provenance: { system: "engram", kind: "decision", id },
|
|
63
|
+
markdown: `${fm}# ${title}\n\n${body || "_No body provided._"}\n\n---\nSource: Engram export · ${wiki}\n`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderArchitectureMarkdown(entry, { generatedAt } = {}) {
|
|
68
|
+
const title = scrubTitle(entry?.title ?? entry?.name ?? entry?.id, "architecture");
|
|
69
|
+
const id = scrubTitle(entry?.id ?? slugify(title), slugify(title));
|
|
70
|
+
const detail = String(entry?.detail ?? entry?.summary ?? "").trim();
|
|
71
|
+
const links = Array.isArray(entry?.related)
|
|
72
|
+
? entry.related.map((r) => `- [[architecture/${slugify(r)}]]`).join("\n")
|
|
73
|
+
: "";
|
|
74
|
+
const fm = formatKnowledgeFrontmatter({
|
|
75
|
+
kairo_kind: "architecture",
|
|
76
|
+
kairo_id: id,
|
|
77
|
+
source: "graphify-export",
|
|
78
|
+
generated_at: generatedAt ?? null,
|
|
79
|
+
title
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
relativePath: `architecture/${slugify(title)}.md`,
|
|
83
|
+
title,
|
|
84
|
+
provenance: { system: "graphify", kind: "architecture", id },
|
|
85
|
+
markdown: `${fm}# ${title}\n\n${detail || "_No summary provided._"}\n${links ? `\n## Related\n${links}\n` : ""}`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pure composer — accepts already-exported records only.
|
|
91
|
+
* Never opens Engram/Graphify internal DBs or vault files.
|
|
92
|
+
*/
|
|
93
|
+
export function buildObsidianKnowledgePreview({
|
|
94
|
+
decisions = [],
|
|
95
|
+
architecture = [],
|
|
96
|
+
generatedAt = new Date().toISOString(),
|
|
97
|
+
maxProposals = MAX_PROPOSALS,
|
|
98
|
+
kairoRoot = "/virtual/Kairo"
|
|
99
|
+
} = {}) {
|
|
100
|
+
const diagnostics = [];
|
|
101
|
+
const proposals = [];
|
|
102
|
+
|
|
103
|
+
const push = (draft) => {
|
|
104
|
+
if (proposals.length >= maxProposals) return;
|
|
105
|
+
const gate = resolveKairoNotePath(kairoRoot, draft.relativePath);
|
|
106
|
+
if (!gate.ok) {
|
|
107
|
+
diagnostics.push(`rejected ${draft.relativePath}: ${gate.reason}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
proposals.push({
|
|
111
|
+
relativePath: draft.relativePath,
|
|
112
|
+
title: draft.title,
|
|
113
|
+
markdown: draft.markdown,
|
|
114
|
+
provenance: draft.provenance,
|
|
115
|
+
absolutePath: gate.path
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const entry of decisions ?? []) {
|
|
120
|
+
if (entry == null || typeof entry !== "object") {
|
|
121
|
+
diagnostics.push("skipped malformed decision");
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
push(renderDecisionMarkdown(entry, { generatedAt }));
|
|
125
|
+
}
|
|
126
|
+
for (const entry of architecture ?? []) {
|
|
127
|
+
if (entry == null || typeof entry !== "object") {
|
|
128
|
+
diagnostics.push("skipped malformed architecture");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
push(renderArchitectureMarkdown(entry, { generatedAt }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (proposals.length === 0 && diagnostics.length === 0) {
|
|
135
|
+
return envelope({
|
|
136
|
+
state: "empty",
|
|
137
|
+
proposals: [],
|
|
138
|
+
diagnostics: ["no export records provided"],
|
|
139
|
+
generatedAt,
|
|
140
|
+
error: null
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return envelope({
|
|
145
|
+
state: proposals.length > 0 ? "available" : "partial",
|
|
146
|
+
proposals,
|
|
147
|
+
diagnostics,
|
|
148
|
+
generatedAt,
|
|
149
|
+
error: null
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load preview via injectable export adapters — never vault writes.
|
|
155
|
+
* Adapters must return plain arrays of records (already exported).
|
|
156
|
+
*/
|
|
157
|
+
export async function loadObsidianKnowledgePreview({
|
|
158
|
+
loadEngramExport = null,
|
|
159
|
+
loadGraphifyExport = null,
|
|
160
|
+
kairoRoot = "/virtual/Kairo",
|
|
161
|
+
generatedAt = new Date().toISOString(),
|
|
162
|
+
maxProposals = MAX_PROPOSALS
|
|
163
|
+
} = {}) {
|
|
164
|
+
const diagnostics = [];
|
|
165
|
+
let decisions = [];
|
|
166
|
+
let architecture = [];
|
|
167
|
+
|
|
168
|
+
if (typeof loadEngramExport === "function") {
|
|
169
|
+
try {
|
|
170
|
+
const raw = await loadEngramExport();
|
|
171
|
+
decisions = Array.isArray(raw) ? raw : Array.isArray(raw?.decisions) ? raw.decisions : [];
|
|
172
|
+
if (!Array.isArray(raw) && raw != null && !Array.isArray(raw?.decisions)) {
|
|
173
|
+
diagnostics.push("engram export shape unrecognized");
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
diagnostics.push(`engram export failed: ${String(err?.message ?? err)}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
diagnostics.push("engram export adapter not provided");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof loadGraphifyExport === "function") {
|
|
183
|
+
try {
|
|
184
|
+
const raw = await loadGraphifyExport();
|
|
185
|
+
architecture = Array.isArray(raw)
|
|
186
|
+
? raw
|
|
187
|
+
: Array.isArray(raw?.architecture)
|
|
188
|
+
? raw.architecture
|
|
189
|
+
: Array.isArray(raw?.communities)
|
|
190
|
+
? raw.communities
|
|
191
|
+
: [];
|
|
192
|
+
if (
|
|
193
|
+
!Array.isArray(raw)
|
|
194
|
+
&& raw != null
|
|
195
|
+
&& !Array.isArray(raw?.architecture)
|
|
196
|
+
&& !Array.isArray(raw?.communities)
|
|
197
|
+
) {
|
|
198
|
+
diagnostics.push("graphify export shape unrecognized");
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
diagnostics.push(`graphify export failed: ${String(err?.message ?? err)}`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
diagnostics.push("graphify export adapter not provided");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const preview = buildObsidianKnowledgePreview({
|
|
208
|
+
decisions, architecture, generatedAt, maxProposals, kairoRoot
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
...preview,
|
|
212
|
+
diagnostics: [...diagnostics, ...(preview.diagnostics ?? [])]
|
|
213
|
+
};
|
|
214
|
+
}
|