@staff0rd/assist 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,7 @@ After installation, the `assist` command will be available globally.
76
76
  - `assist config get <key>` - Get a config value
77
77
  - `assist config list` - List all config values
78
78
  - `assist verify` - Run all verify:* commands in parallel (from run configs in assist.yml and scripts in package.json)
79
+ - `assist verify all` - Run all checks, ignoring diff-based filters
79
80
  - `assist verify init` - Add verify scripts to a project
80
81
  - `assist verify hardcoded-colors` - Check for hardcoded hex colors in src/ (supports `hardcodedColors.ignore` globs in config)
81
82
  - `assist lint` - Run lint checks for conventions not enforced by biomejs
@@ -3,3 +3,5 @@ description: Run all verification commands in parallel
3
3
  ---
4
4
 
5
5
  Run `assist verify 2>&1` (not npm run, not npx - just `assist verify 2>&1` directly). If it fails, fix all errors and run again until it passes.
6
+
7
+ Use `assist verify all 2>&1` to bypass diff-based filters and run every check regardless of which files changed.
@@ -161,6 +161,29 @@ class VoiceDaemon:
161
161
  keyboard.type_text(to_type)
162
162
  self._typed_text = new_text
163
163
 
164
+ def _process_audio_chunk(
165
+ self, chunk, prob: float, sample_count: int, trailing_silence: int
166
+ ) -> tuple[int, int]:
167
+ """Buffer audio chunk, run partial STT, and check for segment end."""
168
+ self._audio_buffer.append(chunk)
169
+ sample_count += len(chunk)
170
+
171
+ if prob > self._vad.threshold:
172
+ trailing_silence = 0
173
+ else:
174
+ trailing_silence += 1
175
+
176
+ if sample_count - self._last_partial_at >= PARTIAL_STT_INTERVAL:
177
+ self._last_partial_at = sample_count
178
+ self._run_partial_stt()
179
+
180
+ if self._check_segment_end(sample_count, trailing_silence):
181
+ self._finalize_utterance()
182
+ sample_count = 0
183
+ trailing_silence = 0
184
+
185
+ return sample_count, trailing_silence
186
+
164
187
  def _check_segment_end(self, sample_count: int, trailing_silence: int) -> bool:
