kushi-agents 5.8.0 → 5.8.2
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/bin/cli.mjs +4 -0
- package/package.json +1 -1
- package/plugin/lib/Get-KushiConfig.ps1 +0 -2
- package/plugin/runners/discover.mjs +20 -8
- package/plugin/runners/lib/m365-auth.mjs +3 -0
- package/src/main.mjs +10 -0
- package/src/multi-host.mjs +1 -0
- package/src/quickstart-m365.mjs +177 -0
package/bin/cli.mjs
CHANGED
|
@@ -206,6 +206,8 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
206
206
|
--yes, -y Skip the project-root check
|
|
207
207
|
--no-settings Skip .vscode/settings.json update (vscode workspace target only)
|
|
208
208
|
--no-instructions Skip .github/copilot-instructions.md merge (vscode workspace target only)
|
|
209
|
+
--no-prompt Skip the post-install m365-auth quickstart (3 fields that
|
|
210
|
+
make 'kushi discover' fast). Also disabled by KUSHI_NO_PROMPT=1.
|
|
209
211
|
|
|
210
212
|
WorkIQ (REQUIRED — Kushi cannot pull evidence without it):
|
|
211
213
|
--with-workiq Auto-install WorkIQ via winget (Windows) / brew (macOS)
|
|
@@ -281,6 +283,7 @@ if (wantsVscode || wantsAllHosts || wantsUninstall) {
|
|
|
281
283
|
uninstall: wantsUninstall,
|
|
282
284
|
profile: getFlag('--profile'),
|
|
283
285
|
includeWorkspace: !args.includes('--no-workspace'),
|
|
286
|
+
noPrompt: args.includes('--no-prompt'),
|
|
284
287
|
}).catch((err) => {
|
|
285
288
|
console.error(`\n ${err.message}\n`);
|
|
286
289
|
process.exit(1);
|
|
@@ -300,6 +303,7 @@ if (wantsVscode || wantsAllHosts || wantsUninstall) {
|
|
|
300
303
|
yes: args.includes('--yes') || args.includes('-y'),
|
|
301
304
|
noSettings: args.includes('--no-settings'),
|
|
302
305
|
noInstructions: args.includes('--no-instructions'),
|
|
306
|
+
noPrompt: args.includes('--no-prompt'),
|
|
303
307
|
target,
|
|
304
308
|
profile: getFlag('--profile'),
|
|
305
309
|
withWorkiq: args.includes('--with-workiq'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.2",
|
|
4
4
|
"description": "Install Kushi — multi-source project evidence agent with Comprehensive Structured Capture (CSC) into weekly-only files across Email, Teams, OneNote, Loop, SharePoint, Meetings, CRM, ADO. Meetings retain a sibling verbatim/ audit folder. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,9 +61,7 @@ $script:RequiredFields = @{
|
|
|
61
61
|
'm365-auth' = @(
|
|
62
62
|
'm365Auth.defaultTenantId',
|
|
63
63
|
'm365Auth.oneNote.defaultNotebookName',
|
|
64
|
-
'm365Auth.oneNote.defaultNotebookId',
|
|
65
64
|
'm365Auth.oneNote.defaultLinkOwner',
|
|
66
|
-
'm365Auth.emailContext.folders',
|
|
67
65
|
'm365Auth.sharePointContext.localProjectsRoot'
|
|
68
66
|
)
|
|
69
67
|
'project-evidence' = @(
|
|
@@ -49,7 +49,10 @@ function emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
|
|
49
49
|
function describeScope(source, scope) {
|
|
50
50
|
if (!scope || scope.enabled === false) return null;
|
|
51
51
|
const parts = [];
|
|
52
|
-
if (source === 'email'
|
|
52
|
+
if (source === 'email') {
|
|
53
|
+
if (scope.folders?.length) parts.push(`${scope.folders.length} folder(s)`);
|
|
54
|
+
else parts.push('default Inbox+subfolders');
|
|
55
|
+
}
|
|
53
56
|
if (scope.dateFloor) parts.push(`from ${scope.dateFloor}`);
|
|
54
57
|
if (source === 'onenote' && scope.notebookName) parts.push(`notebook "${scope.notebookName}"`);
|
|
55
58
|
return parts.length ? 'scope: ' + parts.join(', ') : null;
|
|
@@ -82,13 +85,22 @@ function buildPrompt(source, projectName, scope = null) {
|
|
|
82
85
|
// search, typically dropping query time from 60–180s to 5–15s.
|
|
83
86
|
if (scope && scope.enabled !== false) {
|
|
84
87
|
if (source === 'email') {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
lines.push('
|
|
88
|
+
const folders = (Array.isArray(scope.folders) && scope.folders.length > 0)
|
|
89
|
+
? scope.folders
|
|
90
|
+
: ['Inbox']; // safe default — bounds the query so WorkIQ uses Graph filter, not mailbox-wide semantic search
|
|
91
|
+
const isDefault = !(Array.isArray(scope.folders) && scope.folders.length > 0);
|
|
92
|
+
lines.push('');
|
|
93
|
+
if (scope.fuzzy !== false) {
|
|
94
|
+
lines.push('Restrict your search to Outlook mail folders whose name CONTAINS any of these tokens (case-insensitive, fuzzy substring match — e.g. "FDE" matches "1. FDE", "01 FDE Active", "FDE-archive"):');
|
|
95
|
+
} else {
|
|
96
|
+
lines.push('Restrict your search to ONLY these Outlook mail folders (exact name match):');
|
|
97
|
+
}
|
|
98
|
+
for (const f of folders) {
|
|
99
|
+
lines.push(` • "${f}"${scope.includeSubfolders ? ' (and all subfolders)' : ''}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push('Do NOT scan any other mailbox folders.');
|
|
102
|
+
if (isDefault) {
|
|
103
|
+
lines.push('(Note: no project-specific folders configured — defaulting to Inbox+subfolders. For faster, more accurate results, populate emailContext.folders in m365-auth.json.)');
|
|
92
104
|
}
|
|
93
105
|
if (scope.dateFloor) lines.push(`Only consider mail received on or after ${scope.dateFloor}.`);
|
|
94
106
|
} else if (source === 'teams') {
|
|
@@ -62,11 +62,14 @@ export function scopeForSource(m365Auth, source) {
|
|
|
62
62
|
case 'email': {
|
|
63
63
|
const ec = m365Auth.emailContext || {};
|
|
64
64
|
if (ec.enabled === false) return { enabled: false };
|
|
65
|
+
const mp = ec.matchingPolicy || {};
|
|
65
66
|
return {
|
|
66
67
|
enabled: true,
|
|
67
68
|
folders: cleanArr(ec.folders),
|
|
68
69
|
includeSubfolders: ec.includeSubfolders !== false,
|
|
69
70
|
dateFloor: cleanStr(ec.dateFloor),
|
|
71
|
+
fuzzy: mp.alwaysFuzzy !== false,
|
|
72
|
+
matchMode: mp.mode || 'hybrid',
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
case 'teams': {
|
package/src/main.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { installRunnerDeps } from './install-runner-deps.mjs';
|
|
|
17
17
|
import { mergeSettings } from './settings.mjs';
|
|
18
18
|
import { mergeCopilotInstructions } from './copilot-instructions.mjs';
|
|
19
19
|
import { seedConfig } from './seed-config.mjs';
|
|
20
|
+
import { runM365Quickstart } from './quickstart-m365.mjs';
|
|
20
21
|
import { checkWorkIQ, pingWorkIQ, tryInstallWorkIQ } from './check-workiq.mjs';
|
|
21
22
|
import {
|
|
22
23
|
resolveProfile,
|
|
@@ -115,6 +116,15 @@ async function installVscode(options, resolved, version) {
|
|
|
115
116
|
const seedRes = seedConfig(PKG_ROOT, fullDest);
|
|
116
117
|
printSeedReport(seedRes, `${dest}/`);
|
|
117
118
|
|
|
119
|
+
// v5.8.1: prompt for the 3 fields that most affect discover speed.
|
|
120
|
+
// Skips silently when --no-prompt is set, when not running in a TTY, or
|
|
121
|
+
// when all 3 fields are already populated (re-install case).
|
|
122
|
+
const noPrompt = options.noPrompt || process.env.KUSHI_NO_PROMPT === '1';
|
|
123
|
+
const qs = await runM365Quickstart({ destRoot: fullDest, noPrompt });
|
|
124
|
+
if (qs.ran === false && qs.reason && !['already-populated', 'no-prompt-flag'].includes(qs.reason)) {
|
|
125
|
+
console.log(` Quickstart skipped: ${qs.reason}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
if (!options.noSettings) {
|
|
119
129
|
const { created, keysAdded, keysUnchanged } = mergeSettings(
|
|
120
130
|
projectRoot,
|
package/src/multi-host.mjs
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/quickstart-m365.mjs
|
|
2
|
+
// Post-install interactive prompt for the 3 fields that most affect kushi
|
|
3
|
+
// `discover` wall time (folders, dateFloor via lookback-days, OneNote notebook).
|
|
4
|
+
// Runs at the end of `installWorkspace()` when the destination just got a fresh
|
|
5
|
+
// m365-auth.json seed. Skips silently when not a TTY or when --no-prompt is set.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const SENTINELS = [/__FILL_ME_IN__/, /^<[A-Za-z][^>]*>$/, /^<auto>$/];
|
|
11
|
+
function looksLikeSentinel(s) {
|
|
12
|
+
if (!s) return true;
|
|
13
|
+
return SENTINELS.some((re) => re.test(s));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isoDateNDaysAgo(n) {
|
|
17
|
+
const d = new Date();
|
|
18
|
+
d.setDate(d.getDate() - n);
|
|
19
|
+
return d.toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run the 4-question quickstart. Returns a small report object:
|
|
24
|
+
* { ran: bool, reason?: string, file?: string, fields?: string[] }
|
|
25
|
+
*
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {string} opts.destRoot - absolute path to .kushi/ (the workspace install root)
|
|
28
|
+
* @param {boolean} [opts.force] - overwrite existing non-empty values
|
|
29
|
+
* @param {boolean} [opts.noPrompt]- skip prompting (CI / scripted installs)
|
|
30
|
+
*/
|
|
31
|
+
export async function runM365Quickstart({ destRoot, force = false, noPrompt = false } = {}) {
|
|
32
|
+
if (noPrompt) return { ran: false, reason: 'no-prompt-flag' };
|
|
33
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) return { ran: false, reason: 'not-a-tty' };
|
|
34
|
+
|
|
35
|
+
const target = path.join(destRoot, 'config', 'user', 'm365-auth.json');
|
|
36
|
+
const projectEvidenceTarget = path.join(destRoot, 'config', 'user', 'project-evidence.yml');
|
|
37
|
+
if (!fs.existsSync(target)) return { ran: false, reason: 'm365-auth-not-found' };
|
|
38
|
+
|
|
39
|
+
let parsed;
|
|
40
|
+
try { parsed = JSON.parse(fs.readFileSync(target, 'utf8')); }
|
|
41
|
+
catch (e) { return { ran: false, reason: `parse-error: ${e.message}` }; }
|
|
42
|
+
parsed.m365Auth ??= {};
|
|
43
|
+
const m = parsed.m365Auth;
|
|
44
|
+
m.emailContext ??= {};
|
|
45
|
+
m.teamsChatContext ??= {};
|
|
46
|
+
m.calendarContext ??= {};
|
|
47
|
+
m.oneNote ??= {};
|
|
48
|
+
m.sharePointContext ??= {};
|
|
49
|
+
|
|
50
|
+
// Skip silently if all 4 fields are already populated and --force not passed.
|
|
51
|
+
const foldersSet = Array.isArray(m.emailContext.folders) && m.emailContext.folders.length > 0;
|
|
52
|
+
const floorSet = !looksLikeSentinel(m.emailContext.dateFloor);
|
|
53
|
+
const notebookSet = !looksLikeSentinel(m.oneNote.defaultNotebookName);
|
|
54
|
+
const rootSet = !looksLikeSentinel(m.sharePointContext.localProjectsRoot);
|
|
55
|
+
if (foldersSet && floorSet && notebookSet && rootSet && !force) {
|
|
56
|
+
return { ran: false, reason: 'already-populated' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const readline = await import('node:readline/promises');
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
61
|
+
const ask = async (q, def) => {
|
|
62
|
+
const ans = (await rl.question(`${q} ${def != null && def !== '' ? `[${def}]` : ''} > `)).trim();
|
|
63
|
+
return ans === '' ? def : ans;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
process.stderr.write('\n ┌─ Quickstart: 4 questions that make `kushi discover` fast ──\n');
|
|
67
|
+
process.stderr.write(` │ Editing: ${target}\n`);
|
|
68
|
+
process.stderr.write(' │ Press Enter at any prompt to keep the shown default.\n');
|
|
69
|
+
process.stderr.write(' └────────────────────────────────────────────────────────────\n\n');
|
|
70
|
+
|
|
71
|
+
const fields = [];
|
|
72
|
+
|
|
73
|
+
// 1. Engagement root (where projects live on disk)
|
|
74
|
+
process.stderr.write(' [1/4] Where do your engagement project folders live on disk?\n');
|
|
75
|
+
process.stderr.write(' This is the parent folder containing each <project>/ directory.\n');
|
|
76
|
+
process.stderr.write(' Example: C:\\Users\\<you>\\OneDrive - Microsoft\\ISE\\Engagement Assets\n');
|
|
77
|
+
const curRoot = m.sharePointContext.localProjectsRoot || '';
|
|
78
|
+
const rootAns = await ask(' Engagement root', curRoot);
|
|
79
|
+
let engagementRoot = null;
|
|
80
|
+
if (rootAns) {
|
|
81
|
+
if (!fs.existsSync(rootAns)) {
|
|
82
|
+
process.stderr.write(` ⚠ Path does not exist: ${rootAns}\n`);
|
|
83
|
+
process.stderr.write(' Saving anyway — create it before your first bootstrap, or re-run --force to change.\n');
|
|
84
|
+
}
|
|
85
|
+
if (rootAns !== curRoot) {
|
|
86
|
+
m.sharePointContext.localProjectsRoot = rootAns;
|
|
87
|
+
if (m.sharePointContext.enabled == null) m.sharePointContext.enabled = true;
|
|
88
|
+
fields.push(`sharePointContext.localProjectsRoot=${rootAns}`);
|
|
89
|
+
}
|
|
90
|
+
engagementRoot = rootAns;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Email folders
|
|
94
|
+
process.stderr.write('\n [2/4] Which Outlook mail folders contain your project mail?\n');
|
|
95
|
+
process.stderr.write(' Examples:\n');
|
|
96
|
+
process.stderr.write(' • "1. FDE, 99. FDE Not Active" (Microsoft FDE consultant)\n');
|
|
97
|
+
process.stderr.write(' • "Inbox, Archive, Projects" (generic)\n');
|
|
98
|
+
process.stderr.write(' • "Inbox" (everything in inbox)\n');
|
|
99
|
+
const curFolders = Array.isArray(m.emailContext.folders) ? m.emailContext.folders.join(', ') : '';
|
|
100
|
+
const foldersDefault = curFolders || '1. FDE, 99. FDE Not Active';
|
|
101
|
+
const foldersAns = await ask(' Comma-separated folder names', foldersDefault);
|
|
102
|
+
if (foldersAns && foldersAns !== curFolders) {
|
|
103
|
+
const arr = foldersAns.split(',').map(s => s.trim()).filter(Boolean);
|
|
104
|
+
m.emailContext.folders = arr;
|
|
105
|
+
if (m.emailContext.includeSubfolders == null) m.emailContext.includeSubfolders = true;
|
|
106
|
+
if (m.emailContext.enabled == null) m.emailContext.enabled = true;
|
|
107
|
+
fields.push(`emailContext.folders=[${arr.join(', ')}]`);
|
|
108
|
+
} else if (!foldersAns) {
|
|
109
|
+
process.stderr.write(' (skipped — discover will scan the whole mailbox, which can take minutes per source)\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. Look-back days
|
|
113
|
+
process.stderr.write('\n [3/4] How many days back should discover scan? (smaller = faster)\n');
|
|
114
|
+
process.stderr.write(' Recommended: 60 for active engagements, 90 for setup.\n');
|
|
115
|
+
const curFloor = m.emailContext.dateFloor;
|
|
116
|
+
const defaultDays = curFloor && /^\d{4}-\d{2}-\d{2}$/.test(curFloor)
|
|
117
|
+
? Math.max(1, Math.round((Date.now() - Date.parse(curFloor)) / 86400000))
|
|
118
|
+
: 60;
|
|
119
|
+
const daysAns = await ask(' Look-back days', defaultDays);
|
|
120
|
+
const days = Number(daysAns);
|
|
121
|
+
if (Number.isFinite(days) && days > 0 && days <= 3650) {
|
|
122
|
+
const floor = isoDateNDaysAgo(days);
|
|
123
|
+
m.emailContext.dateFloor = floor;
|
|
124
|
+
m.teamsChatContext.dateFloor = floor;
|
|
125
|
+
m.calendarContext.dateFloor = floor;
|
|
126
|
+
fields.push(`dateFloor=${floor} (${days} days)`);
|
|
127
|
+
} else {
|
|
128
|
+
process.stderr.write(` ⚠ "${daysAns}" is not a valid number 1–3650 — skipping dateFloor.\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. OneNote notebook
|
|
132
|
+
process.stderr.write('\n [4/4] Which OneNote notebook holds your project notes?\n');
|
|
133
|
+
process.stderr.write(' Default for Microsoft consultants: "ISE Work"\n');
|
|
134
|
+
const curNb = m.oneNote.defaultNotebookName || '';
|
|
135
|
+
const nbAns = await ask(' Notebook name', curNb || 'ISE Work');
|
|
136
|
+
if (nbAns && nbAns !== curNb) {
|
|
137
|
+
m.oneNote.defaultNotebookName = nbAns;
|
|
138
|
+
if (m.oneNote.enabled == null) m.oneNote.enabled = true;
|
|
139
|
+
fields.push(`oneNote.defaultNotebookName="${nbAns}"`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
rl.close();
|
|
143
|
+
|
|
144
|
+
if (fields.length === 0) {
|
|
145
|
+
process.stderr.write('\n ⓘ No changes — m365-auth.json left as seeded. Edit it later to enable bounded discover.\n\n');
|
|
146
|
+
return { ran: true, reason: 'no-changes', file: target, fields: [] };
|
|
147
|
+
}
|
|
148
|
+
fs.writeFileSync(target, JSON.stringify(parsed, null, 2) + '\n');
|
|
149
|
+
|
|
150
|
+
// Mirror engagement root into project-evidence.yml#projects_root so bootstrap
|
|
151
|
+
// and the resolver agree. Best-effort: if file is missing or unparseable we
|
|
152
|
+
// skip silently — m365-auth is the canonical source for sharePoint scoping.
|
|
153
|
+
if (engagementRoot && fs.existsSync(projectEvidenceTarget)) {
|
|
154
|
+
try {
|
|
155
|
+
const txt = fs.readFileSync(projectEvidenceTarget, 'utf8');
|
|
156
|
+
// Simple line replacement to avoid pulling in YAML lib at install time.
|
|
157
|
+
// The seed file ships with `projects_root: <value>` on its own line.
|
|
158
|
+
if (/^projects_root:/m.test(txt)) {
|
|
159
|
+
const updated = txt.replace(/^projects_root:.*$/m, `projects_root: ${JSON.stringify(engagementRoot)}`);
|
|
160
|
+
if (updated !== txt) {
|
|
161
|
+
fs.writeFileSync(projectEvidenceTarget, updated);
|
|
162
|
+
fields.push(`project-evidence.yml#projects_root=${engagementRoot}`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
fs.appendFileSync(projectEvidenceTarget, `\nprojects_root: ${JSON.stringify(engagementRoot)}\n`);
|
|
166
|
+
fields.push(`project-evidence.yml#projects_root=${engagementRoot} (appended)`);
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
process.stderr.write(` ⚠ Could not update project-evidence.yml: ${e.message}\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
process.stderr.write(`\n ✓ Wrote ${fields.length} field(s):\n`);
|
|
174
|
+
for (const f of fields) process.stderr.write(` • ${f}\n`);
|
|
175
|
+
process.stderr.write('\n');
|
|
176
|
+
return { ran: true, file: target, fields };
|
|
177
|
+
}
|