@nexrall/code-core 1.4.76 → 1.4.78
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/dist/agent/effortCap.d.ts +24 -0
- package/dist/agent/effortCap.d.ts.map +1 -0
- package/dist/agent/effortCap.js +66 -0
- package/dist/agent/goal.d.ts +55 -0
- package/dist/agent/goal.d.ts.map +1 -0
- package/dist/agent/goal.js +120 -0
- package/dist/agent/handoffBrief.d.ts +82 -0
- package/dist/agent/handoffBrief.d.ts.map +1 -0
- package/dist/agent/handoffBrief.js +121 -0
- package/dist/agent/hooks.d.ts +10 -2
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +9 -3
- package/dist/agent/modelCatalogue.d.ts +23 -0
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +28 -0
- package/dist/agent/outputStyles.d.ts +27 -0
- package/dist/agent/outputStyles.d.ts.map +1 -0
- package/dist/agent/outputStyles.js +174 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +21 -1
- package/dist/api/billingLines.d.ts +11 -0
- package/dist/api/billingLines.d.ts.map +1 -0
- package/dist/api/billingLines.js +56 -0
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/scheduleLines.d.ts +53 -0
- package/dist/api/scheduleLines.d.ts.map +1 -0
- package/dist/api/scheduleLines.js +150 -0
- package/dist/context/contextFiles.d.ts +81 -0
- package/dist/context/contextFiles.d.ts.map +1 -0
- package/dist/context/contextFiles.js +310 -0
- package/dist/diff/collect.d.ts +25 -0
- package/dist/diff/collect.d.ts.map +1 -0
- package/dist/diff/collect.js +169 -0
- package/dist/diff/diffView.d.ts +57 -0
- package/dist/diff/diffView.d.ts.map +1 -0
- package/dist/diff/diffView.js +231 -0
- package/dist/diff/reviewQueue.d.ts +53 -0
- package/dist/diff/reviewQueue.d.ts.map +1 -0
- package/dist/diff/reviewQueue.js +349 -0
- package/dist/events/recap.d.ts +54 -0
- package/dist/events/recap.d.ts.map +1 -0
- package/dist/events/recap.js +161 -0
- package/dist/events/sessionEvents.d.ts +54 -0
- package/dist/events/sessionEvents.d.ts.map +1 -0
- package/dist/events/sessionEvents.js +324 -0
- package/dist/events/sessionStats.d.ts +89 -0
- package/dist/events/sessionStats.d.ts.map +1 -0
- package/dist/events/sessionStats.js +264 -0
- package/dist/import/claudeImport.d.ts +44 -0
- package/dist/import/claudeImport.d.ts.map +1 -0
- package/dist/import/claudeImport.js +337 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -0
- package/dist/mcp/manager.d.ts +8 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +64 -57
- package/dist/permissions/destructiveTokens.d.ts +1 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +93 -0
- package/dist/permissions/rules.d.ts +12 -0
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +89 -7
- package/dist/plugins/configStore.d.ts +43 -0
- package/dist/plugins/configStore.d.ts.map +1 -0
- package/dist/plugins/configStore.js +253 -0
- package/dist/plugins/files.d.ts +10 -0
- package/dist/plugins/files.d.ts.map +1 -0
- package/dist/plugins/files.js +85 -0
- package/dist/plugins/index.d.ts +3 -22
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +30 -40
- package/dist/plugins/resolve.d.ts +18 -0
- package/dist/plugins/resolve.d.ts.map +1 -0
- package/dist/plugins/resolve.js +152 -0
- package/dist/plugins/userConfig.d.ts +90 -0
- package/dist/plugins/userConfig.d.ts.map +1 -0
- package/dist/plugins/userConfig.js +332 -0
- package/dist/plugins/validate.d.ts +14 -0
- package/dist/plugins/validate.d.ts.map +1 -0
- package/dist/plugins/validate.js +222 -0
- package/dist/project/projectCommands.d.ts +33 -0
- package/dist/project/projectCommands.d.ts.map +1 -0
- package/dist/project/projectCommands.js +246 -0
- package/dist/search/conversationTurns.d.ts +53 -0
- package/dist/search/conversationTurns.d.ts.map +1 -0
- package/dist/search/conversationTurns.js +170 -0
- package/dist/search/globalSearch.d.ts +144 -0
- package/dist/search/globalSearch.d.ts.map +1 -0
- package/dist/search/globalSearch.js +350 -0
- package/dist/share/sessionChannel.d.ts +136 -0
- package/dist/share/sessionChannel.d.ts.map +1 -0
- package/dist/share/sessionChannel.js +373 -0
- package/dist/share/sessionShare.d.ts +104 -0
- package/dist/share/sessionShare.d.ts.map +1 -0
- package/dist/share/sessionShare.js +336 -0
- package/dist/ui/configCmd.d.ts +48 -0
- package/dist/ui/configCmd.d.ts.map +1 -0
- package/dist/ui/configCmd.js +146 -0
- package/dist/ui/deployPanel.d.ts +91 -0
- package/dist/ui/deployPanel.d.ts.map +1 -0
- package/dist/ui/deployPanel.js +442 -0
- package/dist/ui/mergeAssistant.d.ts +120 -0
- package/dist/ui/mergeAssistant.d.ts.map +1 -0
- package/dist/ui/mergeAssistant.js +391 -0
- package/dist/ui/notify.d.ts +114 -0
- package/dist/ui/notify.d.ts.map +1 -0
- package/dist/ui/notify.js +235 -0
- package/dist/ui/promptSuggest.d.ts +56 -0
- package/dist/ui/promptSuggest.d.ts.map +1 -0
- package/dist/ui/promptSuggest.js +160 -0
- package/dist/ui/taskGraph.d.ts +44 -0
- package/dist/ui/taskGraph.d.ts.map +1 -0
- package/dist/ui/taskGraph.js +289 -0
- package/dist/util/addDir.d.ts +31 -0
- package/dist/util/addDir.d.ts.map +1 -0
- package/dist/util/addDir.js +124 -0
- package/dist/util/batch.d.ts +16 -0
- package/dist/util/batch.d.ts.map +1 -0
- package/dist/util/batch.js +92 -0
- package/dist/util/cellWidth.d.ts +12 -0
- package/dist/util/cellWidth.d.ts.map +1 -0
- package/dist/util/cellWidth.js +161 -0
- package/dist/util/duration.d.ts +8 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +50 -0
- package/dist/util/loopJobs.d.ts +67 -0
- package/dist/util/loopJobs.d.ts.map +1 -0
- package/dist/util/loopJobs.js +169 -0
- package/dist/util/settingsFile.d.ts +14 -0
- package/dist/util/settingsFile.d.ts.map +1 -0
- package/dist/util/settingsFile.js +99 -0
- package/dist/util/toolLabel.d.ts +2 -0
- package/dist/util/toolLabel.d.ts.map +1 -0
- package/dist/util/toolLabel.js +17 -0
- package/dist/verify/failureTimeline.d.ts +100 -0
- package/dist/verify/failureTimeline.d.ts.map +1 -0
- package/dist/verify/failureTimeline.js +253 -0
- package/dist/verify/verification.d.ts +75 -0
- package/dist/verify/verification.d.ts.map +1 -0
- package/dist/verify/verification.js +315 -0
- package/package.json +5 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── `nex plugin validate` (S24) ─────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// One structural check of a plugin directory, runnable in CI before anyone
|
|
5
|
+
// installs it. It answers the questions the install-time inspector cannot:
|
|
6
|
+
//
|
|
7
|
+
// • does the manifest parse, and is its `userConfig` block legal (strict —
|
|
8
|
+
// an unknown field inside an option is an error, matching Claude Code)?
|
|
9
|
+
// • do hooks.json / mcp.json parse, and resolve through the SAME file table
|
|
10
|
+
// the runtime loader reads (a component under a path the loader ignores
|
|
11
|
+
// would otherwise pass validation and do nothing);
|
|
12
|
+
// • does every `${user_config.KEY}` reference name a DECLARED option
|
|
13
|
+
// (Claude Code 2.1.281's check) — and is it used in a place that accepts
|
|
14
|
+
// it? A shell-form hook `command` never does: the substituted value would
|
|
15
|
+
// be re-parsed by the shell, so the runtime rejects that reference and the
|
|
16
|
+
// validate report says to read NEXRALL_PLUGIN_OPTION_<KEY> from the
|
|
17
|
+
// environment instead;
|
|
18
|
+
// • does every MCP server entry carry what a connection needs (a string
|
|
19
|
+
// `command`, or a string `url`)?
|
|
20
|
+
// • insecure remote URLs (plain http:// to a non-localhost host) as warnings.
|
|
21
|
+
//
|
|
22
|
+
// Pure reads; no plugin code ever executes here.
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.validatePluginDir = validatePluginDir;
|
|
58
|
+
exports.formatValidateReport = formatValidateReport;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const files_1 = require("./files");
|
|
62
|
+
const userConfig_1 = require("./userConfig");
|
|
63
|
+
const LOCALHOST = /^(localhost|127\.0\.0\.1|\[::1\]|::1)$/i;
|
|
64
|
+
function insecureUrl(url) {
|
|
65
|
+
if (!/^http:\/\//i.test(url))
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const host = new URL(url).hostname;
|
|
69
|
+
return !LOCALHOST.test(host);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return true; // unparseable http URL: still the insecure scheme
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Walk one hooks phase entry (flat and nested CC forms) into handler objects. */
|
|
76
|
+
function hookHandlers(entry) {
|
|
77
|
+
if (!entry || typeof entry !== 'object')
|
|
78
|
+
return [];
|
|
79
|
+
const e = entry;
|
|
80
|
+
if (Array.isArray(e.hooks))
|
|
81
|
+
return e.hooks.filter((h) => !!h && typeof h === 'object');
|
|
82
|
+
return [e];
|
|
83
|
+
}
|
|
84
|
+
function validatePluginDir(dir) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
const report = { ok: true, dir, manifest: null, userConfig: [], errors, warnings };
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.statSync(dir).isDirectory()) {
|
|
90
|
+
errors.push(`not a directory: ${dir}`);
|
|
91
|
+
return { ...report, ok: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
errors.push(`does not exist: ${dir}`);
|
|
96
|
+
return { ...report, ok: false };
|
|
97
|
+
}
|
|
98
|
+
// ── manifest ───────────────────────────────────────────────────────────────
|
|
99
|
+
const manifestFile = (0, files_1.resolvePluginFile)(dir, 'manifest');
|
|
100
|
+
report.manifest = manifestFile;
|
|
101
|
+
let manifest = null;
|
|
102
|
+
if (!manifestFile) {
|
|
103
|
+
warnings.push('no plugin.json found (the plugin still loads by directory name, but cannot declare userConfig)');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
try {
|
|
107
|
+
const j = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
108
|
+
if (!j || typeof j !== 'object' || Array.isArray(j)) {
|
|
109
|
+
errors.push(`${manifestFile}: must be a JSON object`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
manifest = j;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
errors.push(`${manifestFile}: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (manifest) {
|
|
120
|
+
for (const field of ['name', 'version', 'description']) {
|
|
121
|
+
if (manifest[field] !== undefined && typeof manifest[field] !== 'string') {
|
|
122
|
+
errors.push(`plugin.json: "${field}" must be a string`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (manifest.userConfig !== undefined) {
|
|
126
|
+
const parsed = (0, userConfig_1.parseUserConfig)(manifest);
|
|
127
|
+
report.userConfig = parsed.options;
|
|
128
|
+
errors.push(...parsed.errors);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const declared = new Set(report.userConfig.map((o) => o.key));
|
|
132
|
+
// ── hooks.json ─────────────────────────────────────────────────────────────
|
|
133
|
+
const hooksFile = (0, files_1.resolvePluginFile)(dir, 'hooks');
|
|
134
|
+
if (hooksFile) {
|
|
135
|
+
try {
|
|
136
|
+
const j = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
137
|
+
const hooks = (j?.hooks ?? j);
|
|
138
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
139
|
+
errors.push(`${hooksFile}: must be an object of hook phases`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
for (const [phase, entries] of Object.entries(hooks)) {
|
|
143
|
+
if (!Array.isArray(entries)) {
|
|
144
|
+
errors.push(`${hooksFile}: "${phase}" must be an array`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
for (const handler of entries.flatMap(hookHandlers)) {
|
|
148
|
+
const type = handler.type ?? (handler.url ? 'http' : handler.prompt ? 'prompt' : 'command');
|
|
149
|
+
if (type === 'command') {
|
|
150
|
+
const cmd = handler.command;
|
|
151
|
+
if (typeof cmd !== 'string' || !cmd.trim()) {
|
|
152
|
+
errors.push(`${hooksFile}: a command hook in "${phase}" has no command string`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if ((0, userConfig_1.findUserConfigRefs)(cmd).length) {
|
|
156
|
+
errors.push(`${hooksFile}: a command hook in "${phase}" uses \${user_config.*} in its shell-form command — ` +
|
|
157
|
+
'the value would be re-parsed by the shell. Read NEXRALL_PLUGIN_OPTION_<KEY> from the hook environment instead');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
errors.push(`${hooksFile}: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ── mcp.json ───────────────────────────────────────────────────────────────
|
|
169
|
+
const mcpFile = (0, files_1.resolvePluginFile)(dir, 'mcp');
|
|
170
|
+
if (mcpFile) {
|
|
171
|
+
try {
|
|
172
|
+
const j = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
173
|
+
const servers = (j?.mcpServers ?? j);
|
|
174
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
175
|
+
errors.push(`${mcpFile}: must be an object of server configs (or { mcpServers: … })`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
179
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
180
|
+
errors.push(`${mcpFile}: server "${name}" is not an object — it will not connect`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const c = cfg;
|
|
184
|
+
const hasCommand = typeof c.command === 'string' && c.command.trim() !== '';
|
|
185
|
+
const hasUrl = typeof c.url === 'string' && c.url.trim() !== '';
|
|
186
|
+
if (!hasCommand && !hasUrl) {
|
|
187
|
+
errors.push(`${mcpFile}: server "${name}" has neither a string "command" nor a "url" — it will not connect`);
|
|
188
|
+
}
|
|
189
|
+
if (hasUrl && insecureUrl(String(c.url))) {
|
|
190
|
+
warnings.push(`${mcpFile}: server "${name}" uses a plain http:// URL (${String(c.url)})`);
|
|
191
|
+
}
|
|
192
|
+
for (const ref of (0, userConfig_1.collectUserConfigRefsDeep)(cfg)) {
|
|
193
|
+
if (!declared.has(ref)) {
|
|
194
|
+
errors.push(`${mcpFile}: server "${name}" references \${user_config.${ref}} but the manifest does not declare "${ref}"`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
errors.push(`${mcpFile}: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
report.ok = errors.length === 0;
|
|
205
|
+
return report;
|
|
206
|
+
}
|
|
207
|
+
/** One-line human summary lines for the CLI printer (warnings then errors). */
|
|
208
|
+
function formatValidateReport(r) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
const label = path.basename(r.dir) || r.dir;
|
|
211
|
+
lines.push(` Plugin: ${label}`);
|
|
212
|
+
lines.push(` Manifest: ${r.manifest ?? '(none)'}`);
|
|
213
|
+
if (r.userConfig.length)
|
|
214
|
+
lines.push(` userConfig options: ${r.userConfig.map((o) => o.key).join(', ')}`);
|
|
215
|
+
for (const w of r.warnings)
|
|
216
|
+
lines.push(` ⚠ ${w}`);
|
|
217
|
+
for (const e of r.errors)
|
|
218
|
+
lines.push(` ✗ ${e}`);
|
|
219
|
+
lines.push(r.ok ? ' ✓ Validation passed' : ` ✗ Validation failed (${r.errors.length} error(s))`);
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ProjectCommand {
|
|
2
|
+
/** What the user types after `/run`: an npm script name, a make target, … */
|
|
3
|
+
name: string;
|
|
4
|
+
/** The full shell command. */
|
|
5
|
+
command: string;
|
|
6
|
+
/** The file it came from, shown in the picker and in errors. */
|
|
7
|
+
source: string;
|
|
8
|
+
}
|
|
9
|
+
/** The package manager whose scripts this project's lockfile asks for. */
|
|
10
|
+
export declare function packageManager(workDir: string): 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
11
|
+
/**
|
|
12
|
+
* Every command this folder can run, in a stable order (package.json first — the most
|
|
13
|
+
* common source — then the rest alphabetically). Missing files are simply absent: a
|
|
14
|
+
* project with no package.json is not an error, it is a Cargo project.
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectProjectCommands(workDir: string): ProjectCommand[];
|
|
17
|
+
/**
|
|
18
|
+
* The commands a shortcut should run: everything in the best tier it finds, best
|
|
19
|
+
* first. More than one means the caller asks which (a repo with `test` and
|
|
20
|
+
* `test:integration` should not silently pick one — running the wrong suite and
|
|
21
|
+
* calling it "the tests" is worse than one extra keystroke).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveShortcut<T extends {
|
|
24
|
+
name: string;
|
|
25
|
+
}>(cmds: T[], want: string): T[];
|
|
26
|
+
/**
|
|
27
|
+
* Runs `git` with the user's arguments: `/git` alone means `git status`, `/git log -5`
|
|
28
|
+
* means exactly that. Not a list of allowed subcommands — the read-only gate in front
|
|
29
|
+
* of this (core's isReadOnlyBash) is what decides whether it may run unprompted, so
|
|
30
|
+
* `/git commit` works in a normal session and is refused under `--readonly`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function gitCommand(arg: string): string;
|
|
33
|
+
//# sourceMappingURL=projectCommands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectCommands.d.ts","sourceRoot":"","sources":["../../src/project/projectCommands.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;CAChB;AAkCD,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAM/E;AAsED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAiEvE;AAsBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,CAKxF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG9C"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Project commands: `/run`, `/test`, `/build`, `/lint` and `/git` (spec §85).
|
|
3
|
+
//
|
|
4
|
+
// Lives in core (moved from the CLI's projectCommands.ts, 2026-10-04) so the
|
|
5
|
+
// VS Code panel detects the same commands and runs them in its own terminal;
|
|
6
|
+
// the CLI module is a re-export shim.
|
|
7
|
+
//
|
|
8
|
+
// WHY: the everyday things you do next to an agent are "run the tests", "build it",
|
|
9
|
+
// "show me the diff" — and the only way to do them was `!npm test`, typed out in
|
|
10
|
+
// full, or asking the model (a whole turn, tokens, and an approval prompt for
|
|
11
|
+
// something YOU decided). `/test` should mean "whatever testing means in this
|
|
12
|
+
// project". That is a detection problem, and detection is what this file is: what
|
|
13
|
+
// `test` IS depends on the folder, so we read the folder instead of guessing.
|
|
14
|
+
//
|
|
15
|
+
// Kept free of chat.ts/Ink so every rule here is unit-testable (test/projectCommands.test.ts).
|
|
16
|
+
// The command that comes out is run through the SAME path as `!cmd` (permission
|
|
17
|
+
// rules, read-only gate, output limits, background, transcript) — this module only
|
|
18
|
+
// decides WHAT to run, never runs it.
|
|
19
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.packageManager = packageManager;
|
|
24
|
+
exports.detectProjectCommands = detectProjectCommands;
|
|
25
|
+
exports.resolveShortcut = resolveShortcut;
|
|
26
|
+
exports.gitCommand = gitCommand;
|
|
27
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
29
|
+
/** Lifecycle hooks: npm runs these itself, so they are noise in a "what can I run" list. */
|
|
30
|
+
const NPM_HOOKS = new Set([
|
|
31
|
+
'preinstall',
|
|
32
|
+
'install',
|
|
33
|
+
'postinstall',
|
|
34
|
+
'prepare',
|
|
35
|
+
'prepublish',
|
|
36
|
+
'prepublishOnly',
|
|
37
|
+
'prepack',
|
|
38
|
+
'postpack',
|
|
39
|
+
'preversion',
|
|
40
|
+
'postversion',
|
|
41
|
+
'dependencies',
|
|
42
|
+
'prepublishOnly',
|
|
43
|
+
]);
|
|
44
|
+
function readText(file) {
|
|
45
|
+
try {
|
|
46
|
+
return node_fs_1.default.readFileSync(file, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isFile(file) {
|
|
53
|
+
try {
|
|
54
|
+
return node_fs_1.default.statSync(file).isFile();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The package manager whose scripts this project's lockfile asks for. */
|
|
61
|
+
function packageManager(workDir) {
|
|
62
|
+
const has = (f) => isFile(node_path_1.default.join(workDir, f));
|
|
63
|
+
if (has('pnpm-lock.yaml') || has('pnpm-workspace.yaml'))
|
|
64
|
+
return 'pnpm';
|
|
65
|
+
if (has('yarn.lock'))
|
|
66
|
+
return 'yarn';
|
|
67
|
+
if (has('bun.lockb') || has('bun.lock'))
|
|
68
|
+
return 'bun';
|
|
69
|
+
return 'npm';
|
|
70
|
+
}
|
|
71
|
+
/** Reads `scripts` out of a package.json-shaped file. Never throws on bad JSON. */
|
|
72
|
+
function readScripts(file, rel) {
|
|
73
|
+
const raw = readText(file);
|
|
74
|
+
if (raw === null)
|
|
75
|
+
return [];
|
|
76
|
+
let json;
|
|
77
|
+
try {
|
|
78
|
+
json = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const scripts = json?.scripts;
|
|
84
|
+
if (!scripts || typeof scripts !== 'object')
|
|
85
|
+
return [];
|
|
86
|
+
const pm = packageManager(node_path_1.default.dirname(file));
|
|
87
|
+
return Object.keys(scripts)
|
|
88
|
+
.filter((n) => !NPM_HOOKS.has(n))
|
|
89
|
+
.map((n) => ({ name: n, command: `${pm} run ${n}`, source: rel }));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Make targets: column-0 `name:` rules, minus assignments (`VAR := x`, `+=`).
|
|
93
|
+
*
|
|
94
|
+
* Pattern rules (`%.o: %.c`) need no test of their own: the name is
|
|
95
|
+
* `[A-Za-z0-9][\w.-]*` and must be followed directly by `:`, so a `%` — which is not in
|
|
96
|
+
* that class — can never reach the name. A guard for it was written, proved
|
|
97
|
+
* unreachable by a mutation run, and removed rather than left looking load-bearing.
|
|
98
|
+
* Targets containing `/` are deliberately not listed: `src/foo.o: src/foo.c` would
|
|
99
|
+
* flood /run with one entry per object file.
|
|
100
|
+
*/
|
|
101
|
+
function makeTargets(file, rel) {
|
|
102
|
+
const raw = readText(file);
|
|
103
|
+
if (raw === null)
|
|
104
|
+
return [];
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const line of raw.split('\n')) {
|
|
107
|
+
const m = /^([A-Za-z0-9][\w.-]*)\s*:(?!=)/.exec(line);
|
|
108
|
+
if (!m)
|
|
109
|
+
continue;
|
|
110
|
+
out.push({ name: m[1], command: `make ${m[1]}`, source: rel });
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** justfile recipes: column-0 `name arg:` / `name:`, minus assignments (`x := y`). */
|
|
115
|
+
function justRecipes(file, rel) {
|
|
116
|
+
const raw = readText(file);
|
|
117
|
+
if (raw === null)
|
|
118
|
+
return [];
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const line of raw.split('\n')) {
|
|
121
|
+
const m = /^([A-Za-z_][\w-]*)(\s[^:]*)?\s*:(?!=)/.exec(line);
|
|
122
|
+
if (!m)
|
|
123
|
+
continue;
|
|
124
|
+
out.push({ name: m[1], command: `just ${m[1]}`, source: rel });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** `deno task` entries. deno.jsonc is skipped: its comments need a real JSONC parser. */
|
|
129
|
+
function denoTasks(file, rel) {
|
|
130
|
+
const raw = readText(file);
|
|
131
|
+
if (raw === null)
|
|
132
|
+
return [];
|
|
133
|
+
let json;
|
|
134
|
+
try {
|
|
135
|
+
json = JSON.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const tasks = json?.tasks;
|
|
141
|
+
if (!tasks || typeof tasks !== 'object')
|
|
142
|
+
return [];
|
|
143
|
+
return Object.keys(tasks).map((n) => ({ name: n, command: `deno task ${n}`, source: rel }));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Every command this folder can run, in a stable order (package.json first — the most
|
|
147
|
+
* common source — then the rest alphabetically). Missing files are simply absent: a
|
|
148
|
+
* project with no package.json is not an error, it is a Cargo project.
|
|
149
|
+
*/
|
|
150
|
+
function detectProjectCommands(workDir) {
|
|
151
|
+
const out = [];
|
|
152
|
+
const at = (f) => node_path_1.default.join(workDir, f);
|
|
153
|
+
out.push(...readScripts(at('package.json'), 'package.json'));
|
|
154
|
+
const makefile = ['Makefile', 'makefile', 'GNUmakefile'].find((f) => isFile(at(f)));
|
|
155
|
+
if (makefile)
|
|
156
|
+
out.push(...makeTargets(at(makefile), makefile));
|
|
157
|
+
const justfile = ['justfile', 'Justfile'].find((f) => isFile(at(f)));
|
|
158
|
+
if (justfile)
|
|
159
|
+
out.push(...justRecipes(at(justfile), justfile));
|
|
160
|
+
if (isFile(at('deno.json')))
|
|
161
|
+
out.push(...denoTasks(at('deno.json'), 'deno.json'));
|
|
162
|
+
const composer = readText(at('composer.json'));
|
|
163
|
+
if (composer !== null) {
|
|
164
|
+
try {
|
|
165
|
+
const json = JSON.parse(composer);
|
|
166
|
+
for (const n of Object.keys(json?.scripts ?? {})) {
|
|
167
|
+
out.push({ name: n, command: `composer run ${n}`, source: 'composer.json' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* a broken composer.json is not this command's problem */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Go and Rust have one obvious command per task; no file to read beyond the manifest.
|
|
175
|
+
if (isFile(at('go.mod'))) {
|
|
176
|
+
out.push({ name: 'build', command: 'go build ./...', source: 'go.mod' }, { name: 'test', command: 'go test ./...', source: 'go.mod' }, { name: 'vet', command: 'go vet ./...', source: 'go.mod' }, { name: 'fmt', command: 'gofmt -l .', source: 'go.mod' });
|
|
177
|
+
}
|
|
178
|
+
if (isFile(at('Cargo.toml'))) {
|
|
179
|
+
out.push({ name: 'build', command: 'cargo build', source: 'Cargo.toml' }, { name: 'test', command: 'cargo test', source: 'Cargo.toml' }, { name: 'check', command: 'cargo check', source: 'Cargo.toml' }, { name: 'clippy', command: 'cargo clippy', source: 'Cargo.toml' }, { name: 'fmt', command: 'cargo fmt --check', source: 'Cargo.toml' });
|
|
180
|
+
}
|
|
181
|
+
// Python: only when the folder actually says how it is tested/linted. `pytest -q` in a
|
|
182
|
+
// project with no pytest is a wasted round trip, so evidence is required.
|
|
183
|
+
const pyproject = readText(at('pyproject.toml')) ?? '';
|
|
184
|
+
const hasPytest = isFile(at('pytest.ini')) ||
|
|
185
|
+
isFile(at('tox.ini')) ||
|
|
186
|
+
pyproject.includes('[tool.pytest') ||
|
|
187
|
+
node_fs_1.default.existsSync(at('tests'));
|
|
188
|
+
if (hasPytest)
|
|
189
|
+
out.push({ name: 'test', command: 'pytest -q', source: pyproject ? 'pyproject.toml' : 'pytest' });
|
|
190
|
+
if (pyproject.includes('[tool.ruff') || isFile(at('ruff.toml'))) {
|
|
191
|
+
out.push({ name: 'lint', command: 'ruff check .', source: 'ruff.toml' });
|
|
192
|
+
}
|
|
193
|
+
if (pyproject.includes('[tool.mypy')) {
|
|
194
|
+
out.push({ name: 'typecheck', command: 'mypy .', source: 'pyproject.toml' });
|
|
195
|
+
}
|
|
196
|
+
// An identical row twice (a Makefile that defines the same target twice, two
|
|
197
|
+
// manifests offering the same command) is always noise; the first source wins.
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
return out.filter((c) => (seen.has(c.command) ? false : (seen.add(c.command), true)));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* How well a command name answers a shortcut, lower is better, -1 means "not this one".
|
|
203
|
+
* `test:unit` and `test-unit` both answer `/test`; `e2e:test` only just.
|
|
204
|
+
*
|
|
205
|
+
* Word-based, never substring: an endsWith rule was tried and dropped, because
|
|
206
|
+
* "latest".endsWith("test") is TRUE — so `/test` in a project whose only script was
|
|
207
|
+
* `latest` would have run it and called it the test suite. `eslint` losing to that
|
|
208
|
+
* rule is the price, and `/run eslint` still works: running the wrong thing quietly
|
|
209
|
+
* is far worse than not finding a name.
|
|
210
|
+
*/
|
|
211
|
+
function tier(name, want) {
|
|
212
|
+
const n = name.toLowerCase();
|
|
213
|
+
const w = want.toLowerCase();
|
|
214
|
+
if (n === w)
|
|
215
|
+
return 0;
|
|
216
|
+
const parts = n.split(/[:._-]+/);
|
|
217
|
+
if (parts[0] === w)
|
|
218
|
+
return 1;
|
|
219
|
+
if (parts.includes(w))
|
|
220
|
+
return 2;
|
|
221
|
+
return -1;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The commands a shortcut should run: everything in the best tier it finds, best
|
|
225
|
+
* first. More than one means the caller asks which (a repo with `test` and
|
|
226
|
+
* `test:integration` should not silently pick one — running the wrong suite and
|
|
227
|
+
* calling it "the tests" is worse than one extra keystroke).
|
|
228
|
+
*/
|
|
229
|
+
function resolveShortcut(cmds, want) {
|
|
230
|
+
const scored = cmds.map((c) => ({ c, t: tier(c.name, want) })).filter((x) => x.t >= 0);
|
|
231
|
+
if (!scored.length)
|
|
232
|
+
return [];
|
|
233
|
+
const best = Math.min(...scored.map((x) => x.t));
|
|
234
|
+
return scored.filter((x) => x.t === best).map((x) => x.c);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Runs `git` with the user's arguments: `/git` alone means `git status`, `/git log -5`
|
|
238
|
+
* means exactly that. Not a list of allowed subcommands — the read-only gate in front
|
|
239
|
+
* of this (core's isReadOnlyBash) is what decides whether it may run unprompted, so
|
|
240
|
+
* `/git commit` works in a normal session and is refused under `--readonly`.
|
|
241
|
+
*/
|
|
242
|
+
function gitCommand(arg) {
|
|
243
|
+
const a = arg.trim();
|
|
244
|
+
return a ? `git ${a}` : 'git status';
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=projectCommands.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Message } from '../types';
|
|
2
|
+
/** `!cmd` transcripts are synthesised user messages; this is their prefix (moved
|
|
3
|
+
* here from commands/bangShell.ts so the synthetic-user list has one home). */
|
|
4
|
+
export declare const BANG_TRANSCRIPT_PREFIX = "I ran this command myself in the terminal:";
|
|
5
|
+
/** Lines of a turn's body shown by a jump before the rest is summarised. */
|
|
6
|
+
export declare const MAX_TURN_LINES = 80;
|
|
7
|
+
/** Tool-result lines kept inline; the rest is a count. */
|
|
8
|
+
export declare const RESULT_PREVIEW_LINES = 3;
|
|
9
|
+
/** Newest turns kept — a 400-turn session does not need all of it in memory. */
|
|
10
|
+
export declare const MAX_TURNS = 200;
|
|
11
|
+
/** Prompt length in a turn header / search label. */
|
|
12
|
+
export declare const PROMPT_CLIP = 100;
|
|
13
|
+
export interface Turn {
|
|
14
|
+
/** 1-based, in conversation order. */
|
|
15
|
+
index: number;
|
|
16
|
+
/** The user's prompt, flattened to one line. */
|
|
17
|
+
prompt: string;
|
|
18
|
+
/** Body lines, ready to print (no colour). */
|
|
19
|
+
lines: string[];
|
|
20
|
+
toolCalls: number;
|
|
21
|
+
/** Lines dropped by MAX_TURN_LINES (0 when everything fit). */
|
|
22
|
+
dropped: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function flatten(s: string): string;
|
|
25
|
+
export declare function clip(s: string, max: number): string;
|
|
26
|
+
/** One display line per content block, or null for blocks that are not conversation. */
|
|
27
|
+
export declare function blockLines(m: Message, b: Message['content'][number]): string[] | null;
|
|
28
|
+
/** Did this user message start a turn, or is it agent bookkeeping? */
|
|
29
|
+
export declare function startsTurn(m: Message): boolean;
|
|
30
|
+
export declare function isSyntheticUserText(text: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Group the conversation into turns. Content before the first real prompt (the
|
|
33
|
+
* opening context block) is not a turn and is skipped.
|
|
34
|
+
*/
|
|
35
|
+
export declare function turnsFrom(messages: Message[], opts?: {
|
|
36
|
+
maxLines?: number;
|
|
37
|
+
maxTurns?: number;
|
|
38
|
+
}): Turn[];
|
|
39
|
+
/**
|
|
40
|
+
* The most recent real prompt in the conversation — the label a `/cost` turn row carries.
|
|
41
|
+
*
|
|
42
|
+
* Scans BACKWARDS so a mid-turn follow-up (folded in by the loop while the agent works) is
|
|
43
|
+
* what the row reads: that is the last thing the user actually asked for. '' when nothing
|
|
44
|
+
* has been typed yet.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lastPrompt(messages: Message[], max?: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* The last prompt the USER actually typed — skips the synthetic user messages (a `!cmd`
|
|
49
|
+
* transcript, a task notification, a peer message), which are turns but were not typed.
|
|
50
|
+
* The §81 handoff goal is built from this; `lastPrompt` deliberately does not skip them.
|
|
51
|
+
*/
|
|
52
|
+
export declare function lastTypedPrompt(messages: Message[], max?: number): string;
|
|
53
|
+
//# sourceMappingURL=conversationTurns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversationTurns.d.ts","sourceRoot":"","sources":["../../src/search/conversationTurns.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyB,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAG/D;gFACgF;AAChF,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AAEnF,4EAA4E;AAC5E,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,gFAAgF;AAChF,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,qDAAqD;AACrD,eAAO,MAAM,WAAW,MAAM,CAAC;AAE/B,MAAM,WAAW,IAAI;IACnB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAwBrF;AAUD,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAG9C;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,EAAE,CA4C1G;AAUD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,SAAc,GAAG,MAAM,CAQzE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,SAAc,GAAG,MAAM,CAQ9E"}
|