kushi-agents 5.8.1 → 5.8.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.3",
|
|
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': {
|
|
@@ -59,7 +59,11 @@ export async function ask(prompt, { bin, timeoutMs = 120_000, env = process.env,
|
|
|
59
59
|
err.code = 'WORKIQ_NOT_FOUND';
|
|
60
60
|
throw err;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
// Pass prompt via stdin instead of `-q "<prompt>"` argument. Multi-line prompts
|
|
63
|
+
// with embedded quotes get mangled by cmd.exe's argument parser even with
|
|
64
|
+
// windowsVerbatimArguments, causing workiq to receive corrupted input and
|
|
65
|
+
// hang indefinitely. stdin is a clean byte channel.
|
|
66
|
+
const { stdout, stderr, exitCode } = await runProcess(exe, ['ask'], { timeoutMs, env, onHeartbeat, heartbeatMs, stdin: prompt });
|
|
63
67
|
if (exitCode !== 0) {
|
|
64
68
|
const err = new Error(`workiq exited ${exitCode}: ${stderr.slice(0, 1000)}`);
|
|
65
69
|
err.code = 'WORKIQ_EXIT_NONZERO';
|
|
@@ -117,7 +121,7 @@ function parseKvLines(s) {
|
|
|
117
121
|
|
|
118
122
|
async function pathExists(p) { try { await fs.access(p); return true; } catch { return false; } }
|
|
119
123
|
|
|
120
|
-
function runProcess(exe, args, { timeoutMs, env, onHeartbeat = null, heartbeatMs = 10_000 }) {
|
|
124
|
+
function runProcess(exe, args, { timeoutMs, env, onHeartbeat = null, heartbeatMs = 10_000, stdin = null }) {
|
|
121
125
|
return new Promise((resolve, reject) => {
|
|
122
126
|
// Node 20.12+ refuses to spawn .cmd/.bat on Windows without shell:true
|
|
123
127
|
// (CVE-2024-27980). Detect and route through cmd.exe explicitly with
|
|
@@ -133,6 +137,10 @@ function runProcess(exe, args, { timeoutMs, env, onHeartbeat = null, heartbeatMs
|
|
|
133
137
|
spawnOpts.windowsVerbatimArguments = true;
|
|
134
138
|
}
|
|
135
139
|
const child = spawn(spawnExe, spawnArgs, spawnOpts);
|
|
140
|
+
if (stdin != null) {
|
|
141
|
+
try { child.stdin.write(stdin); child.stdin.end(); }
|
|
142
|
+
catch (e) { /* if stdin already closed, child error handler will catch */ }
|
|
143
|
+
}
|
|
136
144
|
let stdout = '';
|
|
137
145
|
let stderr = '';
|
|
138
146
|
let stdoutBytes = 0;
|
package/src/quickstart-m365.mjs
CHANGED
|
@@ -20,7 +20,7 @@ function isoDateNDaysAgo(n) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Run the
|
|
23
|
+
* Run the 4-question quickstart. Returns a small report object:
|
|
24
24
|
* { ran: bool, reason?: string, file?: string, fields?: string[] }
|
|
25
25
|
*
|
|
26
26
|
* @param {object} opts
|
|
@@ -33,6 +33,7 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
33
33
|
if (!process.stdin.isTTY || !process.stderr.isTTY) return { ran: false, reason: 'not-a-tty' };
|
|
34
34
|
|
|
35
35
|
const target = path.join(destRoot, 'config', 'user', 'm365-auth.json');
|
|
36
|
+
const projectEvidenceTarget = path.join(destRoot, 'config', 'user', 'project-evidence.yml');
|
|
36
37
|
if (!fs.existsSync(target)) return { ran: false, reason: 'm365-auth-not-found' };
|
|
37
38
|
|
|
38
39
|
let parsed;
|
|
@@ -44,12 +45,14 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
44
45
|
m.teamsChatContext ??= {};
|
|
45
46
|
m.calendarContext ??= {};
|
|
46
47
|
m.oneNote ??= {};
|
|
48
|
+
m.sharePointContext ??= {};
|
|
47
49
|
|
|
48
|
-
// Skip silently if all
|
|
50
|
+
// Skip silently if all 4 fields are already populated and --force not passed.
|
|
49
51
|
const foldersSet = Array.isArray(m.emailContext.folders) && m.emailContext.folders.length > 0;
|
|
50
52
|
const floorSet = !looksLikeSentinel(m.emailContext.dateFloor);
|
|
51
53
|
const notebookSet = !looksLikeSentinel(m.oneNote.defaultNotebookName);
|
|
52
|
-
|
|
54
|
+
const rootSet = !looksLikeSentinel(m.sharePointContext.localProjectsRoot);
|
|
55
|
+
if (foldersSet && floorSet && notebookSet && rootSet && !force) {
|
|
53
56
|
return { ran: false, reason: 'already-populated' };
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -60,21 +63,42 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
60
63
|
return ans === '' ? def : ans;
|
|
61
64
|
};
|
|
62
65
|
|
|
63
|
-
process.stderr.write('\n ┌─ Quickstart:
|
|
66
|
+
process.stderr.write('\n ┌─ Quickstart: 4 questions that make `kushi discover` fast ──\n');
|
|
64
67
|
process.stderr.write(` │ Editing: ${target}\n`);
|
|
65
|
-
process.stderr.write(' │ Press Enter at any prompt to
|
|
68
|
+
process.stderr.write(' │ Press Enter at any prompt to keep the shown default.\n');
|
|
66
69
|
process.stderr.write(' └────────────────────────────────────────────────────────────\n\n');
|
|
67
70
|
|
|
68
71
|
const fields = [];
|
|
69
72
|
|
|
70
|
-
// 1.
|
|
71
|
-
process.stderr.write(' [1/
|
|
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');
|
|
72
95
|
process.stderr.write(' Examples:\n');
|
|
73
96
|
process.stderr.write(' • "1. FDE, 99. FDE Not Active" (Microsoft FDE consultant)\n');
|
|
74
97
|
process.stderr.write(' • "Inbox, Archive, Projects" (generic)\n');
|
|
75
98
|
process.stderr.write(' • "Inbox" (everything in inbox)\n');
|
|
76
99
|
const curFolders = Array.isArray(m.emailContext.folders) ? m.emailContext.folders.join(', ') : '';
|
|
77
|
-
const
|
|
100
|
+
const foldersDefault = curFolders || '1. FDE, 99. FDE Not Active';
|
|
101
|
+
const foldersAns = await ask(' Comma-separated folder names', foldersDefault);
|
|
78
102
|
if (foldersAns && foldersAns !== curFolders) {
|
|
79
103
|
const arr = foldersAns.split(',').map(s => s.trim()).filter(Boolean);
|
|
80
104
|
m.emailContext.folders = arr;
|
|
@@ -85,8 +109,8 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
85
109
|
process.stderr.write(' (skipped — discover will scan the whole mailbox, which can take minutes per source)\n');
|
|
86
110
|
}
|
|
87
111
|
|
|
88
|
-
//
|
|
89
|
-
process.stderr.write('\n [
|
|
112
|
+
// 3. Look-back days
|
|
113
|
+
process.stderr.write('\n [3/4] How many days back should discover scan? (smaller = faster)\n');
|
|
90
114
|
process.stderr.write(' Recommended: 60 for active engagements, 90 for setup.\n');
|
|
91
115
|
const curFloor = m.emailContext.dateFloor;
|
|
92
116
|
const defaultDays = curFloor && /^\d{4}-\d{2}-\d{2}$/.test(curFloor)
|
|
@@ -104,8 +128,8 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
104
128
|
process.stderr.write(` ⚠ "${daysAns}" is not a valid number 1–3650 — skipping dateFloor.\n`);
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
//
|
|
108
|
-
process.stderr.write('\n [
|
|
131
|
+
// 4. OneNote notebook
|
|
132
|
+
process.stderr.write('\n [4/4] Which OneNote notebook holds your project notes?\n');
|
|
109
133
|
process.stderr.write(' Default for Microsoft consultants: "ISE Work"\n');
|
|
110
134
|
const curNb = m.oneNote.defaultNotebookName || '';
|
|
111
135
|
const nbAns = await ask(' Notebook name', curNb || 'ISE Work');
|
|
@@ -122,7 +146,31 @@ export async function runM365Quickstart({ destRoot, force = false, noPrompt = fa
|
|
|
122
146
|
return { ran: true, reason: 'no-changes', file: target, fields: [] };
|
|
123
147
|
}
|
|
124
148
|
fs.writeFileSync(target, JSON.stringify(parsed, null, 2) + '\n');
|
|
125
|
-
|
|
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`);
|
|
126
174
|
for (const f of fields) process.stderr.write(` • ${f}\n`);
|
|
127
175
|
process.stderr.write('\n');
|
|
128
176
|
return { ran: true, file: target, fields };
|