kushi-agents 6.1.0 → 6.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kushi-agents",
3
- "version": "6.1.0",
3
+ "version": "6.1.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": {
@@ -162,7 +162,7 @@ function isPlaceholder(v) {
162
162
  if (!s) return true;
163
163
  if (/^<.*>$/.test(s)) return true; // <value>, <chat_id>, etc.
164
164
  if (/^turn\d+search\d+$/i.test(s)) return true; // WorkIQ web citation tokens
165
- if (/^(unknown|n\/a|none|null|tbd|todo)$/i.test(s)) return true;
165
+ if (/^(unknown|n\/a|none|null|tbd|todo|not\s+explicitly\s+(available|provided|specified)|not\s+(available|provided|specified|applicable)|undisclosed)$/i.test(s)) return true;
166
166
  return false;
167
167
  }
168
168
 
@@ -178,19 +178,26 @@ function isValidValueFor(source, field, raw) {
178
178
  return true;
179
179
  }
180
180
  if (source === 'teams' && field === 'chat_id') {
181
- // Graph chat IDs are long strings ending in '@thread.v2' or similar.
182
- return v.includes('@thread') || v.length > 30;
181
+ // Accept Graph chat IDs ('@thread' / long opaque) OR human-readable
182
+ // chat topics WorkIQ frequently returns the topic when the Graph ID
183
+ // is unavailable. Refresh stage will resolve to a real chat.
184
+ return v.length >= 2 && v.length <= 200;
183
185
  }
184
186
  if (source === 'meetings' && field === 'join_url') {
185
- return /^https:\/\/teams\.microsoft\.com\/.+meetup-join/i.test(v);
187
+ // Prefer real Teams meetup URLs but also accept any https URL or
188
+ // a meeting subject — refresh stage will resolve.
189
+ if (v.startsWith('http')) return true;
190
+ return v.length >= 3 && v.length <= 200;
186
191
  }
187
192
  if (source === 'onenote' && field === 'section_file_id') {
188
- // OneNote section file IDs are hex strings, typically prefixed `0-` and
189
- // dozens of chars long. Reject short citation tokens.
190
- return /^[0-9a-f][0-9a-f\-]{20,}$/i.test(v) || v.startsWith('0-');
193
+ // Accept hex section IDs OR section/page names. WorkIQ often only has
194
+ // the human-readable name; refresh resolves to the file ID.
195
+ return v.length >= 2 && v.length <= 200;
191
196
  }
192
197
  if (source === 'sharepoint' && field === 'site_url') {
193
- return v.startsWith('https://') && v.includes('.sharepoint.com');
198
+ // Prefer real SharePoint URLs; also accept site names/relative paths.
199
+ if (v.startsWith('http')) return v.includes('.sharepoint.com') || v.includes('/sites/');
200
+ return v.length >= 2 && v.length <= 300;
194
201
  }
195
202
  if (source === 'crm' && (field === 'request_id' || field === 'incident_number')) {
196
203
  // CRM IDs vary: FE-2026-001458, REQ-12345, INC-9, plain numerics. Reject
@@ -228,7 +235,16 @@ function applyRows(source, rows, currentBounds, currentInteg) {
228
235
  }
229
236
  if (source === 'teams') {
230
237
  const existing = currentBounds.teams?.chats || [];
231
- const incoming = rows.map(r => r.chat_id).filter(v => isValidValueFor('teams', 'chat_id', v));
238
+ // WorkIQ rarely has Graph chat IDs citation tokens like "turn1search1"
239
+ // are common. Prefer chat_id; fall back to topic so refresh has a usable
240
+ // boundary descriptor instead of nothing.
241
+ const incoming = rows.map(r => {
242
+ const id = r.chat_id;
243
+ if (id && !isPlaceholder(id) && isValidValueFor('teams', 'chat_id', id)) return id;
244
+ const topic = r.topic;
245
+ if (topic && !isPlaceholder(topic)) return topic;
246
+ return null;
247
+ }).filter(Boolean);
232
248
  const merged = dedup([...existing, ...incoming]);
233
249
  const added = merged.filter(v => !existing.includes(v));
234
250
  if (added.length) accepted.push(...added);
@@ -236,7 +252,13 @@ function applyRows(source, rows, currentBounds, currentInteg) {
236
252
  }
237
253
  if (source === 'meetings') {
238
254
  const existing = currentBounds.meetings?.joinUrls || [];
239
- const incoming = rows.map(r => r.join_url).filter(v => isValidValueFor('meetings', 'join_url', v));
255
+ const incoming = rows.map(r => {
256
+ const url = r.join_url;
257
+ if (url && !isPlaceholder(url) && isValidValueFor('meetings', 'join_url', url) && url.startsWith('http')) return url;
258
+ const subj = r.subject;
259
+ if (subj && !isPlaceholder(subj)) return subj;
260
+ return null;
261
+ }).filter(Boolean);
240
262
  const merged = dedup([...existing, ...incoming]);
241
263
  const added = merged.filter(v => !existing.includes(v));
242
264
  if (added.length) accepted.push(...added);
@@ -244,7 +266,16 @@ function applyRows(source, rows, currentBounds, currentInteg) {
244
266
  }
245
267
  if (source === 'onenote') {
246
268
  const existing = currentBounds.onenote?.section_file_ids || [];
247
- const incoming = rows.map(r => r.section_file_id).filter(v => isValidValueFor('onenote', 'section_file_id', v));
269
+ // WorkIQ rarely has section_file_id (Graph property); usually returns
270
+ // citation tokens. Prefer real hex IDs; fall back to section_name so
271
+ // refresh has a usable boundary descriptor.
272
+ const incoming = rows.map(r => {
273
+ const id = r.section_file_id;
274
+ if (id && !isPlaceholder(id) && /^[0-9a-f][0-9a-f\-]{20,}$/i.test(String(id).trim())) return id;
275
+ const name = r.section_name;
276
+ if (name && !isPlaceholder(name)) return name;
277
+ return null;
278
+ }).filter(Boolean);
248
279
  const merged = dedup([...existing, ...incoming]);
249
280
  const added = merged.filter(v => !existing.includes(v));
250
281
  if (added.length) accepted.push(...added);
@@ -391,6 +422,16 @@ async function main() {
391
422
  rows = rowsFromBlocks(blocks, source);
392
423
  const elapsed = Date.now() - t0;
393
424
  const bytes = Buffer.byteLength(workiqStdout || '', 'utf8');
425
+ // Persist raw WorkIQ output for diagnosis. Lets users inspect why a
426
+ // source returned 0 accepted rows without re-running the query.
427
+ if (!args.dryRun) {
428
+ try {
429
+ const discoveryDir = path.join(aliasRoot(args.project, args.alias), '_discovery');
430
+ await fs.mkdir(discoveryDir, { recursive: true });
431
+ const header = `# discover ${source} @ ${new Date().toISOString()}\n# elapsed=${elapsed}ms bytes=${bytes} blocks=${blocks.length} rows=${rows.length}\n# prompt:\n${prompt.split('\n').map(l => '# ' + l).join('\n')}\n# --- workiq stdout ---\n`;
432
+ await fs.writeFile(path.join(discoveryDir, `${source}-raw.txt`), header + (workiqStdout || ''), 'utf8');
433
+ } catch { /* best-effort */ }
434
+ }
394
435
  if (rows.length === 0 && bytes < 8) {
395
436
  // Distinguish "workiq returned empty" from "timeout" — both used to look the same.
396
437
  skipReason = 'workiq-empty-response';
@@ -89,13 +89,16 @@ export async function ask(prompt, { bin, timeoutMs = 120_000, env = process.env,
89
89
  export function parseCscBlocks(text) {
90
90
  if (!text || typeof text !== 'string') return [];
91
91
  const blocks = [];
92
- const blockquoteRe = /(^|\n)>\s*\[block:\s*([a-zA-Z0-9_.-]+)\]\s*\n((?:>\s*[^\n]*\n?)+)/g;
92
+ // Accept either `>` (blockquote) or `|` (table-row style) line prefixes —
93
+ // WorkIQ emits both depending on rendering. Trailing two-space markdown line
94
+ // breaks are tolerated by the per-line strip.
95
+ const blockquoteRe = /(^|\n)[>|]\s*\[block:\s*([a-zA-Z0-9_.-]+)\]\s*\n((?:[>|]\s*[^\n]*\n?)+)/g;
93
96
  let m;
94
97
  while ((m = blockquoteRe.exec(text)) !== null) {
95
98
  const name = m[2];
96
99
  const body = m[3]
97
100
  .split('\n')
98
- .map(l => l.replace(/^>\s?/, ''))
101
+ .map(l => l.replace(/^[>|]\s?/, '').replace(/\s+$/, ''))
99
102
  .filter(l => l.length > 0)
100
103
  .join('\n');
101
104
  blocks.push({ name, raw: body, fields: parseKvLines(body) });