create-byan-agent 2.9.5 → 2.9.7
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/CHANGELOG.md +11 -0
- package/install/bin/create-byan-agent-v2.js +21 -2
- package/install/lib/claude-native-setup.js +9 -0
- package/install/lib/staging-consent.js +149 -0
- package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
- package/install/templates/.claude/settings.json +4 -0
- package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
- package/install/templates/.github/extensions/byan-staging/package.json +8 -0
- package/package.json +1 -1
- package/src/staging/staging.js +394 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.9.7] - 2026-04-21
|
|
11
|
+
|
|
12
|
+
### Fixed - MCP server empty-directory install bug
|
|
13
|
+
|
|
14
|
+
- **`copyMcpServer` now asserts `server.js` exists after copy** — previously, a partial copy could leave `_byan/mcp/byan-mcp-server/` empty, causing Claude Code to fail with `Cannot find module '.../server.js'` on the next launch. The post-copy check now throws a clear error instead of silently succeeding.
|
|
15
|
+
- **`create-byan-agent-v2.js` surfaces Claude native-setup failures in red** — prior behavior showed a yellow "partial" warning that users missed; now the failure is explicit and points at the MCP directory to inspect.
|
|
16
|
+
- **Regression test** added in `claude-native-setup.test.js` that mocks `fs.copy` to a no-op and verifies the post-copy assertion throws.
|
|
17
|
+
- **Observed on**: byan_web install with 2.9.6, dossier `_byan/mcp/byan-mcp-server/` vide, MCP failed in Claude Code.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
10
21
|
## [2.7.0] - 2026-02-21
|
|
11
22
|
|
|
12
23
|
### Added - Soul System + Tao System
|
|
@@ -16,6 +16,7 @@ const { generateProjectAgentsDoc } = require('../lib/project-agents-generator');
|
|
|
16
16
|
const { launchPhase2Chat, generateDefaultConfig } = require('../lib/phase2-chat');
|
|
17
17
|
const { setupByanWebIntegration } = require('../lib/byan-web-integration');
|
|
18
18
|
const { setupClaudeNative } = require('../lib/claude-native-setup');
|
|
19
|
+
const { setupStagingConsent } = require('../lib/staging-consent');
|
|
19
20
|
|
|
20
21
|
const BYAN_VERSION = require('../package.json').version;
|
|
21
22
|
|
|
@@ -1348,18 +1349,36 @@ async function install() {
|
|
|
1348
1349
|
try {
|
|
1349
1350
|
await setupClaudeNative(projectRoot);
|
|
1350
1351
|
} catch (error) {
|
|
1351
|
-
console.log(chalk.
|
|
1352
|
+
console.log(chalk.red(` ✘ Claude native setup failed: ${error.message}`));
|
|
1353
|
+
console.log(
|
|
1354
|
+
chalk.yellow(
|
|
1355
|
+
` → MCP, hooks or skills may be incomplete. Inspect _byan/mcp/byan-mcp-server/ and re-run if empty.`
|
|
1356
|
+
)
|
|
1357
|
+
);
|
|
1352
1358
|
}
|
|
1353
1359
|
}
|
|
1354
1360
|
|
|
1355
1361
|
if (needsClaude || needsCopilot) {
|
|
1356
1362
|
console.log();
|
|
1357
1363
|
console.log(chalk.cyan('byan_web integration (optional — service payant)'));
|
|
1364
|
+
let byanWebResult = { configured: false };
|
|
1358
1365
|
try {
|
|
1359
|
-
await setupByanWebIntegration(projectRoot);
|
|
1366
|
+
byanWebResult = await setupByanWebIntegration(projectRoot);
|
|
1360
1367
|
} catch (error) {
|
|
1361
1368
|
console.log(chalk.yellow(` ⚠ byan_web setup skipped: ${error.message}`));
|
|
1362
1369
|
}
|
|
1370
|
+
|
|
1371
|
+
if (byanWebResult && byanWebResult.configured) {
|
|
1372
|
+
console.log();
|
|
1373
|
+
console.log(chalk.cyan('byan_web memory-sync opt-in (consent)'));
|
|
1374
|
+
try {
|
|
1375
|
+
await setupStagingConsent(projectRoot, {
|
|
1376
|
+
byanWebConfigured: true,
|
|
1377
|
+
});
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
console.log(chalk.yellow(` ⚠ memory-sync setup skipped: ${error.message}`));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1363
1382
|
}
|
|
1364
1383
|
|
|
1365
1384
|
// Step 8: Create config.yaml
|
|
@@ -56,6 +56,15 @@ async function copyMcpServer(projectRoot) {
|
|
|
56
56
|
if (!(await fs.pathExists(src))) return { copied: false };
|
|
57
57
|
await fs.ensureDir(dst);
|
|
58
58
|
await fs.copy(src, dst, { overwrite: true, filter: (s) => !s.includes('node_modules') });
|
|
59
|
+
// Post-copy sanity check: server.js must exist, otherwise the MCP client will
|
|
60
|
+
// fail on next Claude Code launch with "Cannot find module" (seen on 2.9.6).
|
|
61
|
+
const serverFile = path.join(dst, 'server.js');
|
|
62
|
+
if (!(await fs.pathExists(serverFile))) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`MCP server copy produced no server.js at ${serverFile}. ` +
|
|
65
|
+
`Template source: ${src}. Re-run install or copy manually.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
59
68
|
return { copied: true, path: dst };
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer step — BYAN memory-sync opt-in + consent (SM5).
|
|
3
|
+
*
|
|
4
|
+
* Shown during create-byan-agent. Only prompts when the user already
|
|
5
|
+
* provided a byan_web URL + token (via setupByanWebIntegration), since
|
|
6
|
+
* memory-sync without credentials is a no-op.
|
|
7
|
+
*
|
|
8
|
+
* On opt-in, writes :
|
|
9
|
+
* _byan/config.yaml → memory_sync: { enabled: true }
|
|
10
|
+
* OR loadbalancer.yaml if _byan/config.yaml not present
|
|
11
|
+
*
|
|
12
|
+
* Prints a clear consent notice listing what gets sent and how to
|
|
13
|
+
* disable. The user must type "oui" / "yes" to enable — no default to
|
|
14
|
+
* true.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs-extra');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const yaml = require('js-yaml');
|
|
20
|
+
const inquirer = require('inquirer');
|
|
21
|
+
const chalk = require('chalk');
|
|
22
|
+
|
|
23
|
+
const CONSENT_NOTICE = [
|
|
24
|
+
'',
|
|
25
|
+
chalk.yellow.bold('BYAN memory-sync — consent requis'),
|
|
26
|
+
'',
|
|
27
|
+
'Si vous activez cette option, apres chaque interaction avec',
|
|
28
|
+
'Claude Code ou Copilot CLI, BYAN envoie automatiquement a votre',
|
|
29
|
+
'instance byan_web les elements suivants :',
|
|
30
|
+
'',
|
|
31
|
+
' - messages utilisateur (prompts)',
|
|
32
|
+
' - reponses assistant',
|
|
33
|
+
' - chemins de fichiers modifies',
|
|
34
|
+
' - sessionId et timestamp',
|
|
35
|
+
'',
|
|
36
|
+
'Filtrage applique AVANT envoi :',
|
|
37
|
+
' - chit-chat (moins de 50 caracteres) -> ignore',
|
|
38
|
+
' - doublons (hash SHA256 du contenu) -> ignore',
|
|
39
|
+
' - categories : fact | decision | blocker | artifact',
|
|
40
|
+
'',
|
|
41
|
+
'Les donnees sont stockees dans VOTRE instance byan_web',
|
|
42
|
+
'(pas de tierce partie). Le token JWT identifie l auteur.',
|
|
43
|
+
'',
|
|
44
|
+
chalk.cyan('Desactiver plus tard :'),
|
|
45
|
+
' - editer _byan/config.yaml -> memory_sync: { enabled: false }',
|
|
46
|
+
' - OU invoquer le skill -> /byan-no-stage pour un turn',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n');
|
|
49
|
+
|
|
50
|
+
async function promptConsent({ skipPrompts, defaultAnswer } = {}) {
|
|
51
|
+
if (skipPrompts) return { enabled: defaultAnswer === true };
|
|
52
|
+
|
|
53
|
+
console.log(CONSENT_NOTICE);
|
|
54
|
+
|
|
55
|
+
const { enable } = await inquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'confirm',
|
|
58
|
+
name: 'enable',
|
|
59
|
+
message:
|
|
60
|
+
'Activer la synchronisation automatique de vos conversations vers byan_web ?',
|
|
61
|
+
default: false,
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
return { enabled: enable };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function configPaths(projectRoot) {
|
|
69
|
+
return {
|
|
70
|
+
byanConfig: path.join(projectRoot, '_byan', 'config.yaml'),
|
|
71
|
+
lbConfig: path.join(projectRoot, 'loadbalancer.yaml'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeMemorySyncFlag(projectRoot, enabled) {
|
|
76
|
+
const { byanConfig, lbConfig } = configPaths(projectRoot);
|
|
77
|
+
|
|
78
|
+
// Prefer _byan/config.yaml (BYAN primary config). Fall back to
|
|
79
|
+
// loadbalancer.yaml only if _byan/config.yaml is missing AND
|
|
80
|
+
// loadbalancer.yaml already exists.
|
|
81
|
+
let target;
|
|
82
|
+
if (await fs.pathExists(byanConfig)) {
|
|
83
|
+
target = byanConfig;
|
|
84
|
+
} else if (await fs.pathExists(lbConfig)) {
|
|
85
|
+
target = lbConfig;
|
|
86
|
+
} else {
|
|
87
|
+
target = byanConfig;
|
|
88
|
+
}
|
|
89
|
+
await fs.ensureDir(path.dirname(target));
|
|
90
|
+
|
|
91
|
+
let doc = {};
|
|
92
|
+
if (await fs.pathExists(target)) {
|
|
93
|
+
try {
|
|
94
|
+
doc = yaml.load(await fs.readFile(target, 'utf8')) || {};
|
|
95
|
+
} catch {
|
|
96
|
+
doc = {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
doc.memory_sync = { ...(doc.memory_sync || {}), enabled: enabled === true };
|
|
100
|
+
|
|
101
|
+
await fs.writeFile(target, yaml.dump(doc), 'utf8');
|
|
102
|
+
return target;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function setupStagingConsent(projectRoot, options = {}) {
|
|
106
|
+
const tokenPresent = options.byanWebConfigured === true;
|
|
107
|
+
if (!tokenPresent) {
|
|
108
|
+
if (!options.quiet) {
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.gray(' i memory-sync skipped (no byan_web token configured)')
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return { configured: false, reason: 'no_token' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { enabled } = await promptConsent({
|
|
117
|
+
skipPrompts: options.skipPrompts === true,
|
|
118
|
+
defaultAnswer: options.presetEnabled === true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const target = await writeMemorySyncFlag(projectRoot, enabled);
|
|
122
|
+
|
|
123
|
+
if (!options.quiet) {
|
|
124
|
+
if (enabled) {
|
|
125
|
+
console.log(chalk.green(' OK memory-sync ENABLED in ' + path.relative(projectRoot, target)));
|
|
126
|
+
console.log(
|
|
127
|
+
chalk.gray(
|
|
128
|
+
' a chaque fin de turn, votre hook Stop (Claude) et votre'
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.gray(
|
|
133
|
+
' extension Copilot staging enverront les memoires a byan_web.'
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
console.log(chalk.gray(' i memory-sync left DISABLED (opt-in declined)'));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { configured: true, enabled, configPath: target };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
setupStagingConsent,
|
|
146
|
+
writeMemorySyncFlag,
|
|
147
|
+
promptConsent,
|
|
148
|
+
CONSENT_NOTICE,
|
|
149
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook — stage the ending turn to byan_web memory (SM1b).
|
|
4
|
+
*
|
|
5
|
+
* Reads the Stop payload on stdin, extracts user + assistant messages,
|
|
6
|
+
* delegates to src/staging/staging.js processTurn().
|
|
7
|
+
*
|
|
8
|
+
* Config source (first present wins) :
|
|
9
|
+
* - .claude/settings.local.json env.BYAN_API_URL / BYAN_API_TOKEN
|
|
10
|
+
* - process.env.BYAN_API_URL / BYAN_API_TOKEN
|
|
11
|
+
* - loadbalancer.yaml or _byan/config.yaml memory_sync: section
|
|
12
|
+
*
|
|
13
|
+
* Never blocks : this hook always exits 0 with continue:true. Failures
|
|
14
|
+
* are queued locally for retry, not surfaced to the user mid-turn.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
21
|
+
|
|
22
|
+
function readStdin() {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
if (process.stdin.isTTY) return resolve('');
|
|
25
|
+
let data = '';
|
|
26
|
+
process.stdin.on('data', (c) => (data += c));
|
|
27
|
+
process.stdin.on('end', () => resolve(data));
|
|
28
|
+
process.stdin.on('error', () => resolve(data));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readSettingsEnv() {
|
|
33
|
+
const p = path.join(projectDir, '.claude', 'settings.local.json');
|
|
34
|
+
if (!fs.existsSync(p)) return {};
|
|
35
|
+
try {
|
|
36
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
37
|
+
return j.env || {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readMemorySyncConfig() {
|
|
44
|
+
const paths = [
|
|
45
|
+
path.join(projectDir, 'loadbalancer.yaml'),
|
|
46
|
+
path.join(projectDir, '_byan', 'config.yaml'),
|
|
47
|
+
];
|
|
48
|
+
for (const p of paths) {
|
|
49
|
+
if (!fs.existsSync(p)) continue;
|
|
50
|
+
try {
|
|
51
|
+
const yaml = require('js-yaml');
|
|
52
|
+
const doc = yaml.load(fs.readFileSync(p, 'utf8'));
|
|
53
|
+
if (doc && doc.memory_sync) return doc.memory_sync;
|
|
54
|
+
} catch {
|
|
55
|
+
// fall through
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildConfig() {
|
|
62
|
+
const settingsEnv = readSettingsEnv();
|
|
63
|
+
const apiUrl = settingsEnv.BYAN_API_URL || process.env.BYAN_API_URL || null;
|
|
64
|
+
const apiToken = settingsEnv.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN || null;
|
|
65
|
+
const memorySync = readMemorySyncConfig() || {};
|
|
66
|
+
return {
|
|
67
|
+
byan_api_url: apiUrl,
|
|
68
|
+
byan_api_token: apiToken,
|
|
69
|
+
memory_sync: memorySync,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractTurn(payload) {
|
|
74
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
75
|
+
|
|
76
|
+
const transcript = payload.transcript || payload.messages;
|
|
77
|
+
if (!Array.isArray(transcript)) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sessionId: payload.session_id || payload.sessionId || null,
|
|
81
|
+
messages: transcript
|
|
82
|
+
.filter((m) => m && (m.role === 'user' || m.role === 'assistant'))
|
|
83
|
+
.slice(-4),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
(async () => {
|
|
88
|
+
const raw = await readStdin();
|
|
89
|
+
let payload = {};
|
|
90
|
+
try {
|
|
91
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
92
|
+
} catch {
|
|
93
|
+
payload = {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const config = buildConfig();
|
|
97
|
+
const turn = extractTurn(payload);
|
|
98
|
+
|
|
99
|
+
if (!turn) {
|
|
100
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const { processTurn } = require(path.join(projectDir, 'src', 'staging', 'staging.js'));
|
|
106
|
+
await processTurn({
|
|
107
|
+
turn,
|
|
108
|
+
cliSource: 'claude-code',
|
|
109
|
+
config,
|
|
110
|
+
projectRoot: projectDir,
|
|
111
|
+
flushNow: true,
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
// staging must never break the session
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
118
|
+
process.exit(0);
|
|
119
|
+
})();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copilot CLI extension — BYAN staging (SM1c).
|
|
3
|
+
*
|
|
4
|
+
* Attaches to the current Copilot session via joinSession() and
|
|
5
|
+
* triggers the BYAN staging pipeline at the end of each assistant
|
|
6
|
+
* turn. Delegates to src/staging/staging.js so Claude Code and Copilot
|
|
7
|
+
* CLI share the exact same extract / filter / dedup / queue / flush
|
|
8
|
+
* logic.
|
|
9
|
+
*
|
|
10
|
+
* How it's discovered : Copilot CLI scans .github/extensions/
|
|
11
|
+
* (project) and the user's copilot config for subdirectories
|
|
12
|
+
* containing extension.mjs. This file is auto-launched as a child
|
|
13
|
+
* process, gets @github/copilot-sdk on its module path, and calls
|
|
14
|
+
* joinSession with the hook registration below.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { joinSession } from '@github/copilot-sdk/extension';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
const EXTENSION_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
25
|
+
const PROJECT_ROOT =
|
|
26
|
+
process.env.BYAN_PROJECT_ROOT ||
|
|
27
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
28
|
+
findProjectRoot(EXTENSION_DIR);
|
|
29
|
+
|
|
30
|
+
function findProjectRoot(startDir) {
|
|
31
|
+
// Walk up until we find a package.json or .git — else cwd
|
|
32
|
+
let dir = startDir;
|
|
33
|
+
for (let i = 0; i < 6; i++) {
|
|
34
|
+
if (
|
|
35
|
+
fs.existsSync(path.join(dir, 'package.json')) ||
|
|
36
|
+
fs.existsSync(path.join(dir, '.git'))
|
|
37
|
+
) {
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir) break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
return process.cwd();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadStaging() {
|
|
48
|
+
try {
|
|
49
|
+
return require(path.join(PROJECT_ROOT, 'src', 'staging', 'staging.js'));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readSettingsEnv() {
|
|
56
|
+
const p = path.join(PROJECT_ROOT, '.claude', 'settings.local.json');
|
|
57
|
+
if (!fs.existsSync(p)) return {};
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
60
|
+
return j.env || {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readMemorySyncConfig() {
|
|
67
|
+
const paths = [
|
|
68
|
+
path.join(PROJECT_ROOT, 'loadbalancer.yaml'),
|
|
69
|
+
path.join(PROJECT_ROOT, '_byan', 'config.yaml'),
|
|
70
|
+
];
|
|
71
|
+
for (const p of paths) {
|
|
72
|
+
if (!fs.existsSync(p)) continue;
|
|
73
|
+
try {
|
|
74
|
+
const yaml = require('js-yaml');
|
|
75
|
+
const doc = yaml.load(fs.readFileSync(p, 'utf8'));
|
|
76
|
+
if (doc && doc.memory_sync) return doc.memory_sync;
|
|
77
|
+
} catch {
|
|
78
|
+
// fall through
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildConfig() {
|
|
85
|
+
const envFile = readSettingsEnv();
|
|
86
|
+
return {
|
|
87
|
+
byan_api_url: envFile.BYAN_API_URL || process.env.BYAN_API_URL || null,
|
|
88
|
+
byan_api_token: envFile.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN || null,
|
|
89
|
+
memory_sync: readMemorySyncConfig() || {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Per-turn buffer of recent messages/tool calls keyed by sessionId.
|
|
94
|
+
const turnBuffer = new Map();
|
|
95
|
+
|
|
96
|
+
function bufferFor(sessionId) {
|
|
97
|
+
const key = sessionId || 'default';
|
|
98
|
+
if (!turnBuffer.has(key)) {
|
|
99
|
+
turnBuffer.set(key, { userMessages: [], assistantMessages: [], toolCalls: [] });
|
|
100
|
+
}
|
|
101
|
+
return turnBuffer.get(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearBuffer(sessionId) {
|
|
105
|
+
turnBuffer.delete(sessionId || 'default');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const session = await joinSession({
|
|
109
|
+
hooks: {
|
|
110
|
+
onSessionStart: async () => {
|
|
111
|
+
await session.log('BYAN staging extension loaded', { ephemeral: true });
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onUserPromptSubmitted: async (input) => {
|
|
115
|
+
const buf = bufferFor(input.sessionId);
|
|
116
|
+
buf.userMessages.push({ role: 'user', content: input.prompt });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
onPreToolUse: async (input) => {
|
|
120
|
+
const buf = bufferFor(input.sessionId);
|
|
121
|
+
buf.toolCalls.push({
|
|
122
|
+
name: input.toolName,
|
|
123
|
+
input: input.toolArgs || {},
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
onPostToolUse: async (input) => {
|
|
128
|
+
const buf = bufferFor(input.sessionId);
|
|
129
|
+
// If the last tool call has a text result, treat it as assistant output
|
|
130
|
+
if (input.toolResult && typeof input.toolResult.content === 'string') {
|
|
131
|
+
buf.assistantMessages.push({
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
content: input.toolResult.content,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onSessionEnd: async (input) => {
|
|
139
|
+
const staging = loadStaging();
|
|
140
|
+
if (!staging) {
|
|
141
|
+
clearBuffer(input?.sessionId);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const buf = bufferFor(input?.sessionId);
|
|
146
|
+
const turn = {
|
|
147
|
+
sessionId: input?.sessionId || null,
|
|
148
|
+
messages: [...buf.userMessages, ...buf.assistantMessages],
|
|
149
|
+
toolCalls: buf.toolCalls,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await staging.processTurn({
|
|
154
|
+
turn,
|
|
155
|
+
cliSource: 'copilot-cli',
|
|
156
|
+
config: buildConfig(),
|
|
157
|
+
projectRoot: PROJECT_ROOT,
|
|
158
|
+
flushNow: true,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// never block session end
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clearBuffer(input?.sessionId);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
tools: [],
|
|
169
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byan/copilot-staging-extension",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Copilot CLI extension that stages conversation knowledge to byan_web /api/memory via the shared BYAN staging core.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "extension.mjs",
|
|
7
|
+
"private": true
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-byan-agent",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.7",
|
|
4
4
|
"description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Copilot CLI, Claude Code, Codex). Merise Agile + TDD + 64 Mantras. ~54% LLM cost savings.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYAN staging core — extract / filter / dedup / queue / flush conversation
|
|
3
|
+
* knowledge from any supported CLI (claude-code, copilot-cli, codex) to a
|
|
4
|
+
* byan_web instance via POST /api/memory.
|
|
5
|
+
*
|
|
6
|
+
* Usage from a Claude Code Stop hook :
|
|
7
|
+
* const { processTurn } = require('./staging');
|
|
8
|
+
* await processTurn({ turn, cliSource: 'claude-code', config, projectRoot });
|
|
9
|
+
*
|
|
10
|
+
* Usage from a Copilot CLI extension.mjs :
|
|
11
|
+
* import { processTurn } from '<repo>/src/staging/staging.js';
|
|
12
|
+
* await processTurn({ turn, cliSource: 'copilot-cli', config, projectRoot });
|
|
13
|
+
*
|
|
14
|
+
* Contract :
|
|
15
|
+
* - processTurn() is idempotent (dedup by content hash)
|
|
16
|
+
* - never throws — errors go to the retry queue
|
|
17
|
+
* - if enabled=false, it's a pure no-op (zero bytes sent)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const QUEUE_FILENAME = 'staging-queue.jsonl';
|
|
26
|
+
const SEEN_FILENAME = 'staging-seen.json';
|
|
27
|
+
const STAGING_DIR = path.join('_byan-output', 'staging');
|
|
28
|
+
|
|
29
|
+
// Patterns considered "chit-chat" — skipped by the triage filter.
|
|
30
|
+
const CHITCHAT_PATTERNS = [
|
|
31
|
+
/^(hi|hello|ok|thanks|merci|salut|bye|lol|yep|nope)[!. ]*$/i,
|
|
32
|
+
/^(y|yes|n|no|go|stop)$/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const MIN_CONTENT_CHARS = 50;
|
|
36
|
+
|
|
37
|
+
function resolveRoot(projectRoot) {
|
|
38
|
+
return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.env.BYAN_PROJECT_ROOT || process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stagingDir(projectRoot) {
|
|
42
|
+
return path.join(resolveRoot(projectRoot), STAGING_DIR);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function queuePath(projectRoot) {
|
|
46
|
+
return path.join(stagingDir(projectRoot), QUEUE_FILENAME);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function seenPath(projectRoot) {
|
|
50
|
+
return path.join(stagingDir(projectRoot), SEEN_FILENAME);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureDir(dir) {
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(s) {
|
|
58
|
+
return crypto.createHash('sha256').update(String(s)).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Enablement + config
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function isEnabled(config) {
|
|
66
|
+
if (!config || typeof config !== 'object') return false;
|
|
67
|
+
const ms = config.memory_sync || config.memorySync;
|
|
68
|
+
if (!ms) return false;
|
|
69
|
+
return ms.enabled === true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function apiUrl(config) {
|
|
73
|
+
if (!config) return null;
|
|
74
|
+
return config.byan_api_url || config.BYAN_API_URL || process.env.BYAN_API_URL || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function apiToken(config) {
|
|
78
|
+
if (!config) return process.env.BYAN_API_TOKEN || null;
|
|
79
|
+
return config.byan_api_token || config.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Extract — normalize a turn payload into a memory entry draft
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function extractUserText(turn) {
|
|
87
|
+
if (!turn) return '';
|
|
88
|
+
if (typeof turn.userMessage === 'string') return turn.userMessage;
|
|
89
|
+
if (typeof turn.prompt === 'string') return turn.prompt;
|
|
90
|
+
if (Array.isArray(turn.messages)) {
|
|
91
|
+
const u = [...turn.messages].reverse().find((m) => m && m.role === 'user');
|
|
92
|
+
if (u && typeof u.content === 'string') return u.content;
|
|
93
|
+
}
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractAssistantText(turn) {
|
|
98
|
+
if (!turn) return '';
|
|
99
|
+
if (typeof turn.assistantMessage === 'string') return turn.assistantMessage;
|
|
100
|
+
if (Array.isArray(turn.messages)) {
|
|
101
|
+
const a = [...turn.messages].reverse().find((m) => m && m.role === 'assistant');
|
|
102
|
+
if (a) {
|
|
103
|
+
if (typeof a.content === 'string') return a.content;
|
|
104
|
+
if (Array.isArray(a.content)) {
|
|
105
|
+
return a.content
|
|
106
|
+
.map((c) => (c && typeof c === 'object' && c.text ? c.text : ''))
|
|
107
|
+
.join(' ')
|
|
108
|
+
.trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractFilesTouched(turn) {
|
|
116
|
+
if (!turn) return [];
|
|
117
|
+
if (Array.isArray(turn.filesTouched)) return turn.filesTouched.filter(Boolean);
|
|
118
|
+
if (Array.isArray(turn.toolCalls)) {
|
|
119
|
+
const files = [];
|
|
120
|
+
for (const tc of turn.toolCalls) {
|
|
121
|
+
const p = tc?.input?.file_path || tc?.args?.file_path || tc?.input?.path;
|
|
122
|
+
if (p && typeof p === 'string') files.push(p);
|
|
123
|
+
}
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function classify(content, turn) {
|
|
130
|
+
const c = String(content || '').toLowerCase();
|
|
131
|
+
if (/\b(decid(e|ed|ing)|choix|trade-?off|architecture)\b/i.test(c)) return 'decision';
|
|
132
|
+
if (/\b(bug|error|fail|broken|bloque|blocked|can't|impossible)\b/i.test(c)) return 'blocker';
|
|
133
|
+
const files = extractFilesTouched(turn);
|
|
134
|
+
if (files.length > 0) return 'artifact';
|
|
135
|
+
return 'fact';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extract({ turn, cliSource }) {
|
|
139
|
+
const user = extractUserText(turn);
|
|
140
|
+
const assistant = extractAssistantText(turn);
|
|
141
|
+
const filesTouched = extractFilesTouched(turn);
|
|
142
|
+
const content = [user, assistant].filter(Boolean).join('\n\n').trim();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
cliSource: cliSource || 'unknown',
|
|
146
|
+
sessionId: turn?.sessionId || null,
|
|
147
|
+
category: classify(content, turn),
|
|
148
|
+
content,
|
|
149
|
+
metadata: {
|
|
150
|
+
userMessageLen: user.length,
|
|
151
|
+
assistantMessageLen: assistant.length,
|
|
152
|
+
filesTouched,
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
},
|
|
155
|
+
pinned: false,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Filter — triage chit-chat
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
function shouldKeep(entry) {
|
|
164
|
+
if (!entry || typeof entry.content !== 'string') return false;
|
|
165
|
+
if (entry.content.length < MIN_CONTENT_CHARS) return false;
|
|
166
|
+
|
|
167
|
+
const trimmed = entry.content.trim();
|
|
168
|
+
for (const re of CHITCHAT_PATTERNS) {
|
|
169
|
+
if (re.test(trimmed)) return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Must have at least one of : files touched, substantive content, or decision keywords
|
|
173
|
+
if (entry.metadata && Array.isArray(entry.metadata.filesTouched) && entry.metadata.filesTouched.length > 0) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// Otherwise require reasonable content length
|
|
177
|
+
return trimmed.length >= MIN_CONTENT_CHARS * 2;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Dedup — hash-based, persisted
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
function readSeen(projectRoot) {
|
|
185
|
+
const p = seenPath(projectRoot);
|
|
186
|
+
if (!fs.existsSync(p)) return { hashes: [] };
|
|
187
|
+
try {
|
|
188
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
189
|
+
} catch {
|
|
190
|
+
return { hashes: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function writeSeen(projectRoot, seen) {
|
|
195
|
+
ensureDir(stagingDir(projectRoot));
|
|
196
|
+
// Keep only last 500 hashes to cap disk use
|
|
197
|
+
const trimmed = { hashes: seen.hashes.slice(-500) };
|
|
198
|
+
fs.writeFileSync(seenPath(projectRoot), JSON.stringify(trimmed));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isDuplicate(entry, projectRoot) {
|
|
202
|
+
const h = sha256(entry.content);
|
|
203
|
+
const seen = readSeen(projectRoot);
|
|
204
|
+
return seen.hashes.includes(h);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function markSeen(entry, projectRoot) {
|
|
208
|
+
const h = sha256(entry.content);
|
|
209
|
+
const seen = readSeen(projectRoot);
|
|
210
|
+
if (!seen.hashes.includes(h)) {
|
|
211
|
+
seen.hashes.push(h);
|
|
212
|
+
writeSeen(projectRoot, seen);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Queue — local append-only, flushed by flush()
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
function enqueue(entry, projectRoot) {
|
|
221
|
+
ensureDir(stagingDir(projectRoot));
|
|
222
|
+
const p = queuePath(projectRoot);
|
|
223
|
+
const line = JSON.stringify({
|
|
224
|
+
...entry,
|
|
225
|
+
enqueued_at: new Date().toISOString(),
|
|
226
|
+
attempts: 0,
|
|
227
|
+
});
|
|
228
|
+
fs.appendFileSync(p, line + '\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readQueue(projectRoot) {
|
|
232
|
+
const p = queuePath(projectRoot);
|
|
233
|
+
if (!fs.existsSync(p)) return [];
|
|
234
|
+
return fs
|
|
235
|
+
.readFileSync(p, 'utf8')
|
|
236
|
+
.split('\n')
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.map((line) => {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(line);
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
.filter(Boolean);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function writeQueue(projectRoot, entries) {
|
|
249
|
+
const p = queuePath(projectRoot);
|
|
250
|
+
if (entries.length === 0) {
|
|
251
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
p,
|
|
256
|
+
entries.map((e) => JSON.stringify(e)).join('\n') + '\n'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Project ID — derived from git remote or cwd
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
function detectProjectId(projectRoot) {
|
|
265
|
+
const root = resolveRoot(projectRoot);
|
|
266
|
+
try {
|
|
267
|
+
const url = execSync('git remote get-url origin', {
|
|
268
|
+
cwd: root,
|
|
269
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
}).trim();
|
|
272
|
+
if (url) return sha256(url).slice(0, 16);
|
|
273
|
+
} catch {
|
|
274
|
+
// no git remote, fall through
|
|
275
|
+
}
|
|
276
|
+
return sha256(root).slice(0, 16);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Flush — POST queued entries to /api/memory with retry
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
async function postEntry({ entry, url, token, projectId }) {
|
|
284
|
+
const body = {
|
|
285
|
+
projectId,
|
|
286
|
+
sessionId: entry.sessionId,
|
|
287
|
+
cliSource: entry.cliSource,
|
|
288
|
+
category: entry.category,
|
|
289
|
+
content: entry.content,
|
|
290
|
+
metadata: entry.metadata,
|
|
291
|
+
pinned: entry.pinned === true,
|
|
292
|
+
};
|
|
293
|
+
const res = await fetch(`${url.replace(/\/$/, '')}/api/memory`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: {
|
|
296
|
+
'Content-Type': 'application/json',
|
|
297
|
+
Authorization: `Bearer ${token}`,
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify(body),
|
|
300
|
+
});
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const text = await res.text().catch(() => '');
|
|
303
|
+
const err = new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
304
|
+
err.status = res.status;
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
return res.json().catch(() => ({}));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function flush({ config, projectRoot, maxAttempts = 5 } = {}) {
|
|
311
|
+
const url = apiUrl(config);
|
|
312
|
+
const token = apiToken(config);
|
|
313
|
+
if (!url || !token) {
|
|
314
|
+
return { flushed: 0, requeued: 0, dropped: 0, reason: 'missing url or token' };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const projectId = detectProjectId(projectRoot);
|
|
318
|
+
const queue = readQueue(projectRoot);
|
|
319
|
+
if (queue.length === 0) return { flushed: 0, requeued: 0, dropped: 0 };
|
|
320
|
+
|
|
321
|
+
let flushed = 0;
|
|
322
|
+
const remaining = [];
|
|
323
|
+
let dropped = 0;
|
|
324
|
+
|
|
325
|
+
for (const entry of queue) {
|
|
326
|
+
try {
|
|
327
|
+
await postEntry({ entry, url, token, projectId });
|
|
328
|
+
flushed += 1;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const attempts = (entry.attempts || 0) + 1;
|
|
331
|
+
if (attempts >= maxAttempts) {
|
|
332
|
+
dropped += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
remaining.push({ ...entry, attempts, last_error: err.message });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
writeQueue(projectRoot, remaining);
|
|
340
|
+
|
|
341
|
+
return { flushed, requeued: remaining.length, dropped };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Orchestration — the single entry point used by both hooks/extensions
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
async function processTurn({ turn, cliSource, config, projectRoot, flushNow = true } = {}) {
|
|
349
|
+
if (!isEnabled(config)) {
|
|
350
|
+
return { skipped: 'disabled' };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const entry = extract({ turn, cliSource });
|
|
354
|
+
|
|
355
|
+
if (!shouldKeep(entry)) {
|
|
356
|
+
return { skipped: 'filtered', category: entry.category };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (isDuplicate(entry, projectRoot)) {
|
|
360
|
+
return { skipped: 'duplicate', category: entry.category };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
enqueue(entry, projectRoot);
|
|
364
|
+
markSeen(entry, projectRoot);
|
|
365
|
+
|
|
366
|
+
if (!flushNow) {
|
|
367
|
+
return { queued: true, flushed: 0, category: entry.category };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const result = await flush({ config, projectRoot });
|
|
371
|
+
return { queued: true, ...result, category: entry.category };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = {
|
|
375
|
+
processTurn,
|
|
376
|
+
extract,
|
|
377
|
+
shouldKeep,
|
|
378
|
+
isEnabled,
|
|
379
|
+
isDuplicate,
|
|
380
|
+
markSeen,
|
|
381
|
+
enqueue,
|
|
382
|
+
readQueue,
|
|
383
|
+
writeQueue,
|
|
384
|
+
flush,
|
|
385
|
+
detectProjectId,
|
|
386
|
+
sha256,
|
|
387
|
+
classify,
|
|
388
|
+
queuePath,
|
|
389
|
+
seenPath,
|
|
390
|
+
STAGING_DIR,
|
|
391
|
+
QUEUE_FILENAME,
|
|
392
|
+
SEEN_FILENAME,
|
|
393
|
+
MIN_CONTENT_CHARS,
|
|
394
|
+
};
|