@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 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 Brain cannot be deleted. If the active workspace is deleted, the CLI selects the Default Brain.
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(' empirical workspace invite list');
40
- log(' empirical workspace invite revoke <token>');
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]');
@@ -292,21 +292,148 @@ try {
292
292
  }
293
293
  `;
294
294
 
295
- // EMPIRICAL_HOOK_STOP_SOURCE: fires once per assistant turn (Claude: Stop; Codex: Stop, turn-
296
- // scoped per codex-rs/hooks/src/engine/dispatcher.rs; Copilot: agentStop, "the main agent
297
- // finishes a turn" per GitHub's own hooks reference). This is the write-side counterpart to the
298
- // SessionStart hook above (which only covers recall, once per session boundary) - deliberately
299
- // simple: it does NOT parse the transcript to detect whether memory-worthy activity happened
300
- // this turn (transcript formats differ across all three platforms and getting that heuristic
301
- // wrong silently - the exact class of bug this session spent hours finding - is worse than an
302
- // unconditional reminder). It never calls the empirical CLI and makes no network call, so the
303
- // "wasteful per-turn hook" concern that killed the old per-turn UserPromptSubmit hook (see
304
- // installClaudeHooks below) does not apply here - this is a single static stdout write.
305
- // ALWAYS exits 0: per Claude Code's documented Stop hook contract, exit 2 forces the agent to
306
- // keep responding (a blocking decision) - a bug in a hook that's supposed to be an informational
307
- // nudge must never be able to trap a session in a forced-continuation loop, so this is
308
- // unconditional regardless of any error encountered.
309
- const EMPIRICAL_HOOK_STOP_SOURCE = `#!/usr/bin/env node
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('[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.');
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
- // Codex-only variant of the Stop reminder above. Confirmed live (2026-08-08, via a real `codex
371
- // exec` run) that Codex's Stop hook specifically requires a { systemMessage: "..." } JSON shape
372
- // on stdout - plain text (what EMPIRICAL_HOOK_STOP_SOURCE emits, used by Copilot) makes Codex
373
- // report the hook as failed. Distinct from Claude's own JSON shape (hookSpecificOutput.
374
- // additionalContext, see CLAUDE_HOOK_STOP_SOURCE below) - each host has its own schema here.
375
- const CODEX_HOOK_STOP_SOURCE = `#!/usr/bin/env node
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
- 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.'
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
- // EMPIRICAL_HOOK_STOP_SOURCE above still uses plain console.log + exit 0, which Claude Code treats
437
- // as non-blocking output that's logged but never reaches the model - that was this hook's original
438
- // bug (fixed for Claude here, unconfirmed/unfixed for Codex/Copilot's own Stop/agentStop semantics).
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`: 'codex' emits Codex's required hookSpecificOutput.additionalContext JSON shape
538
- // (confirmed live 2026-08-08 via a real `codex exec` run - plain text made Codex report the hook
539
- // as failed); any other value (default) keeps the original plain console.log for Claude/Copilot,
540
- // which is confirmed working for both.
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
- const outputStatement = platform === 'codex'
544
- ? `console.log(JSON.stringify({
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
- : `console.log(${messageLiteral});`;
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, CLAUDE_HOOK_SESSION_START_SOURCE);
811
- writeFile(stopScriptPath, EMPIRICAL_HOOK_STOP_SOURCE);
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
- { type: 'command', command: buildHookCommand(sessionStartScriptPath), timeoutSec: 15 }
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
- { type: 'command', command: buildHookCommand(userPromptScriptPath), timeoutSec: 10 }
1025
+ {
1026
+ type: 'command',
1027
+ command: buildHookCommand(userPromptScriptPath),
1028
+ powershell: buildPowershellHookCommand(userPromptScriptPath),
1029
+ timeoutSec: 10
1030
+ }
839
1031
  ],
840
1032
  agentStop: [
841
- { type: 'command', command: buildHookCommand(stopScriptPath), timeoutSec: 10 }
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
- EMPIRICAL_HOOK_STOP_SOURCE,
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,
@@ -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 Brain'}`);
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 Brain'}`);
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 Brain"`.');
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 Brain cannot be deleted.');
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 Brain'}.`));
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}.`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Empirical CLI installer and local MCP wiring helper",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",