@empirical-gauzza/cli 1.0.14 → 1.0.16
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/lib/constants.js +1 -1
- package/lib/help.js +14 -8
- package/lib/installers/hooks.js +249 -50
- package/lib/workspaceCommands.js +5 -5
- package/package.json +1 -1
package/lib/constants.js
CHANGED
|
@@ -202,7 +202,7 @@ Invite commands operate on the active team workspace. Create prints a shareable
|
|
|
202
202
|
empirical workspace delete <number|name|id>
|
|
203
203
|
empirical workspace delete <number|name|id> --confirm
|
|
204
204
|
|
|
205
|
-
Deletion dry-runs by default. \`--confirm\` calls the existing soft-delete endpoint; the Default
|
|
205
|
+
Deletion dry-runs by default. \`--confirm\` calls the existing soft-delete endpoint; the Default Workspace cannot be deleted. If the active workspace is deleted, the CLI selects the Default Workspace.
|
|
206
206
|
`,
|
|
207
207
|
|
|
208
208
|
doctor: `# Empirical Doctor
|
package/lib/help.js
CHANGED
|
@@ -19,26 +19,32 @@ function showHelp() {
|
|
|
19
19
|
log(' empirical plugin check [--force]');
|
|
20
20
|
log(' empirical policy show [--refresh]');
|
|
21
21
|
log(' empirical policy set --json <policy>');
|
|
22
|
-
log(' empirical auth login');
|
|
23
|
-
log(' empirical auth logout');
|
|
22
|
+
log(' empirical auth login Interactive browser login');
|
|
23
|
+
log(' empirical auth logout Clear the locally stored session');
|
|
24
24
|
log(' empirical oauth bootstrap headless [--print-access-token]');
|
|
25
|
+
log(' First-time token setup for CI/headless/API-key use');
|
|
25
26
|
log(' empirical oauth bootstrap headless --write-env [--env-output <path>]');
|
|
26
27
|
log(' empirical oauth refresh headless [--print-access-token]');
|
|
28
|
+
log(' Renew an existing headless token before/after it expires');
|
|
27
29
|
log(' empirical oauth refresh headless --write-env [--env-output <path>]');
|
|
28
30
|
log(' empirical oauth bootstrap docker [--print-access-token]');
|
|
31
|
+
log(' Same as bootstrap headless, tuned for container startup');
|
|
29
32
|
log(' empirical oauth refresh docker [--print-access-token]');
|
|
30
33
|
log(' empirical oauth client set [--client-id <id>] [--client-secret <secret>]');
|
|
31
34
|
log(' empirical oauth client show');
|
|
32
35
|
log(' empirical oauth client clear');
|
|
33
|
-
log(' empirical workspace list');
|
|
34
|
-
log(' empirical workspace current');
|
|
35
|
-
log(' empirical workspace select [<number|name>]');
|
|
36
|
-
log(' empirical workspace use <number|name|id>');
|
|
36
|
+
log(' empirical workspace list List workspaces you can select, and which one is active');
|
|
37
|
+
log(' empirical workspace current Show the currently active workspace');
|
|
38
|
+
log(' empirical workspace select [<number|name>] Set the active workspace (prompts if omitted)');
|
|
39
|
+
log(' empirical workspace use <number|name|id> Alias for `workspace select`');
|
|
37
40
|
log(' empirical workspace create <name> [--description <text>]');
|
|
41
|
+
log(' Create a new shared team workspace');
|
|
38
42
|
log(' empirical workspace invite create [--role reader|writer|manager] [--expires-in-days <n>] [--max-uses <n>]');
|
|
39
|
-
log('
|
|
40
|
-
log(' empirical workspace invite
|
|
43
|
+
log(' Create a shareable invite link for the active team workspace');
|
|
44
|
+
log(' empirical workspace invite list List active invites for the active team workspace');
|
|
45
|
+
log(' empirical workspace invite revoke <token> Revoke a previously created invite');
|
|
41
46
|
log(' empirical workspace delete <number|name|id> [--confirm]');
|
|
47
|
+
log(' Soft-delete a team workspace (dry-run without --confirm)');
|
|
42
48
|
log(' empirical oauth bootstrap openclaw [--print-access-token] (legacy alias)');
|
|
43
49
|
log(' empirical oauth refresh openclaw [--print-access-token] (legacy alias)');
|
|
44
50
|
log(' empirical --oauth-bootstrap-headless [--print-access-token]');
|
package/lib/installers/hooks.js
CHANGED
|
@@ -292,21 +292,148 @@ try {
|
|
|
292
292
|
}
|
|
293
293
|
`;
|
|
294
294
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
295
|
+
// Copilot-only variant of the SessionStart hook. Per GitHub's own hooks reference (fetched
|
|
296
|
+
// 2026-08-08): unlike Claude/Codex, Copilot's sessionStart event has NO output mechanism at all
|
|
297
|
+
// - there is no additionalContext-equivalent field, so a hook cannot inject memory context into
|
|
298
|
+
// the session this way, full stop (this is a genuine platform limitation, not a bug to work
|
|
299
|
+
// around). Separately, Copilot requires ALL hook stdout to be valid JSON parsed via a single
|
|
300
|
+
// JSON.parse call - plain text (fine for Claude, and previously assumed fine here too) is
|
|
301
|
+
// invalid and makes Copilot log a hook execution failure. So this variant still runs the memory
|
|
302
|
+
// query and logs the result for diagnostic/audit purposes, but emits {} (empty, valid JSON,
|
|
303
|
+
// explicitly documented as "use defaults") instead of attempting to inject anything.
|
|
304
|
+
const COPILOT_HOOK_SESSION_START_SOURCE = `#!/usr/bin/env node
|
|
305
|
+
'use strict';
|
|
306
|
+
|
|
307
|
+
var spawnSync = require('child_process').spawnSync;
|
|
308
|
+
var fs = require('fs');
|
|
309
|
+
var os = require('os');
|
|
310
|
+
var path = require('path');
|
|
311
|
+
|
|
312
|
+
var LOG_PATH = path.join(os.homedir(), '.empirical', 'hooks', 'session-start.log');
|
|
313
|
+
var MAX_LOG_BYTES = 200 * 1024;
|
|
314
|
+
|
|
315
|
+
function appendLog(line) {
|
|
316
|
+
try {
|
|
317
|
+
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
|
|
318
|
+
fs.appendFileSync(LOG_PATH, '[' + new Date().toISOString() + '] ' + line + '\\n');
|
|
319
|
+
var stat = fs.statSync(LOG_PATH);
|
|
320
|
+
if (stat.size > MAX_LOG_BYTES) {
|
|
321
|
+
var tail = fs.readFileSync(LOG_PATH, 'utf8').slice(-MAX_LOG_BYTES / 2);
|
|
322
|
+
fs.writeFileSync(LOG_PATH, tail);
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
// Diagnostics must never break session start.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resolveEmpiricalEntryPoint() {
|
|
330
|
+
if (process.platform !== 'win32') {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
var where = spawnSync('where', ['empirical.cmd'], { encoding: 'utf8', timeout: 5000, shell: true });
|
|
335
|
+
if (where.status !== 0 || !where.stdout) return null;
|
|
336
|
+
var cmdPath = where.stdout.split(/\\r?\\n/).map(function (s) { return s.trim(); }).filter(Boolean)[0];
|
|
337
|
+
if (!cmdPath || !fs.existsSync(cmdPath)) return null;
|
|
338
|
+
var cmdContent = fs.readFileSync(cmdPath, 'utf8');
|
|
339
|
+
var match = cmdContent.match(/"%dp0%\\\\(node_modules\\\\.+?\\.js)"/);
|
|
340
|
+
if (!match) return null;
|
|
341
|
+
var candidate = path.join(path.dirname(cmdPath), match[1]);
|
|
342
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function commandExists(cmd) {
|
|
349
|
+
var probe = process.platform === 'win32'
|
|
350
|
+
? spawnSync('where', [cmd], { stdio: 'ignore', shell: true })
|
|
351
|
+
: spawnSync('which', [cmd], { stdio: 'ignore' });
|
|
352
|
+
return probe.status === 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function runMemoryQuery(args) {
|
|
356
|
+
var entryPoint = resolveEmpiricalEntryPoint();
|
|
357
|
+
if (entryPoint) {
|
|
358
|
+
appendLog('resolved direct entry point: ' + entryPoint);
|
|
359
|
+
return spawnSync(process.execPath, [entryPoint].concat(args), {
|
|
360
|
+
encoding: 'utf8',
|
|
361
|
+
timeout: 10000,
|
|
362
|
+
shell: false
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
appendLog('direct entry point not resolved, falling back to shell invocation (shell=' + (process.platform === 'win32') + ')');
|
|
367
|
+
return spawnSync('empirical', args, {
|
|
368
|
+
encoding: 'utf8',
|
|
369
|
+
timeout: 10000,
|
|
370
|
+
shell: process.platform === 'win32'
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function main() {
|
|
375
|
+
appendLog('hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
|
|
376
|
+
|
|
377
|
+
if (!commandExists('empirical')) {
|
|
378
|
+
appendLog('empirical CLI not found on PATH, exiting');
|
|
379
|
+
console.log('{}');
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var result = runMemoryQuery([
|
|
384
|
+
'memory', 'query',
|
|
385
|
+
'--match', 'project context, conventions, recent decisions, user preferences',
|
|
386
|
+
'--top-k', '8'
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
if (!result || result.status !== 0 || !result.stdout) {
|
|
390
|
+
appendLog('memory query failed or returned no stdout (status=' + (result && result.status) + ', stderr=' + (result && result.stderr ? String(result.stderr).slice(0, 300) : '') + ')');
|
|
391
|
+
console.log('{}');
|
|
392
|
+
process.exit(0);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
var results;
|
|
396
|
+
try {
|
|
397
|
+
results = JSON.parse(result.stdout);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
appendLog('failed to parse memory query stdout as JSON: ' + e.message);
|
|
400
|
+
console.log('{}');
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
405
|
+
appendLog('memory query returned zero results');
|
|
406
|
+
console.log('{}');
|
|
407
|
+
process.exit(0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
var lines = results.slice(0, 8).map(function (r) {
|
|
411
|
+
var category = r && r.category ? r.category : 'memory';
|
|
412
|
+
var summary = r && r.summary ? r.summary : '';
|
|
413
|
+
return '- [' + category + '] ' + summary;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Logged only - Copilot's sessionStart event has no field that surfaces this to the model.
|
|
417
|
+
appendLog('memory query returned ' + results.length + ' result(s) (session-start has no output channel to inject them, logged only): ' + lines.join(' | '));
|
|
418
|
+
console.log('{}');
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
main();
|
|
424
|
+
} catch (e) {
|
|
425
|
+
try { appendLog('uncaught error in main(): ' + (e && e.message)); } catch (_e) { /* best effort */ }
|
|
426
|
+
console.log('{}');
|
|
427
|
+
process.exit(0);
|
|
428
|
+
}
|
|
429
|
+
`;
|
|
430
|
+
|
|
431
|
+
// Codex-only variant of the Stop reminder. Confirmed live (2026-08-08, via a real `codex
|
|
432
|
+
// exec` run) that Codex's Stop hook specifically requires a { systemMessage: "..." } JSON shape
|
|
433
|
+
// on stdout - plain text makes Codex report the hook as failed. Distinct from Claude's own JSON
|
|
434
|
+
// shape (hookSpecificOutput.additionalContext, see CLAUDE_HOOK_STOP_SOURCE below) and Copilot's
|
|
435
|
+
// (decision/reason, see COPILOT_HOOK_STOP_SOURCE) - each host has its own schema here.
|
|
436
|
+
const CODEX_HOOK_STOP_SOURCE = `#!/usr/bin/env node
|
|
310
437
|
'use strict';
|
|
311
438
|
|
|
312
439
|
var fs = require('fs');
|
|
@@ -342,10 +469,6 @@ function readStdinPayload() {
|
|
|
342
469
|
function main() {
|
|
343
470
|
var payload = readStdinPayload();
|
|
344
471
|
|
|
345
|
-
// Defensive loop guard: if a Stop hook's own forced-continuation output somehow triggers
|
|
346
|
-
// another Stop event before the agent produces new output, stop_hook_active (Claude's documented
|
|
347
|
-
// field name for this) prevents a reminder-print-loop. This hook never sets decision:block
|
|
348
|
-
// itself, but checking costs nothing and protects against any future change that does.
|
|
349
472
|
if (payload && payload.stop_hook_active === true) {
|
|
350
473
|
appendLog('stop_hook_active=true, skipping reminder');
|
|
351
474
|
process.exit(0);
|
|
@@ -353,12 +476,12 @@ function main() {
|
|
|
353
476
|
|
|
354
477
|
appendLog('stop hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
|
|
355
478
|
|
|
356
|
-
console.log(
|
|
479
|
+
console.log(JSON.stringify({
|
|
480
|
+
systemMessage: '[Empirical reminder] Before finishing: if this turn included a completed decision, a corrected mistake, or meaningful work worth remembering, record it now via the empirical-memory skill or \`empirical memory record\` - do not wait to be asked.'
|
|
481
|
+
}));
|
|
357
482
|
process.exit(0);
|
|
358
483
|
}
|
|
359
484
|
|
|
360
|
-
// See EMPIRICAL_HOOK_SESSION_START_SOURCE's identical guard above: a crash here must degrade to a
|
|
361
|
-
// silent no-op, never a visible "hook failed" error in the host CLI.
|
|
362
485
|
try {
|
|
363
486
|
main();
|
|
364
487
|
} catch (e) {
|
|
@@ -367,12 +490,16 @@ try {
|
|
|
367
490
|
}
|
|
368
491
|
`;
|
|
369
492
|
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
493
|
+
// Copilot-only variant of the Stop reminder above. Per GitHub's own hooks reference (fetched
|
|
494
|
+
// 2026-08-08): agentStop's output schema is { decision: "block"|"allow", reason, modifiedResponse }
|
|
495
|
+
// - there is no plain message field, so surfacing a reminder means returning decision:"block"
|
|
496
|
+
// with reason as the forced-continuation prompt text. Confirmed this has the same loop-prevention
|
|
497
|
+
// contract Claude Code uses: the input payload includes stop_hook_active ("true when this turn
|
|
498
|
+
// was already forced to continue by a prior 'block' decision" - same semantics, same field name
|
|
499
|
+
// as Claude's), and Copilot has its own hard backstop besides ("After 8 consecutive block
|
|
500
|
+
// continuations, the CLI overrides the hook and ends the turn anyway"). Checking stop_hook_active
|
|
501
|
+
// here means this hook only ever forces one continuation, never chains into that backstop.
|
|
502
|
+
const COPILOT_HOOK_STOP_SOURCE = `#!/usr/bin/env node
|
|
376
503
|
'use strict';
|
|
377
504
|
|
|
378
505
|
var fs = require('fs');
|
|
@@ -409,14 +536,16 @@ function main() {
|
|
|
409
536
|
var payload = readStdinPayload();
|
|
410
537
|
|
|
411
538
|
if (payload && payload.stop_hook_active === true) {
|
|
412
|
-
appendLog('stop_hook_active=true, skipping reminder');
|
|
539
|
+
appendLog('stop_hook_active=true, skipping reminder (already forced one continuation this turn)');
|
|
540
|
+
console.log('{}');
|
|
413
541
|
process.exit(0);
|
|
414
542
|
}
|
|
415
543
|
|
|
416
|
-
appendLog('stop hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
|
|
544
|
+
appendLog('stop hook fired (pid=' + process.pid + ', platform=' + process.platform + '), forcing one continuation to surface reminder');
|
|
417
545
|
|
|
418
546
|
console.log(JSON.stringify({
|
|
419
|
-
|
|
547
|
+
decision: 'block',
|
|
548
|
+
reason: '[Empirical reminder] Before finishing: if this turn included a completed decision, a corrected mistake, or meaningful work worth remembering, record it now via the empirical-memory skill or \`empirical memory record\` - do not wait to be asked.'
|
|
420
549
|
}));
|
|
421
550
|
process.exit(0);
|
|
422
551
|
}
|
|
@@ -425,6 +554,7 @@ try {
|
|
|
425
554
|
main();
|
|
426
555
|
} catch (e) {
|
|
427
556
|
try { appendLog('uncaught error in main(): ' + (e && e.message)); } catch (_e) { /* best effort */ }
|
|
557
|
+
console.log('{}');
|
|
428
558
|
process.exit(0);
|
|
429
559
|
}
|
|
430
560
|
`;
|
|
@@ -433,9 +563,9 @@ try {
|
|
|
433
563
|
// exit 0) is Claude Code's non-blocking Stop-hook feedback channel: the text is fed into context
|
|
434
564
|
// and the conversation continues, same outcome the earlier exit-2 approach achieved, but the
|
|
435
565
|
// transcript UI renders it as a plain system reminder instead of a red "Stop hook error" line.
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
566
|
+
// Codex needs its own systemMessage shape (CODEX_HOOK_STOP_SOURCE) and Copilot its own
|
|
567
|
+
// decision/reason shape (COPILOT_HOOK_STOP_SOURCE) - plain console.log output (what this hook
|
|
568
|
+
// used before the additionalContext fix) never reaches the model on any of the three hosts.
|
|
439
569
|
const CLAUDE_HOOK_STOP_SOURCE = `#!/usr/bin/env node
|
|
440
570
|
'use strict';
|
|
441
571
|
|
|
@@ -534,20 +664,56 @@ function buildPromptReminderMessage(pluginNames) {
|
|
|
534
664
|
// generation time (via JSON.stringify, so the message is embedded as a single already-escaped
|
|
535
665
|
// string literal - substituted through `${}`, not written as raw template-literal source text,
|
|
536
666
|
// which is what caused the earlier comment-escaping bug in this same file. Never do the latter).
|
|
537
|
-
// `platform`:
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
667
|
+
// `platform`:
|
|
668
|
+
// - 'codex': Codex's required hookSpecificOutput.additionalContext JSON shape (confirmed live
|
|
669
|
+
// 2026-08-08 via a real `codex exec` run - plain text made Codex report the hook as failed).
|
|
670
|
+
// - 'copilot': Copilot's userPromptSubmitted event has NO additionalContext-equivalent field at
|
|
671
|
+
// all (confirmed via GitHub's own hooks reference, fetched 2026-08-08) - the only output field
|
|
672
|
+
// is modifiedPrompt, which REPLACES the prompt for the rest of the turn. So this reads the
|
|
673
|
+
// real prompt text from stdin's `prompt` field and returns it with the reminder appended -
|
|
674
|
+
// more invasive than Claude/Codex's side-channel injection (it's now part of the visible
|
|
675
|
+
// prompt), but it's the only mechanism this event actually has. Falls back to {} (leave
|
|
676
|
+
// unchanged) if stdin can't be read/parsed or has no prompt field, so a parsing edge case can
|
|
677
|
+
// never corrupt or blank out the user's actual prompt.
|
|
678
|
+
// - default (Claude/Copilot's plain console.log was the old assumption for both - WRONG for
|
|
679
|
+
// Copilot, which requires valid JSON on stdout for every event, confirmed via the same fetch
|
|
680
|
+
// above): plain console.log, confirmed correct for Claude only.
|
|
541
681
|
function buildUserPromptSubmitHookSource(pluginNames, platform) {
|
|
542
682
|
const messageLiteral = JSON.stringify(buildPromptReminderMessage(pluginNames));
|
|
543
|
-
|
|
544
|
-
|
|
683
|
+
|
|
684
|
+
let stdinHelper = '';
|
|
685
|
+
let outputStatement;
|
|
686
|
+
if (platform === 'codex') {
|
|
687
|
+
outputStatement = `console.log(JSON.stringify({
|
|
545
688
|
hookSpecificOutput: {
|
|
546
689
|
hookEventName: 'UserPromptSubmit',
|
|
547
690
|
additionalContext: ${messageLiteral}
|
|
548
691
|
}
|
|
549
|
-
}))
|
|
550
|
-
|
|
692
|
+
}));`;
|
|
693
|
+
} else if (platform === 'copilot') {
|
|
694
|
+
stdinHelper = `
|
|
695
|
+
function readStdinPayload() {
|
|
696
|
+
try {
|
|
697
|
+
var raw = fs.readFileSync(0, 'utf8');
|
|
698
|
+
return raw ? JSON.parse(raw) : {};
|
|
699
|
+
} catch (e) {
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
outputStatement = `var payload = readStdinPayload();
|
|
705
|
+
if (payload && typeof payload.prompt === 'string' && payload.prompt.length > 0) {
|
|
706
|
+
console.log(JSON.stringify({
|
|
707
|
+
modifiedPrompt: payload.prompt + '\\n\\n' + ${messageLiteral}
|
|
708
|
+
}));
|
|
709
|
+
} else {
|
|
710
|
+
appendLog('no usable prompt field on stdin payload, leaving prompt unchanged');
|
|
711
|
+
console.log('{}');
|
|
712
|
+
}`;
|
|
713
|
+
} else {
|
|
714
|
+
outputStatement = `console.log(${messageLiteral});`;
|
|
715
|
+
}
|
|
716
|
+
|
|
551
717
|
return `#!/usr/bin/env node
|
|
552
718
|
'use strict';
|
|
553
719
|
|
|
@@ -571,7 +737,7 @@ function appendLog(line) {
|
|
|
571
737
|
// Diagnostics must never break the hook.
|
|
572
738
|
}
|
|
573
739
|
}
|
|
574
|
-
|
|
740
|
+
${stdinHelper}
|
|
575
741
|
// A crash here must degrade to a silent no-op, never a visible "hook failed" error in the host CLI.
|
|
576
742
|
try {
|
|
577
743
|
appendLog('user-prompt-submit hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
|
|
@@ -591,6 +757,18 @@ function buildHookCommand(scriptPath) {
|
|
|
591
757
|
return `"${process.execPath}" "${scriptPath}"`;
|
|
592
758
|
}
|
|
593
759
|
|
|
760
|
+
// Confirmed live (2026-08-08, via a real `copilot -p` run with debug logging): Copilot runs a
|
|
761
|
+
// hook's "command"/"powershell" field through powershell.exe on Windows, and a bare
|
|
762
|
+
// `"path" "arg"` string is not valid PowerShell syntax without the call operator (&) - two
|
|
763
|
+
// adjacent quoted string literals with nothing between them is a ParserError
|
|
764
|
+
// ("Unexpected token '...' in expression or statement"), not a "file not found"-style failure.
|
|
765
|
+
// This is a completely different bug from Codex's (which needed 8.3 short paths to avoid
|
|
766
|
+
// quoting) - no short-path workaround is needed here, just the operator PowerShell requires
|
|
767
|
+
// before an executable path.
|
|
768
|
+
function buildPowershellHookCommand(scriptPath) {
|
|
769
|
+
return `& "${process.execPath}" "${scriptPath}"`;
|
|
770
|
+
}
|
|
771
|
+
|
|
594
772
|
function removeEmpiricalHookEntries(entries, scriptPath) {
|
|
595
773
|
if (!Array.isArray(entries)) return [];
|
|
596
774
|
return entries
|
|
@@ -807,9 +985,9 @@ function installCopilotHooks(pluginNames) {
|
|
|
807
985
|
const sessionStartScriptPath = path.join(hooksDir, 'copilot-session-start.js');
|
|
808
986
|
const stopScriptPath = path.join(hooksDir, 'copilot-stop.js');
|
|
809
987
|
const userPromptScriptPath = path.join(hooksDir, 'copilot-user-prompt-submit.js');
|
|
810
|
-
writeFile(sessionStartScriptPath,
|
|
811
|
-
writeFile(stopScriptPath,
|
|
812
|
-
writeFile(userPromptScriptPath, buildUserPromptSubmitHookSource(pluginNames));
|
|
988
|
+
writeFile(sessionStartScriptPath, COPILOT_HOOK_SESSION_START_SOURCE);
|
|
989
|
+
writeFile(stopScriptPath, COPILOT_HOOK_STOP_SOURCE);
|
|
990
|
+
writeFile(userPromptScriptPath, buildUserPromptSubmitHookSource(pluginNames, 'copilot'));
|
|
813
991
|
|
|
814
992
|
const copilotHooksDir = path.join(os.homedir(), '.copilot', 'hooks');
|
|
815
993
|
ensureDir(copilotHooksDir);
|
|
@@ -826,19 +1004,38 @@ function installCopilotHooks(pluginNames) {
|
|
|
826
1004
|
}
|
|
827
1005
|
|
|
828
1006
|
const hookFilePath = path.join(copilotHooksDir, 'empirical-hooks.json');
|
|
1007
|
+
// Both `command` (POSIX fallback) and `powershell` (Windows-explicit, with the & call operator
|
|
1008
|
+
// PowerShell requires - see buildPowershellHookCommand) are always set: per GitHub's hooks
|
|
1009
|
+
// reference, an explicit `powershell` entry takes precedence over `command` on Windows, so this
|
|
1010
|
+
// is correct on both platforms without needing to detect the install-time OS here.
|
|
829
1011
|
const hookFileContent = {
|
|
830
1012
|
version: 1,
|
|
831
1013
|
hooks: {
|
|
832
1014
|
sessionStart: [
|
|
833
|
-
{
|
|
1015
|
+
{
|
|
1016
|
+
type: 'command',
|
|
1017
|
+
command: buildHookCommand(sessionStartScriptPath),
|
|
1018
|
+
powershell: buildPowershellHookCommand(sessionStartScriptPath),
|
|
1019
|
+
timeoutSec: 15
|
|
1020
|
+
}
|
|
834
1021
|
],
|
|
835
1022
|
// "userPromptSubmitted" per GitHub's hooks reference ("The user submits a prompt") - not
|
|
836
1023
|
// "userPromptSubmit" (that camelCase spelling is Claude/Codex's convention, not Copilot's).
|
|
837
1024
|
userPromptSubmitted: [
|
|
838
|
-
{
|
|
1025
|
+
{
|
|
1026
|
+
type: 'command',
|
|
1027
|
+
command: buildHookCommand(userPromptScriptPath),
|
|
1028
|
+
powershell: buildPowershellHookCommand(userPromptScriptPath),
|
|
1029
|
+
timeoutSec: 10
|
|
1030
|
+
}
|
|
839
1031
|
],
|
|
840
1032
|
agentStop: [
|
|
841
|
-
{
|
|
1033
|
+
{
|
|
1034
|
+
type: 'command',
|
|
1035
|
+
command: buildHookCommand(stopScriptPath),
|
|
1036
|
+
powershell: buildPowershellHookCommand(stopScriptPath),
|
|
1037
|
+
timeoutSec: 10
|
|
1038
|
+
}
|
|
842
1039
|
]
|
|
843
1040
|
}
|
|
844
1041
|
};
|
|
@@ -850,13 +1047,15 @@ function installCopilotHooks(pluginNames) {
|
|
|
850
1047
|
module.exports = {
|
|
851
1048
|
CLAUDE_HOOK_SESSION_START_SOURCE,
|
|
852
1049
|
CODEX_HOOK_SESSION_START_SOURCE,
|
|
853
|
-
|
|
1050
|
+
COPILOT_HOOK_SESSION_START_SOURCE,
|
|
854
1051
|
CODEX_HOOK_STOP_SOURCE,
|
|
1052
|
+
COPILOT_HOOK_STOP_SOURCE,
|
|
855
1053
|
CLAUDE_HOOK_STOP_SOURCE,
|
|
856
1054
|
buildPromptReminderMessage,
|
|
857
1055
|
buildUserPromptSubmitHookSource,
|
|
858
1056
|
getEmpiricalHooksDir,
|
|
859
1057
|
buildHookCommand,
|
|
1058
|
+
buildPowershellHookCommand,
|
|
860
1059
|
removeEmpiricalHookEntries,
|
|
861
1060
|
installClaudeHooks,
|
|
862
1061
|
getCodexHomeDir,
|
package/lib/workspaceCommands.js
CHANGED
|
@@ -174,11 +174,11 @@ async function runWorkspaceCommand(args) {
|
|
|
174
174
|
const selected = payload.defaultOption.selected
|
|
175
175
|
? payload.defaultOption
|
|
176
176
|
: (payload.workspaces || []).find((ws) => ws.selected) || payload.defaultOption;
|
|
177
|
-
log(`Current workspace: ${selected.label || 'Default
|
|
177
|
+
log(`Current workspace: ${selected.label || 'Default Workspace'}`);
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
const payload = await httpJson(currentUrl, 'GET', null, authHeaders);
|
|
181
|
-
log(`Current workspace: ${payload?.current?.label || 'Default
|
|
181
|
+
log(`Current workspace: ${payload?.current?.label || 'Default Workspace'}`);
|
|
182
182
|
if (payload?.locked) {
|
|
183
183
|
log('This OAuth client is admin-locked to a workspace.');
|
|
184
184
|
}
|
|
@@ -206,7 +206,7 @@ async function runWorkspaceCommand(args) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (!requested) {
|
|
209
|
-
throw new Error('Please provide a workspace selection, e.g. `empirical workspace use 1` or `empirical workspace use "Default
|
|
209
|
+
throw new Error('Please provide a workspace selection, e.g. `empirical workspace use 1` or `empirical workspace use "Default Workspace"`.');
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
const target = resolveWorkspaceTarget(requested, payload, teamPayload);
|
|
@@ -250,7 +250,7 @@ async function runWorkspaceCommand(args) {
|
|
|
250
250
|
if (target?.ambiguous) throw new Error(`Ambiguous workspace: ${requested}. Use its list number or workspace ID.`);
|
|
251
251
|
if (!target) throw new Error(`Workspace not found: ${requested}`);
|
|
252
252
|
if (target.isDefault || target.key === 'default_brain') {
|
|
253
|
-
throw new Error('The Default
|
|
253
|
+
throw new Error('The Default Workspace cannot be deleted.');
|
|
254
254
|
}
|
|
255
255
|
if (!target.workspaceId) throw new Error(`Workspace is not a deletable team brain: ${target.label}`);
|
|
256
256
|
|
|
@@ -265,7 +265,7 @@ async function runWorkspaceCommand(args) {
|
|
|
265
265
|
if (target.selected && clientId) {
|
|
266
266
|
const updateUrl = new URL(`/oauth/clients/${encodedClientId}/workspace-selection`, baseUrl).toString();
|
|
267
267
|
const updated = await httpJson(updateUrl, 'POST', { key: 'default_brain' }, authHeaders);
|
|
268
|
-
log(uiSuccess(`Soft-deleted workspace ${targetDescription}. Active workspace reset to ${updated?.current?.label || 'Default
|
|
268
|
+
log(uiSuccess(`Soft-deleted workspace ${targetDescription}. Active workspace reset to ${updated?.current?.label || 'Default Workspace'}.`));
|
|
269
269
|
clearSelectedWorkspace();
|
|
270
270
|
} else {
|
|
271
271
|
log(uiSuccess(`Soft-deleted workspace ${targetDescription}.`));
|