@thispointon/kondi-chat 0.1.2 → 0.1.3

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/bin/kondi-chat CHANGED
@@ -5,7 +5,7 @@ set -euo pipefail
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
7
7
 
8
- VERSION="0.1.2"
8
+ VERSION="0.1.3"
9
9
 
10
10
  if [[ "${1:-}" == "--version" || "${1:-}" == "-V" ]]; then
11
11
  echo "kondi-chat $VERSION"
package/bin/kondi-chat.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execFileSync, execSync } from "node:child_process";
4
- import { existsSync } from "node:fs";
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  const projectRoot = join(__dirname, "..");
10
- const version = "0.1.2";
11
+ const version = "0.1.3";
11
12
 
12
13
  const arg = process.argv[2];
13
14
 
@@ -49,7 +50,11 @@ if (arg === "--help" || arg === "-h") {
49
50
  process.exit(0);
50
51
  }
51
52
 
52
- const tuiBinary = join(projectRoot, "tui", "target", "release", "kondi-tui");
53
+ // Windows postinstall writes kondi-tui.exe; POSIX writes kondi-tui.
54
+ const tuiBinary = join(
55
+ projectRoot, "tui", "target", "release",
56
+ process.platform === "win32" ? "kondi-tui.exe" : "kondi-tui",
57
+ );
53
58
 
