@stackwright-pro/otters 0.3.0-alpha.1 → 1.0.0-alpha.10
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 +16 -4
- package/scripts/generate-checksums.js +53 -0
- package/scripts/install-agents.js +16 -10
- package/scripts/verify-checksums.js +61 -0
- package/src/checksums.json +16 -0
- package/src/python-bridge.ts +391 -0
- package/src/question-adapter.ts +296 -0
- package/src/stackwright-pro-api-otter.json +132 -6
- package/src/stackwright-pro-auth-otter.json +132 -52
- package/src/stackwright-pro-dashboard-otter.json +350 -193
- package/src/stackwright-pro-data-otter.json +155 -296
- package/src/stackwright-pro-designer-otter.json +34 -0
- package/src/stackwright-pro-foreman-otter.json +477 -22
- package/src/stackwright-pro-page-otter.json +3 -1
- package/src/stackwright-pro-theme-otter.json +27 -0
- package/src/stackwright-pro-workflow-otter.json +25 -0
package/package.json
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackwright-pro/otters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
4
4
|
"description": "Stackwright Pro Otter Raft - AI agents for enterprise features (CAC auth, API dashboards, government use cases)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/Per-Aspera-LLC/stackwright-pro"
|
|
9
9
|
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"vitest": "^4.0.18",
|
|
12
|
+
"zod": "^3.22.4"
|
|
13
|
+
},
|
|
10
14
|
"exports": {
|
|
11
15
|
"./src": "./src",
|
|
12
|
-
"./pro-foreman": "./src/stackwright-pro-foreman-otter.json"
|
|
16
|
+
"./pro-foreman": "./src/stackwright-pro-foreman-otter.json",
|
|
17
|
+
"./pro-workflow": "./src/stackwright-pro-workflow-otter.json"
|
|
13
18
|
},
|
|
14
19
|
"files": [
|
|
15
20
|
"scripts",
|
|
16
21
|
"src"
|
|
17
22
|
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
18
26
|
"peerDependencies": {
|
|
19
|
-
"@stackwright-pro/mcp": "
|
|
27
|
+
"@stackwright-pro/mcp": "^0.2.0-alpha.1"
|
|
20
28
|
},
|
|
21
29
|
"scripts": {
|
|
30
|
+
"generate-checksums": "node scripts/generate-checksums.js",
|
|
31
|
+
"verify-checksums": "node scripts/verify-checksums.js",
|
|
22
32
|
"postinstall": "node scripts/install-agents.js",
|
|
23
|
-
"test": "
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:coverage": "vitest run --coverage"
|
|
24
36
|
}
|
|
25
37
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-checksums.js
|
|
4
|
+
* Computes SHA-256 for every *-otter.json in src/ and writes src/checksums.json
|
|
5
|
+
* Run: node scripts/generate-checksums.js
|
|
6
|
+
* Auto-run: npm prepare (before publish), npm pretest (before tests)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const scriptDir = __dirname;
|
|
16
|
+
const packageRoot = path.resolve(scriptDir, '..');
|
|
17
|
+
const srcDir = path.join(packageRoot, 'src');
|
|
18
|
+
const outputFile = path.join(srcDir, 'checksums.json');
|
|
19
|
+
|
|
20
|
+
async function generateChecksums() {
|
|
21
|
+
const entries = await fs.promises.readdir(srcDir);
|
|
22
|
+
|
|
23
|
+
const otterFiles = entries
|
|
24
|
+
.filter((f) => f.endsWith('-otter.json'))
|
|
25
|
+
.sort(); // alphabetical — deterministic output, no excuses
|
|
26
|
+
|
|
27
|
+
const files = {};
|
|
28
|
+
for (const filename of otterFiles) {
|
|
29
|
+
const filePath = path.join(srcDir, filename);
|
|
30
|
+
const raw = await fs.promises.readFile(filePath); // raw Buffer → utf-8 bytes, no funny business
|
|
31
|
+
const digest = crypto.createHash('sha256').update(raw).digest('hex');
|
|
32
|
+
files[filename] = digest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const manifest = {
|
|
36
|
+
version: '1.0',
|
|
37
|
+
algorithm: 'sha256',
|
|
38
|
+
generated: new Date().toISOString(),
|
|
39
|
+
files,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await fs.promises.writeFile(outputFile, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
43
|
+
|
|
44
|
+
console.log(`✅ checksums.json written with ${otterFiles.length} entr${otterFiles.length === 1 ? 'y' : 'ies'}:`);
|
|
45
|
+
for (const [name, hash] of Object.entries(files)) {
|
|
46
|
+
console.log(` ${name}: ${hash}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
generateChecksums().catch((err) => {
|
|
51
|
+
console.error('❌ generate-checksums failed:', err.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -12,12 +12,10 @@ const AGENTS_DIR = path.join(os.homedir(), '.code_puppy', 'agents');
|
|
|
12
12
|
|
|
13
13
|
// Resolve package root relative to this script's location
|
|
14
14
|
const scriptDir = __dirname;
|
|
15
|
-
const packageRoot =
|
|
16
|
-
? path.dirname(scriptDir) // Running from within the package
|
|
17
|
-
: path.join(scriptDir, 'packages', 'otters'); // Running from monorepo root
|
|
15
|
+
const packageRoot = path.resolve(scriptDir, '..');
|
|
18
16
|
|
|
19
17
|
// All Pro otters are in the src/ directory
|
|
20
|
-
const
|
|
18
|
+
const srcDir = path.join(packageRoot, 'src');
|
|
21
19
|
|
|
22
20
|
async function installAgents() {
|
|
23
21
|
try {
|
|
@@ -25,29 +23,37 @@ async function installAgents() {
|
|
|
25
23
|
await fs.promises.mkdir(AGENTS_DIR, { recursive: true });
|
|
26
24
|
|
|
27
25
|
// Copy all JSON files from src/ to ~/.code_puppy/agents/
|
|
28
|
-
const files = await fs.promises.readdir(
|
|
26
|
+
const files = await fs.promises.readdir(srcDir);
|
|
29
27
|
|
|
30
28
|
let installedCount = 0;
|
|
31
29
|
for (const file of files) {
|
|
32
30
|
if (file.endsWith('-otter.json')) {
|
|
33
|
-
const srcPath = path.join(
|
|
31
|
+
const srcPath = path.join(srcDir, file);
|
|
34
32
|
const destPath = path.join(AGENTS_DIR, file);
|
|
35
33
|
|
|
36
34
|
await fs.promises.copyFile(srcPath, destPath);
|
|
37
|
-
console.log(`✅ Installed
|
|
35
|
+
console.log(`✅ Installed: ${file}`);
|
|
38
36
|
installedCount++;
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
|
|
40
|
+
// Also install checksums manifest (informational — Python coordinator uses hardcoded constants)
|
|
41
|
+
const checksumsSource = path.join(packageRoot, 'src', 'checksums.json');
|
|
42
|
+
const checksumsDest = path.join(AGENTS_DIR, 'otter-checksums.REFERENCE-ONLY.json');
|
|
43
|
+
if (fs.existsSync(checksumsSource)) {
|
|
44
|
+
await fs.promises.copyFile(checksumsSource, checksumsDest);
|
|
45
|
+
console.log('✅ Installed: otter-checksums.REFERENCE-ONLY.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
if (installedCount > 0) {
|
|
49
|
+
console.log(`Installing otters from: ${srcDir}`);
|
|
43
50
|
console.log(`\n🦦🦦 Pro otters installed to ${AGENTS_DIR}`);
|
|
44
|
-
console.log(' Run "code-puppy -i -a stackwright-pro-foreman-otter" to start!');
|
|
45
51
|
} else {
|
|
46
52
|
console.log('⚠️ No Pro otter files found to install');
|
|
47
53
|
}
|
|
48
54
|
} catch (error) {
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
console.error(`❌ FATAL: Failed to install Pro otters: ${error.message}`);
|
|
56
|
+
process.exit(1); // Fail the npm install — surface it early
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-checksums.js
|
|
4
|
+
* Read-only: verifies that *-otter.json files match the committed checksums.json
|
|
5
|
+
* Does NOT regenerate. For regeneration: node scripts/generate-checksums.js
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = all checksums match
|
|
9
|
+
* 1 = one or more mismatches detected (tamper detected or file missing)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
19
|
+
const srcDir = path.join(packageRoot, 'src');
|
|
20
|
+
const checksumsPath = path.join(srcDir, 'checksums.json');
|
|
21
|
+
|
|
22
|
+
// Read checksums manifest
|
|
23
|
+
let manifest;
|
|
24
|
+
try {
|
|
25
|
+
manifest = JSON.parse(fs.readFileSync(checksumsPath, 'utf8'));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`❌ Cannot read checksums.json: ${err.message}`);
|
|
28
|
+
console.error('Run: node scripts/generate-checksums.js');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const expected = manifest.files || {};
|
|
33
|
+
let failures = 0;
|
|
34
|
+
let verified = 0;
|
|
35
|
+
|
|
36
|
+
for (const [filename, expectedHash] of Object.entries(expected)) {
|
|
37
|
+
const filePath = path.join(srcDir, filename);
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(filePath);
|
|
40
|
+
const actual = crypto.createHash('sha256').update(raw).digest('hex');
|
|
41
|
+
if (actual !== expectedHash) {
|
|
42
|
+
console.error(`❌ MISMATCH: ${filename}`);
|
|
43
|
+
console.error(` Expected: ${expectedHash.substring(0, 16)}...`);
|
|
44
|
+
console.error(` Actual: ${actual.substring(0, 16)}...`);
|
|
45
|
+
failures++;
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`✅ ${filename}`);
|
|
48
|
+
verified++;
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`❌ MISSING: ${filename} — ${err.message}`);
|
|
52
|
+
failures++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (failures > 0) {
|
|
57
|
+
console.error(`\n🚨 ${failures} checksum failure(s) detected. Run \`npm install @stackwright-pro/otters\` to restore.`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`\n✅ All ${verified} otter checksums verified.`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"algorithm": "sha256",
|
|
4
|
+
"generated": "2026-04-21T00:28:46.579Z",
|
|
5
|
+
"files": {
|
|
6
|
+
"stackwright-pro-api-otter.json": "ad0c3694af41000420229edce4108f860eaa58ab321f8618565d03ebce80bcac",
|
|
7
|
+
"stackwright-pro-auth-otter.json": "e8e02ef1389e0d5e55bfa6d960a050ab976bf7960fda4ae805675020874ce4c6",
|
|
8
|
+
"stackwright-pro-dashboard-otter.json": "0b4100afef4946bae259f5759aea872d7b1a25a00af191e1ead32bf9ee304d08",
|
|
9
|
+
"stackwright-pro-data-otter.json": "38ae3a26f064499a5f9773dfea1e2c21f9f358207110224a8e94c19443d236f1",
|
|
10
|
+
"stackwright-pro-designer-otter.json": "46c9fd94a46f1a3f5267f4cb70c3db0adfc28dc7d4ac50256cbe40ea5363b4f0",
|
|
11
|
+
"stackwright-pro-foreman-otter.json": "2e4a13443a8c6bf55d02ab6c0301fa840f63f1df6baaebf14fab06a72d6cc8ca",
|
|
12
|
+
"stackwright-pro-page-otter.json": "0973f1b75a481fd177c5ada1a965f8c32e07f97fc28bbbf03b51d9e6d2af2f74",
|
|
13
|
+
"stackwright-pro-theme-otter.json": "faa100f0530af75a64ae6e9d0ac8adb370542e5d980468e2d129223cb4aa85d7",
|
|
14
|
+
"stackwright-pro-workflow-otter.json": "814305e9d170d28b7215ca63730b9fbeb7b9605113d2070cd5925cf92a9e30d3"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python Bridge - Spawns Python server and communicates via JSON over stdio
|
|
3
|
+
*
|
|
4
|
+
* This module provides the interface between Node.js CLI and the Python
|
|
5
|
+
* Clarification Protocol server.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Node.js CLI spawns Python server (unix socket + HTTP)
|
|
9
|
+
* - Communication via JSON over stdio or HTTP
|
|
10
|
+
* - Supports both blocking (TUI) and non-blocking (API) modes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, ChildProcess, execSync } from 'child_process';
|
|
14
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
|
|
19
|
+
// Types
|
|
20
|
+
export interface ClarificationRequest {
|
|
21
|
+
context: string;
|
|
22
|
+
question_type: 'closed_choice' | 'open_text' | 'conditional' | 'multi_step' | 'reconciliation';
|
|
23
|
+
question: string;
|
|
24
|
+
choices?: string[];
|
|
25
|
+
priority?: 'blocking' | 'preferred' | 'optional';
|
|
26
|
+
target_field?: string;
|
|
27
|
+
partial_result?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ClarificationResponse {
|
|
31
|
+
request_id: string;
|
|
32
|
+
decision: {
|
|
33
|
+
value: unknown;
|
|
34
|
+
explicit: boolean;
|
|
35
|
+
source: string;
|
|
36
|
+
};
|
|
37
|
+
fallback_used: boolean;
|
|
38
|
+
fallback_reason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ConflictCheck {
|
|
42
|
+
stated_preference: string;
|
|
43
|
+
selected_values: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConflictResult {
|
|
47
|
+
conflict: boolean;
|
|
48
|
+
data?: {
|
|
49
|
+
description: string;
|
|
50
|
+
stated: string;
|
|
51
|
+
options: string[];
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SessionInfo {
|
|
56
|
+
id: string;
|
|
57
|
+
phase: string;
|
|
58
|
+
context: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Constants
|
|
62
|
+
const DEFAULT_SOCKET_PATH = join(tmpdir(), `otter-raft-${randomUUID()}.sock`);
|
|
63
|
+
const DEFAULT_HTTP_PORT = 8765;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* PythonBridge - Communicates with Python Clarification Protocol server
|
|
67
|
+
*/
|
|
68
|
+
export class PythonBridge {
|
|
69
|
+
private socketPath: string;
|
|
70
|
+
private httpPort: number;
|
|
71
|
+
private pythonProcess: ChildProcess | null = null;
|
|
72
|
+
private useHttp: boolean;
|
|
73
|
+
private baseUrl: string;
|
|
74
|
+
|
|
75
|
+
constructor(options?: { socketPath?: string; httpPort?: number }) {
|
|
76
|
+
this.socketPath = options?.socketPath || DEFAULT_SOCKET_PATH;
|
|
77
|
+
this.httpPort = options?.httpPort || DEFAULT_HTTP_PORT;
|
|
78
|
+
this.useHttp = true; // Prefer HTTP for reliability
|
|
79
|
+
this.baseUrl = `http://127.0.0.1:${this.httpPort}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start the Python server
|
|
84
|
+
*/
|
|
85
|
+
async start(): Promise<void> {
|
|
86
|
+
// Find Python executable
|
|
87
|
+
const pythonPath = this.findPython();
|
|
88
|
+
|
|
89
|
+
// Find the Python package
|
|
90
|
+
const packageRoot = this.findPackageRoot();
|
|
91
|
+
const serverModule = 'stackwright_pro.raft.server';
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
// Clean up old socket
|
|
95
|
+
if (existsSync(this.socketPath)) {
|
|
96
|
+
try {
|
|
97
|
+
unlinkSync(this.socketPath);
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.pythonProcess = spawn(
|
|
104
|
+
pythonPath,
|
|
105
|
+
['-m', serverModule, '--socket', this.socketPath, '--port', String(this.httpPort)],
|
|
106
|
+
{
|
|
107
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
PYTHONPATH: packageRoot,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
let startupOutput = '';
|
|
116
|
+
|
|
117
|
+
this.pythonProcess.stdout?.on('data', (data: Buffer) => {
|
|
118
|
+
startupOutput += data.toString();
|
|
119
|
+
// Look for "Server ready" message
|
|
120
|
+
if (startupOutput.includes('Server ready') || startupOutput.includes('Starting')) {
|
|
121
|
+
// Give it a moment to fully start
|
|
122
|
+
setTimeout(() => resolve(), 500);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.pythonProcess.stderr?.on('data', (data: Buffer) => {
|
|
127
|
+
console.error('[Python]', data.toString().trim());
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.pythonProcess.on('error', (err) => {
|
|
131
|
+
reject(new Error(`Failed to start Python server: ${err.message}`));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.pythonProcess.on('exit', (code) => {
|
|
135
|
+
if (code !== 0 && code !== null) {
|
|
136
|
+
reject(new Error(`Python server exited with code ${code}`));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Timeout after 10 seconds
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
reject(new Error('Python server startup timeout'));
|
|
143
|
+
}, 10000);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stop the Python server
|
|
149
|
+
*/
|
|
150
|
+
async stop(): Promise<void> {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
if (this.pythonProcess) {
|
|
153
|
+
this.pythonProcess.on('exit', () => resolve());
|
|
154
|
+
this.pythonProcess.kill('SIGTERM');
|
|
155
|
+
|
|
156
|
+
// Force kill after 5 seconds
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
if (this.pythonProcess) {
|
|
159
|
+
this.pythonProcess.kill('SIGKILL');
|
|
160
|
+
}
|
|
161
|
+
resolve();
|
|
162
|
+
}, 5000);
|
|
163
|
+
} else {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Health check
|
|
171
|
+
*/
|
|
172
|
+
async health(): Promise<{ status: string; version: string }> {
|
|
173
|
+
return this.httpRequest('/health');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a new clarification session
|
|
178
|
+
*/
|
|
179
|
+
async createSession(): Promise<{ session_id: string }> {
|
|
180
|
+
return this.httpRequest('/sessions', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get session info
|
|
187
|
+
*/
|
|
188
|
+
async getSession(sessionId: string): Promise<SessionInfo> {
|
|
189
|
+
return this.httpRequest(`/sessions/${sessionId}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Set session phase
|
|
194
|
+
*/
|
|
195
|
+
async setPhase(sessionId: string, phase: string): Promise<{ phase: string }> {
|
|
196
|
+
return this.httpRequest(`/sessions/${sessionId}/phase`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: { phase },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update session context
|
|
204
|
+
*/
|
|
205
|
+
async updateContext(
|
|
206
|
+
sessionId: string,
|
|
207
|
+
context: Record<string, unknown>
|
|
208
|
+
): Promise<{ context: Record<string, unknown> }> {
|
|
209
|
+
return this.httpRequest(`/sessions/${sessionId}/context`, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
body: { context },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Ask for clarification within a session
|
|
217
|
+
*/
|
|
218
|
+
async sessionClarify(
|
|
219
|
+
sessionId: string,
|
|
220
|
+
request: ClarificationRequest
|
|
221
|
+
): Promise<ClarificationResponse> {
|
|
222
|
+
return this.httpRequest(`/sessions/${sessionId}/clarify`, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: { request },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Quick clarify (no session)
|
|
230
|
+
*/
|
|
231
|
+
async clarify(request: ClarificationRequest): Promise<ClarificationResponse> {
|
|
232
|
+
return this.httpRequest('/clarify', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
body: { request },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Detect preference conflict
|
|
240
|
+
*/
|
|
241
|
+
async detectConflict(check: ConflictCheck): Promise<ConflictResult> {
|
|
242
|
+
return this.httpRequest('/conflict', {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
body: check,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --------------------------------------------------------------------------
|
|
249
|
+
// Private methods
|
|
250
|
+
// --------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
private findPython(): string {
|
|
253
|
+
// Try python3 first, fall back to python
|
|
254
|
+
try {
|
|
255
|
+
execSync('python3 --version', { stdio: 'pipe' });
|
|
256
|
+
return 'python3';
|
|
257
|
+
} catch {
|
|
258
|
+
return 'python';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private findPackageRoot(): string {
|
|
263
|
+
// Look for the Python package in common locations
|
|
264
|
+
const candidates = [
|
|
265
|
+
join(__dirname, '../../python/src'),
|
|
266
|
+
join(__dirname, '../../../python/src'),
|
|
267
|
+
join(process.cwd(), 'python/src'),
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
for (const candidate of candidates) {
|
|
271
|
+
if (existsSync(candidate)) {
|
|
272
|
+
return candidate;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default to current directory
|
|
277
|
+
return process.cwd();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async httpRequest<T>(
|
|
281
|
+
path: string,
|
|
282
|
+
options?: {
|
|
283
|
+
method?: string;
|
|
284
|
+
body?: unknown;
|
|
285
|
+
}
|
|
286
|
+
): Promise<T> {
|
|
287
|
+
const http = await import('http');
|
|
288
|
+
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const url = new URL(path, this.baseUrl);
|
|
291
|
+
|
|
292
|
+
const reqOptions = {
|
|
293
|
+
hostname: url.hostname,
|
|
294
|
+
port: url.port,
|
|
295
|
+
path: url.pathname,
|
|
296
|
+
method: options?.method || 'GET',
|
|
297
|
+
headers: {
|
|
298
|
+
'Content-Type': 'application/json',
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const req = http.request(reqOptions, (res) => {
|
|
303
|
+
let data = '';
|
|
304
|
+
|
|
305
|
+
res.on('data', (chunk: Buffer) => {
|
|
306
|
+
data += chunk.toString();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
res.on('end', () => {
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(data);
|
|
312
|
+
|
|
313
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
314
|
+
reject(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
|
|
315
|
+
} else {
|
|
316
|
+
resolve(parsed);
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
reject(new Error(`Failed to parse response: ${data}`));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
req.on('error', (e) => {
|
|
325
|
+
reject(new Error(`HTTP request failed: ${e.message}`));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (options?.body) {
|
|
329
|
+
req.write(JSON.stringify(options.body));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
req.end();
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create a bridge instance and auto-start it
|
|
339
|
+
*/
|
|
340
|
+
export async function createBridge(options?: {
|
|
341
|
+
socketPath?: string;
|
|
342
|
+
httpPort?: number;
|
|
343
|
+
}): Promise<PythonBridge> {
|
|
344
|
+
const bridge = new PythonBridge(options);
|
|
345
|
+
await bridge.start();
|
|
346
|
+
return bridge;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Run a single clarification in stdio mode (no server)
|
|
351
|
+
*
|
|
352
|
+
* Usage:
|
|
353
|
+
* echo '{"type":"clarify","request":{...}}' | python -m stackwright_pro.raft.server --stdio
|
|
354
|
+
*/
|
|
355
|
+
export async function runStdioClarify(
|
|
356
|
+
request: ClarificationRequest
|
|
357
|
+
): Promise<ClarificationResponse> {
|
|
358
|
+
const http = await import('http');
|
|
359
|
+
|
|
360
|
+
// For stdio mode, we communicate directly via stdin/stdout
|
|
361
|
+
// This is handled by the Python server in --stdio mode
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const input = JSON.stringify({
|
|
364
|
+
type: 'clarify',
|
|
365
|
+
request,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const proc = spawn('python', ['-m', 'stackwright_pro.raft.server', '--stdio'], {
|
|
369
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
let output = '';
|
|
373
|
+
|
|
374
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
375
|
+
output += data.toString();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
proc.on('close', () => {
|
|
379
|
+
try {
|
|
380
|
+
resolve(JSON.parse(output));
|
|
381
|
+
} catch {
|
|
382
|
+
reject(new Error(`Failed to parse response: ${output}`));
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
proc.on('error', reject);
|
|
387
|
+
|
|
388
|
+
proc.stdin?.write(input);
|
|
389
|
+
proc.stdin?.end();
|
|
390
|
+
});
|
|
391
|
+
}
|