@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 +1 -1
- package/bin/kondi-chat.js +33 -9
- package/package.json +1 -1
- package/src/cli/backend.ts +1 -1
- package/src/context/bootstrap.ts +11 -16
- package/src/engine/tools.ts +53 -39
- package/src/util/fs-walk.ts +55 -0
package/bin/kondi-chat
CHANGED
package/bin/kondi-chat.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execFileSync
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
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
|
-
|
|
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
package/src/cli/backend.ts
CHANGED
|
@@ -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.
|
|
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);
|
package/src/context/bootstrap.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
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
|
|
120
|
+
const filePath = join(workingDir, fileName);
|
|
129
121
|
const resolved = resolve(filePath);
|
|
130
|
-
|
|
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;
|
package/src/engine/tools.ts
CHANGED
|
@@ -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
|
|
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
|
|
507
|
-
|
|
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
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|