heron-ai 0.1.0

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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +423 -0
  3. package/dist/bin/heron.d.ts +3 -0
  4. package/dist/bin/heron.d.ts.map +1 -0
  5. package/dist/bin/heron.js +198 -0
  6. package/dist/bin/heron.js.map +1 -0
  7. package/dist/src/analysis/analyzer.d.ts +14 -0
  8. package/dist/src/analysis/analyzer.d.ts.map +1 -0
  9. package/dist/src/analysis/analyzer.js +130 -0
  10. package/dist/src/analysis/analyzer.js.map +1 -0
  11. package/dist/src/analysis/risk-scorer.d.ts +20 -0
  12. package/dist/src/analysis/risk-scorer.d.ts.map +1 -0
  13. package/dist/src/analysis/risk-scorer.js +143 -0
  14. package/dist/src/analysis/risk-scorer.js.map +1 -0
  15. package/dist/src/config/loader.d.ts +15 -0
  16. package/dist/src/config/loader.d.ts.map +1 -0
  17. package/dist/src/config/loader.js +39 -0
  18. package/dist/src/config/loader.js.map +1 -0
  19. package/dist/src/config/schema.d.ts +146 -0
  20. package/dist/src/config/schema.d.ts.map +1 -0
  21. package/dist/src/config/schema.js +27 -0
  22. package/dist/src/config/schema.js.map +1 -0
  23. package/dist/src/connectors/http-connector.d.ts +17 -0
  24. package/dist/src/connectors/http-connector.d.ts.map +1 -0
  25. package/dist/src/connectors/http-connector.js +56 -0
  26. package/dist/src/connectors/http-connector.js.map +1 -0
  27. package/dist/src/connectors/index.d.ts +5 -0
  28. package/dist/src/connectors/index.d.ts.map +1 -0
  29. package/dist/src/connectors/index.js +13 -0
  30. package/dist/src/connectors/index.js.map +1 -0
  31. package/dist/src/connectors/interactive-connector.d.ts +13 -0
  32. package/dist/src/connectors/interactive-connector.d.ts.map +1 -0
  33. package/dist/src/connectors/interactive-connector.js +44 -0
  34. package/dist/src/connectors/interactive-connector.js.map +1 -0
  35. package/dist/src/connectors/types.d.ts +15 -0
  36. package/dist/src/connectors/types.d.ts.map +1 -0
  37. package/dist/src/connectors/types.js +2 -0
  38. package/dist/src/connectors/types.js.map +1 -0
  39. package/dist/src/index.d.ts +12 -0
  40. package/dist/src/index.d.ts.map +1 -0
  41. package/dist/src/index.js +60 -0
  42. package/dist/src/index.js.map +1 -0
  43. package/dist/src/interview/interviewer.d.ts +19 -0
  44. package/dist/src/interview/interviewer.d.ts.map +1 -0
  45. package/dist/src/interview/interviewer.js +68 -0
  46. package/dist/src/interview/interviewer.js.map +1 -0
  47. package/dist/src/interview/protocol.d.ts +38 -0
  48. package/dist/src/interview/protocol.d.ts.map +1 -0
  49. package/dist/src/interview/protocol.js +290 -0
  50. package/dist/src/interview/protocol.js.map +1 -0
  51. package/dist/src/interview/questions.d.ts +20 -0
  52. package/dist/src/interview/questions.d.ts.map +1 -0
  53. package/dist/src/interview/questions.js +131 -0
  54. package/dist/src/interview/questions.js.map +1 -0
  55. package/dist/src/llm/client.d.ts +13 -0
  56. package/dist/src/llm/client.d.ts.map +1 -0
  57. package/dist/src/llm/client.js +128 -0
  58. package/dist/src/llm/client.js.map +1 -0
  59. package/dist/src/llm/prompts.d.ts +13 -0
  60. package/dist/src/llm/prompts.d.ts.map +1 -0
  61. package/dist/src/llm/prompts.js +192 -0
  62. package/dist/src/llm/prompts.js.map +1 -0
  63. package/dist/src/report/generator.d.ts +23 -0
  64. package/dist/src/report/generator.d.ts.map +1 -0
  65. package/dist/src/report/generator.js +304 -0
  66. package/dist/src/report/generator.js.map +1 -0
  67. package/dist/src/report/templates.d.ts +3 -0
  68. package/dist/src/report/templates.d.ts.map +1 -0
  69. package/dist/src/report/templates.js +386 -0
  70. package/dist/src/report/templates.js.map +1 -0
  71. package/dist/src/report/types.d.ts +954 -0
  72. package/dist/src/report/types.d.ts.map +1 -0
  73. package/dist/src/report/types.js +161 -0
  74. package/dist/src/report/types.js.map +1 -0
  75. package/dist/src/server/index.d.ts +17 -0
  76. package/dist/src/server/index.d.ts.map +1 -0
  77. package/dist/src/server/index.js +650 -0
  78. package/dist/src/server/index.js.map +1 -0
  79. package/dist/src/server/sessions.d.ts +68 -0
  80. package/dist/src/server/sessions.d.ts.map +1 -0
  81. package/dist/src/server/sessions.js +268 -0
  82. package/dist/src/server/sessions.js.map +1 -0
  83. package/dist/src/util/id.d.ts +2 -0
  84. package/dist/src/util/id.d.ts.map +1 -0
  85. package/dist/src/util/id.js +5 -0
  86. package/dist/src/util/id.js.map +1 -0
  87. package/dist/src/util/logger.d.ts +9 -0
  88. package/dist/src/util/logger.d.ts.map +1 -0
  89. package/dist/src/util/logger.js +32 -0
  90. package/dist/src/util/logger.js.map +1 -0
  91. package/heron.example.yaml +46 -0
  92. package/package.json +40 -0
