fdeops 3.29.0 → 3.30.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.
- package/README.md +2 -0
- package/bin/fde.js +44 -17
- package/bin/install.js +1 -1
- package/bin/lib/context.js +2 -2
- package/bin/lib/setup.js +109 -0
- package/bin/lib/trust.js +4 -4
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ Read-only HTML of the record - promised, measured, accepted, and evidence. Regen
|
|
|
56
56
|
/plugin install fdeops@fdeops
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Make it fit your day: `fde setup` asks three choices for your daily overview, context size, and report masking. [First-use setup](docs/USAGE.md#make-fdeops-fit-your-day).
|
|
60
|
+
|
|
59
61
|
The plugin adds session hooks and the slash commands below. Skill-only installation does not add hooks. See the [installation guide](docs/install.md) for setup details.
|
|
60
62
|
|
|
61
63
|
</details>
|
package/bin/fde.js
CHANGED
|
@@ -46,8 +46,26 @@ const ENGAGEMENTS_ROOT = ((process.env.FDEOPS_ENGAGEMENTS_ROOT || '').trim().rep
|
|
|
46
46
|
|| path.join(HOME, 'fde-engagements')
|
|
47
47
|
const REGISTRY = path.join(ENGAGEMENTS_ROOT, '.registry')
|
|
48
48
|
const masking = require('./lib/masking').createMasking(ENGAGEMENTS_ROOT)
|
|
49
|
+
const setup = require('./lib/setup')
|
|
50
|
+
const setupStore = setup.createSetup(ENGAGEMENTS_ROOT)
|
|
51
|
+
let preferences = setup.DEFAULTS
|
|
52
|
+
function contextBudget(args) {
|
|
53
|
+
return context.budgetArgs(args, preferences.context === 'compact' ? 4096 : 16384)
|
|
54
|
+
}
|
|
55
|
+
function portfolioView(args) {
|
|
56
|
+
if (args.includes('--all') && args.includes('--current')) throw new Error('Choose --all or --current, not both.')
|
|
57
|
+
return args.includes('--all') || (!args.includes('--current') && preferences.view === 'portfolio')
|
|
58
|
+
}
|
|
49
59
|
function maskDisplay(text) {
|
|
50
|
-
return ['dashboard', 'vault'].includes(process.argv[2]) ? String(text) : masking.mask(text)
|
|
60
|
+
return ['dashboard', 'vault'].includes(process.argv[2]) && preferences.privacy !== 'reports' ? String(text) : masking.mask(text)
|
|
61
|
+
}
|
|
62
|
+
// Classify the original record first; aliases must never become evidence or a signer.
|
|
63
|
+
function maskReport(value) {
|
|
64
|
+
if (preferences.privacy !== 'reports') return value
|
|
65
|
+
if (typeof value === 'string') return masking.mask(value)
|
|
66
|
+
if (Array.isArray(value)) return value.map(maskReport)
|
|
67
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskReport(item)]))
|
|
68
|
+
return value
|
|
51
69
|
}
|
|
52
70
|
function maskedSections(sections, maxBytes) {
|
|
53
71
|
return context.boundedSections(sections.map(text => masking.mask(text)), maxBytes)
|
|
@@ -990,7 +1008,7 @@ const {
|
|
|
990
1008
|
countOpenRisks,
|
|
991
1009
|
} = createTrustApi({
|
|
992
1010
|
fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
|
|
993
|
-
stripTemplateNoise, stripLegendLines, extractRisks,
|
|
1011
|
+
stripTemplateNoise, stripLegendLines, extractRisks, maskDisplay,
|
|
994
1012
|
})
|
|
995
1013
|
|
|
996
1014
|
// Stakeholders: columns are matched by header wording, not position - real
|
|
@@ -1023,6 +1041,7 @@ function parseSignalHistoryEntries(eng) {
|
|
|
1023
1041
|
}
|
|
1024
1042
|
|
|
1025
1043
|
function displayNameFromSignalText(text) {
|
|
1044
|
+
if (preferences.privacy === 'reports' && masking.mask(text) !== text) return 'Contact (identifier masked)'
|
|
1026
1045
|
const person = personFromSignalText(text)
|
|
1027
1046
|
if (person) return person
|
|
1028
1047
|
const t = String(text).trim()
|
|
@@ -1162,7 +1181,7 @@ function extractStats(eng) {
|
|
|
1162
1181
|
const key = from + '→' + to
|
|
1163
1182
|
if (seen.has(key)) continue
|
|
1164
1183
|
seen.add(key)
|
|
1165
|
-
const pre = text.slice(
|
|
1184
|
+
const pre = maskDisplay(text.slice(0, m.index)).slice(-40)
|
|
1166
1185
|
const label = pre.split(/\s+/).filter(Boolean).slice(-3).join(' ').replace(/^[,:;.\-]+|[,:;.\-]+$/g, '') || 'metric'
|
|
1167
1186
|
stats.push({ label, from, to })
|
|
1168
1187
|
}
|
|
@@ -1315,7 +1334,7 @@ function cmdScan() {
|
|
|
1315
1334
|
|
|
1316
1335
|
function cmdResume(args) {
|
|
1317
1336
|
let maxBytes
|
|
1318
|
-
try { ({ args, maxBytes } =
|
|
1337
|
+
try { ({ args, maxBytes } = contextBudget(args)) } catch (e) { console.error(e.message); process.exit(2) }
|
|
1319
1338
|
|
|
1320
1339
|
const initIdx = args.indexOf('--init')
|
|
1321
1340
|
if (initIdx !== -1) {
|
|
@@ -1400,6 +1419,10 @@ function cmdResume(args) {
|
|
|
1400
1419
|
const head = memoryHead(fdeDir)
|
|
1401
1420
|
console.log(`memory git: ${head || 'ready'}${owner ? ` owner: ${owner.email}` : ''}`)
|
|
1402
1421
|
}
|
|
1422
|
+
if (!setupStore.read()) {
|
|
1423
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return setup.command(setupStore, [])
|
|
1424
|
+
console.log('Make FDEOps fit your day: run fde setup once for three short choices. Agents: ask these after binding; do not guess the answers.')
|
|
1425
|
+
}
|
|
1403
1426
|
return
|
|
1404
1427
|
}
|
|
1405
1428
|
if (args[0] === '--bind') {
|
|
@@ -2548,7 +2571,7 @@ function cmdReceipts(args) {
|
|
|
2548
2571
|
// overwrites an engagement record, existing file, or symlink.
|
|
2549
2572
|
function cmdHandoff(args, label = 'Handoff') {
|
|
2550
2573
|
let parsed
|
|
2551
|
-
try { parsed =
|
|
2574
|
+
try { parsed = contextBudget(args) } catch (e) { console.error(e.message); process.exit(1) }
|
|
2552
2575
|
let out = ''
|
|
2553
2576
|
if (parsed.args.length) {
|
|
2554
2577
|
if (parsed.args.length !== 2 || parsed.args[0] !== '--out' || !parsed.args[1] || parsed.args[1].startsWith('--')) {
|
|
@@ -2594,7 +2617,7 @@ function cmdHandoff(args, label = 'Handoff') {
|
|
|
2594
2617
|
|
|
2595
2618
|
function cmdRecall(args) {
|
|
2596
2619
|
let maxBytes
|
|
2597
|
-
try { ({ args, maxBytes } =
|
|
2620
|
+
try { ({ args, maxBytes } = contextBudget(args)) } catch (e) { console.error(e.message); process.exit(2) }
|
|
2598
2621
|
const query = args.join(' ').trim()
|
|
2599
2622
|
if (!query || Buffer.byteLength(query) > 2048 || args.some(a => a.startsWith('--'))) {
|
|
2600
2623
|
console.error('usage: fde recall <topic> [--max-bytes 4096..65536]'); process.exit(2)
|
|
@@ -3609,7 +3632,7 @@ function engagementSlugFromPath(eng) {
|
|
|
3609
3632
|
}
|
|
3610
3633
|
|
|
3611
3634
|
function cmdStatus(args) {
|
|
3612
|
-
const all = args
|
|
3635
|
+
const all = portfolioView(args)
|
|
3613
3636
|
if (!fs.existsSync(ENGAGEMENTS_ROOT)) { console.log('no engagements yet - fde resume --init <name>'); return }
|
|
3614
3637
|
const rows = []
|
|
3615
3638
|
if (all) {
|
|
@@ -3678,7 +3701,7 @@ function gatherEngagements(opts = {}) {
|
|
|
3678
3701
|
}
|
|
3679
3702
|
|
|
3680
3703
|
function cmdDashboard(args) {
|
|
3681
|
-
const all = args
|
|
3704
|
+
const all = portfolioView(args)
|
|
3682
3705
|
const outIdx = args.indexOf('--out')
|
|
3683
3706
|
const outPath = outIdx !== -1 && args[outIdx + 1]
|
|
3684
3707
|
? path.resolve(args[outIdx + 1].replace(/^~/, HOME))
|
|
@@ -3734,7 +3757,7 @@ function cmdDashboard(args) {
|
|
|
3734
3757
|
['trust-profile.md', 'Trust profile'],
|
|
3735
3758
|
].map(([f, title]) => [title, readClean(e.dir, f)])
|
|
3736
3759
|
.filter(([, md]) => render.hasRealContent(md))
|
|
3737
|
-
.map(([title, md]) => ({ title, html: render.mdBlockHtml(md, parseMdTable) }))
|
|
3760
|
+
.map(([title, md]) => ({ title, html: render.mdBlockHtml(maskDisplay(md), parseMdTable) }))
|
|
3738
3761
|
e.searchBlob = render.escapeHtml([
|
|
3739
3762
|
e.name, e.next, e.lastSession, e.reality, e.brief,
|
|
3740
3763
|
...e.log.map(g => g.text), ...e.risks.map(r => r.text),
|
|
@@ -3744,7 +3767,7 @@ function cmdDashboard(args) {
|
|
|
3744
3767
|
].join(' ').toLowerCase())
|
|
3745
3768
|
})
|
|
3746
3769
|
|
|
3747
|
-
const html = render.buildFieldbookHtml({ engagements, today, generatedAt: new Date().toISOString() })
|
|
3770
|
+
const html = render.buildFieldbookHtml({ engagements: maskReport(engagements), today, generatedAt: new Date().toISOString() })
|
|
3748
3771
|
|
|
3749
3772
|
try {
|
|
3750
3773
|
const isRecordPath = p => p.split(path.sep).some(part => ['.fde', '.git'].includes(part.toLowerCase()))
|
|
@@ -3915,10 +3938,10 @@ function cmdVault(args) {
|
|
|
3915
3938
|
})
|
|
3916
3939
|
|
|
3917
3940
|
const files = vault.buildVaultFiles({
|
|
3918
|
-
engagements,
|
|
3941
|
+
engagements: maskReport(engagements),
|
|
3919
3942
|
today: render.formatToday(new Date()),
|
|
3920
3943
|
redacted,
|
|
3921
|
-
engagementsRoot: ENGAGEMENTS_ROOT,
|
|
3944
|
+
engagementsRoot: maskReport(ENGAGEMENTS_ROOT),
|
|
3922
3945
|
version: cliVersion(),
|
|
3923
3946
|
})
|
|
3924
3947
|
|
|
@@ -4102,6 +4125,7 @@ ${fs.existsSync(html) ? `\n Open the fieldbook: ${html}` : ''}
|
|
|
4102
4125
|
function printUsage() {
|
|
4103
4126
|
console.log(`fde - deterministic core of fdeops
|
|
4104
4127
|
fde demo the whole loop on a fake client (fde demo --clean removes it)
|
|
4128
|
+
fde setup three first-use choices (or --show for saved preferences)
|
|
4105
4129
|
fde privacy show masking capability and its boundaries
|
|
4106
4130
|
fde scan day-1 recon of this repo (facts, no AI)
|
|
4107
4131
|
fde resume load this workspace's engagement memory (bounded)
|
|
@@ -4131,8 +4155,8 @@ function printUsage() {
|
|
|
4131
4155
|
fde receipts <term> source-backed records versus claims
|
|
4132
4156
|
fde defend sponsor readout: accepted assertions, claims, sources, gaps
|
|
4133
4157
|
fde handoff [--out file] portable redacted packet; stdout by default, new file only
|
|
4134
|
-
fde status [--all]
|
|
4135
|
-
fde dashboard [--all] [--open] [--out <path>]
|
|
4158
|
+
fde status [--all|--current] value ledger, then trust (scope follows setup)
|
|
4159
|
+
fde dashboard [--all|--current] [--open] [--out <path>] fieldbook (scope follows setup)
|
|
4136
4160
|
fde vault derived Obsidian vault of every engagement (--current for one, --redacted for a shared screen, --out <dir>)
|
|
4137
4161
|
hooks call these; you do not: capture (session-end snapshot), preserve (pre-compaction snapshot)
|
|
4138
4162
|
env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
|
|
@@ -4144,7 +4168,7 @@ function printUsage() {
|
|
|
4144
4168
|
const [cmd, ...rawArgs] = process.argv.slice(2)
|
|
4145
4169
|
let outputBudget
|
|
4146
4170
|
if (['resume', 'recall', 'handoff', 'defend'].includes(cmd) && !rawArgs.some(a => ['--full', '--init', '--bind', '--out'].includes(a))) {
|
|
4147
|
-
try { outputBudget = context.budgetArgs(rawArgs).maxBytes } catch (_) {}
|
|
4171
|
+
try { outputBudget = context.budgetArgs(rawArgs, (setupStore.read() || setup.DEFAULTS).context === 'compact' ? 4096 : 16384).maxBytes } catch (_) {}
|
|
4148
4172
|
}
|
|
4149
4173
|
require('./lib/masking').protectOutput(masking, { maxBytes: outputBudget })
|
|
4150
4174
|
let args
|
|
@@ -4162,14 +4186,17 @@ if (args.includes('--help') || args.includes('-h') || cmd === 'help' || cmd ===
|
|
|
4162
4186
|
process.exit(0)
|
|
4163
4187
|
}
|
|
4164
4188
|
try {
|
|
4189
|
+
if (cmd !== 'setup' && cmd !== 'privacy') preferences = setupStore.read() || setup.DEFAULTS
|
|
4190
|
+
const finishAsync = result => { if (result && typeof result.catch === 'function') result.catch(error => { console.error(error.message); process.exitCode = 1 }) }
|
|
4165
4191
|
switch (cmd) {
|
|
4192
|
+
case 'setup': finishAsync(setup.command(setupStore, args)); break
|
|
4166
4193
|
case 'privacy':
|
|
4167
4194
|
if (args.length) { console.error('usage: fde privacy'); process.exitCode = 2; break }
|
|
4168
|
-
console.log(`FDEOps ${require('../package.json').version} - identifier masking enabled by default.\nCLI responses, smart proposals, handoff packets and ingest MCP results use local aliases.\nPatterns: common emails, international/US phones, SSN-shaped identifiers and supported credentials.\nNames and arbitrary sensitive prose are not detected; mark them <private>.\nRaw files, pasted chat
|
|
4195
|
+
console.log(`FDEOps ${require('../package.json').version} - identifier masking enabled by default.\nCLI responses, smart proposals, handoff packets and ingest MCP results use local aliases.\nPatterns: common emails, international/US phones, SSN-shaped identifiers and supported credentials.\nNames and arbitrary sensitive prose are not detected; mark them <private>.\nRaw files, pasted chat and upstream MCP content bypass this protection. Local reports retain identifiers by default; fde setup can also mask newly generated report content.`)
|
|
4169
4196
|
break
|
|
4170
4197
|
case 'demo': cmdDemo(args); break
|
|
4171
4198
|
case 'scan': cmdScan(); break
|
|
4172
|
-
case 'resume': cmdResume(args); break
|
|
4199
|
+
case 'resume': finishAsync(cmdResume(args)); break
|
|
4173
4200
|
case 'recall': cmdRecall(args); break
|
|
4174
4201
|
case 'triage': cmdTriage(); break
|
|
4175
4202
|
case 'log': cmdLog(args); break
|
package/bin/install.js
CHANGED
|
@@ -376,7 +376,7 @@ function cmdInstall(opts = {}) {
|
|
|
376
376
|
// `npx fdeops scan` must recon, not install - any fde subcommand passes straight
|
|
377
377
|
// through to the CLI (fde.js reads process.argv itself, so require() is enough).
|
|
378
378
|
const FDE_SUBCOMMANDS = [
|
|
379
|
-
'demo', 'scan', 'resume', 'triage', 'log', 'debrief', 'ingest', 'prep', 'doctor', 'redact',
|
|
379
|
+
'setup', 'privacy', 'demo', 'scan', 'resume', 'triage', 'log', 'debrief', 'ingest', 'prep', 'doctor', 'redact',
|
|
380
380
|
'tidy', 'garden', 'owner', 'receipts', 'recall', 'handoff', 'defend', 'capture', 'preserve', 'status', 'dashboard', 'vault', 'help',
|
|
381
381
|
]
|
|
382
382
|
|
package/bin/lib/context.js
CHANGED
|
@@ -14,9 +14,9 @@ function clipUtf8(text, bytes) {
|
|
|
14
14
|
while (end > 0 && (buf[end] & 0xc0) === 0x80) end--
|
|
15
15
|
return buf.subarray(0, end).toString('utf8')
|
|
16
16
|
}
|
|
17
|
-
function budgetArgs(args) {
|
|
17
|
+
function budgetArgs(args, defaultBytes = DEFAULT_BYTES) {
|
|
18
18
|
const rest = [...args]
|
|
19
|
-
let maxBytes =
|
|
19
|
+
let maxBytes = defaultBytes
|
|
20
20
|
const at = rest.indexOf('--max-bytes')
|
|
21
21
|
if (at !== -1) {
|
|
22
22
|
const raw = rest[at + 1]
|
package/bin/lib/setup.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const crypto = require('node:crypto')
|
|
5
|
+
const paths = require('./install-paths')
|
|
6
|
+
|
|
7
|
+
const DEFAULTS = { view: 'current', context: 'standard', privacy: 'agent' }
|
|
8
|
+
const QUESTIONS = [
|
|
9
|
+
{ key: 'view', title: 'What should your daily overview show?', options: [
|
|
10
|
+
['current', 'The client I am working on'], ['portfolio', 'All my clients'],
|
|
11
|
+
] },
|
|
12
|
+
{ key: 'context', title: 'How much context should your agent start with?', options: [
|
|
13
|
+
['standard', 'Standard (up to 16 KiB)'], ['compact', 'Compact (up to 4 KiB; retrieve details as needed)'],
|
|
14
|
+
] },
|
|
15
|
+
{ key: 'privacy', title: 'Where should common identifiers be masked?', options: [
|
|
16
|
+
['agent', 'Agent context; keep originals in my local reports'],
|
|
17
|
+
['reports', 'Agent context and newly generated Fieldbook/vault reports'],
|
|
18
|
+
] },
|
|
19
|
+
]
|
|
20
|
+
const LIMITS = 'Both options hide <private> content. Masking covers common identifier formats, not all personal data. Names and sensitive prose need private marking. Your AI host controls provider transmission; setup does not change it. Existing exports are not rewritten.'
|
|
21
|
+
function validate(value) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
23
|
+
Object.keys(value).length !== 3 || QUESTIONS.some(q => !q.options.some(([v]) => value[q.key] === v))) {
|
|
24
|
+
throw new Error('Invalid setup choices; run fde setup to see the supported options.')
|
|
25
|
+
}
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
function createSetup(root) {
|
|
29
|
+
const file = path.join(root, '.preferences.json')
|
|
30
|
+
function read() {
|
|
31
|
+
let fd
|
|
32
|
+
try {
|
|
33
|
+
paths.checkPath(file)
|
|
34
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK)
|
|
35
|
+
const st = fs.fstatSync(fd)
|
|
36
|
+
if (!st.isFile() || st.nlink !== 1 || st.size > 4096) throw new Error('unsafe')
|
|
37
|
+
return validate(JSON.parse(fs.readFileSync(fd, 'utf8')))
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.code === 'ENOENT') return null
|
|
40
|
+
throw new Error('Cannot read setup safely. Check .preferences.json in your engagements folder; no settings were changed.')
|
|
41
|
+
} finally { if (fd !== undefined) fs.closeSync(fd) }
|
|
42
|
+
}
|
|
43
|
+
function save(value) {
|
|
44
|
+
validate(value)
|
|
45
|
+
paths.mkdir(root)
|
|
46
|
+
paths.checkPath(file)
|
|
47
|
+
const temporary = path.join(root, `.preferences-${crypto.randomBytes(12).toString('hex')}.tmp`)
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 })
|
|
50
|
+
paths.checkPath(file)
|
|
51
|
+
fs.renameSync(temporary, file)
|
|
52
|
+
} finally { try { fs.unlinkSync(temporary) } catch (_) {} }
|
|
53
|
+
}
|
|
54
|
+
return { read, save }
|
|
55
|
+
}
|
|
56
|
+
function describe(value) {
|
|
57
|
+
return QUESTIONS.map(q => `${q.title} ${q.options.find(([v]) => v === value[q.key])[1]}`).join('\n')
|
|
58
|
+
}
|
|
59
|
+
function command(store, args) {
|
|
60
|
+
if (args.length === 1 && args[0] === '--show') {
|
|
61
|
+
const saved = store.read()
|
|
62
|
+
console.log(JSON.stringify({ configured: !!saved, ...(saved || DEFAULTS) }))
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (args.length) {
|
|
66
|
+
const choices = {}
|
|
67
|
+
if (!args.includes('--save') || args.filter(a => a === '--save').length !== 1) throw new Error('Use fde setup, or supply all three choices with --save.')
|
|
68
|
+
const rest = args.filter(a => a !== '--save')
|
|
69
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
70
|
+
const key = rest[i].replace(/^--/, '')
|
|
71
|
+
if (!rest[i].startsWith('--') || !Object.hasOwn(DEFAULTS, key) || Object.hasOwn(choices, key)) throw new Error('Unknown or repeated setup option.')
|
|
72
|
+
choices[key] = rest[i + 1]
|
|
73
|
+
}
|
|
74
|
+
validate(choices)
|
|
75
|
+
store.save(choices)
|
|
76
|
+
console.log('Setup saved for this engagements folder. Change it anytime with fde setup.\n' + describe(choices) + '\n' + LIMITS)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return interactive(store)
|
|
80
|
+
console.log('FIRST-USE SETUP: ask these three questions together, then save the answers. No settings have been changed.')
|
|
81
|
+
for (const [i, q] of QUESTIONS.entries()) console.log(`${i + 1}. ${q.title}\n` + q.options.map(([value, label]) => ` ${value}: ${label}`).join('\n'))
|
|
82
|
+
console.log(LIMITS + '\nAfter the user answers: fde setup --view current|portfolio --context standard|compact --privacy agent|reports --save\nUse fde setup --show to inspect existing choices. Never infer client policy from these preferences.')
|
|
83
|
+
}
|
|
84
|
+
async function interactive(store) {
|
|
85
|
+
// Only static prompts and validated enum labels bypass buffered CLI output.
|
|
86
|
+
const emit = text => fs.writeSync(1, text)
|
|
87
|
+
const rl = require('node:readline').createInterface({ input: process.stdin, terminal: false })
|
|
88
|
+
const lines = rl[Symbol.asyncIterator]()
|
|
89
|
+
const current = store.read() || DEFAULTS, choices = {}
|
|
90
|
+
try {
|
|
91
|
+
emit('Make FDEOps fit your day. Three choices; Enter keeps the current choice. Ctrl-C cancels.\n' + LIMITS + '\n')
|
|
92
|
+
for (const q of QUESTIONS) {
|
|
93
|
+
const selected = q.options.findIndex(([v]) => v === current[q.key])
|
|
94
|
+
while (true) {
|
|
95
|
+
emit(q.title + '\n' + q.options.map(([, label], i) => ` ${i + 1}. ${label}${i === selected ? ' (current)' : ''}`).join('\n') + '\n> ')
|
|
96
|
+
const next = await lines.next()
|
|
97
|
+
if (next.done) { emit('\nSetup cancelled; nothing saved.\n'); return }
|
|
98
|
+
const value = next.value.trim(), index = value === '' ? selected : /^[12]$/.test(value) ? Number(value) - 1 : -1
|
|
99
|
+
if (index >= 0) { choices[q.key] = q.options[index][0]; break }
|
|
100
|
+
emit('Choose 1 or 2, or press Enter.\n')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
emit('\n' + describe(choices) + '\nSave these settings? [y/N] ')
|
|
104
|
+
const next = await lines.next()
|
|
105
|
+
if (!next.done && /^y(es)?$/i.test(next.value.trim())) { store.save(choices); emit('\nSetup saved. Change it anytime with fde setup.\n') }
|
|
106
|
+
else emit('\nSetup cancelled; nothing saved.\n')
|
|
107
|
+
} finally { rl.close() }
|
|
108
|
+
}
|
|
109
|
+
module.exports = { createSetup, command, DEFAULTS }
|
package/bin/lib/trust.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
function createTrustApi(deps) {
|
|
4
4
|
const {
|
|
5
5
|
fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
|
|
6
|
-
stripTemplateNoise, stripLegendLines, extractRisks,
|
|
6
|
+
stripTemplateNoise, stripLegendLines, extractRisks, maskDisplay = text => text,
|
|
7
7
|
} = deps
|
|
8
8
|
|
|
9
9
|
// phase / trust / top risk / freshness - identical heuristic for status + dashboard.
|
|
@@ -138,7 +138,7 @@ function createTrustApi(deps) {
|
|
|
138
138
|
const body = sectionBody(ctx, 'Next action', { lastNonEmpty: true })
|
|
139
139
|
for (const raw of body.split('\n')) {
|
|
140
140
|
const t = raw.trim().replace(/^[-*]\s+/, '')
|
|
141
|
-
if (t) return t.slice(0, 120)
|
|
141
|
+
if (t) return maskDisplay(t).slice(0, 120)
|
|
142
142
|
}
|
|
143
143
|
return ''
|
|
144
144
|
}
|
|
@@ -180,7 +180,7 @@ function createTrustApi(deps) {
|
|
|
180
180
|
trustReason = mem.warn
|
|
181
181
|
} else if (worst) {
|
|
182
182
|
trust = worst.sig === 'red' ? 'RED' : worst.sig
|
|
183
|
-
trustReason = (worst.text || '').slice(0, 80)
|
|
183
|
+
trustReason = maskDisplay(worst.text || '').slice(0, 80)
|
|
184
184
|
if (worst.date) {
|
|
185
185
|
signalAge = Math.max(0, Math.floor((Date.now() - Date.parse(worst.date)) / 86400000))
|
|
186
186
|
stale = signalAge > 21
|
|
@@ -195,7 +195,7 @@ function createTrustApi(deps) {
|
|
|
195
195
|
trust = sLines.some(l => /\bred\b/i.test(l)) ? 'RED'
|
|
196
196
|
: sLines.some(l => /amber|gone quiet|routing around|escalat/i.test(l)) ? 'amber' : 'new'
|
|
197
197
|
}
|
|
198
|
-
const topRisk = (extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
198
|
+
const topRisk = maskDisplay((extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim()).slice(0, 80)
|
|
199
199
|
// Prefer trust trigger / memory warn over a random risk line; always keep mem.warn available
|
|
200
200
|
const reason = (trustReason || mem.warn) ? (trustReason || mem.warn) : topRisk
|
|
201
201
|
// What the triage line is actually quoting. A risk bullet printed under
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.30.0",
|
|
4
4
|
"description": "Client delivery tools for Forward Deployed Engineers. One @fde skill, local Markdown engagement records, and an offline dashboard for decisions, evidence, approvals, and next actions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fdeops": "bin/install.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "fdeops",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.30.0",
|
|
5
5
|
"description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code. You confirm; then it lands in .fde/ on your laptop.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|
package/skills/fde/SKILL.md
CHANGED
|
@@ -56,9 +56,13 @@ Use the customer's existing coding, testing, review, and repository instructions
|
|
|
56
56
|
|
|
57
57
|
Fallbacks: `node ~/.claude/fdeops/fde.js …`, then `npx --yes fdeops …`. Skill-only install is not "unavailable."
|
|
58
58
|
|
|
59
|
+
## First-use preferences
|
|
60
|
+
|
|
61
|
+
Run `fde setup --show` before client reads. If unavailable, update the CLI before offering setup; never pretend preferences were saved. If `configured` is false, finish binding the named client, then run `fde setup` and present its **three questions together**, with the two choices each. Use answers already given; do not invent preferences or ask again per client. Save their selections using the displayed `--save` command; their answers authorize this settings write only. If they skip, continue with existing defaults and leave setup unsaved. On later sessions use saved choices; change them only when requested. Setup configures report scope, initial context size and masking of newly generated reports. It does not install a model, approve client data for AI, or change provider settings. Client policy always takes precedence. Reports can still contain names or other unrecognized sensitive prose; never treat them as anonymized.
|
|
62
|
+
|
|
59
63
|
## Entry (every session)
|
|
60
64
|
|
|
61
|
-
1. `fde resume` (16 KiB
|
|
65
|
+
1. `fde resume` (16 KiB by default, 4 KiB with compact setup; a byte ceiling, not a model token count). Read client constraints first, then signer, goals, risks, delivery ledger and current context. This command is the inspectable packet the session hook loads; never substitute a recursive read of `.fde/` or raw transcripts. If truncated or a decision needs evidence, run `fde recall <specific topic>`; narrow the query rather than loading the whole history. `--max-bytes 4096` reduces the allowance for smaller models. `--full` only when the complete log is explicitly needed.
|
|
62
66
|
2. **NO ENGAGEMENT:** ask "What should we call this client?" then **you** init. Pasted notes → debrief after bind.
|
|
63
67
|
3. Playback 2-3 lines. `hygiene:` → offer `fde doctor`; **never auto-rewrite**.
|
|
64
68
|
4. Route. Read **one** `references/*.md`. Confirm, then write.
|