165
188
  """Check if the current segment is done.
166
189
 
@@ -189,6 +212,27 @@ class VoiceDaemon:
189
212
  log("smart_turn_incomplete", "Continuing to listen...")
190
213
  return False
191
214
 
215
+ def _dispatch_result(self, should_submit: bool, stripped: str) -> None:
216
+ """Log and optionally submit a recognized command."""
217
+ if stripped:
218
+ if should_submit:
219
+ log("dispatch_enter", stripped)
220
+ if DEBUG:
221
+ print(f" Final: {stripped} [Enter]", file=sys.stderr)
222
+ keyboard.press_enter()
223
+ else:
224
+ log("dispatch_typed", stripped)
225
+ if DEBUG:
226
+ print(f" Final: {stripped} (no submit)", file=sys.stderr)
227
+ elif should_submit:
228
+ # Submit word only — erase it and press enter
229
+ if self._typed_text:
230
+ keyboard.backspace(len(self._typed_text))
231
+ log("dispatch_enter", "(submit word only)")
232
+ if DEBUG:
233
+ print(" Submit word only [Enter]", file=sys.stderr)
234
+ keyboard.press_enter()
235
+
192
236
  def _finalize_utterance(self) -> None:
193
237
  """End of turn: final STT, correct typed text, press Enter."""
194
238
  if not self._audio_buffer:
@@ -215,23 +259,7 @@ class VoiceDaemon:
215
259
  self._update_typed_text(stripped)
216
260
  else:
217
261
  keyboard.type_text(stripped)
218
- if should_submit:
219
- log("dispatch_enter", stripped)
220
- if DEBUG:
221
- print(f" Final: {stripped} [Enter]", file=sys.stderr)
222
- keyboard.press_enter()
223
- else:
224
- log("dispatch_typed", stripped)
225
- if DEBUG:
226
- print(f" Final: {stripped} (no submit)", file=sys.stderr)
227
- elif should_submit:
228
- # Submit word only — erase it and press enter
229
- if self._typed_text:
230
- keyboard.backspace(len(self._typed_text))
231
- log("dispatch_enter", "(submit word only)")
232
- if DEBUG:
233
- print(" Submit word only [Enter]", file=sys.stderr)
234
- keyboard.press_enter()
262
+ self._dispatch_result(should_submit, stripped)
235
263
  else:
236
264
  if self._typed_text:
237
265
  keyboard.backspace(len(self._typed_text))
@@ -247,27 +275,18 @@ class VoiceDaemon:
247
275
  if stripped:
248
276
  if stripped != self._typed_text:
249
277
  self._update_typed_text(stripped)
250
- if should_submit:
251
- log("dispatch_enter", stripped)
252
- if DEBUG:
253
- print(f" Final: {stripped} [Enter]", file=sys.stderr)
254
- keyboard.press_enter()
255
- else:
256
- log("dispatch_typed", stripped)
257
- if DEBUG:
258
- print(f" Final: {stripped} (no submit)", file=sys.stderr)
259
- elif should_submit:
260
- # Submit word only — erase it and press enter
261
- if self._typed_text:
262
- keyboard.backspace(len(self._typed_text))
263
- log("dispatch_enter", "(submit word only)")
264
- if DEBUG:
265
- print(" Submit word only [Enter]", file=sys.stderr)
266
- keyboard.press_enter()
267
- elif self._typed_text:
268
- # Wake word but no command — clear what we typed
269
- keyboard.backspace(len(self._typed_text))
278
+ self._dispatch_result(should_submit, stripped)
279
+ elif found:
280
+ # Wake word found but no command text after it
281
+ if self._typed_text:
282
+ keyboard.backspace(len(self._typed_text))
270
283
  log("dispatch_cancelled", "No command after wake word")
284
+ elif self._typed_text:
285
+ # Final transcription lost the wake word (e.g. audio clipping
286
+ # turned "computer" into "uter"); fall back to the command
287
+ # captured during streaming
288
+ should_submit, stripped = self._strip_submit_word(self._typed_text)
289
+ self._dispatch_result(should_submit, stripped or self._typed_text)
271
290
  else:
272
291
  # Check final transcription for wake word
273
292
  found, command = check_wake_word(text)
@@ -373,42 +392,14 @@ class VoiceDaemon:
373
392
  log("speech_start", "command after activation")
374
393
 
375
394
  if prob > self._vad.threshold or self._audio_buffer:
376
- self._audio_buffer.append(chunk)
377
- sample_count += len(chunk)
378
-
379
- if prob > self._vad.threshold:
380
- trailing_silence = 0
381
- else:
382
- trailing_silence += 1
383
-
384
- # Periodic STT for incremental typing
385
- if sample_count - self._last_partial_at >= PARTIAL_STT_INTERVAL:
386
- self._last_partial_at = sample_count
387
- self._run_partial_stt()
388
-
389
- if self._check_segment_end(sample_count, trailing_silence):
390
- self._finalize_utterance()
391
- sample_count = 0
392
- trailing_silence = 0
395
+ sample_count, trailing_silence = self._process_audio_chunk(
396
+ chunk, prob, sample_count, trailing_silence
397
+ )
393
398
 
394
399
  elif self._state == LISTENING:
395
- self._audio_buffer.append(chunk)
396
- sample_count += len(chunk)
397
-
398
- if prob > self._vad.threshold:
399
- trailing_silence = 0
400
- else:
401
- trailing_silence += 1
402
-
403
- # Periodic STT for incremental typing
404
- if sample_count - self._last_partial_at >= PARTIAL_STT_INTERVAL:
405
- self._last_partial_at = sample_count
406
- self._run_partial_stt()
407
-
408
- if self._check_segment_end(sample_count, trailing_silence):
409
- self._finalize_utterance()
410
- sample_count = 0
411
- trailing_silence = 0
400
+ sample_count, trailing_silence = self._process_audio_chunk(
401
+ chunk, prob, sample_count, trailing_silence
402
+ )
412
403
 
413
404
  finally:
414
405
  if DEBUG:
@@ -417,7 +408,26 @@ class VoiceDaemon:
417
408
  log("daemon_stop", "Voice daemon stopped")
418
409
 
419
410
 
411
+ def _setup_console() -> None:
412
+ """On Windows, reopen stdout/stderr to the process's own console (CONOUT$).
413
+
414
+ When launched as a detached background process, Node.js redirects stdio to
415
+ NUL. Windows still allocates a console window for the process, but nothing
416
+ appears in it. Opening CONOUT$ gives us a handle to that console so all
417
+ debug output shows up there.
418
+ """
419
+ if sys.platform != "win32":
420
+ return
421
+ try:
422
+ con = open("CONOUT$", "w", encoding="utf-8")
423
+ sys.stdout = con
424
+ sys.stderr = con
425
+ except OSError:
426
+ pass
427
+
428
+
420
429
  def main() -> None:
430
+ _setup_console()
421
431
  log("daemon_launch", f"PID={os.getpid()}")
422
432
  try:
423
433
  daemon = VoiceDaemon()
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  from logger import log
6
6
 
7
- DEFAULT_WAKE_WORDS = ["hi claude"]
7
+ DEFAULT_WAKE_WORDS = ["computer"]
8
8
 
9
9
 
10
10
  def get_wake_words() -> list[str]:
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.82.0",
9
+ version: "0.83.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -22,14 +22,7 @@ var package_default = {
22
22
  scripts: {
23
23
  build: "tsup",
24
24
  start: "node dist/index.js",
25
- "verify:lint": "biome check --write .",
26
- "verify:build": "tsup",
27
- "verify:types": "tsc --noEmit",
28
- "verify:knip": "knip --no-progress --treat-config-hints-as-errors",
29
- "verify:duplicate-code": "jscpd --format 'typescript,tsx,python' --exitCode 1 --ignore '**/*.test.*' -r consoleFull src",
30
- "verify:maintainability": "assist complexity maintainability ./src --threshold 70",
31
- "verify:custom-lint": "assist lint",
32
- "verify:react-build": 'esbuild src/commands/backlog/web/ui/App.tsx --bundle --minify --format=iife --target=es2020 --outfile=dist/commands/backlog/web/bundle.js --jsx=automatic --jsx-import-source=react "--define:process.env.NODE_ENV=\\"production\\""'
25
+ "check:types": "tsc --noEmit"
33
26
  },
34
27
  keywords: [],
35
28
  author: "",
@@ -90,7 +83,8 @@ import { z } from "zod";
90
83
  var runConfigSchema = z.strictObject({
91
84
  name: z.string(),
92
85
  command: z.string(),
93
- args: z.array(z.string()).optional()
86
+ args: z.array(z.string()).optional(),
87
+ filter: z.string().optional()
94
88
  });
95
89
  var transcriptConfigSchema = z.strictObject({
96
90
  vttDir: z.string(),
@@ -132,7 +126,7 @@ var assistConfigSchema = z.strictObject({
132
126
  run: z.array(runConfigSchema).optional(),
133
127
  transcript: transcriptConfigSchema.optional(),
134
128
  voice: z.strictObject({
135
- wakeWords: z.array(z.string()).default(["claude"]),
129
+ wakeWords: z.array(z.string()).default(["computer"]),
136
130
  mic: z.string().optional(),
137
131
  cwd: z.string().optional(),
138
132
  modelsDir: z.string().optional(),
@@ -1348,7 +1342,7 @@ function lint() {
1348
1342
  }
1349
1343
 
1350
1344
  // src/commands/new/registerNew/newCli/index.ts
1351
- import { execSync as execSync8 } from "child_process";
1345
+ import { execSync as execSync9 } from "child_process";
1352
1346
  import { basename as basename2, resolve } from "path";
1353
1347
 
1354
1348
  // src/commands/verify/hardcodedColors.ts
@@ -1408,7 +1402,8 @@ function getRunEntries() {
1408
1402
  if (!run3) return [];
1409
1403
  return run3.filter((r) => r.name.startsWith("verify:")).map((r) => ({
1410
1404
  name: r.name,
1411
- fullCommand: buildFullCommand(r.command, r.args)
1405
+ fullCommand: buildFullCommand(r.command, r.args),
1406
+ filter: r.filter
1412
1407
  }));
1413
1408
  }
1414
1409
  function getPackageJsonEntries() {
@@ -1479,6 +1474,32 @@ function initTaskStatuses(scripts) {
1479
1474
  return scripts.map((script) => ({ script, startTime: Date.now() }));
1480
1475
  }
1481
1476
 
1477
+ // src/commands/verify/run/filterByChangedFiles.ts
1478
+ import { minimatch as minimatch2 } from "minimatch";
1479
+
1480
+ // src/commands/verify/run/getChangedFiles.ts
1481
+ import { execSync as execSync6 } from "child_process";
1482
+ function getChangedFiles() {
1483
+ const output = execSync6("git diff --name-only HEAD", {
1484
+ encoding: "utf-8"
1485
+ }).trim();
1486
+ if (output === "") return [];
1487
+ return output.split("\n");
1488
+ }
1489
+
1490
+ // src/commands/verify/run/filterByChangedFiles.ts
1491
+ function filterByChangedFiles(entries) {
1492
+ const hasFilters = entries.some((e) => e.filter);
1493
+ if (!hasFilters) return entries;
1494
+ const changedFiles = getChangedFiles();
1495
+ return entries.filter((entry) => {
1496
+ const { filter } = entry;
1497
+ if (!filter) return true;
1498
+ if (changedFiles.length === 0) return false;
1499
+ return changedFiles.some((file) => minimatch2(file, filter));
1500
+ });
1501
+ }
1502
+
1482
1503
  // src/commands/verify/run/spawnCommand.ts
1483
1504
  import { spawn } from "child_process";
1484
1505
  var isClaudeCode = !!process.env.CLAUDECODE;
@@ -1548,31 +1569,36 @@ async function run(options2 = {}) {
1548
1569
  console.log("No verify commands found");
1549
1570
  return;
1550
1571
  }
1551
- printEntryList(allEntries);
1552
- const results = await runAllEntries(allEntries, options2.timer ?? false);
1553
- handleResults(results, allEntries.length);
1572
+ const entries = options2.all ? allEntries : filterByChangedFiles(allEntries);
1573
+ if (entries.length === 0) {
1574
+ console.log("No verify commands matched changed files \u2014 skipping");
1575
+ return;
1576
+ }
1577
+ printEntryList(entries);
1578
+ const results = await runAllEntries(entries, options2.timer ?? false);
1579
+ handleResults(results, entries.length);
1554
1580
  }
1555
1581
 
1556
1582
  // src/commands/new/registerNew/initGit.ts
1557
- import { execSync as execSync6 } from "child_process";
1583
+ import { execSync as execSync7 } from "child_process";
1558
1584
  import { writeFileSync as writeFileSync6 } from "fs";
1559
1585
  function initGit() {
1560
1586
  console.log("Initializing git repository...");
1561
- execSync6("git init", { stdio: "inherit" });
1587
+ execSync7("git init", { stdio: "inherit" });
1562
1588
  writeFileSync6(".gitignore", "dist\nnode_modules\n");
1563
1589
  }
1564
1590
 
1565
1591
  // src/commands/new/registerNew/newCli/initPackageJson.ts
1566
- import { execSync as execSync7 } from "child_process";
1592
+ import { execSync as execSync8 } from "child_process";
1567
1593
  function initPackageJson(name) {
1568
1594
  console.log("Initializing package.json...");
1569
- execSync7("npm init -y", { stdio: "inherit" });
1595
+ execSync8("npm init -y", { stdio: "inherit" });
1570
1596
  console.log("Configuring package.json...");
1571
- execSync7("npm pkg delete main", { stdio: "inherit" });
1572
- execSync7("npm pkg set type=module", { stdio: "inherit" });
1573
- execSync7(`npm pkg set bin.${name}=./dist/index.js`, { stdio: "inherit" });
1574
- execSync7("npm pkg set scripts.build=tsup", { stdio: "inherit" });
1575
- execSync7('npm pkg set scripts.start="node dist/index.js"', {
1597
+ execSync8("npm pkg delete main", { stdio: "inherit" });
1598
+ execSync8("npm pkg set type=module", { stdio: "inherit" });
1599
+ execSync8(`npm pkg set bin.${name}=./dist/index.js`, { stdio: "inherit" });
1600
+ execSync8("npm pkg set scripts.build=tsup", { stdio: "inherit" });
1601
+ execSync8('npm pkg set scripts.start="node dist/index.js"', {
1576
1602
  stdio: "inherit"
1577
1603
  });
1578
1604
  }
@@ -1637,8 +1663,8 @@ async function newCli() {
1637
1663
  initGit();
1638
1664
  initPackageJson(name);
1639
1665
  console.log("Installing dependencies...");
1640
- execSync8("npm install commander", { stdio: "inherit" });
1641
- execSync8("npm install -D tsup typescript @types/node", {
1666
+ execSync9("npm install commander", { stdio: "inherit" });
1667
+ execSync9("npm install -D tsup typescript @types/node", {
1642
1668
  stdio: "inherit"
1643
1669
  });
1644
1670
  writeCliTemplate(name);
@@ -1647,11 +1673,11 @@ async function newCli() {
1647
1673
  }
1648
1674
 
1649
1675
  // src/commands/new/registerNew/newProject.ts
1650
- import { execSync as execSync10 } from "child_process";
1676
+ import { execSync as execSync11 } from "child_process";
1651
1677
  import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
1652
1678
 
1653
1679
  // src/commands/deploy/init/index.ts
1654
- import { execSync as execSync9 } from "child_process";
1680
+ import { execSync as execSync10 } from "child_process";
1655
1681
  import chalk21 from "chalk";
1656
1682
  import enquirer3 from "enquirer";
1657
1683
 
@@ -1704,7 +1730,7 @@ Created ${WORKFLOW_PATH}`));
1704
1730
  // src/commands/deploy/init/index.ts
1705
1731
  async function ensureNetlifyCli() {
1706
1732
  try {
1707
- execSync9("netlify sites:create --disable-linking", { stdio: "inherit" });
1733
+ execSync10("netlify sites:create --disable-linking", { stdio: "inherit" });
1708
1734
  } catch (error) {
1709
1735
  if (!(error instanceof Error) || !error.message.includes("command not found"))
1710
1736
  throw error;
@@ -1719,9 +1745,9 @@ async function ensureNetlifyCli() {
1719
1745
  process.exit(1);
1720
1746
  }
1721
1747
  console.log(chalk21.dim("\nInstalling netlify-cli...\n"));
1722
- execSync9("npm install -g netlify-cli", { stdio: "inherit" });
1748
+ execSync10("npm install -g netlify-cli", { stdio: "inherit" });
1723
1749
  console.log();
1724
- execSync9("netlify sites:create --disable-linking", { stdio: "inherit" });
1750
+ execSync10("netlify sites:create --disable-linking", { stdio: "inherit" });
1725
1751
  }
1726
1752
  }
1727
1753
  function printSetupInstructions() {
@@ -1764,7 +1790,7 @@ async function init5() {
1764
1790
  // src/commands/new/registerNew/newProject.ts
1765
1791
  async function newProject() {
1766
1792
  console.log("Initializing Vite with react-ts template...");
1767
- execSync10("npm create vite@latest . -- --template react-ts", {
1793
+ execSync11("npm create vite@latest . -- --template react-ts", {
1768
1794
  stdio: "inherit"
1769
1795
  });
1770
1796
  initGit();
@@ -2402,11 +2428,11 @@ import ts5 from "typescript";
2402
2428
  // src/commands/complexity/findSourceFiles.ts
2403
2429
  import fs10 from "fs";
2404
2430
  import path15 from "path";
2405
- import { minimatch as minimatch2 } from "minimatch";
2431
+ import { minimatch as minimatch3 } from "minimatch";
2406
2432
  function applyIgnoreGlobs(files) {
2407
2433
  const { complexity } = loadConfig();
2408
2434
  return files.filter(
2409
- (f) => !complexity.ignore.some((glob) => minimatch2(f, glob))
2435
+ (f) => !complexity.ignore.some((glob) => minimatch3(f, glob))
2410
2436
  );
2411
2437
  }
2412
2438
  function walk(dir, results) {
@@ -2430,7 +2456,7 @@ function findSourceFiles2(pattern2, baseDir = ".") {
2430
2456
  const results = [];
2431
2457
  if (pattern2.includes("*")) {
2432
2458
  walk(baseDir, results);
2433
- return applyIgnoreGlobs(results.filter((f) => minimatch2(f, pattern2)));
2459
+ return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
2434
2460
  }
2435
2461
  if (fs10.existsSync(pattern2) && fs10.statSync(pattern2).isFile()) {
2436
2462
  return [pattern2];
@@ -2440,7 +2466,7 @@ function findSourceFiles2(pattern2, baseDir = ".") {
2440
2466
  return applyIgnoreGlobs(results);
2441
2467
  }
2442
2468
  walk(baseDir, results);
2443
- return applyIgnoreGlobs(results.filter((f) => minimatch2(f, pattern2)));
2469
+ return applyIgnoreGlobs(results.filter((f) => minimatch3(f, pattern2)));
2444
2470
  }
2445
2471
 
2446
2472
  // src/commands/complexity/shared/getNodeName.ts
@@ -2917,11 +2943,11 @@ function registerDeploy(program2) {
2917
2943
  }
2918
2944
 
2919
2945
  // src/commands/devlog/list/index.ts
2920
- import { execSync as execSync12 } from "child_process";
2946
+ import { execSync as execSync13 } from "child_process";
2921
2947
  import { basename as basename3 } from "path";
2922
2948
 
2923
2949
  // src/commands/devlog/shared.ts
2924
- import { execSync as execSync11 } from "child_process";
2950
+ import { execSync as execSync12 } from "child_process";
2925
2951
  import chalk37 from "chalk";
2926
2952
 
2927
2953
  // src/commands/devlog/loadDevlogEntries.ts
@@ -2965,7 +2991,7 @@ function loadDevlogEntries(repoName) {
2965
2991
  // src/commands/devlog/shared.ts
2966
2992
  function getCommitFiles(hash) {
2967
2993
  try {
2968
- const output = execSync11(`git show --name-only --format="" ${hash}`, {
2994
+ const output = execSync12(`git show --name-only --format="" ${hash}`, {
2969
2995
  encoding: "utf-8"
2970
2996
  });
2971
2997
  return output.trim().split("\n").filter(Boolean);
@@ -3036,7 +3062,7 @@ function list3(options2) {
3036
3062
  const devlogEntries = loadDevlogEntries(repoName);
3037
3063
  const reverseFlag = options2.reverse ? "--reverse " : "";
3038
3064
  const limitFlag = options2.reverse ? "" : "-n 500 ";
3039
- const output = execSync12(
3065
+ const output = execSync13(
3040
3066
  `git log ${reverseFlag}${limitFlag}--pretty=format:'%ad|%h|%s' --date=short`,
3041
3067
  { encoding: "utf-8" }
3042
3068
  );
@@ -3062,11 +3088,11 @@ function list3(options2) {
3062
3088
  }
3063
3089
 
3064
3090
  // src/commands/devlog/getLastVersionInfo.ts
3065
- import { execSync as execSync13 } from "child_process";
3091
+ import { execSync as execSync14 } from "child_process";
3066
3092
  import semver from "semver";
3067
3093
  function getVersionAtCommit(hash) {
3068
3094
  try {
3069
- const content = execSync13(`git show ${hash}:package.json`, {
3095
+ const content = execSync14(`git show ${hash}:package.json`, {
3070
3096
  encoding: "utf-8"
3071
3097
  });
3072
3098
  const pkg = JSON.parse(content);
@@ -3081,7 +3107,7 @@ function stripToMinor(version2) {
3081
3107
  }
3082
3108
  function getLastVersionInfoFromGit() {
3083
3109
  try {
3084
- const output = execSync13(
3110
+ const output = execSync14(
3085
3111
  "git log -1 --pretty=format:'%ad|%h' --date=short",
3086
3112
  {
3087
3113
  encoding: "utf-8"
@@ -3124,7 +3150,7 @@ function bumpVersion(version2, type) {
3124
3150
  }
3125
3151
 
3126
3152
  // src/commands/devlog/next/displayNextEntry/index.ts
3127
- import { execSync as execSync14 } from "child_process";
3153
+ import { execSync as execSync15 } from "child_process";
3128
3154
  import chalk40 from "chalk";
3129
3155
 
3130
3156
  // src/commands/devlog/next/displayNextEntry/displayVersion.ts
@@ -3158,7 +3184,7 @@ function findTargetDate(commitsByDate, skipDays) {
3158
3184
  return Array.from(commitsByDate.keys()).filter((d) => !skipDays.has(d)).sort()[0];
3159
3185
  }
3160
3186
  function fetchCommitsByDate(ignore2, lastDate) {
3161
- const output = execSync14(
3187
+ const output = execSync15(
3162
3188
  "git log --pretty=format:'%ad|%h|%s' --date=short -n 500",
3163
3189
  { encoding: "utf-8" }
3164
3190
  );
@@ -3289,7 +3315,7 @@ import { tmpdir as tmpdir2 } from "os";
3289
3315
  import { join as join12 } from "path";
3290
3316
 
3291
3317
  // src/commands/prs/shared.ts
3292
- import { execSync as execSync15 } from "child_process";
3318
+ import { execSync as execSync16 } from "child_process";
3293
3319
  function isGhNotInstalled(error) {
3294
3320
  if (error instanceof Error) {
3295
3321
  const msg = error.message.toLowerCase();
@@ -3305,14 +3331,14 @@ function isNotFound(error) {
3305
3331
  }
3306
3332
  function getRepoInfo() {
3307
3333
  const repoInfo = JSON.parse(
3308
- execSync15("gh repo view --json owner,name", { encoding: "utf-8" })
3334
+ execSync16("gh repo view --json owner,name", { encoding: "utf-8" })
3309
3335
  );
3310
3336
  return { org: repoInfo.owner.login, repo: repoInfo.name };
3311
3337
  }
3312
3338
  function getCurrentPrNumber() {
3313
3339
  try {
3314
3340
  const prInfo = JSON.parse(
3315
- execSync15("gh pr view --json number", { encoding: "utf-8" })
3341
+ execSync16("gh pr view --json number", { encoding: "utf-8" })
3316
3342
  );
3317
3343
  return prInfo.number;
3318
3344
  } catch (error) {
@@ -3326,7 +3352,7 @@ function getCurrentPrNumber() {
3326
3352
  function getCurrentPrNodeId() {
3327
3353
  try {
3328
3354
  const prInfo = JSON.parse(
3329
- execSync15("gh pr view --json id", { encoding: "utf-8" })
3355
+ execSync16("gh pr view --json id", { encoding: "utf-8" })
3330
3356
  );
3331
3357
  return prInfo.id;
3332
3358
  } catch (error) {
@@ -3397,10 +3423,10 @@ function comment(path31, line, body) {
3397
3423
  }
3398
3424
 
3399
3425
  // src/commands/prs/fixed.ts
3400
- import { execSync as execSync17 } from "child_process";
3426
+ import { execSync as execSync18 } from "child_process";
3401
3427
 
3402
3428
  // src/commands/prs/resolveCommentWithReply.ts
3403
- import { execSync as execSync16 } from "child_process";
3429
+ import { execSync as execSync17 } from "child_process";
3404
3430
  import { unlinkSync as unlinkSync5, writeFileSync as writeFileSync14 } from "fs";
3405
3431
  import { tmpdir as tmpdir3 } from "os";
3406
3432
  import { join as join14 } from "path";
@@ -3430,7 +3456,7 @@ function deleteCommentsCache(prNumber) {
3430
3456
 
3431
3457
  // src/commands/prs/resolveCommentWithReply.ts
3432
3458
  function replyToComment(org, repo, prNumber, commentId, message) {
3433
- execSync16(
3459
+ execSync17(
3434
3460
  `gh api repos/${org}/${repo}/pulls/${prNumber}/comments -f body="${message.replace(/"/g, '\\"')}" -F in_reply_to=${commentId}`,
3435
3461
  { stdio: "inherit" }
3436
3462
  );
@@ -3440,7 +3466,7 @@ function resolveThread(threadId) {
3440
3466
  const queryFile = join14(tmpdir3(), `gh-mutation-${Date.now()}.graphql`);
3441
3467
  writeFileSync14(queryFile, mutation);
3442
3468
  try {
3443
- execSync16(
3469
+ execSync17(
3444
3470
  `gh api graphql -F query=@${queryFile} -f threadId="${threadId}"`,
3445
3471
  { stdio: "inherit" }
3446
3472
  );
@@ -3492,7 +3518,7 @@ function resolveCommentWithReply(commentId, message) {
3492
3518
  // src/commands/prs/fixed.ts
3493
3519
  function verifySha(sha) {
3494
3520
  try {
3495
- return execSync17(`git rev-parse --verify ${sha}`, {
3521
+ return execSync18(`git rev-parse --verify ${sha}`, {
3496
3522
  encoding: "utf-8"
3497
3523
  }).trim();
3498
3524
  } catch {
@@ -3528,7 +3554,7 @@ function isClaudeCode2() {
3528
3554
  }
3529
3555
 
3530
3556
  // src/commands/prs/fetchThreadIds.ts
3531
- import { execSync as execSync18 } from "child_process";
3557
+ import { execSync as execSync19 } from "child_process";
3532
3558
  import { unlinkSync as unlinkSync6, writeFileSync as writeFileSync15 } from "fs";
3533
3559
  import { tmpdir as tmpdir4 } from "os";
3534
3560
  import { join as join15 } from "path";
@@ -3537,7 +3563,7 @@ function fetchThreadIds(org, repo, prNumber) {
3537
3563
  const queryFile = join15(tmpdir4(), `gh-query-${Date.now()}.graphql`);
3538
3564
  writeFileSync15(queryFile, THREAD_QUERY);
3539
3565
  try {
3540
- const result = execSync18(
3566
+ const result = execSync19(
3541
3567
  `gh api graphql -F query=@${queryFile} -F owner="${org}" -F repo="${repo}" -F prNumber=${prNumber}`,
3542
3568
  { encoding: "utf-8" }
3543
3569
  );
@@ -3559,9 +3585,9 @@ function fetchThreadIds(org, repo, prNumber) {
3559
3585
  }
3560
3586
 
3561
3587
  // src/commands/prs/listComments/fetchReviewComments.ts
3562
- import { execSync as execSync19 } from "child_process";
3588
+ import { execSync as execSync20 } from "child_process";
3563
3589
  function fetchJson(endpoint) {
3564
- const result = execSync19(`gh api ${endpoint}`, { encoding: "utf-8" });
3590
+ const result = execSync20(`gh api ${endpoint}`, { encoding: "utf-8" });
3565
3591
  if (!result.trim()) return [];
3566
3592
  return JSON.parse(result);
3567
3593
  }
@@ -3687,7 +3713,7 @@ async function listComments() {
3687
3713
  }
3688
3714
 
3689
3715
  // src/commands/prs/prs/index.ts
3690
- import { execSync as execSync20 } from "child_process";
3716
+ import { execSync as execSync21 } from "child_process";
3691
3717
 
3692
3718
  // src/commands/prs/prs/displayPaginated/index.ts
3693
3719
  import enquirer5 from "enquirer";
@@ -3793,7 +3819,7 @@ async function displayPaginated(pullRequests) {
3793
3819
  async function prs(options2) {
3794
3820
  const state = options2.open ? "open" : options2.closed ? "closed" : "all";
3795
3821
  try {
3796
- const result = execSync20(
3822
+ const result = execSync21(
3797
3823
  `gh pr list --state ${state} --json number,title,url,author,createdAt,mergedAt,closedAt,state,changedFiles --limit 100`,
3798
3824
  { encoding: "utf-8" }
3799
3825
  );
@@ -3816,7 +3842,7 @@ async function prs(options2) {
3816
3842
  }
3817
3843
 
3818
3844
  // src/commands/prs/wontfix.ts
3819
- import { execSync as execSync21 } from "child_process";
3845
+ import { execSync as execSync22 } from "child_process";
3820
3846
  function validateReason(reason) {
3821
3847
  const lowerReason = reason.toLowerCase();
3822
3848
  if (lowerReason.includes("claude") || lowerReason.includes("opus")) {
@@ -3833,7 +3859,7 @@ function validateShaReferences(reason) {
3833
3859
  const invalidShas = [];
3834
3860
  for (const sha of shas) {
3835
3861
  try {
3836
- execSync21(`git cat-file -t ${sha}`, { stdio: "pipe" });
3862
+ execSync22(`git cat-file -t ${sha}`, { stdio: "pipe" });
3837
3863
  } catch {
3838
3864
  invalidShas.push(sha);
3839
3865
  }
@@ -3937,9 +3963,9 @@ Refactor check failed:
3937
3963
  }
3938
3964
 
3939
3965
  // src/commands/refactor/check/getViolations/index.ts
3940
- import { execSync as execSync22 } from "child_process";
3966
+ import { execSync as execSync23 } from "child_process";
3941
3967
  import fs15 from "fs";
3942
- import { minimatch as minimatch3 } from "minimatch";
3968
+ import { minimatch as minimatch4 } from "minimatch";
3943
3969
 
3944
3970
  // src/commands/refactor/check/getViolations/getIgnoredFiles.ts
3945
3971
  import fs14 from "fs";
@@ -3987,7 +4013,7 @@ function getGitFiles(options2) {
3987
4013
  }
3988
4014
  const files = /* @__PURE__ */ new Set();
3989
4015
  if (options2.staged || options2.modified) {
3990
- const staged = execSync22("git diff --cached --name-only", {
4016
+ const staged = execSync23("git diff --cached --name-only", {
3991
4017
  encoding: "utf-8"
3992
4018
  });
3993
4019
  for (const file of staged.trim().split("\n").filter(Boolean)) {
@@ -3995,7 +4021,7 @@ function getGitFiles(options2) {
3995
4021
  }
3996
4022
  }
3997
4023
  if (options2.unstaged || options2.modified) {
3998
- const unstaged = execSync22("git diff --name-only", { encoding: "utf-8" });
4024
+ const unstaged = execSync23("git diff --name-only", { encoding: "utf-8" });
3999
4025
  for (const file of unstaged.trim().split("\n").filter(Boolean)) {
4000
4026
  files.add(file);
4001
4027
  }
@@ -4007,7 +4033,7 @@ function getViolations(pattern2, options2 = {}, maxLines = DEFAULT_MAX_LINES) {
4007
4033
  const ignoredFiles = getIgnoredFiles();
4008
4034
  const gitFiles = getGitFiles(options2);
4009
4035
  if (pattern2) {
4010
- sourceFiles = sourceFiles.filter((f) => minimatch3(f, pattern2));
4036
+ sourceFiles = sourceFiles.filter((f) => minimatch4(f, pattern2));
4011
4037
  }
4012
4038
  if (gitFiles) {
4013
4039
  sourceFiles = sourceFiles.filter((f) => gitFiles.has(f));
@@ -5258,7 +5284,18 @@ function registerTranscript(program2) {
5258
5284
 
5259
5285
  // src/commands/registerVerify.ts
5260
5286
  function registerVerify(program2) {
5261
- const verifyCommand = program2.command("verify").description("Run all verify:* commands in parallel").option("--timer", "Show timing information for each task as they complete").action((options2) => run(options2));
5287
+ const verifyCommand = program2.command("verify").description("Run all verify:* commands in parallel").argument(
5288
+ "[scope]",
5289
+ 'Use "all" to run all checks, ignoring diff-based filters'
5290
+ ).option("--timer", "Show timing information for each task as they complete").action((scope, options2) => {
5291
+ if (scope && scope !== "all") {
5292
+ console.error(
5293
+ `Unknown scope: "${scope}". Use "all" to run all checks.`
5294
+ );
5295
+ process.exit(1);
5296
+ }
5297
+ run({ ...options2, all: scope === "all" });
5298
+ });
5262
5299
  verifyCommand.command("list").description("List configured verify commands").action(list);
5263
5300
  verifyCommand.command("init").description("Add verify scripts to a project").action(init2);
5264
5301
  verifyCommand.command("hardcoded-colors").description("Check for hardcoded hex colors in src/").action(hardcodedColors);
@@ -5308,7 +5345,7 @@ function logs(options2) {
5308
5345
  console.log("No voice log file found");
5309
5346
  return;
5310
5347
  }
5311
- const count = Number.parseInt(options2.lines ?? "20", 10);
5348
+ const count = Number.parseInt(options2.lines ?? "150", 10);
5312
5349
  const content = readFileSync17(voicePaths.log, "utf-8").trim();
5313
5350
  if (!content) {
5314
5351
  console.log("Voice log is empty");
@@ -5330,68 +5367,14 @@ function logs(options2) {
5330
5367
  }
5331
5368
 
5332
5369
  // src/commands/voice/setup.ts
5333
- import { execSync as execSync23, spawnSync as spawnSync4 } from "child_process";
5334
- import { existsSync as existsSync24, mkdirSync as mkdirSync7 } from "fs";
5335
- import { join as join25 } from "path";
5336
- function setup() {
5337
- mkdirSync7(voicePaths.dir, { recursive: true });
5338
- if (!existsSync24(getVenvPython())) {
5339
- console.log("Creating Python virtual environment...");
5340
- execSync23(`uv venv "${voicePaths.venv}"`, { stdio: "inherit" });
5341
- console.log("Installing dependencies...");
5342
- const pythonDir = getPythonDir();
5343
- execSync23(
5344
- `uv pip install --python "${getVenvPython()}" -e "${pythonDir}[dev]"`,
5345
- { stdio: "inherit" }
5346
- );
5347
- }
5348
- console.log("\nDownloading models...\n");
5349
- const script = join25(getPythonDir(), "setup_models.py");
5350
- const result = spawnSync4(getVenvPython(), [script], {
5351
- stdio: "inherit",
5352
- env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
5353
- });
5354
- if (result.status !== 0) {
5355
- console.error("Model setup failed");
5356
- process.exit(1);
5357
- }
5358
- }
5359
-
5360
- // src/commands/voice/start.ts
5361
- import { spawn as spawn4 } from "child_process";
5362
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync19 } from "fs";
5363
- import { join as join27 } from "path";
5364
-
5365
- // src/commands/voice/buildDaemonEnv.ts
5366
- var ENV_MAP = {
5367
- VOICE_WAKE_WORDS: (v) => v.wakeWords?.join(","),
5368
- VOICE_MIC: (v) => v.mic,
5369
- VOICE_CWD: (v) => v.cwd,
5370
- VOICE_MODELS_DIR: (v) => v.modelsDir,
5371
- VOICE_MODEL_VAD: (v) => v.models?.vad,
5372
- VOICE_MODEL_SMART_TURN: (v) => v.models?.smartTurn,
5373
- VOICE_MODEL_STT: (v) => v.models?.stt,
5374
- VOICE_SUBMIT_WORD: (v) => v.submitWord
5375
- };
5376
- function buildDaemonEnv(options2) {
5377
- const config = loadConfig();
5378
- const env = { ...process.env };
5379
- const voice = config.voice;
5380
- if (voice) {
5381
- for (const [key, getter] of Object.entries(ENV_MAP)) {
5382
- const value = getter(voice);
5383
- if (value) env[key] = value;
5384
- }
5385
- }
5386
- env.VOICE_LOG_FILE = voicePaths.log;
5387
- if (options2?.debug) env.VOICE_DEBUG = "1";
5388
- return env;
5389
- }
5370
+ import { spawnSync as spawnSync4 } from "child_process";
5371
+ import { mkdirSync as mkdirSync8 } from "fs";
5372
+ import { join as join26 } from "path";
5390
5373
 
5391
5374
  // src/commands/voice/checkLockFile.ts
5392
5375
  import { execSync as execSync24 } from "child_process";
5393
- import { existsSync as existsSync25, mkdirSync as mkdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "fs";
5394
- import { join as join26 } from "path";
5376
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "fs";
5377
+ import { join as join25 } from "path";
5395
5378
  function isProcessAlive(pid) {
5396
5379
  try {
5397
5380
  process.kill(pid, 0);
@@ -5402,7 +5385,7 @@ function isProcessAlive(pid) {
5402
5385
  }
5403
5386
  function checkLockFile() {
5404
5387
  const lockFile = getLockFile();
5405
- if (!existsSync25(lockFile)) return;
5388
+ if (!existsSync24(lockFile)) return;
5406
5389
  try {
5407
5390
  const lock = JSON.parse(readFileSync18(lockFile, "utf-8"));
5408
5391
  if (lock.pid && isProcessAlive(lock.pid)) {
@@ -5415,7 +5398,7 @@ function checkLockFile() {
5415
5398
  }
5416
5399
  }
5417
5400
  function bootstrapVenv() {
5418
- if (existsSync25(getVenvPython())) return;
5401
+ if (existsSync24(getVenvPython())) return;
5419
5402
  console.log("Creating Python virtual environment...");
5420
5403
  execSync24(`uv venv "${voicePaths.venv}"`, { stdio: "inherit" });
5421
5404
  console.log("Installing dependencies...");
@@ -5427,7 +5410,7 @@ function bootstrapVenv() {
5427
5410
  }
5428
5411
  function writeLockFile(pid) {
5429
5412
  const lockFile = getLockFile();
5430
- mkdirSync8(join26(lockFile, ".."), { recursive: true });
5413
+ mkdirSync7(join25(lockFile, ".."), { recursive: true });
5431
5414
  writeFileSync18(
5432
5415
  lockFile,
5433
5416
  JSON.stringify({
@@ -5438,6 +5421,53 @@ function writeLockFile(pid) {
5438
5421
  );
5439
5422
  }
5440
5423
 
5424
+ // src/commands/voice/setup.ts
5425
+ function setup() {
5426
+ mkdirSync8(voicePaths.dir, { recursive: true });
5427
+ bootstrapVenv();
5428
+ console.log("\nDownloading models...\n");
5429
+ const script = join26(getPythonDir(), "setup_models.py");
5430
+ const result = spawnSync4(getVenvPython(), [script], {
5431
+ stdio: "inherit",
5432
+ env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
5433
+ });
5434
+ if (result.status !== 0) {
5435
+ console.error("Model setup failed");
5436
+ process.exit(1);
5437
+ }
5438
+ }
5439
+
5440
+ // src/commands/voice/start.ts
5441
+ import { spawn as spawn4 } from "child_process";
5442
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync19 } from "fs";
5443
+ import { join as join27 } from "path";
5444
+
5445
+ // src/commands/voice/buildDaemonEnv.ts
5446
+ var ENV_MAP = {
5447
+ VOICE_WAKE_WORDS: (v) => v.wakeWords?.join(","),
5448
+ VOICE_MIC: (v) => v.mic,
5449
+ VOICE_CWD: (v) => v.cwd,
5450
+ VOICE_MODELS_DIR: (v) => v.modelsDir,
5451
+ VOICE_MODEL_VAD: (v) => v.models?.vad,
5452
+ VOICE_MODEL_SMART_TURN: (v) => v.models?.smartTurn,
5453
+ VOICE_MODEL_STT: (v) => v.models?.stt,
5454
+ VOICE_SUBMIT_WORD: (v) => v.submitWord
5455
+ };
5456
+ function buildDaemonEnv(options2) {
5457
+ const config = loadConfig();
5458
+ const env = { ...process.env };
5459
+ const voice = config.voice;
5460
+ if (voice) {
5461
+ for (const [key, getter] of Object.entries(ENV_MAP)) {
5462
+ const value = getter(voice);
5463
+ if (value) env[key] = value;
5464
+ }
5465
+ }
5466
+ env.VOICE_LOG_FILE = voicePaths.log;
5467
+ if (options2?.debug) env.VOICE_DEBUG = "1";
5468
+ return env;
5469
+ }
5470
+
5441
5471
  // src/commands/voice/start.ts
5442
5472
  function spawnForeground(python, script, env) {
5443
5473
  console.log("Starting voice daemon in foreground...");
@@ -5464,7 +5494,7 @@ function start2(options2) {
5464
5494
  mkdirSync9(voicePaths.dir, { recursive: true });
5465
5495
  checkLockFile();
5466
5496
  bootstrapVenv();
5467
- const debug = options2.debug || options2.foreground;
5497
+ const debug = options2.debug || options2.foreground || process.platform === "win32";
5468
5498
  const env = buildDaemonEnv({ debug });
5469
5499
  const script = join27(getPythonDir(), "voice_daemon.py");
5470
5500
  const python = getVenvPython();
@@ -5476,7 +5506,7 @@ function start2(options2) {
5476
5506
  }
5477
5507
 
5478
5508
  // src/commands/voice/status.ts
5479
- import { existsSync as existsSync26, readFileSync as readFileSync19 } from "fs";
5509
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
5480
5510
  function isProcessAlive2(pid) {
5481
5511
  try {
5482
5512
  process.kill(pid, 0);
@@ -5486,12 +5516,12 @@ function isProcessAlive2(pid) {
5486
5516
  }
5487
5517
  }
5488
5518
  function readRecentLogs(count) {
5489
- if (!existsSync26(voicePaths.log)) return [];
5519
+ if (!existsSync25(voicePaths.log)) return [];
5490
5520
  const lines = readFileSync19(voicePaths.log, "utf-8").trim().split("\n");
5491
5521
  return lines.slice(-count);
5492
5522
  }
5493
5523
  function status() {
5494
- if (!existsSync26(voicePaths.pid)) {
5524
+ if (!existsSync25(voicePaths.pid)) {
5495
5525
  console.log("Voice daemon: not running (no PID file)");
5496
5526
  return;
5497
5527
  }
@@ -5514,9 +5544,9 @@ function status() {
5514
5544
  }
5515
5545
 
5516
5546
  // src/commands/voice/stop.ts
5517
- import { existsSync as existsSync27, readFileSync as readFileSync20, unlinkSync as unlinkSync7 } from "fs";
5547
+ import { existsSync as existsSync26, readFileSync as readFileSync20, unlinkSync as unlinkSync7 } from "fs";
5518
5548
  function stop() {
5519
- if (!existsSync27(voicePaths.pid)) {
5549
+ if (!existsSync26(voicePaths.pid)) {
5520
5550
  console.log("Voice daemon is not running (no PID file)");
5521
5551
  return;
5522
5552
  }
@@ -5533,7 +5563,7 @@ function stop() {
5533
5563
  }
5534
5564
  try {
5535
5565
  const lockFile = getLockFile();
5536
- if (existsSync27(lockFile)) unlinkSync7(lockFile);
5566
+ if (existsSync26(lockFile)) unlinkSync7(lockFile);
5537
5567
  } catch {
5538
5568
  }
5539
5569
  console.log("Voice daemon stopped");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -16,14 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsup",
18
18
  "start": "node dist/index.js",
19
- "verify:lint": "biome check --write .",
20
- "verify:build": "tsup",
21
- "verify:types": "tsc --noEmit",
22
- "verify:knip": "knip --no-progress --treat-config-hints-as-errors",
23
- "verify:duplicate-code": "jscpd --format 'typescript,tsx,python' --exitCode 1 --ignore '**/*.test.*' -r consoleFull src",
24
- "verify:maintainability": "assist complexity maintainability ./src --threshold 70",
25
- "verify:custom-lint": "assist lint",
26
- "verify:react-build": "esbuild src/commands/backlog/web/ui/App.tsx --bundle --minify --format=iife --target=es2020 --outfile=dist/commands/backlog/web/bundle.js --jsx=automatic --jsx-import-source=react \"--define:process.env.NODE_ENV=\\\"production\\\"\""
19
+ "check:types": "tsc --noEmit"
27
20
  },
28
21
  "keywords": [],
29
22
  "author": "",