kushi-agents 5.8.0 → 5.8.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/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.0",
3
+ "version": "5.8.1",
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": {
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,
@@ -299,6 +299,7 @@ export async function runMultiHost(opts) {
299
299
  force: true,
300
300
  profile: opts.profile,
301
301
  skipWorkiqCheck: true,
302
+ noPrompt: opts.noPrompt,
302
303
  });
303
304
  } catch (err) {
304
305
  console.error(` Workspace install failed: ${err.message}`);
@@ -0,0 +1,129 @@
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 3-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
+ if (!fs.existsSync(target)) return { ran: false, reason: 'm365-auth-not-found' };
37
+
38
+ let parsed;
39
+ try { parsed = JSON.parse(fs.readFileSync(target, 'utf8')); }
40
+ catch (e) { return { ran: false, reason: `parse-error: ${e.message}` }; }
41
+ parsed.m365Auth ??= {};
42
+ const m = parsed.m365Auth;
43
+ m.emailContext ??= {};
44
+ m.teamsChatContext ??= {};
45
+ m.calendarContext ??= {};
46
+ m.oneNote ??= {};
47
+
48
+ // Skip silently if all 3 fields are already populated and --force not passed.
49
+ const foldersSet = Array.isArray(m.emailContext.folders) && m.emailContext.folders.length > 0;
50
+ const floorSet = !looksLikeSentinel(m.emailContext.dateFloor);
51
+ const notebookSet = !looksLikeSentinel(m.oneNote.defaultNotebookName);
52
+ if (foldersSet && floorSet && notebookSet && !force) {
53
+ return { ran: false, reason: 'already-populated' };
54
+ }
55
+
56
+ const readline = await import('node:readline/promises');
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
58
+ const ask = async (q, def) => {
59
+ const ans = (await rl.question(`${q} ${def != null && def !== '' ? `[${def}]` : ''} > `)).trim();
60
+ return ans === '' ? def : ans;
61
+ };
62
+
63
+ process.stderr.write('\n ┌─ Quickstart: 3 questions that make `kushi discover` fast ──\n');
64
+ process.stderr.write(` │ Editing: ${target}\n`);
65
+ process.stderr.write(' │ Press Enter at any prompt to skip (you can always edit the file later).\n');
66
+ process.stderr.write(' └────────────────────────────────────────────────────────────\n\n');
67
+
68
+ const fields = [];
69
+
70
+ // 1. Email folders
71
+ process.stderr.write(' [1/3] Which Outlook mail folders contain your project mail?\n');
72
+ process.stderr.write(' Examples:\n');
73
+ process.stderr.write(' • "1. FDE, 99. FDE Not Active" (Microsoft FDE consultant)\n');
74
+ process.stderr.write(' • "Inbox, Archive, Projects" (generic)\n');
75
+ process.stderr.write(' • "Inbox" (everything in inbox)\n');
76
+ const curFolders = Array.isArray(m.emailContext.folders) ? m.emailContext.folders.join(', ') : '';
77
+ const foldersAns = await ask(' Comma-separated folder names', curFolders);
78
+ if (foldersAns && foldersAns !== curFolders) {
79
+ const arr = foldersAns.split(',').map(s => s.trim()).filter(Boolean);
80
+ m.emailContext.folders = arr;
81
+ if (m.emailContext.includeSubfolders == null) m.emailContext.includeSubfolders = true;
82
+ if (m.emailContext.enabled == null) m.emailContext.enabled = true;
83
+ fields.push(`emailContext.folders=[${arr.join(', ')}]`);
84
+ } else if (!foldersAns) {
85
+ process.stderr.write(' (skipped — discover will scan the whole mailbox, which can take minutes per source)\n');
86
+ }
87
+
88
+ // 2. Look-back days
89
+ process.stderr.write('\n [2/3] How many days back should discover scan? (smaller = faster)\n');
90
+ process.stderr.write(' Recommended: 60 for active engagements, 90 for setup.\n');
91
+ const curFloor = m.emailContext.dateFloor;
92
+ const defaultDays = curFloor && /^\d{4}-\d{2}-\d{2}$/.test(curFloor)
93
+ ? Math.max(1, Math.round((Date.now() - Date.parse(curFloor)) / 86400000))
94
+ : 60;
95
+ const daysAns = await ask(' Look-back days', defaultDays);
96
+ const days = Number(daysAns);
97
+ if (Number.isFinite(days) && days > 0 && days <= 3650) {
98
+ const floor = isoDateNDaysAgo(days);
99
+ m.emailContext.dateFloor = floor;
100
+ m.teamsChatContext.dateFloor = floor;
101
+ m.calendarContext.dateFloor = floor;
102
+ fields.push(`dateFloor=${floor} (${days} days)`);
103
+ } else {
104
+ process.stderr.write(` ⚠ "${daysAns}" is not a valid number 1–3650 — skipping dateFloor.\n`);
105
+ }
106
+
107
+ // 3. OneNote notebook
108
+ process.stderr.write('\n [3/3] Which OneNote notebook holds your project notes?\n');
109
+ process.stderr.write(' Default for Microsoft consultants: "ISE Work"\n');
110
+ const curNb = m.oneNote.defaultNotebookName || '';
111
+ const nbAns = await ask(' Notebook name', curNb || 'ISE Work');
112
+ if (nbAns && nbAns !== curNb) {
113
+ m.oneNote.defaultNotebookName = nbAns;
114
+ if (m.oneNote.enabled == null) m.oneNote.enabled = true;
115
+ fields.push(`oneNote.defaultNotebookName="${nbAns}"`);
116
+ }
117
+
118
+ rl.close();
119
+
120
+ if (fields.length === 0) {
121
+ process.stderr.write('\n ⓘ No changes — m365-auth.json left as seeded. Edit it later to enable bounded discover.\n\n');
122
+ return { ran: true, reason: 'no-changes', file: target, fields: [] };
123
+ }
124
+ fs.writeFileSync(target, JSON.stringify(parsed, null, 2) + '\n');
125
+ process.stderr.write(`\n ✓ Wrote ${fields.length} field(s) to ${target}:\n`);
126
+ for (const f of fields) process.stderr.write(` • ${f}\n`);
127
+ process.stderr.write('\n');
128
+ return { ran: true, file: target, fields };
129
+ }