@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8
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 +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const BAR_WIDTH = 16;
|
|
2
|
+
|
|
3
|
+
/** Minimal output contract used by the interactive cleanse progress reporter. */
|
|
4
|
+
export interface CleanseProgressOutput {
|
|
5
|
+
isTTY?: boolean;
|
|
6
|
+
write(text: string): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Renders completed repair workers on one transient terminal line. */
|
|
10
|
+
export interface CleanseProgressReporter {
|
|
11
|
+
readonly interactive: boolean;
|
|
12
|
+
start(total: number): void;
|
|
13
|
+
complete(): void;
|
|
14
|
+
finish(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Create the TTY-only worker completion reporter used by `omp cleanse`. */
|
|
18
|
+
export function createCleanseProgressReporter(output: CleanseProgressOutput = process.stdout): CleanseProgressReporter {
|
|
19
|
+
const interactive = output.isTTY === true;
|
|
20
|
+
let total = 0;
|
|
21
|
+
let completed = 0;
|
|
22
|
+
let rendered = false;
|
|
23
|
+
|
|
24
|
+
const render = (): void => {
|
|
25
|
+
if (!interactive || total === 0) return;
|
|
26
|
+
const ratio = Math.min(completed / total, 1);
|
|
27
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
28
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}`;
|
|
29
|
+
output.write(`\rRepairing [${bar}] ${completed}/${total}\x1b[K`);
|
|
30
|
+
rendered = true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
interactive,
|
|
35
|
+
start(nextTotal) {
|
|
36
|
+
total = Math.max(nextTotal, 0);
|
|
37
|
+
completed = 0;
|
|
38
|
+
render();
|
|
39
|
+
},
|
|
40
|
+
complete() {
|
|
41
|
+
completed = Math.min(completed + 1, total);
|
|
42
|
+
render();
|
|
43
|
+
},
|
|
44
|
+
finish() {
|
|
45
|
+
if (!rendered) return;
|
|
46
|
+
output.write("\n");
|
|
47
|
+
rendered = false;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<critical>
|
|
2
|
+
- You MUST fix every assigned diagnostic at its root cause.
|
|
3
|
+
- You MUST stay inside the write scope below.
|
|
4
|
+
- You NEVER suppress valid diagnostics to make checks pass.
|
|
5
|
+
- You NEVER delegate or spawn another agent.
|
|
6
|
+
- You NEVER run project-wide checks or formatters; the orchestrator reruns them.
|
|
7
|
+
</critical>
|
|
8
|
+
|
|
9
|
+
# Assignment
|
|
10
|
+
|
|
11
|
+
Repair wave {{wave}}, worker {{worker}}.
|
|
12
|
+
|
|
13
|
+
## Write scope
|
|
14
|
+
|
|
15
|
+
{{write_scope}}
|
|
16
|
+
|
|
17
|
+
Read related code freely. Project-level diagnostics MAY require the smallest necessary edit outside named files; use `hub` before touching a peer-owned file.
|
|
18
|
+
|
|
19
|
+
## Diagnostics
|
|
20
|
+
|
|
21
|
+
{{diagnostics}}
|
|
22
|
+
|
|
23
|
+
## Checker commands
|
|
24
|
+
|
|
25
|
+
{{checker_commands}}
|
|
26
|
+
|
|
27
|
+
## Concurrent peer ownership
|
|
28
|
+
|
|
29
|
+
{{peer_assignments}}
|
|
30
|
+
|
|
31
|
+
<workflow>
|
|
32
|
+
1. Inspect every assigned location and its local context.
|
|
33
|
+
2. Fix causes, not emitted text or checker configuration.
|
|
34
|
+
3. Preserve behavior unless a diagnostic proves it wrong.
|
|
35
|
+
4. Run only targeted checks for assigned files when available.
|
|
36
|
+
5. Re-read changed sections and resolve every assigned item.
|
|
37
|
+
</workflow>
|
|
38
|
+
|
|
39
|
+
<completeness>
|
|
40
|
+
- Done means every assigned diagnostic has a concrete fix.
|
|
41
|
+
- Blocked items MUST name the missing prerequisite in the final result.
|
|
42
|
+
- Final output MUST list changed files and unresolved items.
|
|
43
|
+
</completeness>
|
|
44
|
+
|
|
45
|
+
<critical>
|
|
46
|
+
You MUST leave peer-owned files untouched and complete the full assignment.
|
|
47
|
+
</critical>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Severity normalized across checker-specific output formats. */
|
|
2
|
+
export type CleanseSeverity = "error" | "warning" | "info";
|
|
3
|
+
|
|
4
|
+
/** One actionable problem reported by a project checker. */
|
|
5
|
+
export interface CleanseDiagnostic {
|
|
6
|
+
checker: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
line?: number;
|
|
9
|
+
column?: number;
|
|
10
|
+
endLine?: number;
|
|
11
|
+
endColumn?: number;
|
|
12
|
+
code?: string;
|
|
13
|
+
severity: CleanseSeverity;
|
|
14
|
+
message: string;
|
|
15
|
+
suggestion?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Result metadata for one checker invocation. */
|
|
19
|
+
export interface CleanseCheckResult {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
language: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
command: string;
|
|
25
|
+
exitCode: number | null;
|
|
26
|
+
diagnostics: CleanseDiagnostic[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Checker discovery result omitted because its required executable was unavailable. */
|
|
30
|
+
export interface SkippedCleanseCheck {
|
|
31
|
+
label: string;
|
|
32
|
+
language: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Aggregate diagnostics from every checker selected for a project. */
|
|
37
|
+
export interface CleanseDiagnosticReport {
|
|
38
|
+
checks: CleanseCheckResult[];
|
|
39
|
+
diagnostics: CleanseDiagnostic[];
|
|
40
|
+
skipped: SkippedCleanseCheck[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** All diagnostics attached to one file, which scheduling never splits across agents. */
|
|
44
|
+
export interface CleanseFileIssues {
|
|
45
|
+
file?: string;
|
|
46
|
+
diagnostics: CleanseDiagnostic[];
|
|
47
|
+
weight: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One weighted, file-disjoint workload handed to a subagent. */
|
|
51
|
+
export interface CleanseAssignment {
|
|
52
|
+
index: number;
|
|
53
|
+
groups: CleanseFileIssues[];
|
|
54
|
+
weight: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Settled result from one cleanse subagent. */
|
|
58
|
+
export interface CleanseAgentOutcome {
|
|
59
|
+
name: string;
|
|
60
|
+
success: boolean;
|
|
61
|
+
output: string;
|
|
62
|
+
error?: string;
|
|
63
|
+
resolvedModel?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Final state after one bounded repair wave and verification pass. */
|
|
67
|
+
export interface CleanseLoopResult {
|
|
68
|
+
status: "clean" | "stalled" | "cancelled";
|
|
69
|
+
waves: number;
|
|
70
|
+
report: CleanseDiagnosticReport;
|
|
71
|
+
outcomes: CleanseAgentOutcome[];
|
|
72
|
+
}
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -231,6 +231,9 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
|
|
|
231
231
|
await fs.promises.chmod(options.targetPath, 0o755);
|
|
232
232
|
} catch (err) {
|
|
233
233
|
await unlinkIfExists(options.targetPath);
|
|
234
|
+
if (isTimeoutError(err)) {
|
|
235
|
+
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
236
|
+
}
|
|
234
237
|
throw err;
|
|
235
238
|
}
|
|
236
239
|
}
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -560,9 +560,34 @@ function formatReloginDeadline(
|
|
|
560
560
|
* automatically (refresh failure, upstream invalidation). Rows the user
|
|
561
561
|
* replaced or deleted deliberately are lifecycle noise, not lost capacity.
|
|
562
562
|
*/
|
|
563
|
-
function isActionableDisable(summary: DisabledCredentialSummary): boolean {
|
|
563
|
+
function isActionableDisable(summary: DisabledCredentialSummary, activeAccounts: UsageAccountIdentity[] = []): boolean {
|
|
564
564
|
if (summary.type !== "oauth") return false;
|
|
565
|
-
|
|
565
|
+
if (/^(replaced by|deleted by user)/i.test(summary.cause)) return false;
|
|
566
|
+
|
|
567
|
+
// Do not display tombstone if there is an active account for the same provider
|
|
568
|
+
// matching the same identity (email, accountId, or org).
|
|
569
|
+
const summaryEmail = summary.email?.toLowerCase();
|
|
570
|
+
const summaryAccountId = summary.accountId?.toLowerCase();
|
|
571
|
+
const summaryOrgId = summary.orgId?.toLowerCase();
|
|
572
|
+
|
|
573
|
+
const matchesActive = activeAccounts.some(account => {
|
|
574
|
+
if (account.provider !== summary.provider) return false;
|
|
575
|
+
|
|
576
|
+
const accountEmail = account.email?.toLowerCase();
|
|
577
|
+
const accountAccountId = account.accountId?.toLowerCase();
|
|
578
|
+
const accountOrgId = account.orgId?.toLowerCase();
|
|
579
|
+
|
|
580
|
+
// If email or accountId match, it's the same identity
|
|
581
|
+
if (summaryEmail && accountEmail && summaryEmail === accountEmail) return true;
|
|
582
|
+
if (summaryAccountId && accountAccountId && summaryAccountId === accountAccountId) return true;
|
|
583
|
+
|
|
584
|
+
// Fallback: if orgId matches and neither email nor accountId contradicts
|
|
585
|
+
if (summaryOrgId && accountOrgId && summaryOrgId === accountOrgId) return true;
|
|
586
|
+
|
|
587
|
+
return false;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return !matchesActive;
|
|
566
591
|
}
|
|
567
592
|
|
|
568
593
|
/** Human-sized disable cause: the upstream `error_description` when embedded, else the first clause. */
|
|
@@ -610,7 +635,7 @@ export function formatUsageBreakdown(
|
|
|
610
635
|
}
|
|
611
636
|
const disabledByProvider = new Map<string, DisabledCredentialSummary[]>();
|
|
612
637
|
for (const summary of disabled) {
|
|
613
|
-
if (!isActionableDisable(summary)) continue;
|
|
638
|
+
if (!isActionableDisable(summary, accounts)) continue;
|
|
614
639
|
const list = disabledByProvider.get(summary.provider) ?? [];
|
|
615
640
|
list.push(summary);
|
|
616
641
|
disabledByProvider.set(summary.provider, list);
|
|
@@ -1018,7 +1043,7 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
1018
1043
|
const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider));
|
|
1019
1044
|
if (stats.length > 0) capacity[report.provider] = stats;
|
|
1020
1045
|
}
|
|
1021
|
-
let disabledForJson = disabled.filter(isActionableDisable);
|
|
1046
|
+
let disabledForJson = disabled.filter(summary => isActionableDisable(summary, accounts));
|
|
1022
1047
|
if (redaction) {
|
|
1023
1048
|
disabledForJson = disabledForJson.map(summary => ({
|
|
1024
1049
|
...summary,
|
package/src/cli/worktree-cli.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* `<parent-repo>/.git/worktrees/<name>/`.
|
|
9
9
|
* - **Task-isolation dirs** (`task/worktree.ts`): a wrapper dir with a
|
|
10
10
|
* compact `m` subdir mounted/cloned by `natives.isoStart`. Legacy `merged`
|
|
11
|
-
* subdirs are still recognized.
|
|
12
|
-
*
|
|
11
|
+
* subdirs are still recognized. `ensureIsolation` writes an ownership
|
|
12
|
+
* marker naming the live omp process; a
|
|
13
|
+
* sandbox whose owner is still running is reported `live` and never
|
|
14
|
+
* removed without `--all`, so `clear` reclaims only crashed leftovers.
|
|
13
15
|
*
|
|
14
16
|
* Legacy entries from before the encoding change keep working because git still
|
|
15
17
|
* tracks them by branch name. This command exists to GC them on demand.
|
|
@@ -18,6 +20,7 @@ import * as fs from "node:fs/promises";
|
|
|
18
20
|
import * as path from "node:path";
|
|
19
21
|
import { getWorktreesDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
20
22
|
import chalk from "chalk";
|
|
23
|
+
import { hasLiveIsolationOwner, ISOLATION_OWNER_FILE } from "../task/isolation-ownership";
|
|
21
24
|
import * as git from "../utils/git";
|
|
22
25
|
|
|
23
26
|
type WorktreeKind = "pr-checkout" | "task-isolation" | "empty" | "stray";
|
|
@@ -211,16 +214,30 @@ async function classifyDir(dir: string): Promise<WorktreeEntry | null> {
|
|
|
211
214
|
if (gitStat?.isFile()) {
|
|
212
215
|
return classifyPrCheckout(dir, gitEntry);
|
|
213
216
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
// A task-isolation sandbox is identified by its ownership marker — written
|
|
218
|
+
// before the backend materialises the mount — or by the `m`/`merged` mount
|
|
219
|
+
// dir itself (legacy dirs and crashed pre-marker runs). Recognizing the
|
|
220
|
+
// marker alone keeps an in-progress sandbox from being mistaken for a stray
|
|
221
|
+
// during the window between marker creation and mount materialisation.
|
|
222
|
+
let isIsolation = await Bun.file(path.join(dir, ISOLATION_OWNER_FILE)).exists();
|
|
223
|
+
if (!isIsolation) {
|
|
224
|
+
for (const mountDir of TASK_ISOLATION_MOUNT_DIRS) {
|
|
225
|
+
const mountStat = await fs.stat(path.join(dir, mountDir)).catch(() => null);
|
|
226
|
+
if (mountStat?.isDirectory()) {
|
|
227
|
+
isIsolation = true;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
222
231
|
}
|
|
223
|
-
return null;
|
|
232
|
+
if (!isIsolation) return null;
|
|
233
|
+
const live = await hasLiveIsolationOwner(dir);
|
|
234
|
+
return {
|
|
235
|
+
path: dir,
|
|
236
|
+
kind: "task-isolation",
|
|
237
|
+
// Only after confirming no live owner is the "no live task" claim true.
|
|
238
|
+
// A running subagent's sandbox stays live so `clear` won't delete it.
|
|
239
|
+
orphanReason: live ? undefined : "task-isolation leftover (no live task owns it)",
|
|
240
|
+
};
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
async function classifyPrCheckout(dir: string, gitEntry: string): Promise<WorktreeEntry> {
|
package/src/cli-commands.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const commands: CommandEntry[] = [
|
|
|
18
18
|
{ name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
|
|
19
19
|
{ name: "agents", load: () => import("./commands/agents").then(m => m.default) },
|
|
20
20
|
{ name: "bench", load: () => import("./commands/bench").then(m => m.default) },
|
|
21
|
+
{ name: "cleanse", load: () => import("./commands/cleanse").then(m => m.default) },
|
|
21
22
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
22
23
|
{ name: "completions", load: () => import("./commands/completions").then(m => m.default) },
|
|
23
24
|
{ name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
|
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
VERSION,
|
|
26
26
|
} from "@oh-my-pi/pi-utils/dirs";
|
|
27
27
|
import { interceptUnhandledRejections } from "@oh-my-pi/pi-utils/postmortem";
|
|
28
|
+
import { setProcessName } from "@oh-my-pi/pi-utils/process-name";
|
|
28
29
|
import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
|
|
29
30
|
import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
|
|
30
31
|
import { extractProfileFlags } from "./cli/profile-bootstrap";
|
|
@@ -42,7 +43,7 @@ if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
|
|
|
42
43
|
process.exit(1);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
setProcessName(APP_NAME);
|
|
46
47
|
|
|
47
48
|
// `Bun.build`-API compiled Windows executables report `import.meta.main ===
|
|
48
49
|
// false`: the standalone loader keys the entry module with native backslashes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
3
|
+
import { runCleanseCommand } from "../cleanse";
|
|
4
|
+
import { CliUsageError } from "../cli/usage-error";
|
|
5
|
+
|
|
6
|
+
export default class Cleanse extends Command {
|
|
7
|
+
static description = "Detect and fix project diagnostics with weighted parallel subagents";
|
|
8
|
+
|
|
9
|
+
static flags = {
|
|
10
|
+
agents: Flags.integer({
|
|
11
|
+
char: "n",
|
|
12
|
+
description: "Maximum number of file-disjoint subagents",
|
|
13
|
+
default: 8,
|
|
14
|
+
}),
|
|
15
|
+
model: Flags.string({
|
|
16
|
+
char: "m",
|
|
17
|
+
description: "Subagent model selector",
|
|
18
|
+
default: "@smol",
|
|
19
|
+
}),
|
|
20
|
+
tests: Flags.boolean({
|
|
21
|
+
char: "t",
|
|
22
|
+
description: "Also run configured project test suites",
|
|
23
|
+
default: false,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
static examples = [
|
|
28
|
+
"omp cleanse",
|
|
29
|
+
"omp cleanse -n 4",
|
|
30
|
+
"omp cleanse -m opus",
|
|
31
|
+
"omp cleanse -t",
|
|
32
|
+
"omp cleanse --agents 12 --model anthropic/claude-opus-4-6",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
async run(): Promise<void> {
|
|
36
|
+
const { flags } = await this.parse(Cleanse);
|
|
37
|
+
if (flags.agents <= 0) throw new CliUsageError("--agents must be a positive integer");
|
|
38
|
+
const result = await runCleanseCommand({
|
|
39
|
+
maxAgents: flags.agents,
|
|
40
|
+
model: flags.model,
|
|
41
|
+
includeTests: flags.tests,
|
|
42
|
+
});
|
|
43
|
+
await postmortem.quit(result.exitCode);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -3835,14 +3835,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
3835
3835
|
},
|
|
3836
3836
|
},
|
|
3837
3837
|
|
|
3838
|
+
// Legacy boolean kept only for back-compat migration to `inspect_image.mode`
|
|
3839
|
+
// (see config/settings.ts). Hidden from UI.
|
|
3838
3840
|
"inspect_image.enabled": {
|
|
3839
3841
|
type: "boolean",
|
|
3840
3842
|
default: false,
|
|
3843
|
+
},
|
|
3844
|
+
|
|
3845
|
+
"inspect_image.mode": {
|
|
3846
|
+
type: "enum",
|
|
3847
|
+
values: ["auto", "on", "off"] as const,
|
|
3848
|
+
default: "auto",
|
|
3841
3849
|
ui: {
|
|
3842
3850
|
tab: "tools",
|
|
3843
3851
|
group: "Available Tools",
|
|
3844
3852
|
label: "Inspect Image",
|
|
3845
|
-
description:
|
|
3853
|
+
description:
|
|
3854
|
+
"Controls the inspect_image tool, which delegates image understanding to a vision-capable model. 'auto' exposes it only when the active model lacks native image input; 'on' always exposes it; 'off' never does.",
|
|
3855
|
+
options: [
|
|
3856
|
+
{ value: "auto", label: "Auto (only for models without vision)" },
|
|
3857
|
+
{ value: "on", label: "On" },
|
|
3858
|
+
{ value: "off", label: "Off" },
|
|
3859
|
+
],
|
|
3846
3860
|
},
|
|
3847
3861
|
},
|
|
3848
3862
|
|
package/src/config/settings.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset }
|
|
|
39
39
|
import { AgentStorage } from "../session/agent-storage";
|
|
40
40
|
import { AUTO_IMAGE_PROVIDER_ORDER, isImageProviderId } from "../tools/image-providers";
|
|
41
41
|
import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
42
|
+
import { INSPECT_IMAGE_MODES } from "../utils/inspect-image-mode";
|
|
42
43
|
import { isSearchProviderId, SEARCH_PROVIDER_ORDER } from "../web/search/types";
|
|
43
44
|
import { withFileLock } from "./file-lock";
|
|
44
45
|
import {
|
|
@@ -1366,6 +1367,40 @@ export class Settings {
|
|
|
1366
1367
|
}
|
|
1367
1368
|
}
|
|
1368
1369
|
|
|
1370
|
+
// inspect_image.enabled (boolean) -> inspect_image.mode (enum). Explicit
|
|
1371
|
+
// user choices are preserved: true -> "on", false -> "off". Configs with
|
|
1372
|
+
// no legacy key get the new "auto" default, which hides the tool for
|
|
1373
|
+
// models with native image input. Handles nested and quoted-dotted
|
|
1374
|
+
// ("inspect_image.enabled") sources; the target is always the nested
|
|
1375
|
+
// form, which is the only shape the resolver reads.
|
|
1376
|
+
const inspectImageObj = isRecord(raw.inspect_image) ? (raw.inspect_image as Record<string, unknown>) : undefined;
|
|
1377
|
+
const legacyEnabled =
|
|
1378
|
+
typeof inspectImageObj?.enabled === "boolean"
|
|
1379
|
+
? inspectImageObj.enabled
|
|
1380
|
+
: typeof raw["inspect_image.enabled"] === "boolean"
|
|
1381
|
+
? (raw["inspect_image.enabled"] as boolean)
|
|
1382
|
+
: undefined;
|
|
1383
|
+
if (legacyEnabled !== undefined) {
|
|
1384
|
+
if (!inspectImageObj) {
|
|
1385
|
+
raw.inspect_image = {};
|
|
1386
|
+
}
|
|
1387
|
+
const target = raw.inspect_image as Record<string, unknown>;
|
|
1388
|
+
const flatMode = raw["inspect_image.mode"];
|
|
1389
|
+
if (target.mode === undefined) {
|
|
1390
|
+
// A quoted-dotted explicit mode wins over the legacy boolean but
|
|
1391
|
+
// must be normalized into the nested form the resolver reads.
|
|
1392
|
+
target.mode =
|
|
1393
|
+
typeof flatMode === "string" && (INSPECT_IMAGE_MODES as readonly string[]).includes(flatMode)
|
|
1394
|
+
? flatMode
|
|
1395
|
+
: legacyEnabled
|
|
1396
|
+
? "on"
|
|
1397
|
+
: "off";
|
|
1398
|
+
}
|
|
1399
|
+
delete target.enabled;
|
|
1400
|
+
delete raw["inspect_image.enabled"];
|
|
1401
|
+
delete raw["inspect_image.mode"];
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1369
1404
|
// task.isolation.enabled (boolean) -> task.isolation.mode (enum)
|
|
1370
1405
|
const taskObj = raw.task as Record<string, unknown> | undefined;
|
|
1371
1406
|
const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
|
package/src/cursor.ts
CHANGED
|
@@ -25,7 +25,8 @@ interface CursorExecBridgeOptions {
|
|
|
25
25
|
cwd: string;
|
|
26
26
|
getCwd?: () => string;
|
|
27
27
|
tools: Map<string, AgentTool>;
|
|
28
|
-
|
|
28
|
+
/** Resolves execution overrides (mounted-device permission wrappers) before the canonical map. */
|
|
29
|
+
getExecutableTool?: (name: string) => AgentTool | undefined;
|
|
29
30
|
getToolContext?: () => AgentToolContext | undefined;
|
|
30
31
|
emitEvent?: (event: AgentEvent) => void;
|
|
31
32
|
/**
|
|
@@ -81,7 +82,7 @@ async function executeTool(
|
|
|
81
82
|
toolCallId: string,
|
|
82
83
|
args: Record<string, unknown>,
|
|
83
84
|
): Promise<ToolResultMessage> {
|
|
84
|
-
const tool = options.
|
|
85
|
+
const tool = options.getExecutableTool?.(toolName) ?? options.tools.get(toolName);
|
|
85
86
|
if (!tool) {
|
|
86
87
|
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
87
88
|
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
@@ -497,7 +498,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
497
498
|
async mcp(call: CursorMcpCall) {
|
|
498
499
|
const toolName = call.toolName || call.name;
|
|
499
500
|
const toolCallId = decodeToolCallId(call.toolCallId);
|
|
500
|
-
const tool = this.options.
|
|
501
|
+
const tool = this.options.getExecutableTool?.(toolName) ?? this.options.tools.get(toolName);
|
|
501
502
|
if (!tool) {
|
|
502
503
|
const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp__"));
|
|
503
504
|
const message = formatMcpToolErrorMessage(toolName, availableTools);
|
|
@@ -28,6 +28,7 @@ import tsNoAny from "./ts-no-any.md" with { type: "text" };
|
|
|
28
28
|
import tsNoDeprecatedLeftovers from "./ts-no-deprecated-leftovers.md" with { type: "text" };
|
|
29
29
|
import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
|
|
30
30
|
import tsNoInlineCastAccess from "./ts-no-inline-cast-access.md" with { type: "text" };
|
|
31
|
+
import tsNoLocalIsRecord from "./ts-no-local-is-record.md" with { type: "text" };
|
|
31
32
|
import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
|
|
32
33
|
import tsNoTestTimers from "./ts-no-test-timers.md" with { type: "text" };
|
|
33
34
|
import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
|
|
@@ -63,6 +64,7 @@ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
|
|
|
63
64
|
{ name: "ts-no-deprecated-leftovers", content: tsNoDeprecatedLeftovers },
|
|
64
65
|
{ name: "ts-no-dynamic-import", content: tsNoDynamicImport },
|
|
65
66
|
{ name: "ts-no-inline-cast-access", content: tsNoInlineCastAccess },
|
|
67
|
+
{ name: "ts-no-local-is-record", content: tsNoLocalIsRecord },
|
|
66
68
|
{ name: "ts-no-return-type", content: tsNoReturnType },
|
|
67
69
|
{ name: "ts-no-test-timers", content: tsNoTestTimers },
|
|
68
70
|
{ name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Never use isRecord"
|
|
3
|
+
condition:
|
|
4
|
+
- "\\bfunction\\s+isRecord(?:\\s*<[^>]*>)?\\s*\\("
|
|
5
|
+
- "\\b(?:const|let|var)\\s+isRecord\\b\\s*(?::[\\s\\S]{0,300}?)?=\\s*(?:async\\s+)?(?:function\\b|(?:<[^>\\n]*>\\s*)?(?:\\([^)]*\\)|[A-Za-z_$][\\w$]*)\\s*(?::[\\s\\S]{0,300}?)?=>)"
|
|
6
|
+
scope: "tool:edit(*.{ts,tsx,mts,cts}), tool:write(*.{ts,tsx,mts,cts})"
|
|
7
|
+
interruptMode: never
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why it's wrong
|
|
11
|
+
|
|
12
|
+
- A `Record<string, unknown>` guard proves only an object, not its fields.
|
|
13
|
+
- It's either unnecessarily complicated, or not strong enough.
|
|
14
|
+
- Repeated guards hide the actual data contract from readers and TypeScript.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
`isRecord` narrows values to `Record<string, unknown>`; each field remains `unknown`.
|
|
19
|
+
|
|
20
|
+
For network, config, IPC, persisted, or reused data shapes, parse once at the boundary with the project's schema validator and consume its named output type:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const Config = z.object({ retries: z.number().int().nonnegative() });
|
|
24
|
+
type Config = z.infer<typeof Config>;
|
|
25
|
+
|
|
26
|
+
const config = Config.parse(raw);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If the runtime shape is uncertain, check the properties you use with `typeof`, `Array.isArray`, `in`, or a discriminant. If an existing invariant guarantees the shape, assert the named type at that boundary instead of duplicating a guard:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const config = value as Config;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Avoid
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
43
|
+
value !== null && typeof value === "object";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Exceptions
|
|
47
|
+
|
|
48
|
+
A standalone package without a shared type-guard module may define its single canonical guard. Export it from the package's type-guard module; never recreate it at individual call sites.
|