@zhongqian97-code/ecode 0.1.0 → 0.2.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/bin.cjs +24 -0
- package/dist/index.js +160 -63
- package/package.json +3 -2
package/bin.cjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Must run as CJS so this synchronous code executes BEFORE any ESM module loads.
|
|
5
|
+
// ESM static imports are resolved during the module linking phase, before any
|
|
6
|
+
// module body code (including tsup banner) runs. A CJS preloader is the only way
|
|
7
|
+
// to patch globals in time.
|
|
8
|
+
|
|
9
|
+
// Fix: ENOENT uv_cwd crash when process CWD has been deleted
|
|
10
|
+
const _rc = process.cwd.bind(process);
|
|
11
|
+
process.cwd = function () { try { return _rc(); } catch { return '/'; } };
|
|
12
|
+
|
|
13
|
+
// Suppress DEP0040 punycode deprecation from openai's transitive deps
|
|
14
|
+
const _ew = process.emitWarning.bind(process);
|
|
15
|
+
process.emitWarning = function (w, ...a) {
|
|
16
|
+
if ((w?.message ?? w)?.includes?.('punycode')) return;
|
|
17
|
+
_ew(w, ...a);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
import(path.join(__dirname, 'dist/index.js')).catch(function (err) {
|
|
22
|
+
process.stderr.write(String(err) + '\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -95,8 +95,8 @@ function loadConfig() {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/ui/App.tsx
|
|
98
|
-
import { useState as useState3, useCallback, useRef, useEffect as useEffect3 } from "react";
|
|
99
|
-
import { Box as
|
|
98
|
+
import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
99
|
+
import { Box as Box5, useInput as useInput2 } from "ink";
|
|
100
100
|
|
|
101
101
|
// src/llm.ts
|
|
102
102
|
import OpenAI from "openai";
|
|
@@ -631,14 +631,23 @@ function ConversationHistory({
|
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
// src/ui/Input.tsx
|
|
634
|
-
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
634
|
+
import { useState as useState2, useEffect as useEffect2, useRef, forwardRef, useImperativeHandle } from "react";
|
|
635
635
|
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
636
636
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
637
637
|
var CURSOR_CHAR = "\u258C";
|
|
638
638
|
var BLINK_INTERVAL_MS = 530;
|
|
639
|
-
|
|
639
|
+
var Input = forwardRef(function Input2({ isActive, onSubmit, onChange, placeholder }, ref) {
|
|
640
640
|
const [lines, setLines] = useState2([""]);
|
|
641
641
|
const [cursorVisible, setCursorVisible] = useState2(true);
|
|
642
|
+
const onChangeRef = useRef(onChange);
|
|
643
|
+
onChangeRef.current = onChange;
|
|
644
|
+
useImperativeHandle(ref, () => ({
|
|
645
|
+
fill(text) {
|
|
646
|
+
const newLines = text ? text.split("\n") : [""];
|
|
647
|
+
setLines(newLines);
|
|
648
|
+
onChangeRef.current?.(text);
|
|
649
|
+
}
|
|
650
|
+
}));
|
|
642
651
|
useEffect2(() => {
|
|
643
652
|
if (!isActive) {
|
|
644
653
|
setCursorVisible(true);
|
|
@@ -654,43 +663,42 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
654
663
|
useInput(
|
|
655
664
|
(input, key) => {
|
|
656
665
|
if (key.return && key.shift) {
|
|
657
|
-
|
|
666
|
+
const newLines = [...lines, ""];
|
|
667
|
+
setLines(newLines);
|
|
668
|
+
onChange?.(newLines.join("\n"));
|
|
658
669
|
return;
|
|
659
670
|
}
|
|
660
671
|
if (key.return) {
|
|
661
672
|
const text = lines.join("\n");
|
|
662
673
|
onSubmit(text);
|
|
663
674
|
setLines([""]);
|
|
675
|
+
onChange?.("");
|
|
664
676
|
return;
|
|
665
677
|
}
|
|
666
678
|
if (key.backspace || key.delete) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
});
|
|
679
|
+
const lastIdx = lines.length - 1;
|
|
680
|
+
const lastLine = lines[lastIdx];
|
|
681
|
+
let newLines;
|
|
682
|
+
if (lastLine.length > 0) {
|
|
683
|
+
newLines = [...lines.slice(0, lastIdx), lastLine.slice(0, -1)];
|
|
684
|
+
} else if (lines.length > 1) {
|
|
685
|
+
newLines = lines.slice(0, -1);
|
|
686
|
+
} else {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
setLines(newLines);
|
|
690
|
+
onChange?.(newLines.join("\n"));
|
|
680
691
|
return;
|
|
681
692
|
}
|
|
682
693
|
if (key.ctrl || key.escape || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab || key.pageUp || key.pageDown) {
|
|
683
694
|
return;
|
|
684
695
|
}
|
|
685
696
|
if (input.length > 0) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
return next;
|
|
690
|
-
});
|
|
697
|
+
const newLines = [...lines.slice(0, -1), lines[lines.length - 1] + input];
|
|
698
|
+
setLines(newLines);
|
|
699
|
+
onChange?.(newLines.join("\n"));
|
|
691
700
|
}
|
|
692
701
|
},
|
|
693
|
-
// 第二个参数:仅在 isActive=true 时注册键盘监听
|
|
694
702
|
{ isActive }
|
|
695
703
|
);
|
|
696
704
|
const isEmpty = lines.every((line) => line === "");
|
|
@@ -712,20 +720,69 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
712
720
|
] }, idx);
|
|
713
721
|
}) });
|
|
714
722
|
};
|
|
715
|
-
return /* @__PURE__ */ jsx3(Box3, { children: isActive ? (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
723
|
+
return /* @__PURE__ */ jsx3(Box3, { children: isActive ? renderLines() : /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
724
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
|
|
725
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
|
|
726
|
+
] }) });
|
|
727
|
+
});
|
|
728
|
+
var Input_default = Input;
|
|
729
|
+
|
|
730
|
+
// src/ui/SkillAutocomplete.tsx
|
|
731
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
732
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
733
|
+
function SkillAutocomplete({
|
|
734
|
+
suggestions,
|
|
735
|
+
selectedIndex,
|
|
736
|
+
isOpen: isOpen2
|
|
737
|
+
}) {
|
|
738
|
+
if (!isOpen2 || suggestions.length === 0) {
|
|
739
|
+
return /* @__PURE__ */ jsx4(Fragment2, {});
|
|
740
|
+
}
|
|
741
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", children: suggestions.map((skill, idx) => {
|
|
742
|
+
const selected = idx === selectedIndex;
|
|
743
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
744
|
+
/* @__PURE__ */ jsx4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: selected ? "> " : " " }),
|
|
745
|
+
/* @__PURE__ */ jsxs4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: [
|
|
746
|
+
"/",
|
|
747
|
+
skill.name
|
|
748
|
+
] }),
|
|
749
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
750
|
+
" \u2014 ",
|
|
751
|
+
skill.description
|
|
752
|
+
] })
|
|
753
|
+
] }, skill.name);
|
|
754
|
+
}) });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/ui/autocompleteLogic.ts
|
|
758
|
+
function getInitialState() {
|
|
759
|
+
return { query: "", selectedIndex: 0, dismissed: false };
|
|
760
|
+
}
|
|
761
|
+
function computeSuggestions(skills, state, maxSuggestions = 5) {
|
|
762
|
+
const { query } = state;
|
|
763
|
+
if (!query.startsWith("/")) return [];
|
|
764
|
+
const term = query.slice(1).toLowerCase();
|
|
765
|
+
if (term.includes(" ")) return [];
|
|
766
|
+
return skills.filter((s) => s.name.toLowerCase().startsWith(term)).slice(0, maxSuggestions);
|
|
767
|
+
}
|
|
768
|
+
function isOpen(state, suggestions) {
|
|
769
|
+
return state.query.startsWith("/") && suggestions.length > 0 && !state.dismissed;
|
|
770
|
+
}
|
|
771
|
+
function handleInputChange(_state, newQuery) {
|
|
772
|
+
return { query: newQuery, selectedIndex: 0, dismissed: false };
|
|
773
|
+
}
|
|
774
|
+
function moveUp(state, count) {
|
|
775
|
+
return { ...state, selectedIndex: (state.selectedIndex - 1 + count) % count };
|
|
776
|
+
}
|
|
777
|
+
function moveDown(state, count) {
|
|
778
|
+
return { ...state, selectedIndex: (state.selectedIndex + 1) % count };
|
|
779
|
+
}
|
|
780
|
+
function dismiss(state) {
|
|
781
|
+
return { ...state, dismissed: true };
|
|
725
782
|
}
|
|
726
783
|
|
|
727
784
|
// src/ui/App.tsx
|
|
728
|
-
import { jsx as
|
|
785
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
729
786
|
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
|
|
730
787
|
const [messages, setMessages] = useState3([]);
|
|
731
788
|
const [status, setStatus] = useState3("idle");
|
|
@@ -738,10 +795,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
738
795
|
const [toolName, setToolName] = useState3(void 0);
|
|
739
796
|
const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
|
|
740
797
|
const [expandTools, setExpandTools] = useState3(false);
|
|
741
|
-
const pendingConfirmRef =
|
|
742
|
-
const llmRef =
|
|
743
|
-
const
|
|
744
|
-
const
|
|
798
|
+
const pendingConfirmRef = useRef2(null);
|
|
799
|
+
const llmRef = useRef2(createLLMClient(config2));
|
|
800
|
+
const inputRef = useRef2(null);
|
|
801
|
+
const [acState, setAcState] = useState3(getInitialState());
|
|
802
|
+
const loggerRef = useRef2(null);
|
|
803
|
+
const loggedCountRef = useRef2(0);
|
|
745
804
|
useEffect3(() => {
|
|
746
805
|
if (config2.logDir) {
|
|
747
806
|
loggerRef.current = createLogger(config2.logDir, /* @__PURE__ */ new Date());
|
|
@@ -765,6 +824,30 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
765
824
|
loggedCountRef.current = messages.length;
|
|
766
825
|
}, [messages]);
|
|
767
826
|
useInput2((_input, key) => {
|
|
827
|
+
const skillList = registry2?.list() ?? [];
|
|
828
|
+
const suggestions = computeSuggestions(skillList, acState);
|
|
829
|
+
const open = isOpen(acState, suggestions);
|
|
830
|
+
if (open) {
|
|
831
|
+
if (key.upArrow) {
|
|
832
|
+
setAcState((prev) => moveUp(prev, suggestions.length));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (key.downArrow) {
|
|
836
|
+
setAcState((prev) => moveDown(prev, suggestions.length));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (key.escape) {
|
|
840
|
+
setAcState((prev) => dismiss(prev));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (key.tab) {
|
|
844
|
+
const selected = suggestions[acState.selectedIndex];
|
|
845
|
+
if (selected) {
|
|
846
|
+
inputRef.current?.fill(`/${selected.name} `);
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
768
851
|
if (key.tab) {
|
|
769
852
|
setExpandTools((prev) => !prev);
|
|
770
853
|
}
|
|
@@ -950,30 +1033,44 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
950
1033
|
[status, messages, runLlmLoop]
|
|
951
1034
|
);
|
|
952
1035
|
const isInputActive = status === "idle" || status === "awaiting_confirm";
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1036
|
+
const handleInputTextChange = useCallback((text) => {
|
|
1037
|
+
if (status !== "awaiting_confirm") {
|
|
1038
|
+
setAcState((prev) => handleInputChange(prev, text));
|
|
1039
|
+
}
|
|
1040
|
+
}, [status]);
|
|
1041
|
+
const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
|
|
1042
|
+
const acOpen = isOpen(acState, skillSuggestions);
|
|
1043
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: "100%", children: [
|
|
1044
|
+
/* @__PURE__ */ jsx5(ConversationHistory, { messages, expandTools }),
|
|
1045
|
+
/* @__PURE__ */ jsx5(
|
|
1046
|
+
StatusBar,
|
|
1047
|
+
{
|
|
1048
|
+
status,
|
|
1049
|
+
toolName,
|
|
1050
|
+
confirmPrompt,
|
|
1051
|
+
version: version2,
|
|
1052
|
+
tokenUsage
|
|
1053
|
+
}
|
|
1054
|
+
),
|
|
1055
|
+
/* @__PURE__ */ jsx5(
|
|
1056
|
+
SkillAutocomplete,
|
|
1057
|
+
{
|
|
1058
|
+
suggestions: skillSuggestions,
|
|
1059
|
+
selectedIndex: acState.selectedIndex,
|
|
1060
|
+
isOpen: acOpen
|
|
1061
|
+
}
|
|
1062
|
+
),
|
|
1063
|
+
/* @__PURE__ */ jsx5(
|
|
1064
|
+
Input_default,
|
|
1065
|
+
{
|
|
1066
|
+
ref: inputRef,
|
|
1067
|
+
isActive: isInputActive,
|
|
1068
|
+
onSubmit: handleSubmit,
|
|
1069
|
+
onChange: handleInputTextChange,
|
|
1070
|
+
placeholder: status === "awaiting_confirm" ? "y / n" : void 0
|
|
1071
|
+
}
|
|
1072
|
+
)
|
|
1073
|
+
] });
|
|
977
1074
|
}
|
|
978
1075
|
|
|
979
1076
|
// src/skills/registry.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhongqian97-code/ecode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A minimal Claude Code clone with REPL interface and bash tool calling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "zhongqian97-code",
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"url": "https://github.com/zhongqian97-code/ecode/issues"
|
|
25
25
|
},
|
|
26
26
|
"bin": {
|
|
27
|
-
"ecode": "
|
|
27
|
+
"ecode": "bin.cjs"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
+
"bin.cjs",
|
|
30
31
|
"dist",
|
|
31
32
|
"skills",
|
|
32
33
|
"README.md",
|