@oh-my-pi/pi-coding-agent 13.18.0 → 14.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/CHANGELOG.md +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
// ── Callback types ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ResolveInfo {
|
|
6
|
+
/** The ToolChoice that was served to the LLM. */
|
|
7
|
+
choice: ToolChoice;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RejectInfo {
|
|
11
|
+
/** The ToolChoice that was yielded but never (or unsuccessfully) served. */
|
|
12
|
+
choice: ToolChoice;
|
|
13
|
+
reason: "aborted" | "error" | "cleared" | "removed";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** "requeue" replays the lost yield next turn; "drop" (or void/undefined) discards it. */
|
|
17
|
+
export type RejectOutcome = "requeue" | "drop";
|
|
18
|
+
|
|
19
|
+
export interface DirectiveCallbacks {
|
|
20
|
+
/** Fires when the yield was served (LLM call completed). The directive is consumed. */
|
|
21
|
+
onResolved?: (info: ResolveInfo) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Fires when the yield is being discarded. Return "requeue" to replay the
|
|
24
|
+
* same value at the head of the queue for the next turn. Default: "drop".
|
|
25
|
+
*/
|
|
26
|
+
onRejected?: (info: RejectInfo) => RejectOutcome | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Handler invoked when the model actually calls the forced tool. The queue
|
|
29
|
+
* directive carries the real execution logic; the tool's own execute() is
|
|
30
|
+
* bypassed. Returns the tool result directly.
|
|
31
|
+
*/
|
|
32
|
+
onInvoked?: (input: unknown) => Promise<unknown> | unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Directive ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface ToolChoiceDirective {
|
|
38
|
+
generator: Iterator<ToolChoice>;
|
|
39
|
+
/** Stable label for targeted removal and debugging (e.g. "user-force"). */
|
|
40
|
+
label: string;
|
|
41
|
+
callbacks: DirectiveCallbacks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PushOptions {
|
|
45
|
+
/** Prepend to head instead of appending to tail. Default: false. */
|
|
46
|
+
now?: boolean;
|
|
47
|
+
label?: string;
|
|
48
|
+
/** Lifecycle callbacks for this directive. */
|
|
49
|
+
onResolved?: DirectiveCallbacks["onResolved"];
|
|
50
|
+
onRejected?: DirectiveCallbacks["onRejected"];
|
|
51
|
+
onInvoked?: DirectiveCallbacks["onInvoked"];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Generators ──────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function* onceGen(choice: ToolChoice): Generator<ToolChoice, void, unknown> {
|
|
57
|
+
yield choice;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── In-flight state ─────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
interface InFlight {
|
|
63
|
+
directive: ToolChoiceDirective;
|
|
64
|
+
yielded: ToolChoice;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Queue ───────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export class ToolChoiceQueue {
|
|
70
|
+
#queue: ToolChoiceDirective[] = [];
|
|
71
|
+
#inFlight: InFlight | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Label of the directive whose last yield was resolved this turn.
|
|
74
|
+
* Consumers (e.g. todo reminder suppression) read via consumeLastServedLabel().
|
|
75
|
+
*/
|
|
76
|
+
#lastResolvedLabel: string | undefined;
|
|
77
|
+
|
|
78
|
+
// ── Push ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
pushOnce(choice: ToolChoice, options?: PushOptions): void {
|
|
81
|
+
this.push(onceGen(choice), options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pushSequence(choices: ToolChoice[], options?: PushOptions): void {
|
|
85
|
+
this.push(choices, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
push(generator: Iterable<ToolChoice>, options?: PushOptions): void {
|
|
89
|
+
const directive: ToolChoiceDirective = {
|
|
90
|
+
generator: generator[Symbol.iterator](),
|
|
91
|
+
label: options?.label ?? "anonymous",
|
|
92
|
+
callbacks: {
|
|
93
|
+
onResolved: options?.onResolved,
|
|
94
|
+
onRejected: options?.onRejected,
|
|
95
|
+
onInvoked: options?.onInvoked,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
if (options?.now) {
|
|
99
|
+
this.#queue.unshift(directive);
|
|
100
|
+
} else {
|
|
101
|
+
this.#queue.push(directive);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Consume ───────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Advance the head directive and return its next yield. Records the value
|
|
109
|
+
* as in-flight until resolve() or reject() is called.
|
|
110
|
+
*/
|
|
111
|
+
nextToolChoice(): ToolChoice | undefined {
|
|
112
|
+
while (this.#queue.length > 0) {
|
|
113
|
+
const head = this.#queue[0]!;
|
|
114
|
+
const result = head.generator.next();
|
|
115
|
+
if (result.done) {
|
|
116
|
+
this.#queue.shift();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
this.#inFlight = { directive: head, yielded: result.value };
|
|
120
|
+
return result.value;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The in-flight yield was served — the LLM call completed normally.
|
|
129
|
+
* Fires onResolved, then clears in-flight state. The directive's generator
|
|
130
|
+
* remains in the queue if it has more values to yield.
|
|
131
|
+
*/
|
|
132
|
+
resolve(): void {
|
|
133
|
+
const inFlight = this.#inFlight;
|
|
134
|
+
this.#inFlight = undefined;
|
|
135
|
+
if (!inFlight) return;
|
|
136
|
+
|
|
137
|
+
this.#lastResolvedLabel = inFlight.directive.label;
|
|
138
|
+
inFlight.directive.callbacks.onResolved?.({ choice: inFlight.yielded });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The in-flight yield was not served, or the turn aborted/errored.
|
|
143
|
+
* Fires onRejected to let the caller decide: "requeue" replays the exact
|
|
144
|
+
* lost value at the head of the queue; anything else drops it.
|
|
145
|
+
*/
|
|
146
|
+
reject(reason: RejectInfo["reason"]): void {
|
|
147
|
+
const inFlight = this.#inFlight;
|
|
148
|
+
this.#inFlight = undefined;
|
|
149
|
+
if (!inFlight) return;
|
|
150
|
+
|
|
151
|
+
const outcome = inFlight.directive.callbacks.onRejected?.({
|
|
152
|
+
choice: inFlight.yielded,
|
|
153
|
+
reason,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (outcome === "requeue") {
|
|
157
|
+
// Re-queue only the lost yield, not the rest of the sequence. Carry forward
|
|
158
|
+
// onInvoked and onRejected so the replayed yield still executes correctly
|
|
159
|
+
// and can requeue itself again if the next turn also aborts.
|
|
160
|
+
this.#queue.unshift({
|
|
161
|
+
generator: onceGen(inFlight.yielded),
|
|
162
|
+
label: `${inFlight.directive.label}-requeued`,
|
|
163
|
+
callbacks: {
|
|
164
|
+
onInvoked: inFlight.directive.callbacks.onInvoked,
|
|
165
|
+
onRejected: inFlight.directive.callbacks.onRejected,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** True if there is an in-flight yield that hasn't been resolved or rejected. */
|
|
172
|
+
get hasInFlight(): boolean {
|
|
173
|
+
return this.#inFlight !== undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Peek the in-flight directive's onInvoked handler, if any. */
|
|
177
|
+
peekInFlightInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
|
|
178
|
+
return this.#inFlight?.directive.callbacks.onInvoked;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/** Remove all directives with the given label. Rejects in-flight if it matches. */
|
|
184
|
+
removeByLabel(label: string): void {
|
|
185
|
+
if (this.#inFlight?.directive.label === label) {
|
|
186
|
+
this.reject("removed");
|
|
187
|
+
}
|
|
188
|
+
this.#queue = this.#queue.filter(d => d.label !== label);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Empty the queue and reject any in-flight yield. */
|
|
192
|
+
clear(): void {
|
|
193
|
+
if (this.#inFlight) {
|
|
194
|
+
this.reject("cleared");
|
|
195
|
+
}
|
|
196
|
+
this.#queue = [];
|
|
197
|
+
this.#lastResolvedLabel = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Observation ───────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/** Return the label of the most recently resolved directive, then clear it. */
|
|
203
|
+
consumeLastServedLabel(): string | undefined {
|
|
204
|
+
const label = this.#lastResolvedLabel;
|
|
205
|
+
this.#lastResolvedLabel = undefined;
|
|
206
|
+
return label;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** For tests/debug: labels of currently queued directives in order. */
|
|
210
|
+
inspect(): readonly string[] {
|
|
211
|
+
return this.#queue.map(d => d.label);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -55,7 +55,15 @@ interface ParsedBuiltinSlashCommand {
|
|
|
55
55
|
interface BuiltinSlashCommandSpec extends BuiltinSlashCommand {
|
|
56
56
|
aliases?: string[];
|
|
57
57
|
allowArgs?: boolean;
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Handle the command. Return a string to pass remaining text through as prompt input.
|
|
60
|
+
* Return void/undefined to consume the input entirely.
|
|
61
|
+
*/
|
|
62
|
+
handle: (
|
|
63
|
+
command: ParsedBuiltinSlashCommand,
|
|
64
|
+
runtime: BuiltinSlashCommandRuntime,
|
|
65
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: void needed so handlers returning nothing are assignable
|
|
66
|
+
) => Promise<string | undefined> | string | void;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
export interface BuiltinSlashCommandRuntime {
|
|
@@ -69,7 +77,11 @@ function parseBuiltinSlashCommand(text: string): ParsedBuiltinSlashCommand | nul
|
|
|
69
77
|
if (!body) return null;
|
|
70
78
|
|
|
71
79
|
const firstWhitespace = body.search(/\s/);
|
|
72
|
-
|
|
80
|
+
const firstColon = body.indexOf(":");
|
|
81
|
+
const firstSeparator =
|
|
82
|
+
firstWhitespace === -1 ? firstColon : firstColon === -1 ? firstWhitespace : Math.min(firstWhitespace, firstColon);
|
|
83
|
+
|
|
84
|
+
if (firstSeparator === -1) {
|
|
73
85
|
return {
|
|
74
86
|
name: body,
|
|
75
87
|
args: "",
|
|
@@ -78,8 +90,8 @@ function parseBuiltinSlashCommand(text: string): ParsedBuiltinSlashCommand | nul
|
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
return {
|
|
81
|
-
name: body.slice(0,
|
|
82
|
-
args: body.slice(
|
|
93
|
+
name: body.slice(0, firstSeparator),
|
|
94
|
+
args: body.slice(firstSeparator + 1).trim(),
|
|
83
95
|
text,
|
|
84
96
|
};
|
|
85
97
|
}
|
|
@@ -888,6 +900,37 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
888
900
|
runtime.ctx.editor.setText("");
|
|
889
901
|
},
|
|
890
902
|
},
|
|
903
|
+
{
|
|
904
|
+
name: "force",
|
|
905
|
+
description: "Force next turn to use a specific tool",
|
|
906
|
+
inlineHint: "<tool-name> [prompt]",
|
|
907
|
+
allowArgs: true,
|
|
908
|
+
handle: (command, runtime) => {
|
|
909
|
+
const spaceIdx = command.args.indexOf(" ");
|
|
910
|
+
const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
|
|
911
|
+
const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
|
|
912
|
+
|
|
913
|
+
if (!toolName) {
|
|
914
|
+
runtime.ctx.showError("Usage: /force:<tool-name> [prompt]");
|
|
915
|
+
runtime.ctx.editor.setText("");
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
try {
|
|
920
|
+
runtime.ctx.session.setForcedToolChoice(toolName);
|
|
921
|
+
runtime.ctx.showStatus(`Next turn forced to use ${toolName}.`);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
runtime.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
924
|
+
runtime.ctx.editor.setText("");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
runtime.ctx.editor.setText("");
|
|
929
|
+
|
|
930
|
+
// If a prompt was provided, pass it through as input
|
|
931
|
+
if (prompt) return prompt;
|
|
932
|
+
},
|
|
933
|
+
},
|
|
891
934
|
{
|
|
892
935
|
name: "quit",
|
|
893
936
|
description: "Quit the application",
|
|
@@ -916,9 +959,14 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
|
|
|
916
959
|
/**
|
|
917
960
|
* Execute a builtin slash command when it matches known command syntax.
|
|
918
961
|
*
|
|
919
|
-
* Returns
|
|
962
|
+
* Returns `false` when no builtin matched. Returns `true` when a command consumed
|
|
963
|
+
* the input entirely. Returns a `string` when the command was handled but remaining
|
|
964
|
+
* text should be sent as a prompt.
|
|
920
965
|
*/
|
|
921
|
-
export async function executeBuiltinSlashCommand(
|
|
966
|
+
export async function executeBuiltinSlashCommand(
|
|
967
|
+
text: string,
|
|
968
|
+
runtime: BuiltinSlashCommandRuntime,
|
|
969
|
+
): Promise<string | boolean> {
|
|
922
970
|
const parsed = parseBuiltinSlashCommand(text);
|
|
923
971
|
if (!parsed) return false;
|
|
924
972
|
|
|
@@ -928,6 +976,6 @@ export async function executeBuiltinSlashCommand(text: string, runtime: BuiltinS
|
|
|
928
976
|
return false;
|
|
929
977
|
}
|
|
930
978
|
|
|
931
|
-
await command.handle(parsed, runtime);
|
|
932
|
-
return true;
|
|
979
|
+
const remaining = await command.handle(parsed, runtime);
|
|
980
|
+
return remaining ?? true;
|
|
933
981
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { $which, getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { $ } from "bun";
|
|
5
5
|
import { buildSshTarget, sanitizeHostName } from "./utils";
|
|
6
6
|
|
|
@@ -106,7 +106,7 @@ async function runSshCaptureSync(args: string[]): Promise<{ exitCode: number | n
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function ensureSshBinary(): void {
|
|
109
|
-
if (
|
|
109
|
+
if (!$which("ssh")) {
|
|
110
110
|
throw new Error("ssh binary not found on PATH");
|
|
111
111
|
}
|
|
112
112
|
}
|
package/src/ssh/sshfs-mount.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { $ } from "bun";
|
|
5
5
|
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
|
|
6
6
|
import { buildSshTarget, sanitizeHostName } from "./utils";
|
|
@@ -60,24 +60,24 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
async function unmountPath(path: string): Promise<boolean> {
|
|
63
|
-
const fusermount =
|
|
63
|
+
const fusermount = $which("fusermount") ?? $which("fusermount3");
|
|
64
64
|
if (fusermount) {
|
|
65
65
|
const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
|
|
66
66
|
if (result.exitCode === 0) return true;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const umount =
|
|
69
|
+
const umount = $which("umount");
|
|
70
70
|
if (!umount) return false;
|
|
71
71
|
const result = await $`${umount} ${path}`.quiet().nothrow();
|
|
72
72
|
return result.exitCode === 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function hasSshfs(): boolean {
|
|
76
|
-
return
|
|
76
|
+
return $which("sshfs") !== null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export async function isMounted(path: string): Promise<boolean> {
|
|
80
|
-
const mountpoint =
|
|
80
|
+
const mountpoint = $which("mountpoint");
|
|
81
81
|
if (!mountpoint) return false;
|
|
82
82
|
const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
|
|
83
83
|
return result.exitCode === 0;
|
package/src/stt/downloader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $which, logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { resolvePython } from "./transcriber";
|
|
4
4
|
|
|
@@ -15,9 +15,9 @@ export interface EnsureOptions {
|
|
|
15
15
|
// ── Recording tool ─────────────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
|
|
18
|
-
if (
|
|
19
|
-
if (
|
|
20
|
-
if (process.platform === "linux" &&
|
|
18
|
+
if ($which("sox")) return;
|
|
19
|
+
if ($which("ffmpeg")) return;
|
|
20
|
+
if (process.platform === "linux" && $which("arecord")) return;
|
|
21
21
|
|
|
22
22
|
// Windows: PowerShell mciSendString is always available as fallback
|
|
23
23
|
if (process.platform === "win32") {
|
package/src/stt/recorder.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $which, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { $ } from "bun";
|
|
6
6
|
|
|
7
7
|
export interface RecordingHandle {
|
|
@@ -15,9 +15,9 @@ const isWindows = process.platform === "win32";
|
|
|
15
15
|
*/
|
|
16
16
|
export function detectRecordingTools(): string[] {
|
|
17
17
|
const tools: string[] = [];
|
|
18
|
-
if (
|
|
19
|
-
if (
|
|
20
|
-
if (!isWindows &&
|
|
18
|
+
if ($which("sox")) tools.push("sox");
|
|
19
|
+
if ($which("ffmpeg")) tools.push("ffmpeg");
|
|
20
|
+
if (!isWindows && $which("arecord")) tools.push("arecord");
|
|
21
21
|
if (isWindows) tools.push("powershell");
|
|
22
22
|
return tools;
|
|
23
23
|
}
|
package/src/stt/transcriber.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $which, logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import transcribeScript from "./transcribe.py" with { type: "text" };
|
|
3
3
|
|
|
4
4
|
export interface TranscribeOptions {
|
|
@@ -14,7 +14,7 @@ const TRANSCRIBE_TIMEOUT_MS = 120_000;
|
|
|
14
14
|
*/
|
|
15
15
|
export function resolvePython(): string | null {
|
|
16
16
|
for (const cmd of ["python", "py", "python3"]) {
|
|
17
|
-
if (
|
|
17
|
+
if ($which(cmd)) return cmd;
|
|
18
18
|
}
|
|
19
19
|
return null;
|
|
20
20
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -6,17 +6,15 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { $ } from "bun";
|
|
11
11
|
import { contextFileCapability } from "./capability/context-file";
|
|
12
12
|
import { systemPromptCapability } from "./capability/system-prompt";
|
|
13
|
-
import { renderPromptTemplate } from "./config/prompt-templates";
|
|
14
13
|
import type { SkillsSettings } from "./config/settings";
|
|
15
14
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
16
15
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
17
16
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
18
17
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
19
|
-
import { formatPromptContent } from "./utils/prompt-format";
|
|
20
18
|
|
|
21
19
|
interface AlwaysApplyRule {
|
|
22
20
|
name: string;
|
|
@@ -25,7 +23,7 @@ interface AlwaysApplyRule {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
function normalizePromptBlock(content: string): string {
|
|
28
|
-
return
|
|
26
|
+
return prompt.format(content, { renderPhase: "post-render" }).trim();
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
function splitComparablePromptBlocks(content: string | null | undefined): string[] {
|
|
@@ -267,14 +265,16 @@ async function saveGpuCache(info: GpuCache): Promise<void> {
|
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
async function getCachedGpu(): Promise<string | undefined> {
|
|
270
|
-
const cached = await logger.
|
|
268
|
+
const cached = await logger.time("getCachedGpu:loadGpuCache", loadGpuCache);
|
|
271
269
|
if (cached) return cached.gpu;
|
|
272
|
-
const gpu = await logger.
|
|
273
|
-
if (gpu)
|
|
270
|
+
const gpu = await logger.time("getCachedGpu:getGpuModel", getGpuModel);
|
|
271
|
+
if (gpu) {
|
|
272
|
+
await logger.time("getCachedGpu:saveGpuCache", saveGpuCache, { gpu });
|
|
273
|
+
}
|
|
274
274
|
return gpu ?? undefined;
|
|
275
275
|
}
|
|
276
276
|
async function getEnvironmentInfo(): Promise<Array<{ label: string; value: string }>> {
|
|
277
|
-
const gpu = await
|
|
277
|
+
const gpu = await getCachedGpu();
|
|
278
278
|
const cpus = os.cpus();
|
|
279
279
|
const entries: Array<{ label: string; value: string | undefined }> = [
|
|
280
280
|
{ label: "OS", value: `${os.platform()} ${os.release()}` },
|
|
@@ -432,6 +432,8 @@ export interface BuildSystemPromptOptions {
|
|
|
432
432
|
eagerTasks?: boolean;
|
|
433
433
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
434
434
|
alwaysApplyRules?: AlwaysApplyRule[];
|
|
435
|
+
/** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
|
|
436
|
+
secretsEnabled?: boolean;
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -456,17 +458,18 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
456
458
|
mcpDiscoveryMode = false,
|
|
457
459
|
mcpDiscoveryServerSummaries = [],
|
|
458
460
|
eagerTasks = false,
|
|
461
|
+
secretsEnabled = false,
|
|
459
462
|
} = options;
|
|
460
463
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
461
464
|
|
|
462
465
|
const prepPromise = (() => {
|
|
463
|
-
const systemPromptCustomizationPromise = logger.
|
|
466
|
+
const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
|
|
464
467
|
cwd: resolvedCwd,
|
|
465
468
|
});
|
|
466
469
|
const contextFilesPromise = providedContextFiles
|
|
467
470
|
? Promise.resolve(providedContextFiles)
|
|
468
|
-
: logger.
|
|
469
|
-
const agentsMdSearchPromise = logger.
|
|
471
|
+
: logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
|
|
472
|
+
const agentsMdSearchPromise = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
|
|
470
473
|
const skillsPromise: Promise<Skill[]> =
|
|
471
474
|
providedSkills !== undefined
|
|
472
475
|
? Promise.resolve(providedSkills)
|
|
@@ -580,7 +583,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
580
583
|
const promptSources = [effectiveSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
|
|
581
584
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
582
585
|
|
|
583
|
-
const environment = await logger.
|
|
586
|
+
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
|
584
587
|
const data = {
|
|
585
588
|
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
586
589
|
customPrompt: resolvedCustomPrompt,
|
|
@@ -603,6 +606,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
603
606
|
hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
|
|
604
607
|
mcpDiscoveryServerSummaries,
|
|
605
608
|
eagerTasks,
|
|
609
|
+
secretsEnabled,
|
|
606
610
|
};
|
|
607
|
-
|
|
611
|
+
let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
612
|
+
|
|
613
|
+
// When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
|
|
614
|
+
if (toolNames.includes("report_tool_issue")) {
|
|
615
|
+
rendered +=
|
|
616
|
+
"\n\n<critical>\nThe `report_tool_issue` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `report_tool_issue` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return rendered;
|
|
608
620
|
}
|
package/src/task/agents.ts
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* Agents are embedded at build time via Bun's import with { type: "text" }.
|
|
5
5
|
*/
|
|
6
6
|
import { Effort } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import {
|
|
7
|
+
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { parseAgentFields } from "../discovery/helpers";
|
|
9
9
|
import designerMd from "../prompts/agents/designer.md" with { type: "text" };
|
|
10
10
|
import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
|
|
11
11
|
// Embed agent markdown files at build time
|
|
12
12
|
import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
|
|
13
13
|
import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
import planMd from "../prompts/agents/plan.md" with { type: "text" };
|
|
16
16
|
import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
|
|
17
17
|
import taskMd from "../prompts/agents/task.md" with { type: "text" };
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
import type { AgentDefinition, AgentSource } from "./types";
|
|
20
20
|
|
|
21
21
|
interface AgentFrontmatter {
|
|
@@ -35,9 +35,9 @@ interface EmbeddedAgentDef {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function buildAgentContent(def: EmbeddedAgentDef): string {
|
|
38
|
-
const body =
|
|
38
|
+
const body = prompt.render(def.template);
|
|
39
39
|
if (!def.frontmatter) return body;
|
|
40
|
-
return
|
|
40
|
+
return prompt.render(agentFrontmatterTemplate, { ...def.frontmatter, body });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
@@ -45,7 +45,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
45
45
|
{ fileName: "plan.md", template: planMd },
|
|
46
46
|
{ fileName: "designer.md", template: designerMd },
|
|
47
47
|
{ fileName: "reviewer.md", template: reviewerMd },
|
|
48
|
-
{ fileName: "oracle.md", template: oracleMd },
|
|
49
48
|
{ fileName: "librarian.md", template: librarianMd },
|
|
50
49
|
{
|
|
51
50
|
fileName: "task.md",
|
package/src/task/commands.ts
CHANGED
|
@@ -4,16 +4,13 @@
|
|
|
4
4
|
* Commands are embedded at build time via Bun's import with { type: "text" }.
|
|
5
5
|
*/
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
8
|
-
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
9
9
|
import { loadCapability } from "../discovery";
|
|
10
10
|
// Embed command markdown files at build time
|
|
11
11
|
import initMd from "../prompts/agents/init.md" with { type: "text" };
|
|
12
|
-
import { parseFrontmatter } from "../utils/frontmatter";
|
|
13
12
|
|
|
14
|
-
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
|
|
15
|
-
{ name: "init.md", content: renderPromptTemplate(initMd) },
|
|
16
|
-
];
|
|
13
|
+
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
|
|
17
14
|
|
|
18
15
|
export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
|
|
19
16
|
|