@@ -0,0 +1,13 @@
1
+ import { HttpConnector } from './http-connector.js';
2
+ import { InteractiveConnector } from './interactive-connector.js';
3
+ export function createConnector(config) {
4
+ switch (config.type) {
5
+ case 'http':
6
+ return new HttpConnector(config);
7
+ case 'interactive':
8
+ return new InteractiveConnector();
9
+ default:
10
+ throw new Error(`Unknown connector type: ${config.type}`);
11
+ }
12
+ }
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/connectors/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,UAAU,eAAe,CAAC,MAAoB;IAClD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;QACnC,KAAK,aAAa;YAChB,OAAO,IAAI,oBAAoB,EAAE,CAAC;QACpC;YACE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { AgentConnector, AgentMetadata } from './types.js';
2
+ /**
3
+ * Interactive connector — the user manually relays questions to the agent
4
+ * and pastes back responses. Useful when the agent doesn't have an HTTP API.
5
+ */
6
+ export declare class InteractiveConnector implements AgentConnector {
7
+ private rl;
8
+ constructor();
9
+ sendMessage(message: string): Promise<string>;
10
+ getMetadata(): Promise<AgentMetadata>;
11
+ close(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=interactive-connector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive-connector.d.ts","sourceRoot":"","sources":["../../../src/connectors/interactive-connector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhE;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,EAAE,CAAqC;;IASzC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB7C,WAAW,IAAI,OAAO,CAAC,aAAa,CAAC;IAOrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,44 @@
1
+ import { createInterface } from 'node:readline';
2
+ import * as logger from '../util/logger.js';
3
+ /**
4
+ * Interactive connector — the user manually relays questions to the agent
5
+ * and pastes back responses. Useful when the agent doesn't have an HTTP API.
6
+ */
7
+ export class InteractiveConnector {
8
+ rl;
9
+ constructor() {
10
+ this.rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stderr, // use stderr so stdout stays clean for report output
13
+ });
14
+ }
15
+ async sendMessage(message) {
16
+ logger.heading('Question for the agent:');
17
+ console.error(`\n${message}\n`);
18
+ console.error('---');
19
+ return new Promise((resolve) => {
20
+ console.error('Paste the agent\'s response below (end with an empty line):');
21
+ const lines = [];
22
+ const lineHandler = (line) => {
23
+ if (line === '' && lines.length > 0) {
24
+ this.rl.removeListener('line', lineHandler);
25
+ resolve(lines.join('\n'));
26
+ }
27
+ else {
28
+ lines.push(line);
29
+ }
30
+ };
31
+ this.rl.on('line', lineHandler);
32
+ });
33
+ }
34
+ async getMetadata() {
35
+ return {
36
+ provider: 'interactive',
37
+ description: 'Manual relay — user copies questions to the agent',
38
+ };
39
+ }
40
+ async close() {
41
+ this.rl.close();
42
+ }
43
+ }
44
+ //# sourceMappingURL=interactive-connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive-connector.js","sourceRoot":"","sources":["../../../src/connectors/interactive-connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAE5C;;;GAGG;AACH,MAAM,OAAO,oBAAoB;IACvB,EAAE,CAAqC;IAE/C;QACE,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC;YACxB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,qDAAqD;SAC9E,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAErB,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YACrC,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;YAC7E,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,EAAE;gBACnC,IAAI,IAAI,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACpC,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;oBAC5C,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5B,CAAC;qBAAM,CAAC;oBACN,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC,CAAC;YACF,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO;YACL,QAAQ,EAAE,aAAa;YACvB,WAAW,EAAE,mDAAmD;SACjE,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,15 @@
1
+ export interface AgentMetadata {
2
+ name?: string;
3
+ model?: string;
4
+ provider?: string;
5
+ description?: string;
6
+ }
7
+ export interface AgentConnector {
8
+ /** Send a message to the target agent and get a response */
9
+ sendMessage(message: string): Promise<string>;
10
+ /** Get metadata about the target agent, if available */
11
+ getMetadata?(): Promise<AgentMetadata>;
12
+ /** Clean up connection resources */
13
+ close(): Promise<void>;
14
+ }
15
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/connectors/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9C,wDAAwD;IACxD,WAAW,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAEvC,oCAAoC;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/connectors/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ import type { HeronConfig } from './config/schema.js';
2
+ export interface RunOptions {
3
+ verbose?: boolean;
4
+ maxFollowUps?: number;
5
+ reportDir?: string;
6
+ }
7
+ /**
8
+ * Main entry point — runs the full interrogation pipeline:
9
+ * connect → interview → analyze → report
10
+ */
11
+ export declare function run(config: HeronConfig, options?: RunOptions): Promise<string>;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDxF"}
@@ -0,0 +1,60 @@
1
+ import { createConnector } from './connectors/index.js';
2
+ import { createLLMClient } from './llm/client.js';
3
+ import { runInterview } from './interview/interviewer.js';
4
+ import { generateReport } from './report/generator.js';
5
+ import * as logger from './util/logger.js';
6
+ import { writeFileSync, mkdirSync } from 'node:fs';
7
+ import { generateId } from './util/id.js';
8
+ /**
9
+ * Main entry point — runs the full interrogation pipeline:
10
+ * connect → interview → analyze → report
11
+ */
12
+ export async function run(config, options = {}) {
13
+ const { verbose = false, maxFollowUps = 3, reportDir = './reports' } = options;
14
+ // 1. Create LLM client for analysis
15
+ const llmClient = await createLLMClient(config.llm);
16
+ // 2. Connect to target agent
17
+ const connector = createConnector(config.target);
18
+ const targetLabel = config.target.url ?? config.target.type;
19
+ const scanId = generateId('scan');
20
+ logger.raw('');
21
+ logger.raw(` \x1b[1mHeron Agent Interrogator\x1b[0m`);
22
+ logger.raw('');
23
+ logger.raw(` Scan: ${scanId}`);
24
+ logger.raw(` Target: ${targetLabel}`);
25
+ try {
26
+ // 3. Run interview
27
+ const session = await runInterview(connector, llmClient, {
28
+ maxFollowUps,
29
+ verbose,
30
+ });
31
+ // 4. Generate report
32
+ logger.raw('');
33
+ logger.raw(` \x1b[33m⏳ Analyzing transcript...\x1b[0m`);
34
+ const { report, reportJson } = await generateReport(session, llmClient, {
35
+ target: targetLabel,
36
+ format: config.output.format,
37
+ });
38
+ const riskLevel = reportJson.overallRiskLevel;
39
+ const riskColor = riskLevel === 'high' || riskLevel === 'critical' ? '\x1b[31m'
40
+ : riskLevel === 'medium' ? '\x1b[33m'
41
+ : '\x1b[32m';
42
+ // 5. Save report to file
43
+ mkdirSync(reportDir, { recursive: true });
44
+ const savePath = config.output.path ?? `${reportDir}/${scanId}.md`;
45
+ writeFileSync(savePath, report, 'utf-8');
46
+ logger.raw('');
47
+ logger.raw(` \x1b[1mAudit complete: ${scanId}\x1b[0m`);
48
+ logger.raw(` Risk: ${riskColor}${riskLevel.toUpperCase()}\x1b[0m`);
49
+ logger.raw(` Data quality: ${reportJson.dataQuality?.score ?? 'N/A'}/100`);
50
+ logger.raw(` Verdict: ${reportJson.recommendation ?? 'APPROVE WITH CONDITIONS'}`);
51
+ logger.raw(` Findings: ${reportJson.risks.length}`);
52
+ logger.raw(` Report: ${savePath}`);
53
+ logger.raw('');
54
+ return report;
55
+ }
56
+ finally {
57
+ await connector.close();
58
+ }
59
+ }
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { AgentConnector } from '../connectors/types.js';
2
+ import type { LLMClient } from '../llm/client.js';
3
+ import type { QAPair } from '../report/types.js';
4
+ export interface InterviewOptions {
5
+ maxFollowUps?: number;
6
+ verbose?: boolean;
7
+ }
8
+ export interface InterviewSession {
9
+ transcript: QAPair[];
10
+ startedAt: Date;
11
+ completedAt: Date;
12
+ questionsAsked: number;
13
+ }
14
+ /**
15
+ * Runs a structured interview with a target agent.
16
+ * Asks core questions, generates follow-ups, collects all answers.
17
+ */
18
+ export declare function runInterview(connector: AgentConnector, llmClient: LLMClient, options?: InterviewOptions): Promise<InterviewSession>;
19
+ //# sourceMappingURL=interviewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interviewer.d.ts","sourceRoot":"","sources":["../../../src/interview/interviewer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAIjD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,cAAc,EACzB,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAyE3B"}
@@ -0,0 +1,68 @@
1
+ import { createProtocol } from './protocol.js';
2
+ import * as logger from '../util/logger.js';
3
+ /**
4
+ * Runs a structured interview with a target agent.
5
+ * Asks core questions, generates follow-ups, collects all answers.
6
+ */
7
+ export async function runInterview(connector, llmClient, options = {}) {
8
+ const { maxFollowUps = 3 } = options;
9
+ const protocol = createProtocol(llmClient, maxFollowUps);
10
+ const startedAt = new Date();
11
+ const total = protocol.totalCoreQuestions;
12
+ let coreNum = 0;
13
+ logger.raw('');
14
+ // Ask all core questions with follow-ups between category changes
15
+ let prevCategory = null;
16
+ for (let i = 0; i < total; i++) {
17
+ const question = protocol.nextQuestion();
18
+ if (!question)
19
+ break;
20
+ // If category changed, try a follow-up on the previous category
21
+ if (prevCategory && question.category !== prevCategory) {
22
+ const followUp = await protocol.generateFollowUp(prevCategory);
23
+ if (followUp) {
24
+ // Show follow-up question
25
+ logger.raw('');
26
+ logger.raw(` \x1b[36mFollow-up\x1b[0m \x1b[2m[${followUp.category}]\x1b[0m`);
27
+ logger.raw(` \x1b[36mQ:\x1b[0m ${followUp.text}`);
28
+ const followUpAnswer = await connector.sendMessage(followUp.text);
29
+ protocol.recordAnswer(followUp, followUpAnswer);
30
+ logger.raw(` \x1b[2mA:\x1b[0m ${followUpAnswer}`);
31
+ }
32
+ }
33
+ coreNum++;
34
+ // Show core question
35
+ logger.raw('');
36
+ logger.raw(` \x1b[36mQ${coreNum}/${total}\x1b[0m \x1b[2m[${question.category}]\x1b[0m`);
37
+ logger.raw(` \x1b[36mQ:\x1b[0m ${question.text}`);
38
+ const answer = await connector.sendMessage(question.text);
39
+ protocol.recordAnswer(question, answer);
40
+ logger.raw(` \x1b[2mA:\x1b[0m ${answer}`);
41
+ prevCategory = question.category;
42
+ }
43
+ // Final follow-up on the last category
44
+ const transcript = protocol.getTranscript();
45
+ if (transcript.length > 0) {
46
+ const lastCategory = transcript[transcript.length - 1].category;
47
+ const finalFollowUp = await protocol.generateFollowUp(lastCategory);
48
+ if (finalFollowUp) {
49
+ logger.raw('');
50
+ logger.raw(` \x1b[36mFollow-up\x1b[0m \x1b[2m[${finalFollowUp.category}]\x1b[0m`);
51
+ logger.raw(` \x1b[36mQ:\x1b[0m ${finalFollowUp.text}`);
52
+ const answer = await connector.sendMessage(finalFollowUp.text);
53
+ protocol.recordAnswer(finalFollowUp, answer);
54
+ logger.raw(` \x1b[2mA:\x1b[0m ${answer}`);
55
+ }
56
+ }
57
+ const completedAt = new Date();
58
+ const finalTranscript = protocol.getTranscript();
59
+ logger.raw('');
60
+ logger.success(`Interview complete — ${finalTranscript.length} questions asked`);
61
+ return {
62
+ transcript: finalTranscript,
63
+ startedAt,
64
+ completedAt,
65
+ questionsAsked: finalTranscript.length,
66
+ };
67
+ }
68
+ //# sourceMappingURL=interviewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interviewer.js","sourceRoot":"","sources":["../../../src/interview/interviewer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { QAPair } from '../report/types.js';
2
+ import type { LLMClient } from '../llm/client.js';
3
+ import { type InterviewQuestion } from './questions.js';
4
+ export interface InterviewProtocol {
5
+ /** Get the next question to ask, or null if interview is complete */
6
+ nextQuestion(): InterviewQuestion | null;
7
+ /** Record an answer. Returns false if answer was skipped (greeting/repeat). */
8
+ recordAnswer(question: InterviewQuestion, answer: string): boolean;
9
+ /** Generate a follow-up question based on context and missing compliance fields */
10
+ generateFollowUp(category: QAPair['category']): Promise<InterviewQuestion | null>;
11
+ /** Get the full transcript so far */
12
+ getTranscript(): QAPair[];
13
+ /** Check if the interview is complete */
14
+ isComplete(): boolean;
15
+ /** Total number of core questions (excluding follow-ups) */
16
+ totalCoreQuestions: number;
17
+ }
18
+ /** Detect if an answer is just a greeting with no substantive content */
19
+ export declare function isGreeting(answer: string): boolean;
20
+ /**
21
+ * Detect if an answer is clearly responding to a different question (stale session).
22
+ * Returns true if the answer strongly matches a DIFFERENT question's topic
23
+ * but has no relevance to the current question.
24
+ *
25
+ * Conservative: only triggers on long answers (300+ chars) that match 2+ signals
26
+ * from a different topic AND zero signals from the current topic. Skips Q1 entirely
27
+ * since first answers are too varied to classify reliably.
28
+ */
29
+ export declare function isStaleAnswer(question: InterviewQuestion, answer: string): boolean;
30
+ /** Detect if an answer is too vague for compliance-grade reporting */
31
+ export declare function isVagueAnswer(answer: string): boolean;
32
+ export declare function createProtocol(llmClient: LLMClient, maxFollowUps?: number): InterviewProtocol;
33
+ /**
34
+ * Enqueue a follow-up question to be asked next.
35
+ * Used by SessionManager to avoid monkey-patching protocol.nextQuestion.
36
+ */
37
+ export declare function enqueueFollowUp(protocol: InterviewProtocol, followUp: InterviewQuestion): void;
38
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/interview/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG/E,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,YAAY,IAAI,iBAAiB,GAAG,IAAI,CAAC;IAEzC,+EAA+E;IAC/E,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAEnE,mFAAmF;IACnF,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAElF,qCAAqC;IACrC,aAAa,IAAI,MAAM,EAAE,CAAC;IAE1B,yCAAyC;IACzC,UAAU,IAAI,OAAO,CAAC;IAEtB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAiBD,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKlD;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAuBlF;AA8BD,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAgED,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,SAAI,GAAG,iBAAiB,CA2HxF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAO9F"}
@@ -0,0 +1,290 @@
1
+ import { getAllQuestionsSorted } from './questions.js';
2
+ import { INTERVIEW_SYSTEM_PROMPT, buildFollowUpPrompt } from '../llm/prompts.js';
3
+ // ─── Greeting detection ──────────────────────────────────────────────────────
4
+ const GREETING_PATTERNS = [
5
+ /^hi\b/i,
6
+ /^hello\b/i,
7
+ /^hey\b/i,
8
+ /ready to answer/i,
9
+ /ready for questions/i,
10
+ /^i am ready/i,
11
+ /^i'm ready/i,
12
+ /let'?s begin/i,
13
+ /let'?s start/i,
14
+ /^greetings/i,
15
+ ];
16
+ /** Detect if an answer is just a greeting with no substantive content */
17
+ export function isGreeting(answer) {
18
+ const trimmed = answer.trim();
19
+ // Short answers that match greeting patterns
20
+ if (trimmed.length > 200)
21
+ return false; // Long answers aren't greetings
22
+ return GREETING_PATTERNS.some(p => p.test(trimmed));
23
+ }
24
+ // ─── Stale / off-topic answer detection ─────────────────────────────────────
25
+ /** Topic keywords per compliance field — used to detect answers to the wrong question */
26
+ const TOPIC_SIGNALS = {
27
+ agentProfile: [
28
+ /\bproject.?name\b/i, /\bowner\b/i, /\btrigger/i, /\bwhat I (do|specifically)\b/i,
29
+ ],
30
+ systemId: [
31
+ /\b(→|->)\s*(REST|API|OAuth|SDK|Bot)\b/i, /\bconnect to\b/i, /\bsystems?\s+I\b/i,
32
+ ],
33
+ scopesRequested: [
34
+ /\boauth\s*scop/i, /\bgoogleapis\.com\/auth\//i, /\b(readonly|readwrite|\.edit|\.send|\.admin)\b/i,
35
+ ],
36
+ dataSensitivity: [
37
+ /\b(PII|financial|credentials|confidential|non.?sensitive)\b/i, /\bclassif(y|ied)\b/i,
38
+ ],
39
+ writeOperations: [
40
+ /\b(→|->)\s*(Yes|No)\s*(→|->)/i, /\b(append|insert|create|delete)\s*(row|record|spreadsheet|message)/i,
41
+ /\bvolume\/day\b/i,
42
+ ],
43
+ blastRadius: [
44
+ /\b(worst.?case|single.?record|single.?user|cross.?tenant|org.?wide)\b/i,
45
+ /\bcan it be (undone|recovered)\b/i,
46
+ ],
47
+ frequencyAndVolume: [
48
+ /\b(times?\s+per|runs?\s+per|calls?\s+per|\/week|\/day)\b/i, /\bbatch\s+size\b/i,
49
+ ],
50
+ scopesDelta: [
51
+ /\bnever\s+(actually\s+)?used\b/i, /\bsafely\s+(be\s+)?revoked\b/i, /\bunused\s+permission/i,
52
+ ],
53
+ riskAssessment: [
54
+ /\bworst\s+realistic\s+failure\b/i, /\bwho\s+is\s+affected\b/i, /\bhow\s+bad\s+is\s+the\s+damage\b/i,
55
+ /\bcan\s+it\s+be\s+recovered\b/i,
56
+ ],
57
+ };
58
+ /**
59
+ * Detect if an answer is clearly responding to a different question (stale session).
60
+ * Returns true if the answer strongly matches a DIFFERENT question's topic
61
+ * but has no relevance to the current question.
62
+ *
63
+ * Conservative: only triggers on long answers (300+ chars) that match 2+ signals
64
+ * from a different topic AND zero signals from the current topic. Skips Q1 entirely
65
+ * since first answers are too varied to classify reliably.
66
+ */
67
+ export function isStaleAnswer(question, answer) {
68
+ if (answer.length < 300)
69
+ return false; // Only check long, detailed answers
70
+ const currentField = question.complianceField;
71
+ if (!currentField)
72
+ return false;
73
+ // Never flag Q1 (agentProfile) — first answers are too varied
74
+ if (currentField === 'agentProfile')
75
+ return false;
76
+ const currentSignals = TOPIC_SIGNALS[currentField] ?? [];
77
+ const matchesCurrent = currentSignals.some(p => p.test(answer));
78
+ // If the answer matches the current question's topic at all, it's not stale
79
+ if (matchesCurrent)
80
+ return false;
81
+ // Check if it strongly matches a different question's topic (need 3+ signals)
82
+ let strongOtherMatch = false;
83
+ for (const [field, signals] of Object.entries(TOPIC_SIGNALS)) {
84
+ if (field === currentField)
85
+ continue;
86
+ const matches = signals.filter(p => p.test(answer)).length;
87
+ if (matches >= 3) {
88
+ strongOtherMatch = true;
89
+ break;
90
+ }
91
+ }
92
+ return strongOtherMatch;
93
+ }
94
+ // ─── Vagueness detection ─────────────────────────────────────────────────────
95
+ /** Vagueness indicators — if an answer matches these patterns, it needs a follow-up */
96
+ const VAGUE_PATTERNS = [
97
+ /\b(the database|a database|some database)\b/i,
98
+ /\b(read and write|read\/write|full access)\b/i,
99
+ /\b(user data|some data|the data)\b/i,
100
+ /\b(regularly|periodically|sometimes|occasionally|as needed|when needed)\b/i,
101
+ /\b(could affect|might affect|may affect)\b/i,
102
+ /\b(various|several|multiple|many|some)\s+(systems?|apis?|services?|databases?)/i,
103
+ /\b(everything|all access|full permissions?)\b/i,
104
+ /\bi[' ']?m not sure\b/i,
105
+ /\bnot sure\b/i,
106
+ /\bi don[' ']?t know\b/i,
107
+ // Hedging language — agent describes theoretical capabilities, not actual behavior
108
+ /\bi may (also )?(read|write|access|connect|use|modify|create|delete)\b/i,
109
+ /\bwhen enabled\b/i,
110
+ /\bif the task (requires|involves|needs)\b/i,
111
+ /\bwhen (available|needed|required|the workflow)\b/i,
112
+ /\b(can include|could include|may include)\b/i,
113
+ /\baccess is (environment|session|task).dependent\b/i,
114
+ /\bdepending on (the )?task\b/i,
115
+ /\bconnectors? (are |is )?enabled\b/i,
116
+ // Generic tool descriptions instead of specific project usage
117
+ /\b(local workspace|active workspace|working directory)\b/i,
118
+ /\bconnected (development )?tools\b/i,
119
+ ];
120
+ /** Detect if an answer is too vague for compliance-grade reporting */
121
+ export function isVagueAnswer(answer) {
122
+ return VAGUE_PATTERNS.some(p => p.test(answer));
123
+ }
124
+ // ─── Repeated answer detection ───────────────────────────────────────────────
125
+ /** Normalize answer text for comparison (trim, lowercase, collapse whitespace) */
126
+ function normalizeForComparison(text) {
127
+ return text.trim().toLowerCase().replace(/\s+/g, ' ');
128
+ }
129
+ /** Check if an answer is a repeat of a previous answer */
130
+ function isRepeatedAnswer(answer, transcript) {
131
+ if (transcript.length === 0)
132
+ return false;
133
+ const normalized = normalizeForComparison(answer);
134
+ // Check for exact or near-exact repeats
135
+ return transcript.some(qa => {
136
+ const prevNormalized = normalizeForComparison(qa.answer);
137
+ // Exact match or >90% overlap (handles minor variations)
138
+ return normalized === prevNormalized ||
139
+ (normalized.length > 50 && prevNormalized.includes(normalized.slice(0, normalized.length * 0.9 | 0)));
140
+ });
141
+ }
142
+ // ─── Missing compliance fields ───────────────────────────────────────────────
143
+ /** Check which compliance fields are missing from the transcript for a given category */
144
+ function findMissingFields(transcript) {
145
+ const allText = transcript.map(qa => qa.answer.toLowerCase()).join(' ');
146
+ const missing = [];
147
+ // Check for specific system identifiers
148
+ if (!/\b(api|oauth|sdk|via|using)\b/i.test(allText)) {
149
+ missing.push('systemId (specific API names, auth methods)');
150
+ }
151
+ // Check for specific scopes
152
+ if (!/\b(scope|permission|role|\.readonly|\.send|\.modify|\.admin)\b/i.test(allText)) {
153
+ missing.push('scopesRequested (specific OAuth scopes, API permissions)');
154
+ }
155
+ // Check for data sensitivity classification
156
+ if (!/\b(pii|sensitive|confidential|financial|personal|classified)\b/i.test(allText)) {
157
+ missing.push('dataSensitivity (PII, financial, confidential classification)');
158
+ }
159
+ // Check for blast radius
160
+ if (!/\b(single.?record|single.?user|team|org.?wide|cross.?tenant|mailbox|affected)\b/i.test(allText)) {
161
+ missing.push('blastRadius (scope of impact: single-record/user/team/org-wide)');
162
+ }
163
+ // Check for frequency numbers
164
+ if (!/\b(\d+\s*(times?|per|\/)\s*(day|hour|minute|week|session)|batch)\b/i.test(allText)) {
165
+ missing.push('frequencyAndVolume (specific numbers: times/day, batch size)');
166
+ }
167
+ // Check for write reversibility
168
+ if (!/\b(revers|rollback|undo|irrevers|cannot be undone|can be restored)\b/i.test(allText)) {
169
+ missing.push('writeOperations.reversible (whether writes can be rolled back)');
170
+ }
171
+ return missing;
172
+ }
173
+ // ─── Protocol factory ────────────────────────────────────────────────────────
174
+ export function createProtocol(llmClient, maxFollowUps = 6) {
175
+ const coreQuestions = getAllQuestionsSorted();
176
+ let currentIndex = 0;
177
+ const transcript = [];
178
+ let globalFollowUpCount = 0;
179
+ const followUpCountPerQuestion = new Map();
180
+ let repeatedAnswerCount = 0;
181
+ // Follow-up queue
182
+ const followUpQueue = [];
183
+ return {
184
+ totalCoreQuestions: coreQuestions.length,
185
+ nextQuestion() {
186
+ // Drain follow-up queue first
187
+ if (followUpQueue.length > 0) {
188
+ return followUpQueue.shift();
189
+ }
190
+ if (currentIndex >= coreQuestions.length) {
191
+ return null;
192
+ }
193
+ return coreQuestions[currentIndex++];
194
+ },
195
+ recordAnswer(question, answer) {
196
+ // Skip greetings — don't record them as answers
197
+ if (transcript.length === 0 && isGreeting(answer)) {
198
+ // Don't rewind currentIndex — SessionManager will re-ask pendingQuestion directly
199
+ return false;
200
+ }
201
+ // Detect stale answers from a lost/different session
202
+ if (isStaleAnswer(question, answer)) {
203
+ // Don't rewind — SessionManager will re-ask pendingQuestion directly
204
+ return false;
205
+ }
206
+ // Detect repeated/canned answers
207
+ if (isRepeatedAnswer(answer, transcript)) {
208
+ repeatedAnswerCount++;
209
+ // Still record it (for transparency in transcript) but mark the category
210
+ transcript.push({
211
+ question: question.text,
212
+ answer: `[REPEATED RESPONSE] ${answer}`,
213
+ category: question.category,
214
+ });
215
+ // After 3+ repeats, stop generating follow-ups — agent is stuck
216
+ return true;
217
+ }
218
+ transcript.push({
219
+ question: question.text,
220
+ answer,
221
+ category: question.category,
222
+ });
223
+ return true;
224
+ },
225
+ async generateFollowUp(category) {
226
+ // Global cap
227
+ if (globalFollowUpCount >= maxFollowUps)
228
+ return null;
229
+ // Per-question cap (2 follow-ups per core question)
230
+ const lastCoreQ = coreQuestions.find(q => q.category === category && transcript.some(t => t.question === q.text));
231
+ if (lastCoreQ) {
232
+ const count = followUpCountPerQuestion.get(lastCoreQ.id) ?? 0;
233
+ if (count >= 2)
234
+ return null;
235
+ }
236
+ // Don't follow up if agent is repeating canned responses
237
+ if (repeatedAnswerCount >= 3)
238
+ return null;
239
+ const categoryQA = transcript.filter(qa => qa.category === category);
240
+ if (categoryQA.length === 0)
241
+ return null;
242
+ // Check if the last answer was vague
243
+ const lastAnswer = categoryQA[categoryQA.length - 1].answer;
244
+ const vague = isVagueAnswer(lastAnswer);
245
+ // Find missing compliance fields across all transcript
246
+ const missingFields = findMissingFields(transcript);
247
+ // Only generate follow-up if answer was vague or compliance fields are missing
248
+ if (!vague && missingFields.length === 0)
249
+ return null;
250
+ try {
251
+ const followUpText = await llmClient.chat(INTERVIEW_SYSTEM_PROMPT, buildFollowUpPrompt(category, categoryQA, missingFields.length > 0 ? missingFields : undefined));
252
+ if (!followUpText.trim())
253
+ return null;
254
+ globalFollowUpCount++;
255
+ if (lastCoreQ) {
256
+ followUpCountPerQuestion.set(lastCoreQ.id, (followUpCountPerQuestion.get(lastCoreQ.id) ?? 0) + 1);
257
+ }
258
+ const followUp = {
259
+ id: `followup_${category}_${globalFollowUpCount}`,
260
+ category,
261
+ text: followUpText.trim(),
262
+ priority: 100 + globalFollowUpCount,
263
+ };
264
+ return followUp;
265
+ }
266
+ catch {
267
+ return null;
268
+ }
269
+ },
270
+ getTranscript() {
271
+ return [...transcript];
272
+ },
273
+ isComplete() {
274
+ return currentIndex >= coreQuestions.length && followUpQueue.length === 0;
275
+ },
276
+ };
277
+ }
278
+ /**
279
+ * Enqueue a follow-up question to be asked next.
280
+ * Used by SessionManager to avoid monkey-patching protocol.nextQuestion.
281
+ */
282
+ export function enqueueFollowUp(protocol, followUp) {
283
+ // The follow-up queue is internal to createProtocol, so we need another approach.
284
+ // Instead, we expose a method to push into the queue via the protocol's nextQuestion behavior.
285
+ // Actually, the clean way is to just have SessionManager track its own queue — see sessions.ts.
286
+ // This function exists for the scan/CLI path where the interviewer handles follow-ups inline.
287
+ void protocol;
288
+ void followUp;
289
+ }
290
+ //# sourceMappingURL=protocol.js.map