tidyf 1.0.0 → 1.0.2
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 +103 -26
- package/dist/cli.js +170 -0
- package/dist/index.js +172 -0
- package/package.json +56 -56
- package/src/commands/modelPicker.ts +155 -0
- package/src/commands/organize.ts +40 -2
package/README.md
CHANGED
|
@@ -2,24 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered file organizer CLI using [opencode.ai](https://opencode.ai)
|
|
4
4
|
|
|
5
|
-
```
|
|
6
|
-
┌ tidyf
|
|
5
|
+
```text
|
|
6
|
+
┌ tidyf
|
|
7
7
|
│
|
|
8
|
-
|
|
9
|
-
│ Found 12 files
|
|
8
|
+
● Source: ~/Downloads
|
|
10
9
|
│
|
|
11
|
-
|
|
10
|
+
● Target: ~/Downloads/Organized
|
|
12
11
|
│
|
|
13
|
-
|
|
14
|
-
│
|
|
15
|
-
|
|
16
|
-
│
|
|
12
|
+
◇ Found 15 files
|
|
13
|
+
│
|
|
14
|
+
● Total size: 79 MB
|
|
15
|
+
│
|
|
16
|
+
◇ Analysis complete
|
|
17
|
+
│
|
|
18
|
+
● Proposed organization for 15 files:
|
|
19
|
+
│
|
|
20
|
+
│ Strategy: Primary categorization by file type and MIME type, secondary categorization
|
|
21
|
+
│ by filename context and naming patterns. Documents go to Work, books
|
|
22
|
+
│ to Education, and camera images to Photos.
|
|
23
|
+
│
|
|
24
|
+
● 📄 Documents (9 files)
|
|
25
|
+
│
|
|
26
|
+
│ [1] 📄 financial-report.pdf (596 KB)
|
|
27
|
+
│ → ~/Downloads/Organized/Documents/Work/financial-report.pdf
|
|
28
|
+
│ 📄 Documents/Work 90%
|
|
29
|
+
│ Document with financial context and report keywords
|
|
30
|
+
│
|
|
31
|
+
│ [2] 📄 research-paper.pdf (448.6 KB)
|
|
32
|
+
│ → ~/Downloads/Organized/Documents/Education/research-paper.pdf
|
|
33
|
+
│ 📄 Documents/Education 85%
|
|
34
|
+
│ Technical document, appears to be educational content
|
|
17
35
|
│
|
|
18
|
-
|
|
19
|
-
│
|
|
36
|
+
● 🖼️ Images (4 files)
|
|
37
|
+
│
|
|
38
|
+
│ [12] 🖼️ vacation-photo.jpg (361.1 KB)
|
|
39
|
+
│ → ~/Downloads/Organized/Images/Photos/vacation-photo.jpg
|
|
40
|
+
│ 🖼️ Images/Photos 95%
|
|
41
|
+
│ Image with metadata indicating it was taken with a camera
|
|
42
|
+
│
|
|
43
|
+
■ What would you like to do?
|
|
44
|
+
│ ● Apply all 15 moves
|
|
20
45
|
│ ○ Select individually
|
|
46
|
+
│ ○ Regenerate analysis
|
|
47
|
+
│ ○ Regenerate analysis (different model)
|
|
21
48
|
│ ○ Cancel
|
|
22
|
-
|
|
49
|
+
│
|
|
50
|
+
└ Organization complete!
|
|
23
51
|
```
|
|
24
52
|
|
|
25
53
|
## Features
|
|
@@ -191,6 +219,11 @@ Edit this file to customize AI behavior for your workflow.
|
|
|
191
219
|
2. **Analyzes with AI** - Sends file info to AI with your configured rules
|
|
192
220
|
3. **Proposes organization** - Shows categorization with confidence levels
|
|
193
221
|
4. **Confirms with you** - Presents interactive UI for approval
|
|
222
|
+
- Apply all moves
|
|
223
|
+
- View file details
|
|
224
|
+
- Regenerate analysis (optional: with different instructions)
|
|
225
|
+
- Regenerate analysis (different model) — choose another provider/model
|
|
226
|
+
- Cancel
|
|
194
227
|
5. **Moves files** - Organizes files into target directory structure
|
|
195
228
|
|
|
196
229
|
## Examples
|
|
@@ -198,24 +231,68 @@ Edit this file to customize AI behavior for your workflow.
|
|
|
198
231
|
### Basic Organization
|
|
199
232
|
|
|
200
233
|
```bash
|
|
201
|
-
$ tidyf
|
|
202
|
-
┌ tidyf
|
|
234
|
+
$ tidyf
|
|
235
|
+
┌ tidyf
|
|
236
|
+
│
|
|
237
|
+
● Source: ~/Downloads
|
|
238
|
+
│
|
|
239
|
+
● Target: ~/Downloads/Organized
|
|
240
|
+
│
|
|
241
|
+
◇ Found 12 files
|
|
242
|
+
│
|
|
243
|
+
● Total size: 45 MB
|
|
244
|
+
│
|
|
245
|
+
◇ Analysis complete
|
|
246
|
+
│
|
|
247
|
+
● Proposed organization for 12 files:
|
|
248
|
+
│
|
|
249
|
+
│ Strategy: Primary categorization by file type and MIME type, secondary categorization
|
|
250
|
+
│ by filename context and naming patterns...
|
|
251
|
+
│
|
|
252
|
+
● 📄 Documents (5 files)
|
|
253
|
+
│
|
|
254
|
+
│ [1] 📄 project-proposal.pdf (245 KB)
|
|
255
|
+
│ → ~/Downloads/Organized/Documents/Work/project-proposal.pdf
|
|
256
|
+
│ 📄 Documents/Work 95%
|
|
257
|
+
│ Business document with project keywords
|
|
258
|
+
│
|
|
259
|
+
■ What would you like to do?
|
|
260
|
+
│ ● Apply all 12 moves
|
|
261
|
+
│ ○ Select individually
|
|
262
|
+
│ ○ Cancel
|
|
263
|
+
│
|
|
264
|
+
└ Organization complete!
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Interactive Configuration
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
$ tidyf config
|
|
271
|
+
┌ tidyf config
|
|
272
|
+
│
|
|
273
|
+
● Configuring global settings
|
|
274
|
+
│
|
|
275
|
+
│ Config: ~/.tidy/settings.json
|
|
276
|
+
│
|
|
277
|
+
◇ What would you like to configure?
|
|
278
|
+
│ AI Model
|
|
279
|
+
│
|
|
280
|
+
◇ Fetched 2 providers
|
|
281
|
+
│
|
|
282
|
+
● Current Model: opencode/big-pickle
|
|
283
|
+
│
|
|
284
|
+
◇ Select AI provider:
|
|
285
|
+
│ OpenCode Zen
|
|
203
286
|
│
|
|
204
|
-
|
|
205
|
-
│
|
|
287
|
+
◇ Select model:
|
|
288
|
+
│ glm-4.7-free
|
|
206
289
|
│
|
|
207
|
-
|
|
290
|
+
◆ Model set to opencode/glm-4.7-free
|
|
208
291
|
│
|
|
209
|
-
|
|
210
|
-
│
|
|
211
|
-
│ 🖼️ vacation-photo.jpg → Images/Photos
|
|
212
|
-
│ 📦 backup.zip → Archives
|
|
213
|
-
│ 🎵 podcast.mp3 → Audio/Podcasts
|
|
214
|
-
│ 💻 installer.dmg → Applications/Installers
|
|
292
|
+
◇ What would you like to configure?
|
|
293
|
+
│ Done
|
|
215
294
|
│
|
|
216
|
-
|
|
217
|
-
│ ● Yes
|
|
218
|
-
└
|
|
295
|
+
└ Configuration saved!
|
|
219
296
|
```
|
|
220
297
|
|
|
221
298
|
### Watch Mode
|
package/dist/cli.js
CHANGED
|
@@ -18813,6 +18813,143 @@ function getStatusIndicator(status) {
|
|
|
18813
18813
|
}
|
|
18814
18814
|
}
|
|
18815
18815
|
|
|
18816
|
+
// src/commands/modelPicker.ts
|
|
18817
|
+
async function pickModel() {
|
|
18818
|
+
const s = Y2();
|
|
18819
|
+
s.start("Fetching available models from OpenCode...");
|
|
18820
|
+
let providers = [];
|
|
18821
|
+
try {
|
|
18822
|
+
const response = await getAvailableModels();
|
|
18823
|
+
if (response.error) {
|
|
18824
|
+
throw new Error("Failed to fetch models");
|
|
18825
|
+
}
|
|
18826
|
+
providers = response.data?.providers || [];
|
|
18827
|
+
s.stop(`Fetched ${providers.length} providers`);
|
|
18828
|
+
} catch (error) {
|
|
18829
|
+
s.stop("Failed to fetch models");
|
|
18830
|
+
M2.error(error.message);
|
|
18831
|
+
M2.warn("Falling back to manual entry.");
|
|
18832
|
+
}
|
|
18833
|
+
let providerId;
|
|
18834
|
+
let modelName;
|
|
18835
|
+
if (providers.length > 0) {
|
|
18836
|
+
const providerOptions = providers.map((prov) => ({
|
|
18837
|
+
value: prov.id,
|
|
18838
|
+
label: prov.name || prov.id
|
|
18839
|
+
}));
|
|
18840
|
+
providerOptions.push({
|
|
18841
|
+
value: "custom",
|
|
18842
|
+
label: "Enter custom provider..."
|
|
18843
|
+
});
|
|
18844
|
+
const selectedProvider = await ve({
|
|
18845
|
+
message: "Select AI provider:",
|
|
18846
|
+
options: providerOptions
|
|
18847
|
+
});
|
|
18848
|
+
if (pD(selectedProvider)) {
|
|
18849
|
+
return;
|
|
18850
|
+
}
|
|
18851
|
+
if (selectedProvider === "custom") {
|
|
18852
|
+
const customProv = await he({
|
|
18853
|
+
message: "Enter provider ID:",
|
|
18854
|
+
placeholder: "opencode",
|
|
18855
|
+
validate: (value) => {
|
|
18856
|
+
if (!value)
|
|
18857
|
+
return "Provider ID is required";
|
|
18858
|
+
}
|
|
18859
|
+
});
|
|
18860
|
+
if (pD(customProv))
|
|
18861
|
+
return;
|
|
18862
|
+
providerId = customProv;
|
|
18863
|
+
const customModel = await he({
|
|
18864
|
+
message: "Enter model ID:",
|
|
18865
|
+
placeholder: "gpt-4o",
|
|
18866
|
+
validate: (value) => {
|
|
18867
|
+
if (!value)
|
|
18868
|
+
return "Model ID is required";
|
|
18869
|
+
}
|
|
18870
|
+
});
|
|
18871
|
+
if (pD(customModel))
|
|
18872
|
+
return;
|
|
18873
|
+
modelName = customModel;
|
|
18874
|
+
} else {
|
|
18875
|
+
providerId = selectedProvider;
|
|
18876
|
+
const providerData = providers.find((p2) => p2.id === providerId);
|
|
18877
|
+
if (!providerData || !providerData.models) {
|
|
18878
|
+
M2.warn(`No models found for provider ${providerId}, please enter manually.`);
|
|
18879
|
+
const customModel = await he({
|
|
18880
|
+
message: "Enter model ID:",
|
|
18881
|
+
placeholder: "gpt-4o",
|
|
18882
|
+
validate: (value) => {
|
|
18883
|
+
if (!value)
|
|
18884
|
+
return "Model ID is required";
|
|
18885
|
+
}
|
|
18886
|
+
});
|
|
18887
|
+
if (pD(customModel))
|
|
18888
|
+
return;
|
|
18889
|
+
modelName = customModel;
|
|
18890
|
+
} else {
|
|
18891
|
+
let modelIds = [];
|
|
18892
|
+
if (Array.isArray(providerData.models)) {
|
|
18893
|
+
modelIds = providerData.models;
|
|
18894
|
+
} else if (typeof providerData.models === "object") {
|
|
18895
|
+
modelIds = Object.keys(providerData.models);
|
|
18896
|
+
}
|
|
18897
|
+
if (modelIds.length === 0) {
|
|
18898
|
+
M2.warn(`No models found for provider ${providerId}`);
|
|
18899
|
+
const customModel = await he({
|
|
18900
|
+
message: "Enter model ID:"
|
|
18901
|
+
});
|
|
18902
|
+
if (pD(customModel))
|
|
18903
|
+
return;
|
|
18904
|
+
modelName = customModel;
|
|
18905
|
+
} else {
|
|
18906
|
+
const modelOptions = modelIds.map((model) => ({
|
|
18907
|
+
value: model,
|
|
18908
|
+
label: model
|
|
18909
|
+
}));
|
|
18910
|
+
modelOptions.push({
|
|
18911
|
+
value: "custom",
|
|
18912
|
+
label: "Enter custom model..."
|
|
18913
|
+
});
|
|
18914
|
+
const selectedModel = await ve({
|
|
18915
|
+
message: "Select model:",
|
|
18916
|
+
options: modelOptions
|
|
18917
|
+
});
|
|
18918
|
+
if (pD(selectedModel))
|
|
18919
|
+
return;
|
|
18920
|
+
if (selectedModel === "custom") {
|
|
18921
|
+
const customModel = await he({
|
|
18922
|
+
message: "Enter model ID:"
|
|
18923
|
+
});
|
|
18924
|
+
if (pD(customModel))
|
|
18925
|
+
return;
|
|
18926
|
+
modelName = customModel;
|
|
18927
|
+
} else {
|
|
18928
|
+
modelName = selectedModel;
|
|
18929
|
+
}
|
|
18930
|
+
}
|
|
18931
|
+
}
|
|
18932
|
+
}
|
|
18933
|
+
} else {
|
|
18934
|
+
const manualEntry = await he({
|
|
18935
|
+
message: "Enter model (format: provider/model):",
|
|
18936
|
+
placeholder: "opencode/gpt-4o",
|
|
18937
|
+
validate: (value) => {
|
|
18938
|
+
if (!value.includes("/")) {
|
|
18939
|
+
return "Model must be in format: provider/model";
|
|
18940
|
+
}
|
|
18941
|
+
}
|
|
18942
|
+
});
|
|
18943
|
+
if (pD(manualEntry)) {
|
|
18944
|
+
return;
|
|
18945
|
+
}
|
|
18946
|
+
const parts = manualEntry.split("/");
|
|
18947
|
+
providerId = parts[0];
|
|
18948
|
+
modelName = parts.slice(1).join("/");
|
|
18949
|
+
}
|
|
18950
|
+
return { provider: providerId, model: modelName };
|
|
18951
|
+
}
|
|
18952
|
+
|
|
18816
18953
|
// src/commands/organize.ts
|
|
18817
18954
|
function displayProposal(proposal, index) {
|
|
18818
18955
|
const icon = getFileIcon(proposal.file.name);
|
|
@@ -19000,6 +19137,11 @@ async function organizeCommand(options) {
|
|
|
19000
19137
|
label: "Regenerate analysis",
|
|
19001
19138
|
hint: "Ask AI to re-analyze with different instructions"
|
|
19002
19139
|
},
|
|
19140
|
+
{
|
|
19141
|
+
value: "regenerate_with_model",
|
|
19142
|
+
label: "Regenerate analysis (different model)",
|
|
19143
|
+
hint: "Re-analyze using another provider/model"
|
|
19144
|
+
},
|
|
19003
19145
|
{
|
|
19004
19146
|
value: "cancel",
|
|
19005
19147
|
label: "Cancel"
|
|
@@ -19043,6 +19185,34 @@ async function organizeCommand(options) {
|
|
|
19043
19185
|
}
|
|
19044
19186
|
break;
|
|
19045
19187
|
}
|
|
19188
|
+
case "regenerate_with_model": {
|
|
19189
|
+
const newInstructions = await he({
|
|
19190
|
+
message: "Enter additional instructions for AI (or press Enter to skip):",
|
|
19191
|
+
placeholder: "e.g., Keep all PDFs together, sort images by date"
|
|
19192
|
+
});
|
|
19193
|
+
if (pD(newInstructions)) {
|
|
19194
|
+
break;
|
|
19195
|
+
}
|
|
19196
|
+
const pickedModel = await pickModel();
|
|
19197
|
+
if (!pickedModel) {
|
|
19198
|
+
break;
|
|
19199
|
+
}
|
|
19200
|
+
spinner.start(`Re-analyzing with ${pickedModel.provider}/${pickedModel.model}...`);
|
|
19201
|
+
try {
|
|
19202
|
+
proposal = await analyzeFiles({
|
|
19203
|
+
files,
|
|
19204
|
+
targetDir: targetPath,
|
|
19205
|
+
instructions: newInstructions || undefined,
|
|
19206
|
+
model: pickedModel
|
|
19207
|
+
});
|
|
19208
|
+
spinner.stop("Analysis complete");
|
|
19209
|
+
displayAllProposals(proposal);
|
|
19210
|
+
} catch (error) {
|
|
19211
|
+
spinner.stop("Analysis failed");
|
|
19212
|
+
M2.error(error.message);
|
|
19213
|
+
}
|
|
19214
|
+
break;
|
|
19215
|
+
}
|
|
19046
19216
|
}
|
|
19047
19217
|
}
|
|
19048
19218
|
}
|
package/dist/index.js
CHANGED
|
@@ -16714,6 +16714,145 @@ function createWatcher(options) {
|
|
|
16714
16714
|
// src/commands/organize.ts
|
|
16715
16715
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
16716
16716
|
import { isAbsolute, resolve } from "path";
|
|
16717
|
+
|
|
16718
|
+
// src/commands/modelPicker.ts
|
|
16719
|
+
async function pickModel() {
|
|
16720
|
+
const s = Y2();
|
|
16721
|
+
s.start("Fetching available models from OpenCode...");
|
|
16722
|
+
let providers = [];
|
|
16723
|
+
try {
|
|
16724
|
+
const response = await getAvailableModels();
|
|
16725
|
+
if (response.error) {
|
|
16726
|
+
throw new Error("Failed to fetch models");
|
|
16727
|
+
}
|
|
16728
|
+
providers = response.data?.providers || [];
|
|
16729
|
+
s.stop(`Fetched ${providers.length} providers`);
|
|
16730
|
+
} catch (error) {
|
|
16731
|
+
s.stop("Failed to fetch models");
|
|
16732
|
+
M2.error(error.message);
|
|
16733
|
+
M2.warn("Falling back to manual entry.");
|
|
16734
|
+
}
|
|
16735
|
+
let providerId;
|
|
16736
|
+
let modelName;
|
|
16737
|
+
if (providers.length > 0) {
|
|
16738
|
+
const providerOptions = providers.map((prov) => ({
|
|
16739
|
+
value: prov.id,
|
|
16740
|
+
label: prov.name || prov.id
|
|
16741
|
+
}));
|
|
16742
|
+
providerOptions.push({
|
|
16743
|
+
value: "custom",
|
|
16744
|
+
label: "Enter custom provider..."
|
|
16745
|
+
});
|
|
16746
|
+
const selectedProvider = await ve({
|
|
16747
|
+
message: "Select AI provider:",
|
|
16748
|
+
options: providerOptions
|
|
16749
|
+
});
|
|
16750
|
+
if (pD(selectedProvider)) {
|
|
16751
|
+
return;
|
|
16752
|
+
}
|
|
16753
|
+
if (selectedProvider === "custom") {
|
|
16754
|
+
const customProv = await he({
|
|
16755
|
+
message: "Enter provider ID:",
|
|
16756
|
+
placeholder: "opencode",
|
|
16757
|
+
validate: (value) => {
|
|
16758
|
+
if (!value)
|
|
16759
|
+
return "Provider ID is required";
|
|
16760
|
+
}
|
|
16761
|
+
});
|
|
16762
|
+
if (pD(customProv))
|
|
16763
|
+
return;
|
|
16764
|
+
providerId = customProv;
|
|
16765
|
+
const customModel = await he({
|
|
16766
|
+
message: "Enter model ID:",
|
|
16767
|
+
placeholder: "gpt-4o",
|
|
16768
|
+
validate: (value) => {
|
|
16769
|
+
if (!value)
|
|
16770
|
+
return "Model ID is required";
|
|
16771
|
+
}
|
|
16772
|
+
});
|
|
16773
|
+
if (pD(customModel))
|
|
16774
|
+
return;
|
|
16775
|
+
modelName = customModel;
|
|
16776
|
+
} else {
|
|
16777
|
+
providerId = selectedProvider;
|
|
16778
|
+
const providerData = providers.find((p2) => p2.id === providerId);
|
|
16779
|
+
if (!providerData || !providerData.models) {
|
|
16780
|
+
M2.warn(`No models found for provider ${providerId}, please enter manually.`);
|
|
16781
|
+
const customModel = await he({
|
|
16782
|
+
message: "Enter model ID:",
|
|
16783
|
+
placeholder: "gpt-4o",
|
|
16784
|
+
validate: (value) => {
|
|
16785
|
+
if (!value)
|
|
16786
|
+
return "Model ID is required";
|
|
16787
|
+
}
|
|
16788
|
+
});
|
|
16789
|
+
if (pD(customModel))
|
|
16790
|
+
return;
|
|
16791
|
+
modelName = customModel;
|
|
16792
|
+
} else {
|
|
16793
|
+
let modelIds = [];
|
|
16794
|
+
if (Array.isArray(providerData.models)) {
|
|
16795
|
+
modelIds = providerData.models;
|
|
16796
|
+
} else if (typeof providerData.models === "object") {
|
|
16797
|
+
modelIds = Object.keys(providerData.models);
|
|
16798
|
+
}
|
|
16799
|
+
if (modelIds.length === 0) {
|
|
16800
|
+
M2.warn(`No models found for provider ${providerId}`);
|
|
16801
|
+
const customModel = await he({
|
|
16802
|
+
message: "Enter model ID:"
|
|
16803
|
+
});
|
|
16804
|
+
if (pD(customModel))
|
|
16805
|
+
return;
|
|
16806
|
+
modelName = customModel;
|
|
16807
|
+
} else {
|
|
16808
|
+
const modelOptions = modelIds.map((model) => ({
|
|
16809
|
+
value: model,
|
|
16810
|
+
label: model
|
|
16811
|
+
}));
|
|
16812
|
+
modelOptions.push({
|
|
16813
|
+
value: "custom",
|
|
16814
|
+
label: "Enter custom model..."
|
|
16815
|
+
});
|
|
16816
|
+
const selectedModel = await ve({
|
|
16817
|
+
message: "Select model:",
|
|
16818
|
+
options: modelOptions
|
|
16819
|
+
});
|
|
16820
|
+
if (pD(selectedModel))
|
|
16821
|
+
return;
|
|
16822
|
+
if (selectedModel === "custom") {
|
|
16823
|
+
const customModel = await he({
|
|
16824
|
+
message: "Enter model ID:"
|
|
16825
|
+
});
|
|
16826
|
+
if (pD(customModel))
|
|
16827
|
+
return;
|
|
16828
|
+
modelName = customModel;
|
|
16829
|
+
} else {
|
|
16830
|
+
modelName = selectedModel;
|
|
16831
|
+
}
|
|
16832
|
+
}
|
|
16833
|
+
}
|
|
16834
|
+
}
|
|
16835
|
+
} else {
|
|
16836
|
+
const manualEntry = await he({
|
|
16837
|
+
message: "Enter model (format: provider/model):",
|
|
16838
|
+
placeholder: "opencode/gpt-4o",
|
|
16839
|
+
validate: (value) => {
|
|
16840
|
+
if (!value.includes("/")) {
|
|
16841
|
+
return "Model must be in format: provider/model";
|
|
16842
|
+
}
|
|
16843
|
+
}
|
|
16844
|
+
});
|
|
16845
|
+
if (pD(manualEntry)) {
|
|
16846
|
+
return;
|
|
16847
|
+
}
|
|
16848
|
+
const parts = manualEntry.split("/");
|
|
16849
|
+
providerId = parts[0];
|
|
16850
|
+
modelName = parts.slice(1).join("/");
|
|
16851
|
+
}
|
|
16852
|
+
return { provider: providerId, model: modelName };
|
|
16853
|
+
}
|
|
16854
|
+
|
|
16855
|
+
// src/commands/organize.ts
|
|
16717
16856
|
function displayProposal(proposal, index) {
|
|
16718
16857
|
const icon = getFileIcon(proposal.file.name);
|
|
16719
16858
|
const size = import_picocolors4.default.dim(`(${formatFileSize(proposal.file.size)})`);
|
|
@@ -16900,6 +17039,11 @@ async function organizeCommand(options) {
|
|
|
16900
17039
|
label: "Regenerate analysis",
|
|
16901
17040
|
hint: "Ask AI to re-analyze with different instructions"
|
|
16902
17041
|
},
|
|
17042
|
+
{
|
|
17043
|
+
value: "regenerate_with_model",
|
|
17044
|
+
label: "Regenerate analysis (different model)",
|
|
17045
|
+
hint: "Re-analyze using another provider/model"
|
|
17046
|
+
},
|
|
16903
17047
|
{
|
|
16904
17048
|
value: "cancel",
|
|
16905
17049
|
label: "Cancel"
|
|
@@ -16943,6 +17087,34 @@ async function organizeCommand(options) {
|
|
|
16943
17087
|
}
|
|
16944
17088
|
break;
|
|
16945
17089
|
}
|
|
17090
|
+
case "regenerate_with_model": {
|
|
17091
|
+
const newInstructions = await he({
|
|
17092
|
+
message: "Enter additional instructions for AI (or press Enter to skip):",
|
|
17093
|
+
placeholder: "e.g., Keep all PDFs together, sort images by date"
|
|
17094
|
+
});
|
|
17095
|
+
if (pD(newInstructions)) {
|
|
17096
|
+
break;
|
|
17097
|
+
}
|
|
17098
|
+
const pickedModel = await pickModel();
|
|
17099
|
+
if (!pickedModel) {
|
|
17100
|
+
break;
|
|
17101
|
+
}
|
|
17102
|
+
spinner.start(`Re-analyzing with ${pickedModel.provider}/${pickedModel.model}...`);
|
|
17103
|
+
try {
|
|
17104
|
+
proposal = await analyzeFiles({
|
|
17105
|
+
files,
|
|
17106
|
+
targetDir: targetPath,
|
|
17107
|
+
instructions: newInstructions || undefined,
|
|
17108
|
+
model: pickedModel
|
|
17109
|
+
});
|
|
17110
|
+
spinner.stop("Analysis complete");
|
|
17111
|
+
displayAllProposals(proposal);
|
|
17112
|
+
} catch (error) {
|
|
17113
|
+
spinner.stop("Analysis failed");
|
|
17114
|
+
M2.error(error.message);
|
|
17115
|
+
}
|
|
17116
|
+
break;
|
|
17117
|
+
}
|
|
16946
17118
|
}
|
|
16947
17119
|
}
|
|
16948
17120
|
}
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
2
|
+
"name": "tidyf",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "AI-powered file organizer using opencode.ai",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tidyf": "./dist/cli.js",
|
|
9
|
+
"td": "./dist/cli.js",
|
|
10
|
+
"tidyfiles": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/yafyx/tidyf.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node dist/cli.js",
|
|
22
|
+
"dev": "bun --watch src/cli.ts",
|
|
23
|
+
"build": "bun build src/cli.ts --outdir=dist --target=node --format=esm && bun build src/index.ts --outdir=dist --target=node --format=esm",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "bun run build"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"file-organizer",
|
|
32
|
+
"ai",
|
|
33
|
+
"opencode",
|
|
34
|
+
"cli",
|
|
35
|
+
"downloads",
|
|
36
|
+
"tidy",
|
|
37
|
+
"automation"
|
|
38
|
+
],
|
|
39
|
+
"author": "",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@clack/prompts": "^0.11.0",
|
|
47
|
+
"@opencode-ai/sdk": "^1.0.155",
|
|
48
|
+
"chokidar": "^3.5.3",
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"mime-types": "^2.1.35",
|
|
51
|
+
"picocolors": "^1.1.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "latest",
|
|
55
|
+
"@types/mime-types": "^2.1.4",
|
|
56
|
+
"typescript": "^5.7.2"
|
|
57
|
+
}
|
|
58
58
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive model picker for regenerate-with-model flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { getAvailableModels } from "../lib/opencode.ts";
|
|
7
|
+
import type { ModelSelection } from "../types/config.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interactive model selection using OpenCode providers
|
|
11
|
+
*/
|
|
12
|
+
export async function pickModel(): Promise<ModelSelection | undefined> {
|
|
13
|
+
const s = p.spinner();
|
|
14
|
+
s.start("Fetching available models from OpenCode...");
|
|
15
|
+
|
|
16
|
+
let providers: any[] = [];
|
|
17
|
+
try {
|
|
18
|
+
const response = await getAvailableModels();
|
|
19
|
+
if (response.error) {
|
|
20
|
+
throw new Error("Failed to fetch models");
|
|
21
|
+
}
|
|
22
|
+
providers = response.data?.providers || [];
|
|
23
|
+
s.stop(`Fetched ${providers.length} providers`);
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
s.stop("Failed to fetch models");
|
|
26
|
+
p.log.error(error.message);
|
|
27
|
+
p.log.warn("Falling back to manual entry.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let providerId: string;
|
|
31
|
+
let modelName: string;
|
|
32
|
+
|
|
33
|
+
if (providers.length > 0) {
|
|
34
|
+
const providerOptions = providers.map((prov) => ({
|
|
35
|
+
value: prov.id,
|
|
36
|
+
label: prov.name || prov.id,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
providerOptions.push({
|
|
40
|
+
value: "custom",
|
|
41
|
+
label: "Enter custom provider...",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const selectedProvider = await p.select({
|
|
45
|
+
message: "Select AI provider:",
|
|
46
|
+
options: providerOptions,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (p.isCancel(selectedProvider)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (selectedProvider === "custom") {
|
|
54
|
+
const customProv = await p.text({
|
|
55
|
+
message: "Enter provider ID:",
|
|
56
|
+
placeholder: "opencode",
|
|
57
|
+
validate: (value) => {
|
|
58
|
+
if (!value) return "Provider ID is required";
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (p.isCancel(customProv)) return undefined;
|
|
62
|
+
providerId = customProv;
|
|
63
|
+
|
|
64
|
+
const customModel = await p.text({
|
|
65
|
+
message: "Enter model ID:",
|
|
66
|
+
placeholder: "gpt-4o",
|
|
67
|
+
validate: (value) => {
|
|
68
|
+
if (!value) return "Model ID is required";
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (p.isCancel(customModel)) return undefined;
|
|
72
|
+
modelName = customModel;
|
|
73
|
+
} else {
|
|
74
|
+
providerId = selectedProvider as string;
|
|
75
|
+
const providerData = providers.find((p) => p.id === providerId);
|
|
76
|
+
|
|
77
|
+
if (!providerData || !providerData.models) {
|
|
78
|
+
p.log.warn(
|
|
79
|
+
`No models found for provider ${providerId}, please enter manually.`,
|
|
80
|
+
);
|
|
81
|
+
const customModel = await p.text({
|
|
82
|
+
message: "Enter model ID:",
|
|
83
|
+
placeholder: "gpt-4o",
|
|
84
|
+
validate: (value) => {
|
|
85
|
+
if (!value) return "Model ID is required";
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (p.isCancel(customModel)) return undefined;
|
|
89
|
+
modelName = customModel;
|
|
90
|
+
} else {
|
|
91
|
+
let modelIds: string[] = [];
|
|
92
|
+
if (Array.isArray(providerData.models)) {
|
|
93
|
+
modelIds = providerData.models;
|
|
94
|
+
} else if (typeof providerData.models === "object") {
|
|
95
|
+
modelIds = Object.keys(providerData.models);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (modelIds.length === 0) {
|
|
99
|
+
p.log.warn(`No models found for provider ${providerId}`);
|
|
100
|
+
const customModel = await p.text({
|
|
101
|
+
message: "Enter model ID:",
|
|
102
|
+
});
|
|
103
|
+
if (p.isCancel(customModel)) return undefined;
|
|
104
|
+
modelName = customModel;
|
|
105
|
+
} else {
|
|
106
|
+
const modelOptions = modelIds.map((model: string) => ({
|
|
107
|
+
value: model,
|
|
108
|
+
label: model,
|
|
109
|
+
}));
|
|
110
|
+
modelOptions.push({
|
|
111
|
+
value: "custom",
|
|
112
|
+
label: "Enter custom model...",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const selectedModel = await p.select({
|
|
116
|
+
message: "Select model:",
|
|
117
|
+
options: modelOptions,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (p.isCancel(selectedModel)) return undefined;
|
|
121
|
+
|
|
122
|
+
if (selectedModel === "custom") {
|
|
123
|
+
const customModel = await p.text({
|
|
124
|
+
message: "Enter model ID:",
|
|
125
|
+
});
|
|
126
|
+
if (p.isCancel(customModel)) return undefined;
|
|
127
|
+
modelName = customModel;
|
|
128
|
+
} else {
|
|
129
|
+
modelName = selectedModel as string;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
const manualEntry = await p.text({
|
|
136
|
+
message: "Enter model (format: provider/model):",
|
|
137
|
+
placeholder: "opencode/gpt-4o",
|
|
138
|
+
validate: (value) => {
|
|
139
|
+
if (!value.includes("/")) {
|
|
140
|
+
return "Model must be in format: provider/model";
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (p.isCancel(manualEntry)) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const parts = manualEntry.split("/");
|
|
150
|
+
providerId = parts[0];
|
|
151
|
+
modelName = parts.slice(1).join("/");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { provider: providerId, model: modelName };
|
|
155
|
+
}
|
package/src/commands/organize.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as p from "@clack/prompts";
|
|
13
|
-
import { homedir } from "os";
|
|
14
13
|
import { isAbsolute, resolve } from "path";
|
|
15
14
|
import color from "picocolors";
|
|
16
15
|
import {
|
|
@@ -27,12 +26,13 @@ import type {
|
|
|
27
26
|
OrganizationProposal,
|
|
28
27
|
OrganizeOptions,
|
|
29
28
|
} from "../types/organizer.ts";
|
|
30
|
-
import {
|
|
29
|
+
import { formatFileSize, moveFile } from "../utils/files.ts";
|
|
31
30
|
import {
|
|
32
31
|
getCategoryIcon,
|
|
33
32
|
getFileIcon,
|
|
34
33
|
getStatusIndicator,
|
|
35
34
|
} from "../utils/icons.ts";
|
|
35
|
+
import { pickModel } from "./modelPicker.ts";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Display a single proposal
|
|
@@ -337,6 +337,11 @@ export async function organizeCommand(options: OrganizeOptions): Promise<void> {
|
|
|
337
337
|
label: "Regenerate analysis",
|
|
338
338
|
hint: "Ask AI to re-analyze with different instructions",
|
|
339
339
|
},
|
|
340
|
+
{
|
|
341
|
+
value: "regenerate_with_model",
|
|
342
|
+
label: "Regenerate analysis (different model)",
|
|
343
|
+
hint: "Re-analyze using another provider/model",
|
|
344
|
+
},
|
|
340
345
|
{
|
|
341
346
|
value: "cancel",
|
|
342
347
|
label: "Cancel",
|
|
@@ -387,6 +392,39 @@ export async function organizeCommand(options: OrganizeOptions): Promise<void> {
|
|
|
387
392
|
}
|
|
388
393
|
break;
|
|
389
394
|
}
|
|
395
|
+
|
|
396
|
+
case "regenerate_with_model": {
|
|
397
|
+
const newInstructions = await p.text({
|
|
398
|
+
message:
|
|
399
|
+
"Enter additional instructions for AI (or press Enter to skip):",
|
|
400
|
+
placeholder: "e.g., Keep all PDFs together, sort images by date",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (p.isCancel(newInstructions)) {
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const pickedModel = await pickModel();
|
|
408
|
+
if (!pickedModel) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
spinner.start(`Re-analyzing with ${pickedModel.provider}/${pickedModel.model}...`);
|
|
413
|
+
try {
|
|
414
|
+
proposal = await analyzeFiles({
|
|
415
|
+
files,
|
|
416
|
+
targetDir: targetPath,
|
|
417
|
+
instructions: newInstructions || undefined,
|
|
418
|
+
model: pickedModel,
|
|
419
|
+
});
|
|
420
|
+
spinner.stop("Analysis complete");
|
|
421
|
+
displayAllProposals(proposal);
|
|
422
|
+
} catch (error: any) {
|
|
423
|
+
spinner.stop("Analysis failed");
|
|
424
|
+
p.log.error(error.message);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
390
428
|
}
|
|
391
429
|
}
|
|
392
430
|
}
|