0xray 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore.default +4 -1
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +1 -7
- package/.xray/features.json +4 -1
- package/.xray/features.schema.json +8 -0
- package/AGENTS-consumer.md +1 -1
- package/AGENTS.md +2 -2
- package/README.md +2 -2
- package/dist/.gitignore.default +4 -1
- package/dist/AGENTS.md +2 -2
- package/dist/CHANGELOG.md +27 -1
- package/dist/README.md +2 -2
- package/dist/cli/commands/opencode-install.js +3 -9
- package/dist/cli/commands/skill-install.js +10 -5
- package/dist/cli/goggles-status.d.ts +2 -5
- package/dist/cli/goggles-status.js +5 -53
- package/dist/cli/index.js +2 -2
- package/dist/enforcement/enforcer-tools.d.ts +1 -1
- package/dist/enforcement/enforcer-tools.js +1 -1
- package/dist/integrations/cursor/README.md +1 -1
- package/dist/integrations/cursor/hooks/pre-tool-use.js +4 -0
- package/dist/integrations/grok/grok-cli.js +5 -7
- package/dist/integrations/grok/hooks/grok-hook-utils.js +4 -1
- package/dist/integrations/grok/hooks/pre-tool-use.js +4 -18
- package/dist/integrations/grok/hooks/session-start.js +4 -2
- package/dist/integrations/hermes-agent/bridge.mjs +4 -1
- package/dist/integrations/hooks/goggles-mcp.mjs +20 -196
- package/dist/integrations/hooks/goggles-pipeline.mjs +606 -616
- package/dist/integrations/hooks/goggles-planes.json +88 -40
- package/dist/integrations/hooks/plates.cjs +84 -0
- package/dist/integrations/hooks/pre-tool-speak.mjs +2 -4
- package/dist/integrations/hooks/skill-seek.mjs +115 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +4 -1
- package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +4 -4
- package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +10 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/memory-routing/plates.d.ts +1 -1
- package/dist/memory-routing/plates.js +14 -0
- package/dist/nucleus/thin-dispatch.d.ts +1 -1
- package/dist/nucleus/thin-dispatch.js +1 -1
- package/dist/nucleus/work-freshness.mjs +10 -0
- package/dist/plugin/xray-codex-injection.js +3 -1
- package/dist/skills/orchestrator/SKILL.md +1 -7
- package/docs-site/docs/plates/asides.md +35 -0
- package/docs-site/docs/plates/confer.md +35 -0
- package/docs-site/docs/plates/consumer-mcp.md +35 -0
- package/docs-site/docs/plates/delegation-gate.md +35 -0
- package/docs-site/docs/plates/dynamo.md +35 -0
- package/docs-site/docs/plates/foundry.md +35 -0
- package/docs-site/docs/plates/goggles.md +8 -8
- package/docs-site/docs/plates/ground.md +35 -0
- package/docs-site/docs/plates/hermes.md +35 -0
- package/docs-site/docs/plates/index.md +12 -5
- package/docs-site/docs/plates/inference-cycle.md +35 -0
- package/docs-site/docs/plates/lens-gate.md +6 -6
- package/docs-site/docs/plates/openclaw.md +35 -0
- package/docs-site/docs/plates/opencode.md +35 -0
- package/docs-site/docs/plates/repertoire.md +35 -0
- package/docs-site/docs/plates/synthesis.md +35 -0
- package/docs-site/docs/plates/temperament.md +35 -0
- package/docs-site/docs/plates/trail-state.md +1 -1
- package/package.json +1 -1
- package/scripts/foundry/README.md +3 -1
- package/scripts/foundry/mint-suit.cjs +120 -2
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/foundry/reconcile-version.mjs +10 -3
- package/scripts/foundry/release.mjs +1 -1
- package/scripts/foundry/validate-release-docs.mjs +2 -2
- package/scripts/foundry/version-manager.mjs +12 -4
- package/scripts/node/install-bridges.cjs +63 -34
- package/scripts/node/pack-tmp-suit-proof.mjs +23 -4
- package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +10 -1
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/skills/orchestrator/SKILL.md +1 -7
- package/xray/features.json +4 -1
- package/xray/features.schema.json +8 -0
|
@@ -1,144 +1,279 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Goggles.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* A
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* The hook does not author the plate.
|
|
2
|
+
* Goggles. The plane is state. A session starts on dev.
|
|
3
|
+
* The card is in LENS.md: Entry, Exit, and the other rows.
|
|
4
|
+
* A search inside the current card runs. Any other search returns the card and does not run.
|
|
5
|
+
* Opening a file on that card stays quiet.
|
|
6
|
+
* Opening a file on another plane switches, returns that card, and the open proceeds.
|
|
7
|
+
* A file on no plane stays put and returns the current card.
|
|
8
|
+
* A thing on that plane has its own card. One card can name many plates.
|
|
9
|
+
* A missing plate names the stamp-plate skill. The hook does not write the plate.
|
|
10
|
+
* Compact writes the current card back. It does not add a plane.
|
|
12
11
|
*/
|
|
13
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { seekSkill, skillIdsFromRow, staleSkillRow } from './skill-seek.mjs';
|
|
15
15
|
import { dirname, join, resolve } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
|
|
18
18
|
const FIELD_ORDER = ['plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
|
|
19
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
/** Map names. A look does not answer them. Their files stay on the lens. */
|
|
20
21
|
const WORN_PLANES = ['dichotomy', 'syncopate', 'synthesis', 'digest', 'triage', 'loop'];
|
|
21
|
-
export const CARD_PLANES = ['
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export const CARD_PLANES = ['dev', 'integration', 'domain', 'ecosystem'];
|
|
23
|
+
|
|
24
|
+
const PLANE_PATHS = {
|
|
25
|
+
dev: ['src', 'grok-bot', 'docs-site', 'AGENTS.md', 'package.json', 'README.md'],
|
|
26
|
+
integration: [
|
|
27
|
+
'src/integrations',
|
|
28
|
+
'src/nucleus',
|
|
29
|
+
'src/governance',
|
|
30
|
+
'src/mcps',
|
|
31
|
+
'src/processors',
|
|
32
|
+
'src/reporting',
|
|
33
|
+
'src/core/boot-orchestrator.ts',
|
|
34
|
+
'grok-bot/lib',
|
|
35
|
+
'grok-bot/templates',
|
|
36
|
+
],
|
|
37
|
+
domain: ['docs-site/docs/plates', 'src/integrations/hooks/plates.cjs', '.agents/skills'],
|
|
38
|
+
ecosystem: ['scripts/foundry', 'node_modules/@0xray'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PLANE_ROWS = {
|
|
42
|
+
dev: {
|
|
43
|
+
from: '',
|
|
44
|
+
digest: 'This checkout.',
|
|
45
|
+
plate: '',
|
|
46
|
+
entry: 'package.json, src, and grok-bot/OP-PROC.md.',
|
|
47
|
+
exit: 'A change that stays in this repo.',
|
|
48
|
+
files: 'src, grok-bot, docs-site, AGENTS.md, package.json, README.md',
|
|
49
|
+
skills: 'orchestrator, researcher, project-analysis, git-workflow, tech-writer',
|
|
50
|
+
setup: 'You are in this repo.',
|
|
51
|
+
teardown: 'Leave other repos alone.',
|
|
52
|
+
worn: '',
|
|
53
|
+
},
|
|
54
|
+
integration: {
|
|
55
|
+
from: 'dev',
|
|
56
|
+
digest: 'How one part calls the next.',
|
|
57
|
+
plate: '',
|
|
58
|
+
entry: 'The hook, the dispatch, or the bridge this file sits on.',
|
|
59
|
+
exit: 'The next call. Stop there.',
|
|
60
|
+
files: 'src/integrations, src/nucleus, src/governance, src/mcps, src/processors, src/reporting, src/core/boot-orchestrator.ts, grok-bot/lib, grok-bot/templates',
|
|
61
|
+
skills: 'routing, orchestration, boot-orchestrator, enforcer, hermes-agent',
|
|
62
|
+
setup: 'One connection at a time.',
|
|
63
|
+
teardown: 'Do not sweep the rest of the repo.',
|
|
64
|
+
worn: '',
|
|
65
|
+
},
|
|
66
|
+
domain: {
|
|
67
|
+
from: 'dev',
|
|
68
|
+
digest: 'The drawing of one area.',
|
|
69
|
+
plate: 'docs-site/docs/plates',
|
|
70
|
+
entry: 'The plate under docs-site/docs/plates, or the plate reader.',
|
|
71
|
+
exit: 'The entry and exit written on that plate.',
|
|
72
|
+
files: 'docs-site/docs/plates, src/integrations/hooks/plates.cjs, .agents/skills',
|
|
73
|
+
skills: 'lens-gate, stamp-plate, record-map, station-card, memory-recall',
|
|
74
|
+
setup: 'One plate is in the frame.',
|
|
75
|
+
teardown: 'The plate is the drawing. The code stays on dev or integration.',
|
|
76
|
+
worn: '',
|
|
77
|
+
},
|
|
78
|
+
ecosystem: {
|
|
79
|
+
from: 'dev',
|
|
80
|
+
digest: 'What this repo wears from outside.',
|
|
81
|
+
plate: '',
|
|
82
|
+
entry: 'scripts/foundry, and the @0xray packages this repo wears.',
|
|
83
|
+
exit: 'Back to this checkout when the change is here.',
|
|
84
|
+
files: 'scripts/foundry, node_modules/@0xray',
|
|
85
|
+
skills: 'foundry, suit-wear, consumer-mcp, house, package-face',
|
|
86
|
+
setup: 'The outside package is not this repo.',
|
|
87
|
+
teardown: 'Do not edit a worn package from here.',
|
|
88
|
+
worn: 'node_modules/@0xray',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function builtinCard(id) {
|
|
93
|
+
const row = PLANE_ROWS[id];
|
|
94
|
+
if (!row) return '';
|
|
95
|
+
const lines = [
|
|
96
|
+
['Plane', id],
|
|
97
|
+
['From', row.from],
|
|
98
|
+
['Digest', row.digest],
|
|
99
|
+
['Plate', row.plate],
|
|
100
|
+
['Entry', row.entry],
|
|
101
|
+
['Exit', row.exit],
|
|
102
|
+
['Files', row.files],
|
|
103
|
+
['Skills', row.skills],
|
|
104
|
+
['Setup', row.setup],
|
|
105
|
+
['Teardown', row.teardown],
|
|
106
|
+
['Worn', row.worn],
|
|
107
|
+
];
|
|
108
|
+
return lines.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
|
|
30
109
|
}
|
|
31
110
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
111
|
+
function planeOfPath(file) {
|
|
112
|
+
const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
113
|
+
let best = '';
|
|
114
|
+
let winner = '';
|
|
115
|
+
for (const [id, paths] of Object.entries(PLANE_PATHS)) {
|
|
116
|
+
for (const prefix of paths) {
|
|
117
|
+
if (rel === prefix || rel.startsWith(`${prefix}/`)) {
|
|
118
|
+
if (prefix.length > best.length) {
|
|
119
|
+
best = prefix;
|
|
120
|
+
winner = id;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
38
124
|
}
|
|
125
|
+
return winner;
|
|
39
126
|
}
|
|
40
127
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
128
|
+
/** Empty GOGGLES_PLANES_PATH keeps the file beside the organ. */
|
|
129
|
+
const STAMP_LINE = 'Stamp: .agents/skills/stamp-plate/SKILL.md — one plate for this card.';
|
|
130
|
+
|
|
131
|
+
function mapEntry(id) {
|
|
132
|
+
const table = overlayTable();
|
|
133
|
+
const entry = table[id];
|
|
134
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
|
135
|
+
return entry;
|
|
46
136
|
}
|
|
47
137
|
|
|
48
|
-
|
|
49
|
-
|
|
138
|
+
/** A plane, or a thing that has a card. */
|
|
139
|
+
export function isCardName(id) {
|
|
140
|
+
const name = String(id || '').trim();
|
|
141
|
+
if (!name || name.includes(' ')) return false;
|
|
142
|
+
if (CARD_PLANES.includes(name)) return true;
|
|
143
|
+
return Boolean(mapEntry(name));
|
|
50
144
|
}
|
|
51
145
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
146
|
+
function thingFiles(entry) {
|
|
147
|
+
if (!entry) return [];
|
|
148
|
+
if (Array.isArray(entry.files)) return entry.files.map(String);
|
|
149
|
+
if (typeof entry.file === 'string' && entry.file.trim()) return [entry.file.trim()];
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function planeSpan(file) {
|
|
154
|
+
const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
155
|
+
let best = 0;
|
|
156
|
+
for (const paths of Object.values(PLANE_PATHS)) {
|
|
157
|
+
for (const prefix of paths) {
|
|
158
|
+
if (rel === prefix || rel.startsWith(`${prefix}/`)) best = Math.max(best, prefix.length);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return best;
|
|
55
162
|
}
|
|
56
163
|
|
|
57
|
-
|
|
58
|
-
|
|
164
|
+
export const PATH_OWNER = {
|
|
165
|
+
'src/integrations/hooks/station-hook-runtime.cjs': 'station-card',
|
|
166
|
+
'src/integrations/hooks/wake-cascade.cjs': 'record-map',
|
|
167
|
+
'src/integrations/hooks/session-note-index.cjs': 'notes-page',
|
|
168
|
+
'src/reporting/framework-reporting-system.ts': 'reporting',
|
|
169
|
+
'src/inference/inference-accumulator.ts': 'inference-files',
|
|
170
|
+
'src/integrations/hooks/goggles-pipeline.mjs': 'lens-gate',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function covers(listed, rel) {
|
|
174
|
+
return rel === listed || rel.startsWith(`${listed}/`);
|
|
59
175
|
}
|
|
60
176
|
|
|
61
|
-
function
|
|
62
|
-
|
|
177
|
+
function rootsOf(entry) {
|
|
178
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
|
|
179
|
+
if (Array.isArray(entry.root)) return entry.root.map(String).filter((item) => item.trim());
|
|
180
|
+
if (typeof entry.root === 'string' && entry.root.trim()) return [entry.root.trim()];
|
|
181
|
+
return [];
|
|
63
182
|
}
|
|
64
183
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return ids;
|
|
184
|
+
function cardMatchPaths(id) {
|
|
185
|
+
if (CARD_PLANES.includes(id)) return PLANE_PATHS[id] || [];
|
|
186
|
+
const row = mapEntry(id);
|
|
187
|
+
if (!row) return [];
|
|
188
|
+
return [...thingFiles(row), ...rootsOf(row)];
|
|
71
189
|
}
|
|
72
190
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
191
|
+
/** Longest listed path. A root may equal the plane. A tie uses PATH_OWNER, or stays the plane. */
|
|
192
|
+
export function thingOfPath(file) {
|
|
193
|
+
const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
194
|
+
const table = overlayTable();
|
|
195
|
+
let best = -1;
|
|
196
|
+
const hits = [];
|
|
197
|
+
for (const [id, entry] of Object.entries(table)) {
|
|
198
|
+
if (CARD_PLANES.includes(id)) continue;
|
|
199
|
+
const row = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
|
|
200
|
+
if (!row) continue;
|
|
201
|
+
const span = planeSpan(rel);
|
|
202
|
+
const consider = (listed, floor) => {
|
|
203
|
+
if (!covers(listed, rel) || listed.length < floor) return;
|
|
204
|
+
if (listed.length > best) {
|
|
205
|
+
best = listed.length;
|
|
206
|
+
hits.length = 0;
|
|
207
|
+
}
|
|
208
|
+
if (listed.length === best) hits.push({ id, listed });
|
|
209
|
+
};
|
|
210
|
+
for (const listed of thingFiles(row)) consider(listed, span + 1);
|
|
211
|
+
for (const listed of rootsOf(row)) consider(listed, span);
|
|
81
212
|
}
|
|
213
|
+
const ids = [];
|
|
214
|
+
for (const hit of hits) {
|
|
215
|
+
if (!ids.includes(hit.id)) ids.push(hit.id);
|
|
216
|
+
}
|
|
217
|
+
if (ids.length === 1) return ids[0];
|
|
218
|
+
if (!ids.length) return '';
|
|
219
|
+
const owner = PATH_OWNER[hits[0].listed];
|
|
220
|
+
return owner && ids.includes(owner) ? owner : '';
|
|
82
221
|
}
|
|
83
222
|
|
|
84
|
-
|
|
85
|
-
|
|
223
|
+
|
|
224
|
+
function platePath(id) {
|
|
225
|
+
if (CARD_PLANES.includes(id)) return '';
|
|
226
|
+
const dir = findPlatesDir(HERE);
|
|
227
|
+
if (!dir) return '';
|
|
228
|
+
const abs = join(dir, `${id}.md`);
|
|
229
|
+
return existsSync(abs) ? join('docs-site', 'docs', 'plates', `${id}.md`) : '';
|
|
86
230
|
}
|
|
87
231
|
|
|
88
|
-
function
|
|
89
|
-
|
|
232
|
+
function showCard(id, block) {
|
|
233
|
+
if (CARD_PLANES.includes(id)) return block;
|
|
234
|
+
const path = platePath(id);
|
|
235
|
+
let shown = block;
|
|
236
|
+
if (path && /^Plate:[ \t]*$/m.test(shown)) shown = shown.replace(/^Plate:[ \t]*$/m, `Plate: ${path}`);
|
|
237
|
+
if (!path && !shown.includes('Stamp:')) shown = `${shown}\n${STAMP_LINE}`;
|
|
238
|
+
return shown;
|
|
90
239
|
}
|
|
91
240
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (drawn.length) {
|
|
103
|
-
const allowed = associatedPlates(plane);
|
|
104
|
-
const foreign = drawn.filter((id) => !allowed.has(id));
|
|
105
|
-
if (foreign.length) {
|
|
106
|
-
return organDeny(`The reading is ${plane}. That drawing is not the plane.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const otherFile = otherPlaneFile(plane, spoken);
|
|
110
|
-
if (otherFile) return organDeny(`The reading is ${plane}. The action is ${otherFile}.`);
|
|
111
|
-
const stripped = withoutPaths(text);
|
|
112
|
-
const others = acceptedPlanes().filter((name) => name !== plane && planeWord(name, stripped));
|
|
113
|
-
const namesThis = planeWord(plane, stripped);
|
|
114
|
-
if (others.length > 1 || (others.length === 1 && namesThis)) {
|
|
115
|
-
return organDeny(`The reading is ${plane}. The action names two planes.`);
|
|
116
|
-
}
|
|
117
|
-
if (others.length === 1) {
|
|
118
|
-
return organDeny(`The reading is ${plane}. The action is ${others[0]}.`);
|
|
241
|
+
function readHold(root) {
|
|
242
|
+
try {
|
|
243
|
+
const data = JSON.parse(readFileSync(join(root, '.xray', 'state', 'goggles-plane.json'), 'utf8'));
|
|
244
|
+
const named = typeof data.card === 'string' ? data.card : '';
|
|
245
|
+
const plane = CARD_PLANES.includes(data.plane) ? data.plane : '';
|
|
246
|
+
const card = isCardName(named) ? named : (plane || '');
|
|
247
|
+
if (plane) return { plane, card: card || plane };
|
|
248
|
+
if (card && CARD_PLANES.includes(card)) return { plane: card, card };
|
|
249
|
+
} catch {
|
|
250
|
+
/* no hold yet */
|
|
119
251
|
}
|
|
120
|
-
return
|
|
252
|
+
return null;
|
|
121
253
|
}
|
|
122
254
|
|
|
123
|
-
function
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
return null;
|
|
255
|
+
function planeForCard(id) {
|
|
256
|
+
if (CARD_PLANES.includes(id)) return id;
|
|
257
|
+
for (const file of thingFiles(mapEntry(id))) {
|
|
258
|
+
const plane = planeOfPath(file);
|
|
259
|
+
if (plane) return plane;
|
|
129
260
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
261
|
+
return 'dev';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function planesFile() {
|
|
265
|
+
const override = process.env.GOGGLES_PLANES_PATH;
|
|
266
|
+
if (typeof override === 'string' && override.trim()) return override.trim();
|
|
267
|
+
return join(HERE, 'goggles-planes.json');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function drawnPlanes() {
|
|
271
|
+
try {
|
|
272
|
+
const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
|
|
273
|
+
return Array.isArray(data.planes) ? data.planes.map((name) => String(name)) : [];
|
|
274
|
+
} catch {
|
|
275
|
+
return [];
|
|
140
276
|
}
|
|
141
|
-
return null;
|
|
142
277
|
}
|
|
143
278
|
|
|
144
279
|
function readingPath(root) {
|
|
@@ -157,61 +292,89 @@ function loadReading(root) {
|
|
|
157
292
|
}
|
|
158
293
|
}
|
|
159
294
|
|
|
160
|
-
function saveReading(root, rec) {
|
|
161
|
-
const file = readingPath(root);
|
|
162
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
163
|
-
writeFileSync(file, `${JSON.stringify(rec)}\n`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
295
|
function clearReading(root) {
|
|
167
296
|
const file = readingPath(root);
|
|
168
297
|
if (existsSync(file)) unlinkSync(file);
|
|
169
298
|
}
|
|
170
299
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const ordered = [...SCOPE_ZOOM].sort((a, b) => b.length - a.length);
|
|
175
|
-
for (const scope of ordered) {
|
|
176
|
-
const needle = ` ${scope} `;
|
|
177
|
-
if (!rest.includes(needle)) continue;
|
|
178
|
-
hits.push(scope);
|
|
179
|
-
rest = rest.split(needle).join(' ');
|
|
300
|
+
function lensRoot(projectRoot) {
|
|
301
|
+
if (typeof projectRoot === 'string' && projectRoot.trim() && projectRoot.trim() !== '.') {
|
|
302
|
+
return resolve(projectRoot.trim());
|
|
180
303
|
}
|
|
181
|
-
return
|
|
304
|
+
return stateRoot();
|
|
182
305
|
}
|
|
183
306
|
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
307
|
+
function cardRow(block, label) {
|
|
308
|
+
const match = String(block || '').match(new RegExp(`^${label}:[ \t]*(.*)$`, 'm'));
|
|
309
|
+
return match ? match[1].trim() : '';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function splitRow(block, label) {
|
|
313
|
+
return cardRow(block, label).split(',').map((part) => part.trim()).filter(Boolean);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function installedPack(root) {
|
|
317
|
+
return /\/node_modules\/0xray$/.test(String(root || '').replace(/\\/g, '/'));
|
|
193
318
|
}
|
|
194
319
|
|
|
195
|
-
function
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
320
|
+
export function liveDrift(block, root) {
|
|
321
|
+
if (installedPack(root)) return '';
|
|
322
|
+
const files = splitRow(block, 'Files');
|
|
323
|
+
const worn = cardRow(block, 'Worn');
|
|
324
|
+
const held = Boolean(worn) && existsSync(join(root, worn));
|
|
325
|
+
const packed = (rel) => held && (rel.startsWith('src/') || rel.startsWith('grok-bot/'));
|
|
326
|
+
for (const rel of files) {
|
|
327
|
+
if (existsSync(join(root, rel))) continue;
|
|
328
|
+
if (packed(rel)) continue;
|
|
329
|
+
return `Drift: ${rel} is not there`;
|
|
200
330
|
}
|
|
201
|
-
|
|
331
|
+
const skills = cardRow(block, 'Skills');
|
|
332
|
+
if (skills.includes('/') && !existsSync(join(root, skills)) && !packed(skills)) {
|
|
333
|
+
return `Drift: ${skills} is not there`;
|
|
334
|
+
}
|
|
335
|
+
return '';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function viewFromBlock(block, drift) {
|
|
339
|
+
const plane = cardRow(block, 'Plane');
|
|
340
|
+
return {
|
|
341
|
+
card: cardRow(block, 'Card') || plane,
|
|
342
|
+
plane,
|
|
343
|
+
stamp: cardRow(block, 'Stamp'),
|
|
344
|
+
scope: '',
|
|
345
|
+
from: cardRow(block, 'From'),
|
|
346
|
+
digest: cardRow(block, 'Digest'),
|
|
347
|
+
plate: cardRow(block, 'Plate'),
|
|
348
|
+
plates: splitRow(block, 'Plates'),
|
|
349
|
+
entry: cardRow(block, 'Entry'),
|
|
350
|
+
exit: cardRow(block, 'Exit'),
|
|
351
|
+
files: splitRow(block, 'Files'),
|
|
352
|
+
skills: cardRow(block, 'Skills'),
|
|
353
|
+
setup: cardRow(block, 'Setup'),
|
|
354
|
+
teardown: cardRow(block, 'Teardown'),
|
|
355
|
+
worn: cardRow(block, 'Worn'),
|
|
356
|
+
exam: drift || '',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function storedCard(root, id) {
|
|
361
|
+
const kept = readLensCard(root, id);
|
|
362
|
+
if (kept) return kept;
|
|
363
|
+
return fillLensCard(root, id);
|
|
202
364
|
}
|
|
203
365
|
|
|
204
|
-
function
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
366
|
+
function presentCard(projectRoot, id) {
|
|
367
|
+
const block = storedCard(lensRoot(projectRoot), id);
|
|
368
|
+
if (!block) return null;
|
|
369
|
+
const shown = showCard(id, block);
|
|
370
|
+
const drift = liveDrift(shown, sourceRoot(findPlatesDir(HERE)));
|
|
371
|
+
return {
|
|
372
|
+
text: drift ? `${shown}\n${drift}` : shown,
|
|
373
|
+
view: viewFromBlock(shown, drift),
|
|
374
|
+
};
|
|
212
375
|
}
|
|
213
376
|
|
|
214
|
-
function gogglesAnswer(argv, projectRoot) {
|
|
377
|
+
function gogglesAnswer(argv, projectRoot, hold) {
|
|
215
378
|
const words = [];
|
|
216
379
|
for (const raw of argv || []) {
|
|
217
380
|
const arg = String(raw || '').trim();
|
|
@@ -219,120 +382,49 @@ function gogglesAnswer(argv, projectRoot) {
|
|
|
219
382
|
words.push(arg);
|
|
220
383
|
}
|
|
221
384
|
const lower = words.map((word) => word.toLowerCase());
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (flavors.length === 1) {
|
|
235
|
-
const others = acceptedPlanes().filter((name) => name !== flavors[0] && lower.includes(name));
|
|
236
|
-
if (others.length) return none('');
|
|
237
|
-
const cards = collectCards(flavors[0], lower, projectRoot);
|
|
238
|
-
if (!cards) return none('');
|
|
239
|
-
return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
|
|
240
|
-
}
|
|
241
|
-
const planes = acceptedPlanes().filter((name) => lower.includes(name));
|
|
242
|
-
if (planes.length !== 1) {
|
|
243
|
-
if (!planes.length && cardPlanesNamed(lower).length) {
|
|
244
|
-
const cards = collectCards('digest', lower, projectRoot);
|
|
245
|
-
if (cards && cards.length) {
|
|
246
|
-
return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return none('');
|
|
250
|
-
}
|
|
251
|
-
const scopes = findScopes(words.filter((word) => word.toLowerCase() !== planes[0]));
|
|
252
|
-
if (scopes.length > 1) return none('');
|
|
253
|
-
const line = readingLine(planes[0], scopes[0] || '');
|
|
254
|
-
const file = moduleFile(planes[0]);
|
|
255
|
-
return none(file ? `${line} File: ${file}` : line);
|
|
385
|
+
const named = cardsNamed(lower);
|
|
386
|
+
const root = lensRoot(projectRoot);
|
|
387
|
+
let id = '';
|
|
388
|
+
if (named.length === 1) id = named[0];
|
|
389
|
+
else if (words.length === 0) {
|
|
390
|
+
if (hold) ensurePlane(root);
|
|
391
|
+
id = currentCard(root);
|
|
392
|
+
} else return { text: 'Name one plane.', cards: null };
|
|
393
|
+
if (hold && named.length === 1) holdPlane(root, id);
|
|
394
|
+
const card = presentCard(root, id);
|
|
395
|
+
if (!card) return { text: 'Name one plane.', cards: null };
|
|
396
|
+
return { text: card.text, cards: [card.view] };
|
|
256
397
|
}
|
|
257
398
|
|
|
258
399
|
export function readGoggles(argv, projectRoot) {
|
|
259
|
-
return ok(gogglesAnswer(argv, projectRoot).text);
|
|
400
|
+
return ok(gogglesAnswer(argv, projectRoot, true).text);
|
|
260
401
|
}
|
|
261
402
|
|
|
262
403
|
export function lookCards(argv, projectRoot) {
|
|
263
|
-
return gogglesAnswer(argv, projectRoot).cards;
|
|
404
|
+
return gogglesAnswer(argv, projectRoot, false).cards;
|
|
264
405
|
}
|
|
265
406
|
|
|
266
|
-
function
|
|
267
|
-
|
|
407
|
+
function cardsNamed(lower) {
|
|
408
|
+
const found = [];
|
|
409
|
+
for (const word of lower) {
|
|
410
|
+
if (isCardName(word) && !found.includes(word)) found.push(word);
|
|
411
|
+
}
|
|
412
|
+
return found;
|
|
268
413
|
}
|
|
269
414
|
|
|
270
415
|
function listedFiles(plane) {
|
|
271
416
|
return Array.isArray(plane.files) ? plane.files.map(String) : [];
|
|
272
417
|
}
|
|
273
418
|
|
|
274
|
-
function cascadeApi() {
|
|
275
|
-
return createRequire(import.meta.url)('./wake-cascade.cjs');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function sleeveApi() {
|
|
279
|
-
return createRequire(import.meta.url)('./sleeve.cjs');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function appendLining(text, view) {
|
|
283
|
-
let next = text;
|
|
284
|
-
if (view.cascade) next = `${next}\n${cascadeApi().formatCascadeReading(view.cascade)}`;
|
|
285
|
-
if (view.sleeve) next = `${next}\n${sleeveApi().formatSleeveReading(view.sleeve)}`;
|
|
286
|
-
return next;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function cardView(plane, scope, flavor, root, cascadeRoot) {
|
|
290
|
-
const listed = listedFiles(plane);
|
|
291
|
-
const view = {
|
|
292
|
-
plane: plane.id,
|
|
293
|
-
flavor,
|
|
294
|
-
scope: scope || '',
|
|
295
|
-
from: plane.id === 'ground' ? '' : (plane.from || 'ground'),
|
|
296
|
-
digest: plane.digest || '',
|
|
297
|
-
plate: plane.plate || '',
|
|
298
|
-
plates: Array.isArray(plane.plates) ? plane.plates : [],
|
|
299
|
-
entry: plane.entry || '',
|
|
300
|
-
exit: plane.exit || '',
|
|
301
|
-
files: listed,
|
|
302
|
-
skills: plane.skills || '',
|
|
303
|
-
setup: plane.setup || '',
|
|
304
|
-
teardown: plane.teardown || '',
|
|
305
|
-
worn: plane.worn || '',
|
|
306
|
-
};
|
|
307
|
-
view.exam = examinePlane(plane, root).text;
|
|
308
|
-
if (cascadeApi().isCascadePlane(plane.id)) {
|
|
309
|
-
try {
|
|
310
|
-
view.cascade = cascadeApi().readWakeCascade(cascadeRoot || stateRoot());
|
|
311
|
-
} catch {
|
|
312
|
-
view.cascade = null;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (sleeveApi().isSleevePlane(plane.id)) {
|
|
316
|
-
try {
|
|
317
|
-
view.sleeve = sleeveApi().readSleeve(cascadeRoot || stateRoot());
|
|
318
|
-
} catch {
|
|
319
|
-
view.sleeve = null;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (flavor === 'triage') {
|
|
323
|
-
const filled = new Set(filledOf(plane));
|
|
324
|
-
view.empty = FIELD_ORDER.filter((name) => !filled.has(name));
|
|
325
|
-
}
|
|
326
|
-
return view;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
419
|
function cardRows(view) {
|
|
330
|
-
const rows = [
|
|
420
|
+
const rows = [];
|
|
421
|
+
if (view.card && view.card !== view.plane) rows.push(['Card', view.card]);
|
|
422
|
+
rows.push(
|
|
331
423
|
['Plane', view.plane],
|
|
332
424
|
['From', view.from],
|
|
333
425
|
['Digest', view.digest],
|
|
334
426
|
['Plate', view.plate],
|
|
335
|
-
|
|
427
|
+
);
|
|
336
428
|
if (Array.isArray(view.plates) && view.plates.length) rows.push(['Plates', view.plates.join(', ')]);
|
|
337
429
|
rows.push(
|
|
338
430
|
['Entry', view.entry],
|
|
@@ -346,52 +438,19 @@ function cardRows(view) {
|
|
|
346
438
|
return rows;
|
|
347
439
|
}
|
|
348
440
|
|
|
349
|
-
function formatCardText(view) {
|
|
350
|
-
const rows = cardRows(view);
|
|
351
|
-
let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
|
|
352
|
-
if (view.flavor === 'triage') {
|
|
353
|
-
const lines = [];
|
|
354
|
-
if (view.empty.length) lines.push(`Empty: ${view.empty.join(', ')}`);
|
|
355
|
-
lines.push(view.exam);
|
|
356
|
-
text = `${text}\n${lines.join('\n')}`;
|
|
357
|
-
} else if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
|
|
358
|
-
text = `${text}\n${view.exam}`;
|
|
359
|
-
}
|
|
360
|
-
return appendLining(text, view);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
441
|
export function formatCardPane(view) {
|
|
364
442
|
const rows = cardRows(view);
|
|
365
|
-
if (view.
|
|
366
|
-
if (Array.isArray(view.empty) && view.empty.length) rows.push(['Empty', view.empty.join(', ')]);
|
|
367
|
-
if (view.exam) rows.push(['', view.exam]);
|
|
368
|
-
} else if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
|
|
443
|
+
if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
|
|
369
444
|
rows.push(['', view.exam]);
|
|
370
445
|
}
|
|
371
446
|
const body = rows.map(([label, value]) => (label ? `${label.padEnd(8)} ${value}` : value).trimEnd());
|
|
372
|
-
const title =
|
|
447
|
+
const title = view.card && view.card !== view.plane ? view.card : view.plane;
|
|
373
448
|
const width = Math.max(title.length + 2, ...body.map((line) => line.length));
|
|
374
449
|
const top = `┌ ${title} ${'─'.repeat(Math.max(0, width - title.length - 1))}┐`;
|
|
375
450
|
const mid = body.map((line) => `│ ${line.padEnd(width)} │`);
|
|
376
451
|
const bot = `└${'─'.repeat(width + 2)}┘`;
|
|
377
452
|
const pane = [top, ...mid, bot].join('\n');
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function collectCards(flavor, lower, projectRoot) {
|
|
382
|
-
const named = cardPlanesNamed(lower);
|
|
383
|
-
const scopes = findScopes(lower.filter((word) => word !== flavor && !named.includes(word)));
|
|
384
|
-
if (scopes.length > 1) return null;
|
|
385
|
-
const scope = scopes[0] || '';
|
|
386
|
-
const platesDir = findPlatesDir(HERE);
|
|
387
|
-
const ids = named.length ? named : CARD_PLANES;
|
|
388
|
-
const root = sourceRoot(platesDir);
|
|
389
|
-
const cards = [];
|
|
390
|
-
for (const id of ids) {
|
|
391
|
-
const plane = growPlane(id, platesDir, stateRoot());
|
|
392
|
-
cards.push(cardView(plane, scope, flavor, root, projectRoot));
|
|
393
|
-
}
|
|
394
|
-
return cards;
|
|
453
|
+
return pane;
|
|
395
454
|
}
|
|
396
455
|
|
|
397
456
|
export function findPlatesDir(start) {
|
|
@@ -530,7 +589,7 @@ export function filledOf(plane) {
|
|
|
530
589
|
}
|
|
531
590
|
|
|
532
591
|
function plateSource(id, platesDir) {
|
|
533
|
-
if (!platesDir
|
|
592
|
+
if (!platesDir) return { body: '', path: '' };
|
|
534
593
|
const abs = join(platesDir, `${id}.md`);
|
|
535
594
|
if (!existsSync(abs)) return { body: '', path: '' };
|
|
536
595
|
return {
|
|
@@ -567,7 +626,11 @@ export function assemblePlane(id, platesDir) {
|
|
|
567
626
|
const doors = src.body ? doorsOf(src.body) : { entry: null, exit: null };
|
|
568
627
|
const flow = src.body ? flowEnds(src.body) : null;
|
|
569
628
|
const digest = extra.digest || (src.body ? takeOf(stripFrontmatter(src.body)) : '');
|
|
570
|
-
const
|
|
629
|
+
const listed = Array.isArray(extra.files)
|
|
630
|
+
? extra.files.map(String)
|
|
631
|
+
: (typeof extra.file === 'string' && extra.file.trim() ? [extra.file.trim()] : []);
|
|
632
|
+
const roots = rootsOf(extra);
|
|
633
|
+
const files = [...roots.filter((item) => !listed.includes(item)), ...listed];
|
|
571
634
|
return {
|
|
572
635
|
id,
|
|
573
636
|
digest,
|
|
@@ -602,25 +665,6 @@ function sourceRoot(platesDir) {
|
|
|
602
665
|
return platesDir ? repoRootFrom(platesDir) : stateRoot();
|
|
603
666
|
}
|
|
604
667
|
|
|
605
|
-
function viewsPath(root) {
|
|
606
|
-
return join(root, '.xray', 'state', 'goggles-views.json');
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function loadViews(root) {
|
|
610
|
-
try {
|
|
611
|
-
const data = JSON.parse(readFileSync(viewsPath(root), 'utf8'));
|
|
612
|
-
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
613
|
-
} catch {
|
|
614
|
-
return {};
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function saveViews(root, views) {
|
|
619
|
-
const file = viewsPath(root);
|
|
620
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
621
|
-
writeFileSync(file, `${JSON.stringify(views, null, 2)}\n`);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
668
|
function cluesOf(body) {
|
|
625
669
|
if (!body) return [];
|
|
626
670
|
const clues = [];
|
|
@@ -714,29 +758,8 @@ function skillFor(id, root) {
|
|
|
714
758
|
return existsSync(join(root, rel)) ? rel : '';
|
|
715
759
|
}
|
|
716
760
|
|
|
717
|
-
function mappedFile(name) {
|
|
718
|
-
try {
|
|
719
|
-
const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
|
|
720
|
-
const entry = data[name];
|
|
721
|
-
if (!entry || typeof entry !== 'object') return '';
|
|
722
|
-
if (typeof entry.file === 'string' && entry.file) return entry.file;
|
|
723
|
-
} catch {
|
|
724
|
-
return '';
|
|
725
|
-
}
|
|
726
|
-
return '';
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function moduleFile(name) {
|
|
730
|
-
const mapped = mappedFile(name);
|
|
731
|
-
if (mapped) return mapped;
|
|
732
|
-
const platesDir = findPlatesDir(HERE);
|
|
733
|
-
const root = sourceRoot(platesDir);
|
|
734
|
-
const hit = sourceTexts(root).find((file) => file.rel.endsWith(`/${name}.ts`) || file.rel.endsWith(`/${name}.mjs`));
|
|
735
|
-
return hit ? hit.rel : '';
|
|
736
|
-
}
|
|
737
|
-
|
|
738
761
|
function hintLine(name) {
|
|
739
|
-
const cards = lookCards([
|
|
762
|
+
const cards = lookCards([name]);
|
|
740
763
|
const files = cards && cards[0] && Array.isArray(cards[0].files) ? cards[0].files : [];
|
|
741
764
|
return files.length ? `${name}: ${files.join(', ')}` : '';
|
|
742
765
|
}
|
|
@@ -749,7 +772,6 @@ export function suitHint(text) {
|
|
|
749
772
|
}
|
|
750
773
|
|
|
751
774
|
const DEEP_SEARCH = /search_codebase|find_implementation|get_documentation/i;
|
|
752
|
-
const HOLDS_ON_NAME = new Set(['dichotomy', 'syncopate', 'synthesis']);
|
|
753
775
|
const SHELL_SEARCH = /(?:^|&&|\|\||[;&|`(\n])\s*(?:(?:\/[\w.+/-]+\/)|(?:npx\s+))?(?:rg|grep|ag|ack|fd|find)\b|\bgit\s+grep\b/;
|
|
754
776
|
|
|
755
777
|
function shellSearches(text) {
|
|
@@ -769,15 +791,6 @@ function mappedPlaneFiles(name) {
|
|
|
769
791
|
}
|
|
770
792
|
}
|
|
771
793
|
|
|
772
|
-
function otherPlaneFile(held, spoken) {
|
|
773
|
-
const text = String(spoken || '');
|
|
774
|
-
for (const name of acceptedPlanes()) {
|
|
775
|
-
if (name === held) continue;
|
|
776
|
-
if (mappedPlaneFiles(name).some((file) => text.includes(file))) return name;
|
|
777
|
-
}
|
|
778
|
-
return '';
|
|
779
|
-
}
|
|
780
|
-
|
|
781
794
|
function mapFiles() {
|
|
782
795
|
try {
|
|
783
796
|
const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
|
|
@@ -826,186 +839,205 @@ function shellReadsSource(text) {
|
|
|
826
839
|
return SOURCE_READER.test(spoken) && openedSourcePaths(spoken).length > 0;
|
|
827
840
|
}
|
|
828
841
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
842
|
+
/** One plane: the files the suit continues with. A look with no plane stops. Two planes stay quiet. */
|
|
843
|
+
function researchCall(toolName, text) {
|
|
844
|
+
const tool = String(toolName || '');
|
|
845
|
+
const spoken = String(text || '');
|
|
846
|
+
if (/researcher|explorer|deep[- ]?research/i.test(tool) || DEEP_SEARCH.test(tool)) return true;
|
|
847
|
+
if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken) || shellReadsSource(spoken))) return true;
|
|
848
|
+
if (/^(grep|glob)$/i.test(tool)) return true;
|
|
849
|
+
return /^read_file$/i.test(tool) && !opensLensFile(spoken);
|
|
835
850
|
}
|
|
836
851
|
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
852
|
+
export function lensPlane(text) {
|
|
853
|
+
const hint = suitHint(String(text || ''));
|
|
854
|
+
if (hint === 'Name one plane.') return { stop: true, files: [] };
|
|
855
|
+
if (!hint || hint.includes('\n')) return { stop: false, files: [] };
|
|
856
|
+
const mark = hint.indexOf(': ');
|
|
857
|
+
if (mark < 0) return { stop: false, files: [] };
|
|
858
|
+
const files = hint.slice(mark + 2).split(',').map((part) => part.trim()).filter(Boolean);
|
|
859
|
+
return { stop: false, files };
|
|
844
860
|
}
|
|
845
861
|
|
|
846
|
-
function
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const dest = debtFile(root);
|
|
852
|
-
if (!unique.length) {
|
|
853
|
-
try {
|
|
854
|
-
unlinkSync(dest);
|
|
855
|
-
} catch {
|
|
856
|
-
/* already clear */
|
|
857
|
-
}
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
862
|
+
export function holdPlane(root, id) {
|
|
863
|
+
const name = String(id || '').trim();
|
|
864
|
+
if (!name || name.includes(' ') || !isCardName(name)) return;
|
|
865
|
+
const plane = planeForCard(name);
|
|
866
|
+
const dest = join(root, '.xray', 'state', 'goggles-plane.json');
|
|
860
867
|
mkdirSync(dirname(dest), { recursive: true });
|
|
861
|
-
writeFileSync(dest, `${JSON.stringify({
|
|
868
|
+
writeFileSync(dest, `${JSON.stringify({ plane, card: name })}\n`);
|
|
869
|
+
maintainLens(root);
|
|
862
870
|
}
|
|
863
871
|
|
|
864
|
-
function
|
|
872
|
+
function stampPlane(root, id) {
|
|
865
873
|
try {
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
for (const [name, entry] of Object.entries(data)) {
|
|
870
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
871
|
-
if (!Array.isArray(entry.files)) continue;
|
|
872
|
-
records.push([name, entry.files.filter((file) => typeof file === 'string')]);
|
|
873
|
-
}
|
|
874
|
-
return records;
|
|
874
|
+
const plates = createRequire(import.meta.url)('./plates.cjs');
|
|
875
|
+
if (!plates.PLATE_IDS.includes(id)) return;
|
|
876
|
+
plates.stampPlateIfMissing(root, id);
|
|
875
877
|
} catch {
|
|
876
|
-
|
|
878
|
+
/* a missed stamp must not fail the stop */
|
|
877
879
|
}
|
|
878
880
|
}
|
|
879
881
|
|
|
880
|
-
function
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (!files.includes(file)) continue;
|
|
889
|
-
if (name === 'ground' || plateIsOnDisk(root, name)) return true;
|
|
882
|
+
function openPaths(text) {
|
|
883
|
+
const raw = String(text || '').replace(/\\/g, '/');
|
|
884
|
+
const found = [];
|
|
885
|
+
const re = /(?:^|[^A-Za-z0-9_])((?:src|scripts|docs-site|\.agents|grok-bot|node_modules\/@0xray)\/[\w./@-]*|AGENTS\.md|package\.json|README\.md|\.mcp\.json)/g;
|
|
886
|
+
let match = re.exec(raw);
|
|
887
|
+
while (match) {
|
|
888
|
+
found.push(match[1].replace(/\/+$/, ''));
|
|
889
|
+
match = re.exec(raw);
|
|
890
890
|
}
|
|
891
|
-
return
|
|
891
|
+
return found;
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
-
function
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
if (
|
|
898
|
-
return
|
|
894
|
+
function isSearch(tool, text) {
|
|
895
|
+
const name = String(tool || '');
|
|
896
|
+
const spoken = String(text || '');
|
|
897
|
+
if (/researcher|explorer|deep[- ]?research/i.test(name) || DEEP_SEARCH.test(name)) return true;
|
|
898
|
+
if (/semanticsearch|codebase_search/i.test(name)) return true;
|
|
899
|
+
if (/^(grep|glob|rg|ripgrep)$/i.test(name)) return true;
|
|
900
|
+
return /bash|shell/i.test(name) && (shellSearches(spoken) || DEEP_SEARCH.test(spoken) || /researcher|explorer|deep[- ]?research/i.test(spoken));
|
|
899
901
|
}
|
|
900
902
|
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
saveDebt(root, [...loadDebt(root), ...fresh]);
|
|
903
|
+
function isRead(tool, text) {
|
|
904
|
+
if (/^(read_file|read)$/i.test(String(tool || ''))) return true;
|
|
905
|
+
return /bash|shell/i.test(String(tool || '')) && shellReadsSource(text) && !shellSearches(text);
|
|
905
906
|
}
|
|
906
907
|
|
|
907
|
-
function
|
|
908
|
-
|
|
908
|
+
function currentPlane(root) {
|
|
909
|
+
const held = readHold(root);
|
|
910
|
+
if (held && CARD_PLANES.includes(held.plane)) return held.plane;
|
|
911
|
+
return 'dev';
|
|
909
912
|
}
|
|
910
913
|
|
|
911
|
-
function
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken) || shellReadsSource(spoken))) return true;
|
|
916
|
-
if (/^(grep|glob)$/i.test(tool)) return true;
|
|
917
|
-
return /^read_file$/i.test(tool) && !opensLensFile(spoken);
|
|
914
|
+
function currentCard(root) {
|
|
915
|
+
const held = readHold(root);
|
|
916
|
+
if (held && held.card) return held.card;
|
|
917
|
+
return currentPlane(root);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
-
function
|
|
921
|
-
|
|
920
|
+
function ensurePlane(root) {
|
|
921
|
+
const held = readHold(root);
|
|
922
|
+
if (held && CARD_PLANES.includes(held.plane)) return held.plane;
|
|
923
|
+
holdPlane(root, 'dev');
|
|
924
|
+
return 'dev';
|
|
922
925
|
}
|
|
923
926
|
|
|
924
|
-
function
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (!data || data.read !== true) return null;
|
|
928
|
-
return data.spent === true ? 'spent' : 'armed';
|
|
929
|
-
} catch {
|
|
930
|
-
return null;
|
|
931
|
-
}
|
|
927
|
+
function cardText(root, id) {
|
|
928
|
+
const card = presentCard(root, id);
|
|
929
|
+
return card && card.text ? card.text : '';
|
|
932
930
|
}
|
|
933
931
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
932
|
+
/** The current card, for the boot payload. Starts on dev. */
|
|
933
|
+
export function planeCard(root) {
|
|
934
|
+
const plane = ensurePlane(root);
|
|
935
|
+
return { plane, card: cardText(root, currentCard(root)) };
|
|
938
936
|
}
|
|
939
937
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
if (
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (mark < 0) return { stop: false, files: [] };
|
|
947
|
-
const files = hint.slice(mark + 2).split(',').map((part) => part.trim()).filter(Boolean);
|
|
948
|
-
return { stop: false, files };
|
|
938
|
+
|
|
939
|
+
function rankedSkillRow(id) {
|
|
940
|
+
const builtin = PLANE_ROWS[id];
|
|
941
|
+
if (builtin && builtin.skills) return builtin.skills;
|
|
942
|
+
const row = overlayTable()[id];
|
|
943
|
+
return row && typeof row.skills === 'string' ? row.skills : '';
|
|
949
944
|
}
|
|
950
945
|
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
946
|
+
function skillNamesOnCards() {
|
|
947
|
+
const names = new Set();
|
|
948
|
+
for (const row of Object.values(PLANE_ROWS)) {
|
|
949
|
+
for (const id of skillIdsFromRow(row.skills)) names.add(id);
|
|
950
|
+
}
|
|
951
|
+
for (const row of Object.values(overlayTable())) {
|
|
952
|
+
if (!row || typeof row.skills !== 'string') continue;
|
|
953
|
+
for (const id of skillIdsFromRow(row.skills)) names.add(id);
|
|
958
954
|
}
|
|
959
|
-
return
|
|
955
|
+
return names;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Replace a stored Skills row that still points at the catalog or one file. */
|
|
959
|
+
export function migrateLensSkills(root) {
|
|
960
|
+
const prev = readLensText(root);
|
|
961
|
+
if (!prev) return false;
|
|
962
|
+
const cards = lensCards(prev);
|
|
963
|
+
let next = prev;
|
|
964
|
+
let changed = false;
|
|
965
|
+
for (const [id, block] of cards) {
|
|
966
|
+
const current = cardRow(block, 'Skills');
|
|
967
|
+
const ranked = rankedSkillRow(id);
|
|
968
|
+
if (!ranked || !staleSkillRow(current) || current === ranked) continue;
|
|
969
|
+
const updated = block.replace(/^Skills:.*$/m, `Skills: ${ranked}`);
|
|
970
|
+
if (!next.includes(block)) continue;
|
|
971
|
+
next = next.replace(block, updated);
|
|
972
|
+
changed = true;
|
|
973
|
+
}
|
|
974
|
+
if (!changed) return false;
|
|
975
|
+
writeFileSync(lensPath(root), next.endsWith('\n') ? next : `${next}\n`);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Read the held card's skill list. Plate recall runs only when the list has no fit. */
|
|
980
|
+
export function seekHeldCard(root, need, opts = {}) {
|
|
981
|
+
const id = currentCard(root);
|
|
982
|
+
migrateLensSkills(root);
|
|
983
|
+
const shown = lookCards([id], root);
|
|
984
|
+
const skills = (shown && shown[0] && shown[0].skills) || rankedSkillRow(id);
|
|
985
|
+
const recall = typeof opts.recall === 'function'
|
|
986
|
+
? opts.recall
|
|
987
|
+
: (text) => {
|
|
988
|
+
try {
|
|
989
|
+
return createRequire(import.meta.url)('./plates.cjs').recallPlate(text);
|
|
990
|
+
} catch {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
return seekSkill({
|
|
995
|
+
skills,
|
|
996
|
+
need,
|
|
997
|
+
taskType: opts.taskType || '',
|
|
998
|
+
workKind: opts.workKind || '',
|
|
999
|
+
recall,
|
|
1000
|
+
known: skillNamesOnCards(),
|
|
1001
|
+
cardSkills: rankedSkillRow,
|
|
1002
|
+
});
|
|
960
1003
|
}
|
|
961
1004
|
|
|
962
1005
|
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
writeFileSync(dest, `${JSON.stringify({ plane })}\n`);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function stampPlane(root, id) {
|
|
972
|
-
try {
|
|
973
|
-
const plates = createRequire(import.meta.url)('./plates.cjs');
|
|
974
|
-
if (!plates.PLATE_IDS.includes(id)) return;
|
|
975
|
-
plates.stampPlateIfMissing(root, id);
|
|
976
|
-
} catch {
|
|
977
|
-
/* a missed stamp must not fail the stop */
|
|
978
|
-
}
|
|
1006
|
+
function targetOf(file) {
|
|
1007
|
+
const thing = thingOfPath(file);
|
|
1008
|
+
const plane = planeOfPath(file) || (thing ? planeForCard(thing) : '');
|
|
1009
|
+
if (!plane) return null;
|
|
1010
|
+
return { plane, card: thing || plane };
|
|
979
1011
|
}
|
|
980
1012
|
|
|
981
|
-
/**
|
|
1013
|
+
/** A search inside the current card runs. A file open follows the path. */
|
|
982
1014
|
export function lensBeforeResearch(root, toolName, text) {
|
|
983
1015
|
const spoken = String(text || '');
|
|
984
|
-
const
|
|
985
|
-
const
|
|
986
|
-
if (
|
|
987
|
-
|
|
1016
|
+
const searching = isSearch(toolName, spoken);
|
|
1017
|
+
const reading = isRead(toolName, spoken);
|
|
1018
|
+
if (!searching && !reading) return null;
|
|
1019
|
+
ensurePlane(root);
|
|
1020
|
+
const current = currentCard(root);
|
|
1021
|
+
if (searching) {
|
|
1022
|
+
const owned = cardMatchPaths(current);
|
|
1023
|
+
const paths = openPaths(spoken);
|
|
1024
|
+
const inside = owned.length > 0
|
|
1025
|
+
&& paths.length > 0
|
|
1026
|
+
&& paths.every((file) => owned.some((listed) => covers(listed, file)));
|
|
1027
|
+
if (inside) return null;
|
|
1028
|
+
return { gate: 'lens', decision: 'deny', reason: cardText(root, current) };
|
|
988
1029
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const names = CARD_PLANES.filter((name) => intentWords(spoken).includes(name));
|
|
994
|
-
if (names.length === 1) {
|
|
995
|
-
holdPlane(root, names[0]);
|
|
996
|
-
stampPlane(root, names[0]);
|
|
997
|
-
rememberOpened(root, spoken);
|
|
998
|
-
return null;
|
|
1030
|
+
const targets = [];
|
|
1031
|
+
for (const file of openPaths(spoken)) {
|
|
1032
|
+
const target = targetOf(file);
|
|
1033
|
+
if (target && !targets.includes(target.card)) targets.push(target.card);
|
|
999
1034
|
}
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
if (loadReadPass(root) !== 'spent') saveReadPass(root, false);
|
|
1006
|
-
return { gate: 'lens', decision: 'deny', reason: 'Name one plane.' };
|
|
1035
|
+
if (targets.length === 1 && targets[0] === current) return null;
|
|
1036
|
+
if (targets.length === 1) {
|
|
1037
|
+
holdPlane(root, targets[0]);
|
|
1038
|
+
return { gate: 'lens', decision: 'allow', reason: cardText(root, targets[0]) };
|
|
1007
1039
|
}
|
|
1008
|
-
return { gate: 'lens', decision: '
|
|
1040
|
+
return { gate: 'lens', decision: 'allow', reason: cardText(root, current) };
|
|
1009
1041
|
}
|
|
1010
1042
|
|
|
1011
1043
|
function seenFields(plane, root) {
|
|
@@ -1052,134 +1084,23 @@ function freshField(seen, key) {
|
|
|
1052
1084
|
return seen[key] || '';
|
|
1053
1085
|
}
|
|
1054
1086
|
|
|
1055
|
-
export function growPlane(id, platesDir,
|
|
1087
|
+
export function growPlane(id, platesDir, _root) {
|
|
1056
1088
|
const seen = seenFields(assemblePlane(id, platesDir), sourceRoot(platesDir));
|
|
1057
|
-
const views = loadViews(root);
|
|
1058
1089
|
const grown = { id };
|
|
1059
1090
|
for (const key of VIEW_KEYS) grown[key] = freshField(seen, key);
|
|
1060
1091
|
if (id === 'ground') grown.from = '';
|
|
1061
|
-
views[id] = {};
|
|
1062
|
-
for (const key of VIEW_KEYS) views[id][key] = grown[key];
|
|
1063
|
-
saveViews(root, views);
|
|
1064
1092
|
const base = assemblePlane(id, platesDir);
|
|
1065
1093
|
return { ...base, ...grown };
|
|
1066
1094
|
}
|
|
1067
1095
|
|
|
1068
|
-
/** Paths the package ships: package.json files, plus what npm always packs. */
|
|
1069
|
-
function shippedSet(root) {
|
|
1070
|
-
try {
|
|
1071
|
-
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
1072
|
-
if (!Array.isArray(pkg.files)) return null;
|
|
1073
|
-
return pkg.files
|
|
1074
|
-
.map(String)
|
|
1075
|
-
.filter((entry) => entry && !entry.startsWith('!'))
|
|
1076
|
-
.map((entry) => entry.replace(/\/+$/, ''))
|
|
1077
|
-
.concat(['package.json', 'README.md', 'LICENSE']);
|
|
1078
|
-
} catch {
|
|
1079
|
-
return null;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function ships(shipped, rel) {
|
|
1084
|
-
if (!shipped) return true;
|
|
1085
|
-
const path = String(rel || '').replace(/\/+$/, '');
|
|
1086
|
-
if (!path) return false;
|
|
1087
|
-
return shipped.some((entry) => path === entry || path.startsWith(`${entry}/`) || entry.startsWith(`${path}/`));
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Kind 0 card sweep: triage's file check on every card plane, limited to
|
|
1092
|
-
* paths the package ships, so a packed install and the repo read the same.
|
|
1093
|
-
* Returns the first drift line, or '' when every card holds.
|
|
1094
|
-
*/
|
|
1095
|
-
export function shippedDrift(projectRoot) {
|
|
1096
|
-
const platesDir = findPlatesDir(HERE);
|
|
1097
|
-
const root = sourceRoot(platesDir);
|
|
1098
|
-
const shipped = shippedSet(root);
|
|
1099
|
-
for (const id of CARD_PLANES) {
|
|
1100
|
-
const plane = growPlane(id, platesDir, projectRoot || stateRoot());
|
|
1101
|
-
const kept = {
|
|
1102
|
-
...plane,
|
|
1103
|
-
files: (Array.isArray(plane.files) ? plane.files : []).filter((rel) => ships(shipped, rel)),
|
|
1104
|
-
skills: ships(shipped, plane.skills) ? plane.skills : '',
|
|
1105
|
-
worn: ships(shipped, plane.worn) ? plane.worn : '',
|
|
1106
|
-
};
|
|
1107
|
-
const exam = examinePlane(kept, root);
|
|
1108
|
-
if (exam.kind === 'drift') return `Actuality. ${id}: ${exam.text}`;
|
|
1109
|
-
}
|
|
1110
|
-
return '';
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
1096
|
function repoRootFrom(platesDir) {
|
|
1114
1097
|
return dirname(dirname(dirname(platesDir)));
|
|
1115
1098
|
}
|
|
1116
1099
|
|
|
1117
|
-
function miss(text) {
|
|
1118
|
-
return { ok: false, text };
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
1100
|
function ok(text) {
|
|
1122
1101
|
return { ok: true, text };
|
|
1123
1102
|
}
|
|
1124
1103
|
|
|
1125
|
-
function holds() {
|
|
1126
|
-
return { kind: 'holds', text: 'Holds.' };
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function drift(text) {
|
|
1130
|
-
return { kind: 'drift', text: `Drift: ${text}` };
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function drawing() {
|
|
1134
|
-
return { kind: 'drawing', text: 'Drawing only.' };
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function fileHas(root, rel, mark) {
|
|
1138
|
-
if (!rel || !mark) return false;
|
|
1139
|
-
const full = join(root, rel);
|
|
1140
|
-
return existsSync(full) && readFileSync(full, 'utf8').includes(mark);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
function factoryName(rel) {
|
|
1145
|
-
return rel.startsWith('src/') || rel.startsWith('grok-bot/');
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function wornHolds(plane, root) {
|
|
1149
|
-
if (!plane.worn || !existsSync(join(root, plane.worn))) return false;
|
|
1150
|
-
if (!plane.wornMark) return true;
|
|
1151
|
-
return fileHas(root, plane.worn, plane.wornMark);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
export function examinePlane(plane, root) {
|
|
1155
|
-
const files = Array.isArray(plane.files) ? plane.files : [];
|
|
1156
|
-
const checkable = files.length > 0 || plane.skills || plane.worn;
|
|
1157
|
-
if (!checkable) return drawing();
|
|
1158
|
-
const held = wornHolds(plane, root);
|
|
1159
|
-
if (plane.worn && !held) {
|
|
1160
|
-
return drift(`the worn build is not ${files[0] || plane.worn}`);
|
|
1161
|
-
}
|
|
1162
|
-
for (const rel of files) {
|
|
1163
|
-
if (existsSync(join(root, rel))) continue;
|
|
1164
|
-
if (held && factoryName(rel)) continue;
|
|
1165
|
-
return drift(`${rel} is not there`);
|
|
1166
|
-
}
|
|
1167
|
-
if (plane.mark && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.mark)) {
|
|
1168
|
-
return drift(`${plane.mark} is not in ${files[0]}`);
|
|
1169
|
-
}
|
|
1170
|
-
if (plane.skills && !existsSync(join(root, plane.skills)) && !(held && factoryName(plane.skills))) {
|
|
1171
|
-
return drift(`${plane.skills} is not there`);
|
|
1172
|
-
}
|
|
1173
|
-
if (plane.entryIsMark && plane.entry && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.entry)) {
|
|
1174
|
-
return drift(`${plane.entry} is not in ${files[0]}`);
|
|
1175
|
-
}
|
|
1176
|
-
if (plane.exitIsMark && plane.exit && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.exit)) {
|
|
1177
|
-
return drift(`${plane.exit} is not in ${files[0]}`);
|
|
1178
|
-
}
|
|
1179
|
-
return holds();
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
1104
|
export function look(argv, projectRoot) {
|
|
1184
1105
|
return readGoggles(argv, projectRoot);
|
|
1185
1106
|
}
|
|
@@ -1190,15 +1111,6 @@ function isGogglesWork(text) {
|
|
|
1190
1111
|
}
|
|
1191
1112
|
|
|
1192
1113
|
|
|
1193
|
-
export function cardStop(root, toolName, text, paths = []) {
|
|
1194
|
-
const held = loadReading(root);
|
|
1195
|
-
if (!held) return null;
|
|
1196
|
-
const drift = actualityLine();
|
|
1197
|
-
if (drift) return organDeny(drift);
|
|
1198
|
-
const stopped = organStop(held, { tool: toolName, text, paths });
|
|
1199
|
-
if (stopped) return stopped;
|
|
1200
|
-
return { gate: 'goggles', decision: 'allow' };
|
|
1201
|
-
}
|
|
1202
1114
|
|
|
1203
1115
|
function stationFile(root) {
|
|
1204
1116
|
return join(root, '.xray', 'state', 'STATION.md');
|
|
@@ -1233,35 +1145,10 @@ function lookVerb(words) {
|
|
|
1233
1145
|
export function handCard(root, intent) {
|
|
1234
1146
|
try {
|
|
1235
1147
|
const text = String(intent || '');
|
|
1236
|
-
const words = intentWords(text);
|
|
1237
1148
|
if (isGogglesWork(text) && text.length <= 200) {
|
|
1238
1149
|
clearReading(root);
|
|
1239
1150
|
clearCardLine(root);
|
|
1240
|
-
return null;
|
|
1241
|
-
}
|
|
1242
|
-
const asked = lookVerb(words);
|
|
1243
|
-
const planes = acceptedPlanes().filter((name) => words.includes(name));
|
|
1244
|
-
if (!asked && planes.length === 0) return null;
|
|
1245
|
-
if (!asked && planes.length === 1 && !HOLDS_ON_NAME.has(planes[0])) return null;
|
|
1246
|
-
clearCardLine(root);
|
|
1247
|
-
const looked = readGoggles(words);
|
|
1248
|
-
if (looked.text.startsWith('Actuality. Drift')) {
|
|
1249
|
-
saveReading(root, { drift: looked.text });
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
if (planes.length !== 1) {
|
|
1253
|
-
clearReading(root);
|
|
1254
|
-
return null;
|
|
1255
|
-
}
|
|
1256
|
-
const scopes = findScopes(words.filter((word) => word !== planes[0]));
|
|
1257
|
-
if (scopes.length > 1) {
|
|
1258
|
-
clearReading(root);
|
|
1259
|
-
return null;
|
|
1260
1151
|
}
|
|
1261
|
-
const next = { plane: planes[0], scope: scopes[0] || '' };
|
|
1262
|
-
const prev = loadReading(root);
|
|
1263
|
-
if (prev && prev.plane === next.plane && (prev.scope || '') === next.scope && !prev.drift) return null;
|
|
1264
|
-
saveReading(root, next);
|
|
1265
1152
|
return null;
|
|
1266
1153
|
} catch {
|
|
1267
1154
|
return null;
|
|
@@ -1274,12 +1161,115 @@ export function lensPath(root) {
|
|
|
1274
1161
|
}
|
|
1275
1162
|
|
|
1276
1163
|
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1164
|
+
function heldCardPlane(root) {
|
|
1165
|
+
const held = readHold(root);
|
|
1166
|
+
if (!held || !held.card) return null;
|
|
1167
|
+
return { id: held.card, scope: '' };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function readLensText(root) {
|
|
1171
|
+
try {
|
|
1172
|
+
return readFileSync(lensPath(root), 'utf8');
|
|
1173
|
+
} catch {
|
|
1174
|
+
return '';
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function blockId(part) {
|
|
1179
|
+
const card = String(part || '').match(/^Card:\s*(\S+)/m);
|
|
1180
|
+
if (card) return card[1];
|
|
1181
|
+
const plane = String(part || '').match(/^Plane:\s*(\S+)/);
|
|
1182
|
+
return plane ? plane[1] : '';
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function lensCards(text) {
|
|
1186
|
+
const cards = new Map();
|
|
1187
|
+
for (const part of String(text || '').split(/\n\n/)) {
|
|
1188
|
+
const id = blockId(part);
|
|
1189
|
+
if (!id) continue;
|
|
1190
|
+
const block = part.replace(/\s*$/, '');
|
|
1191
|
+
if (!isCardName(id) && !cardIsFilled(block)) continue;
|
|
1192
|
+
cards.set(id, block);
|
|
1193
|
+
}
|
|
1194
|
+
return cards;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function cardIsFilled(block) {
|
|
1198
|
+
return /^Entry:/m.test(block) && /^Exit:/m.test(block);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function cardOrder(text, cards) {
|
|
1202
|
+
const order = [];
|
|
1203
|
+
for (const part of String(text || '').split(/\n\n/)) {
|
|
1204
|
+
const id = blockId(part);
|
|
1205
|
+
if (!id || !cards.has(id) || order.includes(id)) continue;
|
|
1206
|
+
order.push(id);
|
|
1207
|
+
}
|
|
1208
|
+
for (const id of cards.keys()) {
|
|
1209
|
+
if (!order.includes(id)) order.push(id);
|
|
1210
|
+
}
|
|
1211
|
+
return order;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function writeLensBody(root, prev, cards) {
|
|
1215
|
+
const parts = [];
|
|
1216
|
+
for (const id of cardOrder(prev, cards)) parts.push(cards.get(id));
|
|
1280
1217
|
mkdirSync(dirname(lensPath(root)), { recursive: true });
|
|
1281
|
-
writeFileSync(lensPath(root), `${
|
|
1282
|
-
|
|
1218
|
+
writeFileSync(lensPath(root), parts.length ? `${parts.join('\n\n')}\n` : '');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function lensCardText(root, held) {
|
|
1222
|
+
const built = builtinCard(held.id);
|
|
1223
|
+
if (built) return built;
|
|
1224
|
+
const platesDir = findPlatesDir(HERE);
|
|
1225
|
+
const plane = growPlane(held.id, platesDir, root);
|
|
1226
|
+
const place = planeForCard(held.id);
|
|
1227
|
+
const view = {
|
|
1228
|
+
card: held.id,
|
|
1229
|
+
plane: place,
|
|
1230
|
+
from: place,
|
|
1231
|
+
digest: plane.digest || '',
|
|
1232
|
+
plate: plane.plate || '',
|
|
1233
|
+
plates: Array.isArray(plane.plates) ? plane.plates : [],
|
|
1234
|
+
entry: plane.entry || '',
|
|
1235
|
+
exit: plane.exit || '',
|
|
1236
|
+
files: listedFiles(plane),
|
|
1237
|
+
skills: plane.skills || '',
|
|
1238
|
+
setup: plane.setup || '',
|
|
1239
|
+
teardown: plane.teardown || '',
|
|
1240
|
+
worn: plane.worn || '',
|
|
1241
|
+
};
|
|
1242
|
+
return cardRows(view).map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** The stored card. Empty until Entry and Exit are in the file. */
|
|
1246
|
+
export function readLensCard(root, id) {
|
|
1247
|
+
const block = lensCards(readLensText(root)).get(String(id || '').trim()) || '';
|
|
1248
|
+
return cardIsFilled(block) ? block : '';
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** Write the plane card once. A later pass reads the stored rows. */
|
|
1252
|
+
export function fillLensCard(root, id) {
|
|
1253
|
+
const plane = String(id || '').trim();
|
|
1254
|
+
if (!isCardName(plane)) return '';
|
|
1255
|
+
const kept = readLensCard(root, plane);
|
|
1256
|
+
if (kept) return kept;
|
|
1257
|
+
const prev = readLensText(root);
|
|
1258
|
+
const cards = lensCards(prev);
|
|
1259
|
+
const block = lensCardText(root, { id: plane, scope: '' });
|
|
1260
|
+
cards.set(plane, block);
|
|
1261
|
+
writeLensBody(root, prev, cards);
|
|
1262
|
+
return block;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export function maintainLens(root) {
|
|
1266
|
+
migrateLensSkills(root);
|
|
1267
|
+
const held = heldCardPlane(root);
|
|
1268
|
+
if (held) fillLensCard(root, held.id);
|
|
1269
|
+
const prev = readLensText(root);
|
|
1270
|
+
const cards = lensCards(prev);
|
|
1271
|
+
writeLensBody(root, prev, cards);
|
|
1272
|
+
return ok('');
|
|
1283
1273
|
}
|
|
1284
1274
|
|
|
1285
1275
|
|
|
@@ -1293,7 +1283,7 @@ function sameInvoked(argvPath, modulePath) {
|
|
|
1293
1283
|
|
|
1294
1284
|
const isMain = Boolean(process.argv[1]) && sameInvoked(process.argv[1], fileURLToPath(import.meta.url));
|
|
1295
1285
|
if (isMain) {
|
|
1296
|
-
const answered = gogglesAnswer(process.argv.slice(2));
|
|
1286
|
+
const answered = gogglesAnswer(process.argv.slice(2), undefined, true);
|
|
1297
1287
|
const pane = process.stdout.isTTY && Array.isArray(answered.cards) && answered.cards.length > 0;
|
|
1298
1288
|
const text = pane ? answered.cards.map((card) => formatCardPane(card)).join('\n\n') : answered.text;
|
|
1299
1289
|
process.stdout.write(`${text}\n`);
|