@indigoai-us/hq-cli 5.201.4 → 5.202.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 +14 -0
- package/dist/commands/core-ontology.js +12 -4
- package/dist/commands/integrations-connect.d.ts +38 -0
- package/dist/commands/integrations-connect.js +39 -38
- package/dist/commands/integrations-core.d.ts +4 -6
- package/dist/commands/integrations-core.js +15 -12
- package/dist/commands/integrations-import.js +8 -6
- package/dist/commands/integrations-manage.js +8 -6
- package/dist/commands/integrations.js +9 -5
- package/dist/lib/checkpoint-ontology/apply.d.ts +3 -0
- package/dist/lib/checkpoint-ontology/apply.js +5 -0
- package/dist/lib/checkpoint-ontology/company-brief.d.ts +18 -0
- package/dist/lib/checkpoint-ontology/company-brief.js +337 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +9 -0
- package/dist/lib/checkpoint-ontology/plan.js +16 -2
- package/dist/lib/doctor/checks/integrations.js +11 -0
- package/dist/utils/redact-error-text.js +2 -1
- package/dist/utils/redact-url.d.ts +14 -0
- package/dist/utils/redact-url.js +218 -0
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `company-brief.md` for one company.
|
|
3
|
+
*
|
|
4
|
+
* The cloud gardener used to write this file. The checkpoint renders the same
|
|
5
|
+
* shape from the local ontology with no model call. Signal dates come from
|
|
6
|
+
* `sources/_index`, because signal files have no `created_at`.
|
|
7
|
+
*/
|
|
8
|
+
import { type OntologyCompany } from "./company.js";
|
|
9
|
+
export declare const COMPANY_BRIEF_FILE = "company-brief.md";
|
|
10
|
+
export interface CompanyBriefResult {
|
|
11
|
+
status: "written" | "unchanged" | "skipped" | "failed";
|
|
12
|
+
/** Set for skipped and failed. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
/** `null` when fewer than three valid person, project, company, or concept entities exist. */
|
|
16
|
+
export declare function renderCompanyBrief(company: OntologyCompany, now: Date): string | null;
|
|
17
|
+
export declare function writeCompanyBrief(hqRoot: string, company: OntologyCompany, now: Date): CompanyBriefResult;
|
|
18
|
+
//# sourceMappingURL=company-brief.d.ts.map
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `company-brief.md` for one company.
|
|
3
|
+
*
|
|
4
|
+
* The cloud gardener used to write this file. The checkpoint renders the same
|
|
5
|
+
* shape from the local ontology with no model call. Signal dates come from
|
|
6
|
+
* `sources/_index`, because signal files have no `created_at`.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { safeWriteFile, WriteBoundsError } from "./company.js";
|
|
11
|
+
import { parseEntityFile, validateEntityFrontmatter } from "./entity-file.js";
|
|
12
|
+
export const COMPANY_BRIEF_FILE = "company-brief.md";
|
|
13
|
+
const SIGNAL_TYPES = [
|
|
14
|
+
{ dir: "decision", heading: "Decisions" },
|
|
15
|
+
{ dir: "risk", heading: "Risks" },
|
|
16
|
+
{ dir: "question", heading: "Questions" },
|
|
17
|
+
];
|
|
18
|
+
const ENTITY_TYPES = [
|
|
19
|
+
{ dir: "person", heading: "People" },
|
|
20
|
+
{ dir: "project", heading: "Projects" },
|
|
21
|
+
{ dir: "company", heading: "Companies" },
|
|
22
|
+
{ dir: "concept", heading: "Concepts" },
|
|
23
|
+
];
|
|
24
|
+
const CHANNELS = [
|
|
25
|
+
{ id: "meeting", label: "Meeting" },
|
|
26
|
+
{ id: "email", label: "Email" },
|
|
27
|
+
{ id: "slack", label: "Slack" },
|
|
28
|
+
{ id: "slack-dm", label: "Slack DM" },
|
|
29
|
+
{ id: "linear", label: "Linear" },
|
|
30
|
+
{ id: "notion", label: "Notion" },
|
|
31
|
+
];
|
|
32
|
+
const NO_SOURCES = "<!-- source channels: no sources/_index entries in last 7d -->";
|
|
33
|
+
const SIGNAL_PREVIEW_MAX = 160;
|
|
34
|
+
const SIGNAL_TOP = 5;
|
|
35
|
+
const ENTITY_TOP = 8;
|
|
36
|
+
/** Cloud budget: `floor(2000 * 0.4)` reserved tokens, minus the source block. */
|
|
37
|
+
const SIGNAL_TOKEN_RESERVE = Math.floor(2000 * 0.4);
|
|
38
|
+
const MIN_ENTITIES = 3;
|
|
39
|
+
function byCodeUnit(a, b) {
|
|
40
|
+
if (a < b)
|
|
41
|
+
return -1;
|
|
42
|
+
if (a > b)
|
|
43
|
+
return 1;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
function tokenCount(text) {
|
|
47
|
+
return Math.ceil(text.length / 4);
|
|
48
|
+
}
|
|
49
|
+
/** UTC dates `now`, `now − 1 day`, … from the epoch day, not the local calendar. */
|
|
50
|
+
function windowDates(now) {
|
|
51
|
+
const day = Math.floor(now.getTime() / 86_400_000);
|
|
52
|
+
const dates = [];
|
|
53
|
+
for (let i = 0; i < 7; i += 1)
|
|
54
|
+
dates.push(new Date((day - i) * 86_400_000).toISOString().slice(0, 10));
|
|
55
|
+
return dates;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `false` when `dir` does not exist. Throws when it resolves outside the company:
|
|
59
|
+
* a symlinked directory would otherwise copy another company's files into this brief.
|
|
60
|
+
*/
|
|
61
|
+
function readableInside(companyDir, dir) {
|
|
62
|
+
let real;
|
|
63
|
+
try {
|
|
64
|
+
real = fs.realpathSync(dir);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code === "ENOENT")
|
|
68
|
+
return false;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const relative = path.relative(fs.realpathSync(companyDir), real);
|
|
72
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
73
|
+
throw new WriteBoundsError(`${path.relative(companyDir, dir)} resolves outside the company`);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function listDirectMd(companyDir, dir) {
|
|
78
|
+
if (!readableInside(companyDir, dir))
|
|
79
|
+
return [];
|
|
80
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
+
const names = [];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
// Files only. A directory whose name starts with `@` or `_` is never entered.
|
|
84
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
85
|
+
continue;
|
|
86
|
+
names.push(entry.name);
|
|
87
|
+
}
|
|
88
|
+
names.sort(byCodeUnit);
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
/** `null` when the file is missing, unparsable, or not a JSON array. */
|
|
92
|
+
function readIndexArray(file) {
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
raw = fs.readFileSync(file, "utf8");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === "ENOENT")
|
|
99
|
+
return null;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof SyntaxError)
|
|
108
|
+
return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function indexKey(entry) {
|
|
113
|
+
const keys = entry.keys;
|
|
114
|
+
const markdown = keys && typeof keys === "object" && !Array.isArray(keys)
|
|
115
|
+
? keys.markdown
|
|
116
|
+
: undefined;
|
|
117
|
+
if (typeof markdown === "string" && markdown.length > 0)
|
|
118
|
+
return markdown;
|
|
119
|
+
const sourceRef = entry.source_ref;
|
|
120
|
+
const fallback = sourceRef && typeof sourceRef === "object" && !Array.isArray(sourceRef)
|
|
121
|
+
? sourceRef.key
|
|
122
|
+
: undefined;
|
|
123
|
+
return typeof fallback === "string" && fallback.length > 0 ? fallback : null;
|
|
124
|
+
}
|
|
125
|
+
function mapChannel(value) {
|
|
126
|
+
if (value === "meeting" || value === "meetings")
|
|
127
|
+
return "meeting";
|
|
128
|
+
if (value === "email" || value === "slack" || value === "slack-dm" || value === "linear" || value === "notion") {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function loadSources(companyDir, now) {
|
|
134
|
+
const times = new Map();
|
|
135
|
+
const counts = new Map();
|
|
136
|
+
const dir = path.join(companyDir, "sources", "_index");
|
|
137
|
+
// Regular files only: a symlinked index file could point at another company's index.
|
|
138
|
+
const names = readableInside(companyDir, dir)
|
|
139
|
+
? fs
|
|
140
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
141
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
142
|
+
.map((entry) => entry.name)
|
|
143
|
+
.sort(byCodeUnit)
|
|
144
|
+
: [];
|
|
145
|
+
const windowFiles = new Set(windowDates(now).map((date) => `${date}.json`));
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
const entries = readIndexArray(path.join(dir, name));
|
|
148
|
+
if (!entries)
|
|
149
|
+
continue;
|
|
150
|
+
const inWindow = windowFiles.has(name);
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
153
|
+
continue;
|
|
154
|
+
const record = entry;
|
|
155
|
+
const key = indexKey(record);
|
|
156
|
+
if (key && typeof record.written_at === "string")
|
|
157
|
+
times.set(key, record.written_at);
|
|
158
|
+
if (!inWindow)
|
|
159
|
+
continue;
|
|
160
|
+
const channel = mapChannel(record.channel);
|
|
161
|
+
if (channel)
|
|
162
|
+
counts.set(channel, (counts.get(channel) ?? 0) + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { times, counts };
|
|
166
|
+
}
|
|
167
|
+
function loadSignals(companyDir, times) {
|
|
168
|
+
const items = [];
|
|
169
|
+
for (const type of SIGNAL_TYPES) {
|
|
170
|
+
const dir = path.join(companyDir, "signals", type.dir);
|
|
171
|
+
for (const name of listDirectMd(companyDir, dir)) {
|
|
172
|
+
const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
173
|
+
if (!parsed)
|
|
174
|
+
continue;
|
|
175
|
+
const content = parsed.frontmatter.canonical_content;
|
|
176
|
+
if (typeof content !== "string" || content.length === 0)
|
|
177
|
+
continue;
|
|
178
|
+
const sourceRef = parsed.frontmatter.source_ref;
|
|
179
|
+
const time = typeof sourceRef === "string" ? times.get(sourceRef) ?? null : null;
|
|
180
|
+
items.push({ type: type.dir, rel: `signals/${type.dir}/${name}`, content, time });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
function compareSignals(a, b) {
|
|
186
|
+
if (a.time !== null && b.time !== null) {
|
|
187
|
+
if (a.time !== b.time)
|
|
188
|
+
return a.time < b.time ? 1 : -1;
|
|
189
|
+
}
|
|
190
|
+
else if (a.time !== b.time) {
|
|
191
|
+
return a.time === null ? 1 : -1;
|
|
192
|
+
}
|
|
193
|
+
if (a.rel !== b.rel)
|
|
194
|
+
return a.rel < b.rel ? 1 : -1;
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
function previewContent(content) {
|
|
198
|
+
const collapsed = content.replace(/\s+/g, " ").trim();
|
|
199
|
+
if (collapsed.length <= SIGNAL_PREVIEW_MAX)
|
|
200
|
+
return collapsed;
|
|
201
|
+
return `${collapsed.slice(0, SIGNAL_PREVIEW_MAX).trimEnd()}…`;
|
|
202
|
+
}
|
|
203
|
+
function renderSignalBlock(items) {
|
|
204
|
+
const sections = [];
|
|
205
|
+
for (const type of SIGNAL_TYPES) {
|
|
206
|
+
const rows = items.filter((item) => item.type === type.dir);
|
|
207
|
+
if (rows.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
const lines = rows.map((item) => {
|
|
210
|
+
const base = `- [${previewContent(item.content)}](${item.rel})`;
|
|
211
|
+
return item.time ? `${base} _(${item.time.slice(0, 10)})_` : base;
|
|
212
|
+
});
|
|
213
|
+
sections.push(`### ${type.heading}\n${lines.join("\n")}`);
|
|
214
|
+
}
|
|
215
|
+
if (sections.length === 0)
|
|
216
|
+
return null;
|
|
217
|
+
return ["## Recent Signals", ...sections].join("\n\n").trimEnd();
|
|
218
|
+
}
|
|
219
|
+
function confidenceOf(frontmatter) {
|
|
220
|
+
const value = frontmatter.confidence;
|
|
221
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0;
|
|
222
|
+
}
|
|
223
|
+
function compareEntities(a, b) {
|
|
224
|
+
if (a.score !== b.score)
|
|
225
|
+
return b.score - a.score;
|
|
226
|
+
if (a.name !== b.name)
|
|
227
|
+
return a.name < b.name ? -1 : 1;
|
|
228
|
+
if (a.file !== b.file)
|
|
229
|
+
return a.file < b.file ? -1 : 1;
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
function loadEntities(companyDir) {
|
|
233
|
+
let total = 0;
|
|
234
|
+
const groups = [];
|
|
235
|
+
for (const type of ENTITY_TYPES) {
|
|
236
|
+
const dir = path.join(companyDir, "ontology", "entities", type.dir);
|
|
237
|
+
const items = [];
|
|
238
|
+
for (const name of listDirectMd(companyDir, dir)) {
|
|
239
|
+
const parsed = parseEntityFile(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
240
|
+
if (!parsed)
|
|
241
|
+
continue;
|
|
242
|
+
const valid = validateEntityFrontmatter(parsed.frontmatter);
|
|
243
|
+
if (typeof valid === "string")
|
|
244
|
+
continue;
|
|
245
|
+
items.push({
|
|
246
|
+
file: name,
|
|
247
|
+
name: valid.canonical_name,
|
|
248
|
+
signalCount: valid.signal_count,
|
|
249
|
+
score: valid.signal_count * confidenceOf(parsed.frontmatter),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
total += items.length;
|
|
253
|
+
items.sort(compareEntities);
|
|
254
|
+
groups.push({
|
|
255
|
+
heading: type.heading,
|
|
256
|
+
lines: items.slice(0, ENTITY_TOP).map((item) => `- [${item.name}](ontology/entities/${type.dir}/${item.file}) (${item.signalCount} signals)`),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { total, groups };
|
|
260
|
+
}
|
|
261
|
+
function renderEntityBlock(slug, groups) {
|
|
262
|
+
const parts = [
|
|
263
|
+
`# ${slug} company brief`,
|
|
264
|
+
"_Rendered by hq core checkpoint from the local ontology._",
|
|
265
|
+
"## Key Entities",
|
|
266
|
+
];
|
|
267
|
+
for (const group of groups) {
|
|
268
|
+
if (group.lines.length === 0)
|
|
269
|
+
continue;
|
|
270
|
+
parts.push(`### ${group.heading}\n${group.lines.join("\n")}`);
|
|
271
|
+
}
|
|
272
|
+
return parts.join("\n\n");
|
|
273
|
+
}
|
|
274
|
+
function renderSourceBlock(counts) {
|
|
275
|
+
const total = CHANNELS.reduce((sum, channel) => sum + (counts.get(channel.id) ?? 0), 0);
|
|
276
|
+
if (total === 0)
|
|
277
|
+
return NO_SOURCES;
|
|
278
|
+
const rows = CHANNELS.map((channel) => `| ${channel.label} | ${counts.get(channel.id) ?? 0} |`);
|
|
279
|
+
return ["## Source Channels", "", "_Last 7 days_", "", "| Channel | Count |", "| --- | ---: |", ...rows].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function fitSignals(ranked, budget) {
|
|
282
|
+
const kept = ranked.slice();
|
|
283
|
+
while (kept.length > 0) {
|
|
284
|
+
const block = renderSignalBlock(kept);
|
|
285
|
+
if (block === null || tokenCount(block) <= budget)
|
|
286
|
+
break;
|
|
287
|
+
kept.pop();
|
|
288
|
+
}
|
|
289
|
+
return kept;
|
|
290
|
+
}
|
|
291
|
+
/** `null` when fewer than three valid person, project, company, or concept entities exist. */
|
|
292
|
+
export function renderCompanyBrief(company, now) {
|
|
293
|
+
const entities = loadEntities(company.dir);
|
|
294
|
+
if (entities.total < MIN_ENTITIES)
|
|
295
|
+
return null;
|
|
296
|
+
const sources = loadSources(company.dir, now);
|
|
297
|
+
const signals = loadSignals(company.dir, sources.times);
|
|
298
|
+
const ranked = [];
|
|
299
|
+
for (const type of SIGNAL_TYPES) {
|
|
300
|
+
ranked.push(...signals.filter((item) => item.type === type.dir).sort(compareSignals).slice(0, SIGNAL_TOP));
|
|
301
|
+
}
|
|
302
|
+
const sourceBlock = renderSourceBlock(sources.counts);
|
|
303
|
+
const budget = Math.max(0, SIGNAL_TOKEN_RESERVE - tokenCount(sourceBlock));
|
|
304
|
+
const signalBlock = renderSignalBlock(fitSignals(ranked, budget));
|
|
305
|
+
const blocks = [renderEntityBlock(company.slug, entities.groups)];
|
|
306
|
+
if (signalBlock)
|
|
307
|
+
blocks.push(signalBlock);
|
|
308
|
+
blocks.push(sourceBlock);
|
|
309
|
+
return `${blocks.join("\n\n")}\n`;
|
|
310
|
+
}
|
|
311
|
+
export function writeCompanyBrief(hqRoot, company, now) {
|
|
312
|
+
try {
|
|
313
|
+
const content = renderCompanyBrief(company, now);
|
|
314
|
+
if (content === null)
|
|
315
|
+
return { status: "skipped", reason: "fewer than 3 entities" };
|
|
316
|
+
const target = path.join(company.dir, COMPANY_BRIEF_FILE);
|
|
317
|
+
let existing = null;
|
|
318
|
+
try {
|
|
319
|
+
// Never follow a symlinked brief for the equality check; safeWriteFile rejects it below.
|
|
320
|
+
if (!fs.lstatSync(target).isSymbolicLink())
|
|
321
|
+
existing = fs.readFileSync(target);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (error.code !== "ENOENT")
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
if (existing && existing.equals(Buffer.from(content, "utf8")))
|
|
328
|
+
return { status: "unchanged" };
|
|
329
|
+
safeWriteFile(hqRoot, [company.dir], target, content);
|
|
330
|
+
return { status: "written" };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
const failed = error;
|
|
334
|
+
return { status: "failed", reason: `${failed.name}: ${failed.message}` };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=company-brief.js.map
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* backend model to read. The model only fills a candidates file; everything
|
|
8
8
|
* that affects identity, scope and cost stays in code.
|
|
9
9
|
*/
|
|
10
|
+
import { type CompanyBriefResult } from "./company-brief.js";
|
|
10
11
|
import { type OntologyCompany } from "./company.js";
|
|
11
12
|
export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
12
13
|
export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
|
|
14
|
+
export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
13
15
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
14
16
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
15
17
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -66,6 +68,11 @@ export interface OntologyPlan {
|
|
|
66
68
|
first_run: boolean;
|
|
67
69
|
boundary: string | null;
|
|
68
70
|
shared_inputs: boolean;
|
|
71
|
+
/** Present only when `checkpoint.ontology.company-brief` was on for this plan. */
|
|
72
|
+
company_brief?: {
|
|
73
|
+
status: CompanyBriefResult["status"];
|
|
74
|
+
reason?: string;
|
|
75
|
+
};
|
|
69
76
|
inputs: PlanInput[];
|
|
70
77
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
71
78
|
session_text: string | null;
|
|
@@ -98,10 +105,12 @@ export interface PlanOptions {
|
|
|
98
105
|
export type PlanOutcome = {
|
|
99
106
|
status: "skipped";
|
|
100
107
|
reason: string;
|
|
108
|
+
companyBrief?: CompanyBriefResult;
|
|
101
109
|
} | {
|
|
102
110
|
status: "planned";
|
|
103
111
|
plan: OntologyPlan;
|
|
104
112
|
planPath: string;
|
|
113
|
+
companyBrief?: CompanyBriefResult;
|
|
105
114
|
};
|
|
106
115
|
export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
|
|
107
116
|
export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { writeCompanyBrief } from "./company-brief.js";
|
|
13
14
|
import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
|
|
14
15
|
import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
|
|
15
16
|
export const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
16
17
|
export const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
|
|
18
|
+
export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
17
19
|
export const PLAN_FILE = "ontology-plan.json";
|
|
18
20
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
19
21
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -173,6 +175,10 @@ export async function planOntologyRun(options) {
|
|
|
173
175
|
if (!(await options.readFlag(ONTOLOGY_FLAG, company)))
|
|
174
176
|
return { status: "skipped", reason: "flag off" };
|
|
175
177
|
const sharedInputs = await options.readFlag(SHARED_INPUTS_FLAG, company);
|
|
178
|
+
// Before the ledger, so a nothing-changed plan still records the brief.
|
|
179
|
+
const companyBrief = (await options.readFlag(COMPANY_BRIEF_FLAG, company))
|
|
180
|
+
? writeCompanyBrief(hqRoot, company, options.now ?? new Date())
|
|
181
|
+
: undefined;
|
|
176
182
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
177
183
|
const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched, sharedInputs);
|
|
178
184
|
const pending = Object.entries(ledger.entries)
|
|
@@ -214,7 +220,8 @@ export async function planOntologyRun(options) {
|
|
|
214
220
|
}
|
|
215
221
|
writeLedger(hqRoot, company, ledger);
|
|
216
222
|
if (inputs.length === 0) {
|
|
217
|
-
|
|
223
|
+
const reason = pending.length > 0 ? "over budget" : "nothing changed";
|
|
224
|
+
return companyBrief ? { status: "skipped", reason, companyBrief } : { status: "skipped", reason };
|
|
218
225
|
}
|
|
219
226
|
// Session content is scoped to the session's user (design section 6, D2).
|
|
220
227
|
// v1 records the existing entities it names as links in the report and
|
|
@@ -243,6 +250,13 @@ export async function planOntologyRun(options) {
|
|
|
243
250
|
first_run: firstRun,
|
|
244
251
|
boundary: ledger.boundary,
|
|
245
252
|
shared_inputs: sharedInputs,
|
|
253
|
+
...(companyBrief
|
|
254
|
+
? {
|
|
255
|
+
company_brief: companyBrief.reason
|
|
256
|
+
? { status: companyBrief.status, reason: companyBrief.reason }
|
|
257
|
+
: { status: companyBrief.status },
|
|
258
|
+
}
|
|
259
|
+
: {}),
|
|
246
260
|
inputs,
|
|
247
261
|
session_text: sessionText,
|
|
248
262
|
pending_after_run: remaining,
|
|
@@ -258,7 +272,7 @@ export async function planOntologyRun(options) {
|
|
|
258
272
|
fs.mkdirSync(runDir, { recursive: true });
|
|
259
273
|
const planPath = path.join(runDir, PLAN_FILE);
|
|
260
274
|
fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
261
|
-
return { status: "planned", plan, planPath };
|
|
275
|
+
return companyBrief ? { status: "planned", plan, planPath, companyBrief } : { status: "planned", plan, planPath };
|
|
262
276
|
}
|
|
263
277
|
const SESSION_TEXT_MAX_BYTES = 8_000;
|
|
264
278
|
/** The session summary, decisions and learnings from the payload, capped. */
|
|
@@ -11,6 +11,7 @@ import { ensureCognitoIdToken } from "../../../utils/cognito-session.js";
|
|
|
11
11
|
import { isAuthError } from "../../../utils/auth-error.js";
|
|
12
12
|
import { isCompanySelectionError } from "../../../utils/company-selection-error.js";
|
|
13
13
|
import { getCompanyUid } from "../../../utils/vault-api.js";
|
|
14
|
+
import { redactUrlsInText } from "../../../utils/redact-url.js";
|
|
14
15
|
import { fetchConnections, IntegrationsCliError } from "../../../commands/integrations-core.js";
|
|
15
16
|
import { classifyConnection } from "../../integrations/health.js";
|
|
16
17
|
/**
|
|
@@ -26,6 +27,16 @@ export const INTEGRATIONS_PREFIX = "integrations";
|
|
|
26
27
|
* becoming a false connection failure. An unreadable inventory remains UNKNOWN.
|
|
27
28
|
*/
|
|
28
29
|
export async function checkIntegrations(context, deps = {}) {
|
|
30
|
+
const results = await checkIntegrationsUnredacted(context, deps);
|
|
31
|
+
return results.map((result) => ({
|
|
32
|
+
...result,
|
|
33
|
+
message: redactUrlsInText(result.message),
|
|
34
|
+
...(result.remediation !== undefined
|
|
35
|
+
? { remediation: redactUrlsInText(result.remediation) }
|
|
36
|
+
: {}),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
async function checkIntegrationsUnredacted(context, deps) {
|
|
29
40
|
if (context.integrations === false) {
|
|
30
41
|
return [{
|
|
31
42
|
status: "NA",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// expected branch instead, which prints `err.message` verbatim. Rather than
|
|
10
10
|
// duplicate the chain at the second print site, it lives here and both callers
|
|
11
11
|
// share it: provider-minted text is untrusted and must never be printed raw.
|
|
12
|
+
import { redactUrlsInText } from "./redact-url.js";
|
|
12
13
|
/** Upper bound on any redacted diagnostic — a message, not a payload dump. */
|
|
13
14
|
export const REDACTED_TEXT_MAX_LENGTH = 1_000;
|
|
14
15
|
/**
|
|
@@ -18,7 +19,7 @@ export const REDACTED_TEXT_MAX_LENGTH = 1_000;
|
|
|
18
19
|
* their own fallback wording.
|
|
19
20
|
*/
|
|
20
21
|
export function redactErrorText(text) {
|
|
21
|
-
return text
|
|
22
|
+
return redactUrlsInText(text)
|
|
22
23
|
.replace(/-----BEGIN[^-]+PRIVATE KEY-----[\s\S]*?-----END[^-]+PRIVATE KEY-----/g, "[REDACTED]")
|
|
23
24
|
.replace(/\p{Cc}/gu, " ")
|
|
24
25
|
.replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Marker used when a value claims to be a URL but cannot be parsed safely. */
|
|
2
|
+
export declare const REDACTED_URL = "[REDACTED]";
|
|
3
|
+
/**
|
|
4
|
+
* Redact the values of secret-looking URL parameters while retaining URL
|
|
5
|
+
* structure and safe query values. An invalid URL or malformed encoded
|
|
6
|
+
* parameter name is replaced in full because its secret boundaries are
|
|
7
|
+
* unknowable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function redactSensitiveUrl(value: string): string;
|
|
10
|
+
/** Redact URLs embedded in an error or human-readable sentence. */
|
|
11
|
+
export declare function redactUrlsInText(text: string): string;
|
|
12
|
+
/** Clone a JSON-like value with all URL-valued fields and embedded URLs safe. */
|
|
13
|
+
export declare function redactSensitiveUrls(value: unknown): unknown;
|
|
14
|
+
//# sourceMappingURL=redact-url.d.ts.map
|