@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337
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/CHANGELOG.md +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
1
|
import {
|
|
4
2
|
appendFileSync,
|
|
5
3
|
closeSync,
|
|
@@ -11,16 +9,18 @@ import {
|
|
|
11
9
|
readSync,
|
|
12
10
|
statSync,
|
|
13
11
|
writeFileSync,
|
|
14
|
-
} from "fs";
|
|
15
|
-
import { join, resolve } from "path";
|
|
16
|
-
import {
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
15
|
+
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
16
|
+
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
17
17
|
import {
|
|
18
18
|
type BashExecutionMessage,
|
|
19
19
|
createBranchSummaryMessage,
|
|
20
20
|
createCompactionSummaryMessage,
|
|
21
21
|
createHookMessage,
|
|
22
22
|
type HookMessage,
|
|
23
|
-
} from "./messages
|
|
23
|
+
} from "./messages";
|
|
24
24
|
|
|
25
25
|
export const CURRENT_SESSION_VERSION = 2;
|
|
26
26
|
|
|
@@ -28,6 +28,7 @@ export interface SessionHeader {
|
|
|
28
28
|
type: "session";
|
|
29
29
|
version?: number; // v1 sessions don't have this
|
|
30
30
|
id: string;
|
|
31
|
+
title?: string; // Auto-generated title from first message
|
|
31
32
|
timestamp: string;
|
|
32
33
|
cwd: string;
|
|
33
34
|
parentSession?: string;
|
|
@@ -56,8 +57,10 @@ export interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
|
|
56
57
|
|
|
57
58
|
export interface ModelChangeEntry extends SessionEntryBase {
|
|
58
59
|
type: "model_change";
|
|
59
|
-
provider
|
|
60
|
-
|
|
60
|
+
/** Model in "provider/modelId" format */
|
|
61
|
+
model: string;
|
|
62
|
+
/** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
|
|
63
|
+
role?: string;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
@@ -149,12 +152,14 @@ export interface SessionTreeNode {
|
|
|
149
152
|
export interface SessionContext {
|
|
150
153
|
messages: AgentMessage[];
|
|
151
154
|
thinkingLevel: string;
|
|
152
|
-
|
|
155
|
+
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
156
|
+
models: Record<string, string>;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
export interface SessionInfo {
|
|
156
160
|
path: string;
|
|
157
161
|
id: string;
|
|
162
|
+
title?: string;
|
|
158
163
|
created: Date;
|
|
159
164
|
modified: Date;
|
|
160
165
|
messageCount: number;
|
|
@@ -290,7 +295,7 @@ export function buildSessionContext(
|
|
|
290
295
|
let leaf: SessionEntry | undefined;
|
|
291
296
|
if (leafId === null) {
|
|
292
297
|
// Explicitly null - return no messages (navigated to before first entry)
|
|
293
|
-
return { messages: [], thinkingLevel: "off",
|
|
298
|
+
return { messages: [], thinkingLevel: "off", models: {} };
|
|
294
299
|
}
|
|
295
300
|
if (leafId) {
|
|
296
301
|
leaf = byId.get(leafId);
|
|
@@ -301,7 +306,7 @@ export function buildSessionContext(
|
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
if (!leaf) {
|
|
304
|
-
return { messages: [], thinkingLevel: "off",
|
|
309
|
+
return { messages: [], thinkingLevel: "off", models: {} };
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
// Walk from leaf to root, collecting path
|
|
@@ -314,16 +319,21 @@ export function buildSessionContext(
|
|
|
314
319
|
|
|
315
320
|
// Extract settings and find compaction
|
|
316
321
|
let thinkingLevel = "off";
|
|
317
|
-
|
|
322
|
+
const models: Record<string, string> = {};
|
|
318
323
|
let compaction: CompactionEntry | null = null;
|
|
319
324
|
|
|
320
325
|
for (const entry of path) {
|
|
321
326
|
if (entry.type === "thinking_level_change") {
|
|
322
327
|
thinkingLevel = entry.thinkingLevel;
|
|
323
328
|
} else if (entry.type === "model_change") {
|
|
324
|
-
|
|
329
|
+
// New format: { model: "provider/id", role?: string }
|
|
330
|
+
if (entry.model) {
|
|
331
|
+
const role = entry.role ?? "default";
|
|
332
|
+
models[role] = entry.model;
|
|
333
|
+
}
|
|
325
334
|
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
326
|
-
|
|
335
|
+
// Infer default model from assistant messages
|
|
336
|
+
models.default = `${entry.message.provider}/${entry.message.model}`;
|
|
327
337
|
} else if (entry.type === "compaction") {
|
|
328
338
|
compaction = entry;
|
|
329
339
|
}
|
|
@@ -379,7 +389,7 @@ export function buildSessionContext(
|
|
|
379
389
|
}
|
|
380
390
|
}
|
|
381
391
|
|
|
382
|
-
return { messages, thinkingLevel,
|
|
392
|
+
return { messages, thinkingLevel, models };
|
|
383
393
|
}
|
|
384
394
|
|
|
385
395
|
/**
|
|
@@ -454,6 +464,67 @@ export function findMostRecentSession(sessionDir: string): string | null {
|
|
|
454
464
|
}
|
|
455
465
|
}
|
|
456
466
|
|
|
467
|
+
/** Recent session info for display */
|
|
468
|
+
export interface RecentSessionInfo {
|
|
469
|
+
name: string;
|
|
470
|
+
path: string;
|
|
471
|
+
timeAgo: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Format a time difference as a human-readable string */
|
|
475
|
+
function formatTimeAgo(date: Date): string {
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
const diffMs = now - date.getTime();
|
|
478
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
479
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
480
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
481
|
+
|
|
482
|
+
if (diffMins < 1) return "just now";
|
|
483
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
484
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
485
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
486
|
+
return date.toLocaleDateString();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Get recent sessions for display in welcome screen */
|
|
490
|
+
export function getRecentSessions(sessionDir: string, limit = 3): RecentSessionInfo[] {
|
|
491
|
+
try {
|
|
492
|
+
const files = readdirSync(sessionDir)
|
|
493
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
494
|
+
.map((f) => join(sessionDir, f))
|
|
495
|
+
.filter(isValidSessionFile)
|
|
496
|
+
.map((path) => {
|
|
497
|
+
const stat = statSync(path);
|
|
498
|
+
// Try to get session title or id from first line
|
|
499
|
+
let name = path.split("/").pop()?.replace(".jsonl", "") ?? "Unknown";
|
|
500
|
+
try {
|
|
501
|
+
const content = readFileSync(path, "utf-8");
|
|
502
|
+
const firstLine = content.split("\n")[0];
|
|
503
|
+
if (firstLine) {
|
|
504
|
+
const header = JSON.parse(firstLine) as SessionHeader;
|
|
505
|
+
if (header.type === "session") {
|
|
506
|
+
// Prefer title over id
|
|
507
|
+
name = header.title ?? header.id ?? name;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// Use filename as fallback
|
|
512
|
+
}
|
|
513
|
+
return { path, name, mtime: stat.mtime };
|
|
514
|
+
})
|
|
515
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
|
516
|
+
.slice(0, limit);
|
|
517
|
+
|
|
518
|
+
return files.map((f) => ({
|
|
519
|
+
name: f.name.length > 40 ? `${f.name.slice(0, 37)}...` : f.name,
|
|
520
|
+
path: f.path,
|
|
521
|
+
timeAgo: formatTimeAgo(f.mtime),
|
|
522
|
+
}));
|
|
523
|
+
} catch {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
457
528
|
/**
|
|
458
529
|
* Manages conversation sessions as append-only trees stored in JSONL files.
|
|
459
530
|
*
|
|
@@ -467,6 +538,7 @@ export function findMostRecentSession(sessionDir: string): string | null {
|
|
|
467
538
|
*/
|
|
468
539
|
export class SessionManager {
|
|
469
540
|
private sessionId: string = "";
|
|
541
|
+
private sessionTitle: string | undefined;
|
|
470
542
|
private sessionFile: string | undefined;
|
|
471
543
|
private sessionDir: string;
|
|
472
544
|
private cwd: string;
|
|
@@ -499,6 +571,7 @@ export class SessionManager {
|
|
|
499
571
|
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
|
500
572
|
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
|
|
501
573
|
this.sessionId = header?.id ?? crypto.randomUUID();
|
|
574
|
+
this.sessionTitle = header?.title;
|
|
502
575
|
|
|
503
576
|
if (migrateToCurrentVersion(this.fileEntries)) {
|
|
504
577
|
this._rewriteFile();
|
|
@@ -579,6 +652,31 @@ export class SessionManager {
|
|
|
579
652
|
return this.sessionFile;
|
|
580
653
|
}
|
|
581
654
|
|
|
655
|
+
getSessionTitle(): string | undefined {
|
|
656
|
+
return this.sessionTitle;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
setSessionTitle(title: string): void {
|
|
660
|
+
this.sessionTitle = title;
|
|
661
|
+
// Update the session file header with the title
|
|
662
|
+
if (this.persist && this.sessionFile && existsSync(this.sessionFile)) {
|
|
663
|
+
try {
|
|
664
|
+
const content = readFileSync(this.sessionFile, "utf-8");
|
|
665
|
+
const lines = content.split("\n");
|
|
666
|
+
if (lines.length > 0) {
|
|
667
|
+
const header = JSON.parse(lines[0]) as SessionHeader;
|
|
668
|
+
if (header.type === "session") {
|
|
669
|
+
header.title = title;
|
|
670
|
+
lines[0] = JSON.stringify(header);
|
|
671
|
+
writeFileSync(this.sessionFile, lines.join("\n"));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
// Ignore errors updating title
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
582
680
|
_persist(entry: SessionEntry): void {
|
|
583
681
|
if (!this.persist || !this.sessionFile) return;
|
|
584
682
|
|
|
@@ -633,15 +731,19 @@ export class SessionManager {
|
|
|
633
731
|
return entry.id;
|
|
634
732
|
}
|
|
635
733
|
|
|
636
|
-
/**
|
|
637
|
-
|
|
734
|
+
/**
|
|
735
|
+
* Append a model change as child of current leaf, then advance leaf. Returns entry id.
|
|
736
|
+
* @param model Model in "provider/modelId" format
|
|
737
|
+
* @param role Optional role (default: "default")
|
|
738
|
+
*/
|
|
739
|
+
appendModelChange(model: string, role?: string): string {
|
|
638
740
|
const entry: ModelChangeEntry = {
|
|
639
741
|
type: "model_change",
|
|
640
742
|
id: generateId(this.byId),
|
|
641
743
|
parentId: this.leafId,
|
|
642
744
|
timestamp: new Date().toISOString(),
|
|
643
|
-
|
|
644
|
-
|
|
745
|
+
model,
|
|
746
|
+
role,
|
|
645
747
|
};
|
|
646
748
|
this._appendEntry(entry);
|
|
647
749
|
return entry.id;
|
|
@@ -1061,7 +1163,7 @@ export class SessionManager {
|
|
|
1061
1163
|
if (lines.length === 0) continue;
|
|
1062
1164
|
|
|
1063
1165
|
// Check first line for valid session header
|
|
1064
|
-
let header: { type: string; id: string; timestamp: string } | null = null;
|
|
1166
|
+
let header: { type: string; id: string; title?: string; timestamp: string } | null = null;
|
|
1065
1167
|
try {
|
|
1066
1168
|
const first = JSON.parse(lines[0]);
|
|
1067
1169
|
if (first.type === "session" && first.id) {
|
|
@@ -1107,6 +1209,7 @@ export class SessionManager {
|
|
|
1107
1209
|
sessions.push({
|
|
1108
1210
|
path: file,
|
|
1109
1211
|
id: header.id,
|
|
1212
|
+
title: header.title,
|
|
1110
1213
|
created: new Date(header.timestamp),
|
|
1111
1214
|
modified: stats.mtime,
|
|
1112
1215
|
messageCount,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import { dirname, join } from "path";
|
|
3
|
-
import { CONFIG_DIR_NAME, getAgentDir } from "../config
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "../config";
|
|
4
4
|
|
|
5
5
|
export interface CompactionSettings {
|
|
6
6
|
enabled?: boolean; // default: true
|
|
@@ -30,6 +30,11 @@ export interface SkillsSettings {
|
|
|
30
30
|
includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface CommandsSettings {
|
|
34
|
+
enableClaudeUser?: boolean; // default: true (load from ~/.claude/commands/)
|
|
35
|
+
enableClaudeProject?: boolean; // default: true (load from .claude/commands/)
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
export interface TerminalSettings {
|
|
34
39
|
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
|
35
40
|
}
|
|
@@ -51,12 +56,23 @@ export interface MCPSettings {
|
|
|
51
56
|
enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
export interface LspSettings {
|
|
60
|
+
formatOnWrite?: boolean; // default: true (format files using LSP after write tool writes code files)
|
|
61
|
+
diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
|
|
62
|
+
diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EditSettings {
|
|
66
|
+
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
export interface Settings {
|
|
55
70
|
lastChangelogVersion?: string;
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
/** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
72
|
+
modelRoles?: Record<string, string>;
|
|
58
73
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
59
74
|
queueMode?: "all" | "one-at-a-time";
|
|
75
|
+
interruptMode?: "immediate" | "wait";
|
|
60
76
|
theme?: string;
|
|
61
77
|
compaction?: CompactionSettings;
|
|
62
78
|
branchSummary?: BranchSummarySettings;
|
|
@@ -67,11 +83,14 @@ export interface Settings {
|
|
|
67
83
|
hooks?: string[]; // Array of hook file paths
|
|
68
84
|
customTools?: string[]; // Array of custom tool file paths
|
|
69
85
|
skills?: SkillsSettings;
|
|
86
|
+
commands?: CommandsSettings;
|
|
70
87
|
terminal?: TerminalSettings;
|
|
71
88
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
72
89
|
exa?: ExaSettings;
|
|
73
90
|
bashInterceptor?: BashInterceptorSettings;
|
|
74
91
|
mcp?: MCPSettings;
|
|
92
|
+
lsp?: LspSettings;
|
|
93
|
+
edit?: EditSettings;
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -200,28 +219,29 @@ export class SettingsManager {
|
|
|
200
219
|
this.save();
|
|
201
220
|
}
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Get model for a role. Returns "provider/modelId" string or undefined.
|
|
224
|
+
*/
|
|
225
|
+
getModelRole(role: string): string | undefined {
|
|
226
|
+
return this.settings.modelRoles?.[role];
|
|
205
227
|
}
|
|
206
228
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Set model for a role. Model should be "provider/modelId" format.
|
|
231
|
+
*/
|
|
232
|
+
setModelRole(role: string, model: string): void {
|
|
233
|
+
if (!this.globalSettings.modelRoles) {
|
|
234
|
+
this.globalSettings.modelRoles = {};
|
|
235
|
+
}
|
|
236
|
+
this.globalSettings.modelRoles[role] = model;
|
|
213
237
|
this.save();
|
|
214
238
|
}
|
|
215
239
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
setDefaultModelAndProvider(provider: string, modelId: string): void {
|
|
222
|
-
this.globalSettings.defaultProvider = provider;
|
|
223
|
-
this.globalSettings.defaultModel = modelId;
|
|
224
|
-
this.save();
|
|
240
|
+
/**
|
|
241
|
+
* Get all model roles.
|
|
242
|
+
*/
|
|
243
|
+
getModelRoles(): Record<string, string> {
|
|
244
|
+
return { ...this.settings.modelRoles };
|
|
225
245
|
}
|
|
226
246
|
|
|
227
247
|
getQueueMode(): "all" | "one-at-a-time" {
|
|
@@ -233,6 +253,15 @@ export class SettingsManager {
|
|
|
233
253
|
this.save();
|
|
234
254
|
}
|
|
235
255
|
|
|
256
|
+
getInterruptMode(): "immediate" | "wait" {
|
|
257
|
+
return this.settings.interruptMode || "immediate";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setInterruptMode(mode: "immediate" | "wait"): void {
|
|
261
|
+
this.globalSettings.interruptMode = mode;
|
|
262
|
+
this.save();
|
|
263
|
+
}
|
|
264
|
+
|
|
236
265
|
getTheme(): string | undefined {
|
|
237
266
|
return this.settings.theme;
|
|
238
267
|
}
|
|
@@ -376,6 +405,13 @@ export class SettingsManager {
|
|
|
376
405
|
};
|
|
377
406
|
}
|
|
378
407
|
|
|
408
|
+
getCommandsSettings(): Required<CommandsSettings> {
|
|
409
|
+
return {
|
|
410
|
+
enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
|
|
411
|
+
enableClaudeProject: this.settings.commands?.enableClaudeProject ?? true,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
379
415
|
getShowImages(): boolean {
|
|
380
416
|
return this.settings.terminal?.showImages ?? true;
|
|
381
417
|
}
|
|
@@ -474,4 +510,52 @@ export class SettingsManager {
|
|
|
474
510
|
this.globalSettings.mcp.enableProjectConfig = enabled;
|
|
475
511
|
this.save();
|
|
476
512
|
}
|
|
513
|
+
|
|
514
|
+
getLspFormatOnWrite(): boolean {
|
|
515
|
+
return this.settings.lsp?.formatOnWrite ?? true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
setLspFormatOnWrite(enabled: boolean): void {
|
|
519
|
+
if (!this.globalSettings.lsp) {
|
|
520
|
+
this.globalSettings.lsp = {};
|
|
521
|
+
}
|
|
522
|
+
this.globalSettings.lsp.formatOnWrite = enabled;
|
|
523
|
+
this.save();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
getLspDiagnosticsOnWrite(): boolean {
|
|
527
|
+
return this.settings.lsp?.diagnosticsOnWrite ?? true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
setLspDiagnosticsOnWrite(enabled: boolean): void {
|
|
531
|
+
if (!this.globalSettings.lsp) {
|
|
532
|
+
this.globalSettings.lsp = {};
|
|
533
|
+
}
|
|
534
|
+
this.globalSettings.lsp.diagnosticsOnWrite = enabled;
|
|
535
|
+
this.save();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getLspDiagnosticsOnEdit(): boolean {
|
|
539
|
+
return this.settings.lsp?.diagnosticsOnEdit ?? false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setLspDiagnosticsOnEdit(enabled: boolean): void {
|
|
543
|
+
if (!this.globalSettings.lsp) {
|
|
544
|
+
this.globalSettings.lsp = {};
|
|
545
|
+
}
|
|
546
|
+
this.globalSettings.lsp.diagnosticsOnEdit = enabled;
|
|
547
|
+
this.save();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
getEditFuzzyMatch(): boolean {
|
|
551
|
+
return this.settings.edit?.fuzzyMatch ?? true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
setEditFuzzyMatch(enabled: boolean): void {
|
|
555
|
+
if (!this.globalSettings.edit) {
|
|
556
|
+
this.globalSettings.edit = {};
|
|
557
|
+
}
|
|
558
|
+
this.globalSettings.edit.fuzzyMatch = enabled;
|
|
559
|
+
this.save();
|
|
560
|
+
}
|
|
477
561
|
}
|
package/src/core/skills.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
2
4
|
import { minimatch } from "minimatch";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
|
6
|
-
import type { SkillsSettings } from "./settings-manager.js";
|
|
5
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "../config";
|
|
6
|
+
import type { SkillsSettings } from "./settings-manager";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Standard frontmatter fields per Agent Skills spec.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { CONFIG_DIR_NAME, getCommandsDir } from "../config";
|
|
5
|
+
import { logger } from "./logger";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Represents a custom slash command loaded from a file
|
|
@@ -98,14 +100,12 @@ export function substituteArgs(content: string, args: string[]): string {
|
|
|
98
100
|
return result;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
type CommandSource = "builtin" | "claude-user" | "claude-project" | "user" | "project";
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands
|
|
103
107
|
*/
|
|
104
|
-
function loadCommandsFromDir(
|
|
105
|
-
dir: string,
|
|
106
|
-
source: "builtin" | "user" | "project",
|
|
107
|
-
subdir: string = "",
|
|
108
|
-
): FileSlashCommand[] {
|
|
108
|
+
function loadCommandsFromDir(dir: string, source: CommandSource, subdir: string = ""): FileSlashCommand[] {
|
|
109
109
|
const commands: FileSlashCommand[] = [];
|
|
110
110
|
|
|
111
111
|
if (!existsSync(dir)) {
|
|
@@ -129,15 +129,18 @@ function loadCommandsFromDir(
|
|
|
129
129
|
|
|
130
130
|
const name = entry.name.slice(0, -3); // Remove .md extension
|
|
131
131
|
|
|
132
|
-
// Build source string
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
// Build source string based on source type
|
|
133
|
+
const sourceLabel =
|
|
134
|
+
source === "builtin"
|
|
135
|
+
? "builtin"
|
|
136
|
+
: source === "claude-user"
|
|
137
|
+
? "claude-user"
|
|
138
|
+
: source === "claude-project"
|
|
139
|
+
? "claude-project"
|
|
140
|
+
: source === "user"
|
|
141
|
+
? "user"
|
|
142
|
+
: "project";
|
|
143
|
+
const sourceStr = subdir ? `(${sourceLabel}:${subdir})` : `(${sourceLabel})`;
|
|
141
144
|
|
|
142
145
|
// Get description from frontmatter or first non-empty line
|
|
143
146
|
let description = frontmatter.description || "";
|
|
@@ -159,13 +162,13 @@ function loadCommandsFromDir(
|
|
|
159
162
|
content,
|
|
160
163
|
source: sourceStr,
|
|
161
164
|
});
|
|
162
|
-
} catch (
|
|
163
|
-
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.debug("Failed to read slash command file", { error: String(err) });
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
|
-
} catch (
|
|
168
|
-
|
|
170
|
+
} catch (err) {
|
|
171
|
+
logger.debug("Failed to read slash command directory", { error: String(err) });
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
return commands;
|
|
@@ -176,54 +179,66 @@ export interface LoadSlashCommandsOptions {
|
|
|
176
179
|
cwd?: string;
|
|
177
180
|
/** Agent config directory for global commands. Default: from getCommandsDir() */
|
|
178
181
|
agentDir?: string;
|
|
182
|
+
/** Enable loading from ~/.claude/commands/. Default: true */
|
|
183
|
+
enableClaudeUser?: boolean;
|
|
184
|
+
/** Enable loading from .claude/commands/. Default: true */
|
|
185
|
+
enableClaudeProject?: boolean;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
/**
|
|
182
189
|
* Load all custom slash commands from:
|
|
183
190
|
* 1. Builtin: package commands/
|
|
184
|
-
* 2.
|
|
185
|
-
* 3.
|
|
191
|
+
* 2. Claude user: ~/.claude/commands/
|
|
192
|
+
* 3. Claude project: .claude/commands/
|
|
193
|
+
* 4. Pi user: agentDir/commands/
|
|
194
|
+
* 5. Pi project: cwd/{CONFIG_DIR_NAME}/commands/
|
|
195
|
+
*
|
|
196
|
+
* First occurrence wins (earlier sources have priority).
|
|
186
197
|
*/
|
|
187
198
|
export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
|
|
188
199
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
189
200
|
const resolvedAgentDir = options.agentDir ?? getCommandsDir();
|
|
201
|
+
const enableClaudeUser = options.enableClaudeUser ?? true;
|
|
202
|
+
const enableClaudeProject = options.enableClaudeProject ?? true;
|
|
190
203
|
|
|
191
204
|
const commands: FileSlashCommand[] = [];
|
|
192
205
|
const seenNames = new Set<string>();
|
|
193
206
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (existsSync(builtinDir)) {
|
|
197
|
-
const builtinCommands = loadCommandsFromDir(builtinDir, "builtin");
|
|
198
|
-
for (const cmd of builtinCommands) {
|
|
207
|
+
const addCommands = (newCommands: FileSlashCommand[]) => {
|
|
208
|
+
for (const cmd of newCommands) {
|
|
199
209
|
if (!seenNames.has(cmd.name)) {
|
|
200
210
|
commands.push(cmd);
|
|
201
211
|
seenNames.add(cmd.name);
|
|
202
212
|
}
|
|
203
213
|
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 1. Builtin commands (from package)
|
|
217
|
+
const builtinDir = join(import.meta.dir, "../commands");
|
|
218
|
+
if (existsSync(builtinDir)) {
|
|
219
|
+
addCommands(loadCommandsFromDir(builtinDir, "builtin"));
|
|
204
220
|
}
|
|
205
221
|
|
|
206
|
-
// 2.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
for (const cmd of globalCommands) {
|
|
211
|
-
if (!seenNames.has(cmd.name)) {
|
|
212
|
-
commands.push(cmd);
|
|
213
|
-
seenNames.add(cmd.name);
|
|
214
|
-
}
|
|
222
|
+
// 2. Claude user commands (~/.claude/commands/)
|
|
223
|
+
if (enableClaudeUser) {
|
|
224
|
+
const claudeUserDir = join(homedir(), ".claude", "commands");
|
|
225
|
+
addCommands(loadCommandsFromDir(claudeUserDir, "claude-user"));
|
|
215
226
|
}
|
|
216
227
|
|
|
217
|
-
// 3.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!seenNames.has(cmd.name)) {
|
|
222
|
-
commands.push(cmd);
|
|
223
|
-
seenNames.add(cmd.name);
|
|
224
|
-
}
|
|
228
|
+
// 3. Claude project commands (.claude/commands/)
|
|
229
|
+
if (enableClaudeProject) {
|
|
230
|
+
const claudeProjectDir = resolve(resolvedCwd, ".claude", "commands");
|
|
231
|
+
addCommands(loadCommandsFromDir(claudeProjectDir, "claude-project"));
|
|
225
232
|
}
|
|
226
233
|
|
|
234
|
+
// 4. Pi user commands (agentDir/commands/)
|
|
235
|
+
const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
|
|
236
|
+
addCommands(loadCommandsFromDir(globalCommandsDir, "user"));
|
|
237
|
+
|
|
238
|
+
// 5. Pi project commands (cwd/{CONFIG_DIR_NAME}/commands/)
|
|
239
|
+
const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
|
|
240
|
+
addCommands(loadCommandsFromDir(projectCommandsDir, "project"));
|
|
241
|
+
|
|
227
242
|
return commands;
|
|
228
243
|
}
|
|
229
244
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* System prompt construction and project context loading
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
5
7
|
import chalk from "chalk";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
|
|
11
|
-
import type { ToolName } from "./tools/index.js";
|
|
8
|
+
import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config";
|
|
9
|
+
import type { SkillsSettings } from "./settings-manager";
|
|
10
|
+
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
|
|
11
|
+
import type { ToolName } from "./tools/index";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Execute a git command synchronously and return stdout or null on failure.
|