greprag 5.77.0 → 5.78.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/app/app.css +342 -0
- package/app/app.js +360 -0
- package/app/favicon.svg +4 -0
- package/app/index.html +54 -0
- package/dist/app-settings.js +162 -0
- package/dist/commands/app-model.js +308 -0
- package/dist/commands/app.js +310 -0
- package/dist/commands/opencode-interrupt.js +4 -3
- package/dist/hook.js +5 -2
- package/dist/index.js +12 -1
- package/dist/opencode-plugin.bundle.js +300 -272
- package/dist/reminder-injector.js +2 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +2 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Read model for the local GrepRAG customer app.
|
|
3
|
+
* adr: adr/local-app-control-plane.md
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.discoverLocalProjects = discoverLocalProjects;
|
|
40
|
+
exports.buildAppModel = buildAppModel;
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const os = __importStar(require("node:os"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const app_settings_1 = require("../app-settings");
|
|
45
|
+
const project_anchor_1 = require("../project-anchor");
|
|
46
|
+
const native_skill_mirror_1 = require("../native-skill-mirror");
|
|
47
|
+
const status_1 = require("./status");
|
|
48
|
+
const config_1 = require("./config");
|
|
49
|
+
const reminder_registry_1 = require("./reminder-registry");
|
|
50
|
+
const reminder_injector_1 = require("../reminder-injector");
|
|
51
|
+
const INTERNAL_STORE_KINDS = new Set([
|
|
52
|
+
'memory', 'inbox', 'fix', 'docptr', 'skillgain', 'skill', 'parity', 'doctrine',
|
|
53
|
+
]);
|
|
54
|
+
const INTERRUPT_COPY = {
|
|
55
|
+
'os-primer': { label: 'Operating guidance', description: 'Teaches agents GrepRAG’s recovery and coordination rules.', group: 'Core guidance' },
|
|
56
|
+
'inbox-primer': { label: 'Inbox guidance', description: 'Explains live cross-agent messages and watcher setup.', group: 'Communication' },
|
|
57
|
+
'load-primer': { label: 'Feature discovery', description: 'Shows agents how to load GrepRAG capabilities on demand.', group: 'Core guidance' },
|
|
58
|
+
'chip-spawn-pointer': { label: 'Claude delegation', description: 'Guides Claude Code when work should move to another task.', group: 'Delegation' },
|
|
59
|
+
'codex-chip-spawn-pointer': { label: 'Codex delegation', description: 'Guides Codex when work should move to another task.', group: 'Delegation' },
|
|
60
|
+
'email-primer': { label: 'Agent email', description: 'Explains real email separately from internal agent messages.', group: 'Communication' },
|
|
61
|
+
'corpus-announce': { label: 'Knowledge reminders', description: 'Points agents to relevant reference collections.', group: 'Knowledge' },
|
|
62
|
+
'doc-pointer-announce': { label: 'Project documents', description: 'Shows the project’s core reference documents at startup.', group: 'Knowledge' },
|
|
63
|
+
'skill-gain-announce': { label: 'Skill improvements', description: 'Surfaces newly learned improvements for installed skills.', group: 'Skills' },
|
|
64
|
+
'skill-mirror-announce': { label: 'Available skills', description: 'Makes mirrored skills discoverable to each agent.', group: 'Skills' },
|
|
65
|
+
'procedure-announce': { label: 'Saved procedures', description: 'Reminds agents about active project procedures.', group: 'Automation' },
|
|
66
|
+
'delivery-control': { label: 'Delivery guidance', description: 'Keeps code delivery and production verification consistent.', group: 'Automation' },
|
|
67
|
+
'persona-announce': { label: 'Speaking style', description: 'Applies the selected GrepRAG persona across agents.', group: 'Personalization' },
|
|
68
|
+
'loadout-registrar': { label: 'Equipped loadouts', description: 'Makes equipped skill collections available when relevant.', group: 'Skills' },
|
|
69
|
+
'setup-warning': { label: 'Setup warnings', description: 'Calls out missing configuration that prevents GrepRAG from working.', group: 'Health' },
|
|
70
|
+
'version-upgrade': { label: 'Update notices', description: 'Warns when the installed CLI is behind the current release.', group: 'Health' },
|
|
71
|
+
'enrichment-health': { label: 'Knowledge health', description: 'Warns when knowledge enrichment is unavailable.', group: 'Health' },
|
|
72
|
+
'assistant-doctrine': { label: 'Assistant role', description: 'Loads the executive-assistant behavior for its assigned project.', group: 'Roles' },
|
|
73
|
+
'memory-primer': { label: 'Memory guidance', description: 'Teaches agents how and when to retrieve project memory.', group: 'Memory' },
|
|
74
|
+
'watcher-arm': { label: 'Live inbox checks', description: 'Reminds Claude Code to keep live message delivery connected.', group: 'Communication' },
|
|
75
|
+
'mechanic-friction': { label: 'Automatic repair prompts', description: 'Surfaces repeated harness friction for durable repair.', group: 'Automation' },
|
|
76
|
+
collision: { label: 'Collision warnings', description: 'Warns before shared Git actions when another session is active.', group: 'Delegation' },
|
|
77
|
+
};
|
|
78
|
+
function readObject(file) {
|
|
79
|
+
try {
|
|
80
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
81
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
82
|
+
? value
|
|
83
|
+
: {};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function countDirectories(root) {
|
|
90
|
+
let names = [];
|
|
91
|
+
try {
|
|
92
|
+
names = fs.readdirSync(root, { withFileTypes: true })
|
|
93
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
94
|
+
.map(entry => entry.name)
|
|
95
|
+
.sort();
|
|
96
|
+
}
|
|
97
|
+
catch { /* missing root */ }
|
|
98
|
+
const state = readObject(path.join(root, '.greprag-mirror.json'));
|
|
99
|
+
const skills = state.skills && typeof state.skills === 'object' && !Array.isArray(state.skills)
|
|
100
|
+
? state.skills
|
|
101
|
+
: {};
|
|
102
|
+
return { count: names.length, names, managed: Object.keys(skills).length };
|
|
103
|
+
}
|
|
104
|
+
function discoverLocalProjects(cwd, homeDir = os.homedir()) {
|
|
105
|
+
const candidates = [path.resolve(cwd)];
|
|
106
|
+
const registry = readObject(path.join(homeDir, '.greprag', 'projects.json'));
|
|
107
|
+
for (const value of Object.values(registry)) {
|
|
108
|
+
if (typeof value === 'string' && value.trim())
|
|
109
|
+
candidates.push(path.resolve(value));
|
|
110
|
+
}
|
|
111
|
+
const byId = new Map();
|
|
112
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
113
|
+
if (!fs.existsSync(candidate))
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const anchor = (0, project_anchor_1.readAnchor)(candidate);
|
|
117
|
+
if (!byId.has(anchor.projectId)) {
|
|
118
|
+
byId.set(anchor.projectId, {
|
|
119
|
+
id: anchor.projectId,
|
|
120
|
+
name: anchor.projectName,
|
|
121
|
+
path: candidate,
|
|
122
|
+
initialized: anchor.initialized,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch { /* an unreadable registered path should not break the app */ }
|
|
127
|
+
}
|
|
128
|
+
return [...byId.values()];
|
|
129
|
+
}
|
|
130
|
+
function asRefresh(store) {
|
|
131
|
+
const metadata = store.metadata;
|
|
132
|
+
if (!metadata || typeof metadata !== 'object')
|
|
133
|
+
return null;
|
|
134
|
+
const refresh = metadata.refresh;
|
|
135
|
+
return refresh && typeof refresh === 'object' && !Array.isArray(refresh)
|
|
136
|
+
? refresh
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
function corpusStoreView(store) {
|
|
140
|
+
const refresh = asRefresh(store);
|
|
141
|
+
const failures = typeof refresh?.consecutiveFailures === 'number'
|
|
142
|
+
? refresh.consecutiveFailures
|
|
143
|
+
: 0;
|
|
144
|
+
const policy = typeof refresh?.policy === 'string' ? refresh.policy : 'off';
|
|
145
|
+
const hasSource = typeof refresh?.url === 'string' && refresh.url.length > 0;
|
|
146
|
+
return {
|
|
147
|
+
id: store.id,
|
|
148
|
+
name: store.name,
|
|
149
|
+
kind: store.kind,
|
|
150
|
+
tags: store.tags || [],
|
|
151
|
+
nodes: store.nodeCount || 0,
|
|
152
|
+
words: store.wordCount || 0,
|
|
153
|
+
policy: hasSource ? policy : 'frozen',
|
|
154
|
+
lastCheckedAt: typeof refresh?.lastFetchedAt === 'string'
|
|
155
|
+
? refresh.lastFetchedAt
|
|
156
|
+
: store.updatedAt || store.createdAt,
|
|
157
|
+
nextCheckAt: typeof refresh?.nextCheckAt === 'string' ? refresh.nextCheckAt : null,
|
|
158
|
+
failures,
|
|
159
|
+
health: failures > 0 ? 'attention' : 'healthy',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function fetchJson(url, apiKey) {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), 7_000);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok)
|
|
171
|
+
throw new Error(`HTTP ${response.status}`);
|
|
172
|
+
return response.json();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function readCloud(apiUrl, apiKey) {
|
|
179
|
+
if (!apiKey)
|
|
180
|
+
return { status: 'unconfigured', stores: [], skills: [], projects: [], error: null };
|
|
181
|
+
const [storesResult, skillsResult, projectsResult] = await Promise.allSettled([
|
|
182
|
+
fetchJson(`${apiUrl}/v1/stores`, apiKey),
|
|
183
|
+
fetchJson(`${apiUrl}/v1/skillmirror`, apiKey),
|
|
184
|
+
fetchJson(`${apiUrl}/v1/projects/discover`, apiKey),
|
|
185
|
+
]);
|
|
186
|
+
const stores = storesResult.status === 'fulfilled' ? storesResult.value.stores || [] : [];
|
|
187
|
+
const skills = skillsResult.status === 'fulfilled' ? skillsResult.value.skills || [] : [];
|
|
188
|
+
const projects = projectsResult.status === 'fulfilled' ? projectsResult.value.projects || [] : [];
|
|
189
|
+
const errors = [storesResult, skillsResult, projectsResult]
|
|
190
|
+
.filter((result) => result.status === 'rejected')
|
|
191
|
+
.map(result => result.reason instanceof Error ? result.reason.message : String(result.reason));
|
|
192
|
+
return {
|
|
193
|
+
status: errors.length === 3 ? 'offline' : errors.length ? 'partial' : 'connected',
|
|
194
|
+
stores,
|
|
195
|
+
skills,
|
|
196
|
+
projects,
|
|
197
|
+
error: errors.length ? errors.join('; ') : null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function platformLabel(key) {
|
|
201
|
+
return ({ claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', grok: 'Grok Build' })[key];
|
|
202
|
+
}
|
|
203
|
+
function platformSkillRoots(key, projectPath, homeDir) {
|
|
204
|
+
if (key === 'grok')
|
|
205
|
+
return [path.join(homeDir, '.grok', 'skills')];
|
|
206
|
+
const native = key === 'claude' ? 'claude-code' : key;
|
|
207
|
+
return [(0, native_skill_mirror_1.nativeSkillRoot)(native, homeDir), (0, native_skill_mirror_1.nativeProjectSkillRoot)(native, projectPath)];
|
|
208
|
+
}
|
|
209
|
+
async function buildAppModel(cwd, requestedProjectId, homeDir = os.homedir()) {
|
|
210
|
+
const projects = discoverLocalProjects(cwd, homeDir);
|
|
211
|
+
const selected = projects.find(project => project.id === requestedProjectId) || projects[0];
|
|
212
|
+
const selectedPath = selected?.path || path.resolve(cwd);
|
|
213
|
+
const status = (0, status_1.buildStatus)(selectedPath);
|
|
214
|
+
const config = (0, config_1.getConfig)();
|
|
215
|
+
const cloud = await readCloud(config.apiUrl, config.apiKey);
|
|
216
|
+
const localSettings = (0, app_settings_1.readLocalAppSettings)(homeDir);
|
|
217
|
+
const disabled = new Set(localSettings.interrupts.disabled);
|
|
218
|
+
const platforms = Object.keys(status.platforms).map(key => {
|
|
219
|
+
const roots = platformSkillRoots(key, selectedPath, homeDir).map(countDirectories);
|
|
220
|
+
const names = [...new Set(roots.flatMap(root => root.names))].sort();
|
|
221
|
+
return {
|
|
222
|
+
id: key,
|
|
223
|
+
label: platformLabel(key),
|
|
224
|
+
configured: status.platforms[key].configured,
|
|
225
|
+
installed: status.platforms[key].config_present,
|
|
226
|
+
skillCount: names.length,
|
|
227
|
+
managedSkillCount: roots.reduce((sum, root) => sum + root.managed, 0),
|
|
228
|
+
skills: names,
|
|
229
|
+
note: status.platforms[key].note || '',
|
|
230
|
+
canToggleSkillAdapter: key === 'codex' || key === 'opencode',
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
const corpusStores = cloud.stores.filter(store => !INTERNAL_STORE_KINDS.has(store.kind)).map(corpusStoreView);
|
|
234
|
+
const projectMemory = cloud.projects.find(project => project.id === selected?.id);
|
|
235
|
+
const memoryRows = projectMemory
|
|
236
|
+
? Object.values(projectMemory.row_counts || {}).reduce((sum, count) => sum + count, 0)
|
|
237
|
+
: 0;
|
|
238
|
+
const corpusFailures = corpusStores.reduce((sum, store) => sum + store.failures, 0);
|
|
239
|
+
const configuredAgents = platforms.filter(platform => platform.configured).length;
|
|
240
|
+
const issues = [];
|
|
241
|
+
if (!status.auth.api_key_present)
|
|
242
|
+
issues.push('Connect a GrepRAG account to enable cloud memory and knowledge.');
|
|
243
|
+
if (configuredAgents === 0)
|
|
244
|
+
issues.push('No agent integration is fully connected.');
|
|
245
|
+
if (status.windows_shims?.mismatch)
|
|
246
|
+
issues.push('An older GrepRAG command is still active on this machine.');
|
|
247
|
+
if (cloud.status === 'offline')
|
|
248
|
+
issues.push('Cloud metrics are unavailable right now. Local settings still work.');
|
|
249
|
+
if (corpusFailures > 0)
|
|
250
|
+
issues.push(`${corpusFailures} knowledge refresh failure${corpusFailures === 1 ? '' : 's'} need attention.`);
|
|
251
|
+
return {
|
|
252
|
+
app: { version: status.version, cwd: path.resolve(cwd) },
|
|
253
|
+
account: {
|
|
254
|
+
connected: status.auth.api_key_present,
|
|
255
|
+
handle: status.identity.handle,
|
|
256
|
+
tenantId: status.identity.tenant_id,
|
|
257
|
+
cloudStatus: cloud.status,
|
|
258
|
+
cloudError: cloud.error,
|
|
259
|
+
},
|
|
260
|
+
health: { state: issues.length ? 'attention' : 'healthy', issues },
|
|
261
|
+
projects,
|
|
262
|
+
selectedProject: selected ? {
|
|
263
|
+
...selected,
|
|
264
|
+
settings: (0, app_settings_1.readProjectAppSettings)(selectedPath),
|
|
265
|
+
} : null,
|
|
266
|
+
agents: platforms,
|
|
267
|
+
memory: {
|
|
268
|
+
nodes: memoryRows,
|
|
269
|
+
stores: projectMemory ? 1 : 0,
|
|
270
|
+
firstActivity: projectMemory?.first_activity || null,
|
|
271
|
+
lastActivity: projectMemory?.last_activity || null,
|
|
272
|
+
captureEnabled: status.project.memory_capture,
|
|
273
|
+
recapEnabled: status.project.session_start_recap,
|
|
274
|
+
},
|
|
275
|
+
skills: {
|
|
276
|
+
mirrored: cloud.skills.length,
|
|
277
|
+
installedUnique: new Set(platforms.flatMap(platform => platform.skills)).size,
|
|
278
|
+
adaptersActive: platforms.filter(platform => platform.managedSkillCount > 0).length,
|
|
279
|
+
},
|
|
280
|
+
corpus: {
|
|
281
|
+
status: cloud.status,
|
|
282
|
+
stores: corpusStores,
|
|
283
|
+
count: corpusStores.length,
|
|
284
|
+
nodes: corpusStores.reduce((sum, store) => sum + store.nodes, 0),
|
|
285
|
+
words: corpusStores.reduce((sum, store) => sum + store.words, 0),
|
|
286
|
+
failures: corpusFailures,
|
|
287
|
+
},
|
|
288
|
+
notifications: {
|
|
289
|
+
modules: reminder_registry_1.REGISTRY.map(module => ({
|
|
290
|
+
id: module.id,
|
|
291
|
+
...(INTERRUPT_COPY[module.id] || {
|
|
292
|
+
label: module.id,
|
|
293
|
+
description: 'Controls this GrepRAG startup or just-in-time guidance.',
|
|
294
|
+
group: 'Other',
|
|
295
|
+
}),
|
|
296
|
+
enabled: !disabled.has(module.id),
|
|
297
|
+
surface: module.source === 'command' ? 'Before sensitive actions' : 'Agent guidance',
|
|
298
|
+
harnesses: module.harnesses || ['claude-code', 'codex', 'opencode', 'grok'],
|
|
299
|
+
})),
|
|
300
|
+
},
|
|
301
|
+
advanced: {
|
|
302
|
+
mechanicEnabled: !(0, reminder_injector_1.mechanicKilled)(),
|
|
303
|
+
settingsPath: path.join(homeDir, '.greprag', 'settings.json'),
|
|
304
|
+
projectSettingsPath: status.project.anchor_path,
|
|
305
|
+
apiUrl: config.apiUrl,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** `greprag` local customer app launcher and loopback server.
|
|
3
|
+
* adr: adr/local-app-control-plane.md
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.browserLaunchCommand = browserLaunchCommand;
|
|
40
|
+
exports.openDefaultBrowser = openDefaultBrowser;
|
|
41
|
+
exports.startAppServer = startAppServer;
|
|
42
|
+
exports.runApp = runApp;
|
|
43
|
+
const crypto = __importStar(require("node:crypto"));
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const http = __importStar(require("node:http"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const app_model_1 = require("./app-model");
|
|
49
|
+
const app_settings_1 = require("../app-settings");
|
|
50
|
+
const reminder_registry_1 = require("./reminder-registry");
|
|
51
|
+
const config_1 = require("./config");
|
|
52
|
+
const native_skill_mirror_1 = require("../native-skill-mirror");
|
|
53
|
+
const LOOPBACK = '127.0.0.1';
|
|
54
|
+
const BODY_LIMIT = 64 * 1024;
|
|
55
|
+
function appAssetRoot() {
|
|
56
|
+
return path.join(__dirname, '..', '..', 'app');
|
|
57
|
+
}
|
|
58
|
+
function mimeType(file) {
|
|
59
|
+
if (file.endsWith('.html'))
|
|
60
|
+
return 'text/html; charset=utf-8';
|
|
61
|
+
if (file.endsWith('.css'))
|
|
62
|
+
return 'text/css; charset=utf-8';
|
|
63
|
+
if (file.endsWith('.js'))
|
|
64
|
+
return 'text/javascript; charset=utf-8';
|
|
65
|
+
if (file.endsWith('.svg'))
|
|
66
|
+
return 'image/svg+xml';
|
|
67
|
+
return 'application/octet-stream';
|
|
68
|
+
}
|
|
69
|
+
function sendJson(res, status, body) {
|
|
70
|
+
const payload = JSON.stringify(body);
|
|
71
|
+
res.writeHead(status, {
|
|
72
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
73
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
74
|
+
'Cache-Control': 'no-store',
|
|
75
|
+
});
|
|
76
|
+
res.end(payload);
|
|
77
|
+
}
|
|
78
|
+
function safeEqual(a, b) {
|
|
79
|
+
const left = Buffer.from(a);
|
|
80
|
+
const right = Buffer.from(b);
|
|
81
|
+
return left.length === right.length && crypto.timingSafeEqual(left, right);
|
|
82
|
+
}
|
|
83
|
+
function cookieValue(req, name) {
|
|
84
|
+
for (const part of String(req.headers.cookie || '').split(';')) {
|
|
85
|
+
const [key, ...value] = part.trim().split('=');
|
|
86
|
+
if (key === name)
|
|
87
|
+
return decodeURIComponent(value.join('='));
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
async function readBody(req) {
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
for await (const chunk of req) {
|
|
95
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
96
|
+
size += buffer.length;
|
|
97
|
+
if (size > BODY_LIMIT)
|
|
98
|
+
throw new Error('Request is too large.');
|
|
99
|
+
chunks.push(buffer);
|
|
100
|
+
}
|
|
101
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
102
|
+
if (!raw)
|
|
103
|
+
return {};
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
106
|
+
throw new Error('Expected a JSON object.');
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
function resolveProject(cwd, projectId) {
|
|
110
|
+
if (typeof projectId !== 'string')
|
|
111
|
+
throw new Error('Project id is required.');
|
|
112
|
+
const project = (0, app_model_1.discoverLocalProjects)(cwd).find(candidate => candidate.id === projectId);
|
|
113
|
+
if (!project)
|
|
114
|
+
throw new Error('That project is not registered on this machine.');
|
|
115
|
+
return project.path;
|
|
116
|
+
}
|
|
117
|
+
function projectPatch(body) {
|
|
118
|
+
const allowed = new Set([
|
|
119
|
+
'memoryCapture', 'sessionStartRecap', 'inboxNotify', 'crushWrap',
|
|
120
|
+
'contextGovernor', 'contextThreshold', 'pngArchive', 'emailAutosave',
|
|
121
|
+
]);
|
|
122
|
+
const patch = body.patch;
|
|
123
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch))
|
|
124
|
+
throw new Error('Settings patch is required.');
|
|
125
|
+
for (const key of Object.keys(patch))
|
|
126
|
+
if (!allowed.has(key))
|
|
127
|
+
throw new Error(`Unknown setting: ${key}`);
|
|
128
|
+
return patch;
|
|
129
|
+
}
|
|
130
|
+
async function setAdapter(body, cwd) {
|
|
131
|
+
const platform = body.platform;
|
|
132
|
+
if (platform !== 'codex' && platform !== 'opencode')
|
|
133
|
+
throw new Error('Only Codex and OpenCode use optional skill adapters.');
|
|
134
|
+
if (typeof body.enabled !== 'boolean')
|
|
135
|
+
throw new Error('enabled must be true or false.');
|
|
136
|
+
const projectDir = resolveProject(cwd, body.projectId);
|
|
137
|
+
const project = (0, app_model_1.discoverLocalProjects)(cwd).find(candidate => candidate.path === projectDir);
|
|
138
|
+
const nativePlatform = platform;
|
|
139
|
+
if (!body.enabled) {
|
|
140
|
+
return (0, native_skill_mirror_1.applyNativeSkillMirror)({
|
|
141
|
+
platform: nativePlatform,
|
|
142
|
+
skills: [],
|
|
143
|
+
projectDir,
|
|
144
|
+
prune: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const config = (0, config_1.getConfig)();
|
|
148
|
+
if (!config.apiKey)
|
|
149
|
+
throw new Error('Connect GrepRAG before enabling skill adapters.');
|
|
150
|
+
return (0, native_skill_mirror_1.syncNativeSkillMirror)({
|
|
151
|
+
platform: nativePlatform,
|
|
152
|
+
apiUrl: config.apiUrl,
|
|
153
|
+
apiKey: config.apiKey,
|
|
154
|
+
projectId: project.id,
|
|
155
|
+
projectDir,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function browserLaunchCommand(url, platform = process.platform) {
|
|
159
|
+
if (platform === 'win32')
|
|
160
|
+
return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] };
|
|
161
|
+
if (platform === 'darwin')
|
|
162
|
+
return { command: 'open', args: [url] };
|
|
163
|
+
return { command: 'xdg-open', args: [url] };
|
|
164
|
+
}
|
|
165
|
+
function openDefaultBrowser(url) {
|
|
166
|
+
const launch = browserLaunchCommand(url);
|
|
167
|
+
const child = (0, node_child_process_1.spawn)(launch.command, launch.args, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
168
|
+
child.unref();
|
|
169
|
+
}
|
|
170
|
+
async function startAppServer(options = {}) {
|
|
171
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
172
|
+
const token = crypto.randomBytes(24).toString('base64url');
|
|
173
|
+
const idleTimeoutMs = options.idleTimeoutMs === undefined ? 90_000 : options.idleTimeoutMs;
|
|
174
|
+
const startedAt = Date.now();
|
|
175
|
+
let lastHeartbeat = Date.now();
|
|
176
|
+
let authenticatedOnce = false;
|
|
177
|
+
let origin = '';
|
|
178
|
+
const server = http.createServer(async (req, res) => {
|
|
179
|
+
try {
|
|
180
|
+
const requestUrl = new URL(req.url || '/', origin || `http://${LOOPBACK}`);
|
|
181
|
+
if (requestUrl.pathname === '/health')
|
|
182
|
+
return sendJson(res, 200, { ok: true });
|
|
183
|
+
const queryToken = requestUrl.searchParams.get('token');
|
|
184
|
+
if (requestUrl.pathname === '/' && queryToken && safeEqual(queryToken, token)) {
|
|
185
|
+
authenticatedOnce = true;
|
|
186
|
+
lastHeartbeat = Date.now();
|
|
187
|
+
res.writeHead(303, {
|
|
188
|
+
Location: '/',
|
|
189
|
+
'Set-Cookie': `greprag_session=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/`,
|
|
190
|
+
'Cache-Control': 'no-store',
|
|
191
|
+
});
|
|
192
|
+
res.end();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const authenticated = safeEqual(cookieValue(req, 'greprag_session') || '', token);
|
|
196
|
+
if (!authenticated)
|
|
197
|
+
return sendJson(res, 401, { ok: false, error: 'Open GrepRAG from the CLI again.' });
|
|
198
|
+
lastHeartbeat = Date.now();
|
|
199
|
+
if (req.method !== 'GET' && req.headers.origin !== origin) {
|
|
200
|
+
return sendJson(res, 403, { ok: false, error: 'Request origin was rejected.' });
|
|
201
|
+
}
|
|
202
|
+
if (requestUrl.pathname === '/api/heartbeat' && req.method === 'POST') {
|
|
203
|
+
return sendJson(res, 200, { ok: true });
|
|
204
|
+
}
|
|
205
|
+
if (requestUrl.pathname === '/api/model' && req.method === 'GET') {
|
|
206
|
+
const model = await (0, app_model_1.buildAppModel)(cwd, requestUrl.searchParams.get('project') || undefined);
|
|
207
|
+
return sendJson(res, 200, { ok: true, model });
|
|
208
|
+
}
|
|
209
|
+
if (requestUrl.pathname === '/api/project-settings' && req.method === 'PATCH') {
|
|
210
|
+
const body = await readBody(req);
|
|
211
|
+
const projectDir = resolveProject(cwd, body.projectId);
|
|
212
|
+
const settings = (0, app_settings_1.updateProjectAppSettings)(projectDir, projectPatch(body));
|
|
213
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
214
|
+
}
|
|
215
|
+
if (requestUrl.pathname === '/api/interrupts' && req.method === 'PATCH') {
|
|
216
|
+
const body = await readBody(req);
|
|
217
|
+
if (typeof body.id !== 'string' || !reminder_registry_1.REGISTRY.some(module => module.id === body.id)) {
|
|
218
|
+
throw new Error('Unknown guidance feature.');
|
|
219
|
+
}
|
|
220
|
+
if (typeof body.enabled !== 'boolean')
|
|
221
|
+
throw new Error('enabled must be true or false.');
|
|
222
|
+
const settings = (0, app_settings_1.setInterruptEnabled)(body.id, body.enabled);
|
|
223
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
224
|
+
}
|
|
225
|
+
if (requestUrl.pathname === '/api/skill-adapter' && req.method === 'POST') {
|
|
226
|
+
const body = await readBody(req);
|
|
227
|
+
const result = await setAdapter(body, cwd);
|
|
228
|
+
return sendJson(res, 200, { ok: true, result });
|
|
229
|
+
}
|
|
230
|
+
const assetName = requestUrl.pathname === '/' ? 'index.html' : requestUrl.pathname.slice(1);
|
|
231
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(assetName))
|
|
232
|
+
return sendJson(res, 404, { ok: false, error: 'Not found.' });
|
|
233
|
+
const file = path.join(appAssetRoot(), assetName);
|
|
234
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
235
|
+
return sendJson(res, 404, { ok: false, error: 'Not found.' });
|
|
236
|
+
const payload = fs.readFileSync(file);
|
|
237
|
+
res.writeHead(200, {
|
|
238
|
+
'Content-Type': mimeType(file),
|
|
239
|
+
'Content-Length': payload.length,
|
|
240
|
+
'Cache-Control': assetName === 'index.html' ? 'no-store' : 'private, max-age=300',
|
|
241
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'",
|
|
242
|
+
'X-Content-Type-Options': 'nosniff',
|
|
243
|
+
'Referrer-Policy': 'no-referrer',
|
|
244
|
+
});
|
|
245
|
+
res.end(payload);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : 'Request failed.';
|
|
249
|
+
sendJson(res, 400, { ok: false, error: message });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
await new Promise((resolve, reject) => {
|
|
253
|
+
server.once('error', reject);
|
|
254
|
+
server.listen(options.port || 0, LOOPBACK, () => resolve());
|
|
255
|
+
});
|
|
256
|
+
const address = server.address();
|
|
257
|
+
if (!address || typeof address === 'string')
|
|
258
|
+
throw new Error('Could not resolve the local app port.');
|
|
259
|
+
origin = `http://${LOOPBACK}:${address.port}`;
|
|
260
|
+
const idleTimer = idleTimeoutMs > 0 ? setInterval(() => {
|
|
261
|
+
const idle = Date.now() - lastHeartbeat > idleTimeoutMs;
|
|
262
|
+
const neverOpened = !authenticatedOnce && Date.now() - startedAt > Math.max(idleTimeoutMs, 300_000);
|
|
263
|
+
if ((authenticatedOnce && idle) || neverOpened)
|
|
264
|
+
server.close();
|
|
265
|
+
}, Math.min(15_000, Math.max(1_000, Math.floor(idleTimeoutMs / 3)))) : null;
|
|
266
|
+
idleTimer?.unref();
|
|
267
|
+
const close = async () => {
|
|
268
|
+
if (idleTimer)
|
|
269
|
+
clearInterval(idleTimer);
|
|
270
|
+
if (!server.listening)
|
|
271
|
+
return;
|
|
272
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
273
|
+
};
|
|
274
|
+
const launchUrl = `${origin}/?token=${encodeURIComponent(token)}`;
|
|
275
|
+
if (options.openBrowser !== false)
|
|
276
|
+
openDefaultBrowser(launchUrl);
|
|
277
|
+
return { url: origin, launchUrl, token, close };
|
|
278
|
+
}
|
|
279
|
+
function flagValue(args, flag) {
|
|
280
|
+
const index = args.indexOf(flag);
|
|
281
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
282
|
+
}
|
|
283
|
+
async function runApp(args = []) {
|
|
284
|
+
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|
285
|
+
console.log(`greprag — open the local GrepRAG app\n\n greprag\n greprag app [--port <number>] [--no-open]\n greprag dashboard [--port <number>] [--no-open]\n\nThe app binds only to 127.0.0.1 and stops after the browser disconnects.`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const portRaw = flagValue(args, '--port');
|
|
289
|
+
const port = portRaw === undefined ? 0 : Number(portRaw);
|
|
290
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535)
|
|
291
|
+
throw new Error('--port must be an integer from 0 to 65535.');
|
|
292
|
+
const openBrowser = !args.includes('--no-open');
|
|
293
|
+
const running = await startAppServer({ port, openBrowser });
|
|
294
|
+
console.log(`GrepRAG is open at ${running.url}`);
|
|
295
|
+
if (!openBrowser)
|
|
296
|
+
console.log(`Open this one-time link: ${running.launchUrl}`);
|
|
297
|
+
console.log('Keep this window open while using the app. It closes automatically after the browser disconnects.');
|
|
298
|
+
await new Promise(resolve => {
|
|
299
|
+
const stop = () => { void running.close().then(resolve); };
|
|
300
|
+
process.once('SIGINT', stop);
|
|
301
|
+
process.once('SIGTERM', stop);
|
|
302
|
+
const poll = setInterval(() => {
|
|
303
|
+
fetch(`${running.url}/health`).catch(() => {
|
|
304
|
+
clearInterval(poll);
|
|
305
|
+
resolve();
|
|
306
|
+
});
|
|
307
|
+
}, 2_000);
|
|
308
|
+
poll.unref();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -6,6 +6,7 @@ exports.getOpenCodeAnnounces = getOpenCodeAnnounces;
|
|
|
6
6
|
exports.getOpenCodePersistentAnnounces = getOpenCodePersistentAnnounces;
|
|
7
7
|
exports.getOpenCodeReminders = getOpenCodeReminders;
|
|
8
8
|
const reminder_registry_1 = require("./reminder-registry");
|
|
9
|
+
const app_settings_1 = require("../app-settings");
|
|
9
10
|
/** The opencode registry is DERIVED from the canon, never hand-whitelisted.
|
|
10
11
|
* Every REGISTRY module ships to opencode unless it declares a `harnesses`
|
|
11
12
|
* list excluding it (reminder-types.ts) — so a new module wired into
|
|
@@ -49,14 +50,14 @@ function buildOpenCodeEnv(params) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
function getOpenCodeAnnounces(env) {
|
|
52
|
-
return [...(0, reminder_registry_1.collectAnnounces)(env, exports.opencodeRegistry), exports.OPENCODE_QUOTA_WAITLIST_ANNOUNCE];
|
|
53
|
+
return [...(0, reminder_registry_1.collectAnnounces)(env, (0, app_settings_1.filterEnabledInterrupts)(exports.opencodeRegistry)), exports.OPENCODE_QUOTA_WAITLIST_ANNOUNCE];
|
|
53
54
|
}
|
|
54
55
|
/** OpenCode rebuilds output.system for every model call. Persona is the one
|
|
55
56
|
* startup primer that must remain behaviorally active on later calls, so the
|
|
56
57
|
* plugin replays this shared-registry module from its bounded session cache. */
|
|
57
58
|
function getOpenCodePersistentAnnounces(env) {
|
|
58
|
-
return (0, reminder_registry_1.collectAnnounces)(env, exports.opencodeRegistry.filter((m) => m.id === 'persona-announce'));
|
|
59
|
+
return (0, reminder_registry_1.collectAnnounces)(env, (0, app_settings_1.filterEnabledInterrupts)(exports.opencodeRegistry.filter((m) => m.id === 'persona-announce')));
|
|
59
60
|
}
|
|
60
61
|
function getOpenCodeReminders(env) {
|
|
61
|
-
return (0, reminder_registry_1.collectReminders)(env, exports.opencodeRegistry);
|
|
62
|
+
return (0, reminder_registry_1.collectReminders)(env, (0, app_settings_1.filterEnabledInterrupts)(exports.opencodeRegistry));
|
|
62
63
|
}
|
package/dist/hook.js
CHANGED
|
@@ -83,6 +83,7 @@ const guard_1 = require("./guard");
|
|
|
83
83
|
// auto-silences when resolved (the arm-check pattern). The hook below assembles the
|
|
84
84
|
// live env (ALL I/O) and emits; the modules are pure.
|
|
85
85
|
const reminder_registry_1 = require("./commands/reminder-registry");
|
|
86
|
+
const app_settings_1 = require("./app-settings");
|
|
86
87
|
const inbox_primer_reminder_1 = require("./commands/inbox-primer-reminder");
|
|
87
88
|
const skill_activation_manifest_1 = require("./skill-activation-manifest");
|
|
88
89
|
const worktree_state_1 = require("./worktree-state");
|
|
@@ -1728,7 +1729,9 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1728
1729
|
activeCommandDrift: activeCommandDrift(),
|
|
1729
1730
|
loadoutUpdateRequired,
|
|
1730
1731
|
};
|
|
1731
|
-
let announceReg = (0,
|
|
1732
|
+
let announceReg = (0, app_settings_1.filterEnabledInterrupts)(reminder_registry_1.REGISTRY);
|
|
1733
|
+
if ((0, reminder_injector_1.mechanicKilled)())
|
|
1734
|
+
announceReg = announceReg.filter(m => m.id !== 'mechanic-friction');
|
|
1732
1735
|
if (!announceShort)
|
|
1733
1736
|
announceReg = announceReg.filter(m => m.id !== 'watcher-arm');
|
|
1734
1737
|
// PostCompact: a compaction wiped the once-loaded SessionStart announces from context.
|
|
@@ -2211,7 +2214,7 @@ async function coordinateGate(input) {
|
|
|
2211
2214
|
toolCommand: typeof toolInput.command === 'string' ? toolInput.command : '',
|
|
2212
2215
|
collisionPeers: peers,
|
|
2213
2216
|
};
|
|
2214
|
-
const fired = (0, reminder_registry_1.collectReminders)(env, (0, reminder_registry_1.commandModules)());
|
|
2217
|
+
const fired = (0, reminder_registry_1.collectReminders)(env, (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.commandModules)()));
|
|
2215
2218
|
if (!fired.length)
|
|
2216
2219
|
return;
|
|
2217
2220
|
// Effect: inject as additionalContext — advisory heads-up, NO permission pause.
|