54
59
  if (existsSync(tuiBinary)) {
55
60
  try {
@@ -58,12 +63,31 @@ if (existsSync(tuiBinary)) {
58
63
  process.exit(e.status ?? 1);
59
64
  }
60
65
  } else {
61
- // Run the Node backend from the user's current working directory — NOT from
62
- // the install dir. Setting cwd: projectRoot here would make the agent operate
63
- // on the kondi-chat install instead of the user's project, which was the
64
- // common failure mode for any install where the TUI binary download failed.
66
+ // No prebuilt TUI binary run the Node backend directly. Resolve the
67
+ // package-local `tsx` (a declared dependency) and run it with the current
68
+ // Node, instead of `npx tsx`: in a global install `npx` can't see the
69
+ // package's local tsx and prompts to install it. execFileSync with an
70
+ // argv array also avoids shell quoting issues with user args on Windows.
71
+ //
72
+ // cwd is intentionally NOT set to projectRoot — the backend must operate
73
+ // on the user's working directory, not the kondi-chat install dir.
74
+ const backend = join(projectRoot, "src", "cli", "backend.ts");
75
+ let tsxCli;
65
76
  try {
66
- execSync(`npx tsx ${join(projectRoot, "src", "cli", "backend.ts")} ${process.argv.slice(2).join(" ")}`, {
77
+ const require = createRequire(import.meta.url);
78
+ const tsxPkgPath = require.resolve("tsx/package.json");
79
+ const tsxPkg = JSON.parse(readFileSync(tsxPkgPath, "utf-8"));
80
+ const binRel = typeof tsxPkg.bin === "string" ? tsxPkg.bin : tsxPkg.bin.tsx;
81
+ tsxCli = join(dirname(tsxPkgPath), binRel);
82
+ } catch {
83
+ console.error(
84
+ "kondi-chat: could not locate the 'tsx' runtime inside the package. " +
85
+ "Try reinstalling: npm install -g @thispointon/kondi-chat",
86
+ );
87
+ process.exit(1);
88
+ }
89
+ try {
90
+ execFileSync(process.execPath, [tsxCli, backend, ...process.argv.slice(2)], {
67
91
  stdio: "inherit",
68
92
  });
69
93
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thispointon/kondi-chat",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Multi-model AI coding CLI with intelligent routing, budget profiles, and council deliberation",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -100,7 +100,7 @@ async function main() {
100
100
  // Spec 16 — first-run setup wizard (non-interactive; safe on every start).
101
101
  runFirstRunWizard(storageDir);
102
102
  // Spec 16 — async update check; swallow failures. Don't block startup.
103
- checkForUpdate('0.1.2').then(b => { if (b) emit({ type: 'status', text: b }); }).catch(() => {});
103
+ checkForUpdate('0.1.3').then(b => { if (b) emit({ type: 'status', text: b }); }).catch(() => {});
104
104
 
105
105
  // Spec 06 — session resume
106
106
  const sessionStore = new SessionStore(storageDir);
@@ -5,9 +5,9 @@
5
5
  * Based on kondi-council's context-bootstrap.ts — simplified, two-tier.
6
6
  */
7
7
 
8
- import { execSync } from 'node:child_process';
9
8
  import { readFileSync, existsSync } from 'node:fs';
10
- import { join, resolve } from 'node:path';
9
+ import { join, resolve, relative, isAbsolute } from 'node:path';
10
+ import { walkFiles } from '../util/fs-walk.ts';
11
11
 
12
12
  const MAX_FILE_SIZE = 2048;
13
13
 
@@ -54,19 +54,11 @@ export async function bootstrapDirectory(
54
54
  let fileList: string[] = [];
55
55
 
56
56
  try {
57
- const raw = execSync(
58
- `find . -maxdepth 4 -type f ` +
59
- `-not -path '*/node_modules/*' -not -path '*/.git/*' ` +
60
- `-not -path '*/target/*' -not -path '*/__pycache__/*' ` +
61
- `-not -path '*/.next/*' -not -path '*/dist/*' ` +
62
- `-not -path '*/package-lock.json' ` +
63
- `| sort | head -${maxFiles}`,
64
- { cwd: workingDir, encoding: 'utf-8', timeout: 10_000 },
65
- ).trim();
66
- tree = raw;
67
- fileList = raw.split('\n').filter(Boolean);
57
+ fileList = walkFiles(workingDir, { maxDepth: 4, maxFiles })
58
+ .filter(f => f !== 'package-lock.json' && !f.endsWith('/package-lock.json'));
59
+ tree = fileList.join('\n');
68
60
  } catch {
69
- // find failed, continue
61
+ // walk failed, continue
70
62
  }
71
63
 
72
64
  const files: Array<{ name: string; content: string }> = [];
@@ -125,9 +117,12 @@ export async function bootstrapDirectory(
125
117
 
126
118
  function safeRead(workingDir: string, base: string, fileName: string, maxSize: number): string | null {
127
119
  try {
128
- const filePath = join(workingDir.replace(/\/$/, ''), fileName);
120
+ const filePath = join(workingDir, fileName);
129
121
  const resolved = resolve(filePath);
130
- if (!resolved.startsWith(base + '/') && resolved !== base) return null;
122
+ // Cross-platform containment check — `startsWith(base + '/')` breaks on
123
+ // Windows backslash paths. relative() is separator-agnostic.
124
+ const rel = relative(base, resolved);
125
+ if (rel !== '' && (rel.startsWith('..') || isAbsolute(rel))) return null;
131
126
  if (!existsSync(filePath)) return null;
132
127
  const content = readFileSync(filePath, 'utf-8');
133
128
  if (!content) return null;
@@ -15,7 +15,8 @@ function isPathSafe(base: string, fullPath: string): boolean {
15
15
  const rel = relative(base, fullPath);
16
16
  return !rel.startsWith('..') && !resolve(fullPath).includes('\0');
17
17
  }
18
- import { execSync, execFileSync } from 'node:child_process';
18
+ import { execSync } from 'node:child_process';
19
+ import { walkFiles } from '../util/fs-walk.ts';
19
20
  import type { ToolDefinition, Session, TaskKind } from '../types.ts';
20
21
  import type { Ledger } from '../audit/ledger.ts';
21
22
  import { runPipeline, type PipelineConfig } from './pipeline.ts';
@@ -503,15 +504,8 @@ function toolListFiles(
503
504
 
504
505
  if (recursive) {
505
506
  try {
506
- const output = execSync(
507
- `find . -maxdepth 4 -type f ` +
508
- `-not -path '*/node_modules/*' -not -path '*/.git/*' ` +
509
- `-not -path '*/target/*' -not -path '*/__pycache__/*' ` +
510
- `-not -path '*/.next/*' -not -path '*/dist/*' ` +
511
- `| sort | head -100`,
512
- { cwd: fullPath, encoding: 'utf-8', timeout: 10_000 },
513
- ).trim();
514
- return { content: output || '(empty directory)' };
507
+ const files = walkFiles(fullPath, { maxDepth: 4, maxFiles: 100 });
508
+ return { content: files.join('\n') || '(empty directory)' };
515
509
  } catch {
516
510
  return { content: '(failed to list files)', isError: true };
517
511
  }
@@ -546,41 +540,61 @@ function toolSearchCode(
546
540
 
547
541
  // process.stderr.write(`[tool] search_code: "${pattern}" in ${relPath}\n`);
548
542
 
549
- // Sanitize glob (defense-in-depth even though execFileSync skips the shell).
550
- const safeGlob = glob ? glob.replace(/[^a-zA-Z0-9.*?_\-\/]/g, '') : '';
551
- const grepArgs: string[] = [
552
- '-rnE', // recursive, line numbers, extended regex
553
- '--exclude-dir=node_modules',
554
- '--exclude-dir=.git',
555
- ];
556
- if (safeGlob) grepArgs.push(`--include=${safeGlob}`);
557
- grepArgs.push('-e', pattern, searchPath);
543
+ // Cross-platform content search pure Node, no `grep` (absent on Windows).
544
+ let regex: RegExp;
545
+ try {
546
+ regex = new RegExp(pattern);
547
+ } catch {
548
+ return {
549
+ content: `Invalid regex: ${pattern} — could not compile it. Escape special characters or simplify the pattern.`,
550
+ isError: true,
551
+ };
552
+ }
558
553
 
554
+ // Optional filename glob → a basename matcher.
555
+ let nameMatches: ((name: string) => boolean) | null = null;
556
+ if (glob) {
557
+ const safeGlob = glob.replace(/[^a-zA-Z0-9.*?_\-\/]/g, '');
558
+ if (safeGlob) {
559
+ const rx = '^' + safeGlob
560
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
561
+ .replace(/\*/g, '.*')
562
+ .replace(/\?/g, '.') + '$';
563
+ const globRe = new RegExp(rx);
564
+ nameMatches = (name: string) => globRe.test(name);
565
+ }
566
+ }
567
+
568
+ const MAX_MATCHES = 50;
569
+ const results: string[] = [];
570
+ let files: string[];
559
571
  try {
560
- const raw = execFileSync('grep', grepArgs, {
561
- encoding: 'utf-8',
562
- timeout: 15_000,
563
- cwd: ctx.workingDir,
564
- maxBuffer: 4 * 1024 * 1024,
565
- });
566
- const lines = raw.split('\n');
567
- const head = lines.slice(0, 50).join('\n').trim();
568
- return { content: head || 'No matches found.' };
572
+ files = walkFiles(searchPath, { maxDepth: 12, maxFiles: 5000 });
569
573
  } catch (error: any) {
570
- // grep returns exit code 1 for no matches.
571
- if (error.status === 1) {
572
- return { content: 'No matches found.' };
574
+ return { content: `Search error: ${error.message}`, isError: true };
575
+ }
576
+
577
+ for (const rel of files) {
578
+ if (results.length >= MAX_MATCHES) break;
579
+ const fileName = rel.split('/').pop() || rel;
580
+ if (nameMatches && !nameMatches(fileName)) continue;
581
+ let buf: Buffer;
582
+ try {
583
+ buf = readFileSync(join(searchPath, rel));
584
+ } catch {
585
+ continue;
573
586
  }
574
- // Exit 2 = invalid regex / IO error. Surface a useful message rather
575
- // than the raw shell complaint so the model can correct its pattern.
576
- if (error.status === 2) {
577
- return {
578
- content: `Invalid regex: ${pattern} grep -E rejected it. Try escaping special chars or use search_files for a literal lookup.`,
579
- isError: true,
580
- };
587
+ if (buf.includes(0)) continue; // skip binary files
588
+ const lines = buf.toString('utf-8').split('\n');
589
+ for (let i = 0; i < lines.length && results.length < MAX_MATCHES; i++) {
590
+ if (regex.test(lines[i])) {
591
+ const text = lines[i].length > 240 ? lines[i].slice(0, 240) + '…' : lines[i];
592
+ results.push(`${rel}:${i + 1}:${text}`);
593
+ }
581
594
  }
582
- return { content: `Search error: ${error.message}`, isError: true };
583
595
  }
596
+
597
+ return { content: results.length > 0 ? results.join('\n') : 'No matches found.' };
584
598
  }
585
599
 
586
600
  function toolRunCommand(
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Cross-platform recursive file walk.
3
+ *
4
+ * Replaces Unix `find ... | sort | head` pipelines, which fail on Windows
5
+ * (`head` / `grep` don't exist, `find` differs). Pure Node `fs` — behaves
6
+ * identically on Windows and POSIX.
7
+ */
8
+
9
+ import { readdirSync } from 'node:fs';
10
+ import { join, relative, sep } from 'node:path';
11
+
12
+ export interface WalkOptions {
13
+ /** Max directory depth — files directly in root are depth 1. Default 4. */
14
+ maxDepth?: number;
15
+ /** Stop after collecting this many files. Default 100. */
16
+ maxFiles?: number;
17
+ /** Directory names to skip entirely. */
18
+ excludeDirs?: string[];
19
+ }
20
+
21
+ const DEFAULT_EXCLUDES = ['node_modules', '.git', 'target', '__pycache__', '.next', 'dist'];
22
+
23
+ /**
24
+ * Walk `root` recursively. Returns file paths relative to `root`, using
25
+ * forward slashes on every platform, sorted, and capped at `maxFiles`.
26
+ */
27
+ export function walkFiles(root: string, opts: WalkOptions = {}): string[] {
28
+ const maxDepth = opts.maxDepth ?? 4;
29
+ const maxFiles = opts.maxFiles ?? 100;
30
+ const exclude = new Set(opts.excludeDirs ?? DEFAULT_EXCLUDES);
31
+ const out: string[] = [];
32
+
33
+ const walk = (dir: string, depth: number): void => {
34
+ if (out.length >= maxFiles || depth > maxDepth) return;
35
+ let entries;
36
+ try {
37
+ entries = readdirSync(dir, { withFileTypes: true });
38
+ } catch {
39
+ return; // unreadable directory — skip
40
+ }
41
+ entries.sort((a, b) => a.name.localeCompare(b.name));
42
+ for (const e of entries) {
43
+ if (out.length >= maxFiles) return;
44
+ const full = join(dir, e.name);
45
+ if (e.isDirectory()) {
46
+ if (!exclude.has(e.name)) walk(full, depth + 1);
47
+ } else if (e.isFile()) {
48
+ out.push(relative(root, full).split(sep).join('/'));
49
+ }
50
+ }
51
+ };
52
+
53
+ walk(root, 1);
54
+ return out;
55
+ }