one-code-extension 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -10
- package/docs/guide/README.md +1 -0
- package/docs/guide/configuration.md +2 -2
- package/docs/guide/differences-from-claude-code.md +13 -4
- package/docs/guide/hooks.md +14 -2
- package/docs/guide/installation.md +54 -6
- package/docs/guide/permissions-modes-and-auto-mode.md +23 -7
- package/docs/guide/reference.md +5 -2
- package/docs/guide/sessions-and-context.md +18 -0
- package/docs/guide/terminal-interface.md +1 -1
- package/docs/guide/tools.md +24 -2
- package/docs/guide/troubleshooting.md +14 -4
- package/docs/guide/windows.md +184 -0
- package/extensions/auto-mode/actions.ts +3 -1
- package/extensions/auto-mode/paths.ts +21 -23
- package/extensions/auto-mode/safety-floor.ts +44 -6
- package/extensions/auto-mode/shell-analysis.ts +25 -10
- package/extensions/auto-mode/transcript.ts +15 -5
- package/extensions/background/index.ts +83 -34
- package/extensions/background/monitor-batch.ts +7 -5
- package/extensions/background/wakeup.ts +18 -24
- package/extensions/bash/background.ts +19 -10
- package/extensions/bash/guards.ts +36 -16
- package/extensions/bash/index.ts +36 -166
- package/extensions/branding/index.ts +35 -4
- package/extensions/btw/index.ts +207 -0
- package/extensions/btw/panel.ts +179 -0
- package/extensions/btw/prompt.ts +45 -0
- package/extensions/clear/index.ts +15 -0
- package/extensions/compaction/fit.ts +9 -6
- package/extensions/doctor/build.ts +22 -2
- package/extensions/doctor/dependencies.ts +63 -31
- package/extensions/doctor/fix-prompt.ts +12 -4
- package/extensions/doctor/index.ts +1 -0
- package/extensions/doctor/models.ts +2 -1
- package/extensions/doctor/report.ts +2 -3
- package/extensions/doctor/update-lookup.ts +19 -11
- package/extensions/effort/index.ts +2 -1
- package/extensions/footer/footer-line.ts +19 -0
- package/extensions/footer/index.ts +25 -5
- package/extensions/hooks/executor.ts +84 -18
- package/extensions/hooks/index.ts +1 -0
- package/extensions/hooks/matcher.ts +23 -9
- package/extensions/hooks/settings.ts +15 -0
- package/extensions/lib/branch-restore.ts +12 -0
- package/extensions/lib/claude-context.ts +2 -3
- package/extensions/lib/claude-settings.ts +19 -0
- package/extensions/lib/command-launch.ts +71 -0
- package/extensions/lib/deferred.ts +173 -33
- package/extensions/lib/git.ts +16 -0
- package/extensions/lib/keys.ts +38 -0
- package/extensions/lib/local-command.ts +112 -0
- package/extensions/lib/model-facts.generated.json +263 -68
- package/extensions/lib/model-policy.ts +13 -0
- package/extensions/lib/model-tier.ts +16 -5
- package/extensions/lib/notifications.ts +547 -53
- package/extensions/lib/paths.ts +130 -2
- package/extensions/lib/pi-ai-estimate.ts +57 -26
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/process-tree.ts +131 -5
- package/extensions/lib/reminders.ts +80 -27
- package/extensions/lib/scratchpad.ts +11 -10
- package/extensions/lib/shell-spawn.ts +298 -0
- package/extensions/lib/shell-tool.ts +252 -0
- package/extensions/lib/shell-tools.ts +13 -0
- package/extensions/lib/side-call.ts +76 -0
- package/extensions/lib/tui-render.ts +62 -11
- package/extensions/lib/update-check.mjs +155 -0
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/which.ts +62 -0
- package/extensions/lib/worktree-channel.ts +18 -0
- package/extensions/lib/worktree-isolation.ts +2 -1
- package/extensions/lsp/client.ts +14 -3
- package/extensions/lsp/index.ts +12 -5
- package/extensions/lsp/install-hints.ts +33 -3
- package/extensions/lsp/plugin-servers.ts +2 -2
- package/extensions/lsp/servers.ts +29 -7
- package/extensions/mcp/index.ts +3 -2
- package/extensions/memory/entries.ts +9 -6
- package/extensions/memory/index.ts +3 -2
- package/extensions/memory/open-external.ts +4 -1
- package/extensions/model-default/index.ts +21 -2
- package/extensions/permissions/index.ts +57 -28
- package/extensions/permissions/matcher.ts +120 -33
- package/extensions/permissions/modes.ts +14 -6
- package/extensions/permissions/powershell-rules.ts +549 -0
- package/extensions/permissions/session-grant.ts +9 -9
- package/extensions/plugins/index.ts +3 -2
- package/extensions/plugins/install/install.ts +1 -1
- package/extensions/powershell/description.ts +139 -0
- package/extensions/powershell/guards.ts +91 -0
- package/extensions/powershell/index.ts +89 -0
- package/extensions/powershell/policy.ts +91 -0
- package/extensions/recap/index.ts +10 -24
- package/extensions/recap/prompt.ts +3 -3
- package/extensions/session-title/index.ts +120 -0
- package/extensions/session-title/title.ts +111 -0
- package/extensions/skill/index.ts +40 -18
- package/extensions/skill/invoke.ts +1 -0
- package/extensions/subagents/hand-back-review.ts +10 -12
- package/extensions/subagents/index.ts +221 -78
- package/extensions/subagents/model-select.ts +75 -7
- package/extensions/system-prompt/environment.ts +14 -1
- package/extensions/system-reminder/index.ts +9 -0
- package/extensions/tasks/index.ts +2 -1
- package/extensions/tool-search/index.ts +41 -20
- package/extensions/workflow/agent-session.ts +6 -4
- package/extensions/workflow/index.ts +28 -5
- package/extensions/worktree/guards.ts +5 -4
- package/extensions/worktree/index.ts +15 -2
- package/extensions/worktree/rewrite.ts +17 -0
- package/package.json +7 -4
- package/types/app.d.ts +18 -2
package/extensions/mcp/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
type McpResourceContents,
|
|
65
65
|
namespacedToolName,
|
|
66
66
|
} from "./schema.ts";
|
|
67
|
+
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
67
68
|
|
|
68
69
|
/** Anthropic's hard limit on a tool name; a longer one fails the whole request. */
|
|
69
70
|
const MAX_TOOL_NAME_LENGTH = 128;
|
|
@@ -842,9 +843,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
842
843
|
});
|
|
843
844
|
};
|
|
844
845
|
|
|
845
|
-
pi
|
|
846
|
+
registerLocalCommand(pi, "mcp", {
|
|
846
847
|
description: "Manage MCP servers (status, reconnect, authenticate, enable/disable)",
|
|
847
|
-
handler: async (
|
|
848
|
+
handler: async (args, ctx) => {
|
|
848
849
|
if (servers.length === 0) {
|
|
849
850
|
ctx.ui.notify("No MCP servers configured. Add them to .mcp.json or ~/.claude.json.", "info");
|
|
850
851
|
return;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* concept at all), and the auto-memory folder as the last entry.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { basename, dirname, join } from "node:path";
|
|
15
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
AGENTS_DESCRIPTOR,
|
|
18
18
|
ancestorDirs,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ONECODE_GLOBAL_DESCRIPTOR,
|
|
26
26
|
PROJECT_DESCRIPTOR,
|
|
27
27
|
} from "../lib/claude-context.ts";
|
|
28
|
+
import { forwardSlashes, isRelativeInside, tildify } from "../lib/paths.ts";
|
|
28
29
|
import { tryReadFile } from "../lib/plugins.ts";
|
|
29
30
|
|
|
30
31
|
export interface MemoryEntry {
|
|
@@ -39,12 +40,14 @@ export interface MemoryEntry {
|
|
|
39
40
|
exists: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Display form of a path: a file under the cwd as `./name`, under home as
|
|
45
|
+
* `~/…`, else as given — always with forward slashes (Claude Code's spelling
|
|
46
|
+
* in this picker), whatever the platform's separator.
|
|
47
|
+
*/
|
|
43
48
|
function displayPath(path: string, cwd: string, home: string): string {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (home && path.startsWith(`${home}/`)) return `~${path.slice(home.length)}`;
|
|
47
|
-
return path;
|
|
49
|
+
const rel = relative(resolve(cwd), resolve(path));
|
|
50
|
+
return isRelativeInside(rel) ? `./${forwardSlashes(rel)}` : tildify(path, home);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -40,6 +40,7 @@ import { boundedDockHeight, safeThemeBold, safeThemePaint, truncateLine } from "
|
|
|
40
40
|
import { buildMemoryEntries, entryName, type MemoryEntry } from "./entries.ts";
|
|
41
41
|
import { openPath } from "./open-external.ts";
|
|
42
42
|
import { applyMemoryKey, decodeMemoryKey, initialMemoryState, renderMemoryPanel } from "./panel.ts";
|
|
43
|
+
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
43
44
|
|
|
44
45
|
const MEMORY_PANEL_MAX_HEIGHT = 20;
|
|
45
46
|
|
|
@@ -144,9 +145,9 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
144
145
|
// ---- /memory: Claude Code's Memory picker --------------------------------
|
|
145
146
|
// Pick a memory-related file (or the auto-memory folder) and open it in the
|
|
146
147
|
// external editor, matching CC. Pure panel logic lives in ./panel.ts.
|
|
147
|
-
pi
|
|
148
|
+
registerLocalCommand(pi, "memory", {
|
|
148
149
|
description: "View or edit CLAUDE.md and memory files",
|
|
149
|
-
handler: async (
|
|
150
|
+
handler: async (args, ctx) => {
|
|
150
151
|
const entries = memoryEntriesFor(ctx.cwd);
|
|
151
152
|
if (!ctx.hasUI) {
|
|
152
153
|
const lines = entries.map(
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
|
+
import { commandLaunch } from "../lib/command-launch.ts";
|
|
17
18
|
|
|
18
19
|
export interface OpenPlan {
|
|
19
20
|
command: string;
|
|
@@ -78,7 +79,9 @@ export function openPath(path: string, kind: "file" | "folder"): Promise<OpenRes
|
|
|
78
79
|
};
|
|
79
80
|
let child: ReturnType<typeof spawn>;
|
|
80
81
|
try {
|
|
81
|
-
|
|
82
|
+
// `$EDITOR=code` is VS Code's `code.cmd` shim on Windows, which a bare spawn cannot start.
|
|
83
|
+
const launch = commandLaunch(command, args, process.env);
|
|
84
|
+
child = spawn(launch.command, launch.args, { stdio: "ignore", detached: true, windowsVerbatimArguments: launch.windowsVerbatimArguments });
|
|
82
85
|
} catch (error) {
|
|
83
86
|
finish({ message: `Could not open ${path}: ${error instanceof Error ? error.message : error}`, ok: false });
|
|
84
87
|
return;
|
|
@@ -14,10 +14,20 @@
|
|
|
14
14
|
* startup with its normal precedence (an explicit --model still wins). Safe to
|
|
15
15
|
* write externally: pi's settings save re-reads the file under its lock and
|
|
16
16
|
* merges only the fields pi itself modified, so this key survives pi's writes.
|
|
17
|
+
*
|
|
18
|
+
* The switch is also shown to the model the way Claude Code shows `/model`
|
|
19
|
+
* (a local-command breadcrumb before the next prompt — lib/local-command.ts):
|
|
20
|
+
* CC's stdout is "Set model to `<name>` and saved as your default for new
|
|
21
|
+
* sessions", and since persisting is what this extension does, the claim is
|
|
22
|
+
* true here too (a failed write announces the switch without it). pi handles
|
|
23
|
+
* `/model` before any extension sees the text, so `model_select` — which also
|
|
24
|
+
* covers ctrl+p cycling — is the one signal; the debounce means only the model
|
|
25
|
+
* the user settles on is announced.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
import { join } from "node:path";
|
|
20
29
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { announceLocalCommand } from "../lib/local-command.ts";
|
|
21
31
|
import { readSettingsForWrite, writeSettings } from "../lib/one-code-settings.ts";
|
|
22
32
|
|
|
23
33
|
/** Pure merge: set the default model, preserving every other settings key. */
|
|
@@ -29,11 +39,16 @@ export function applyDefaultModel(
|
|
|
29
39
|
return { ...file, defaultProvider: provider, defaultModel: modelId };
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
/** Claude Code's `/model` stdout line, as captured (2.1.278); without the persisted half when the write failed. */
|
|
43
|
+
export function modelSwitchStdout(displayName: string, saved: boolean): string {
|
|
44
|
+
return `Set model to \`${displayName}\`${saved ? " and saved as your default for new sessions" : ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/** Quiet period after the last switch before it is written (ctrl+p cycles fire one event per step). */
|
|
33
48
|
export const PERSIST_DEBOUNCE_MS = 400;
|
|
34
49
|
|
|
35
50
|
export default function modelDefaultExtension(pi: ExtensionAPI) {
|
|
36
|
-
let pending: { provider: string; id: string; notify: (message: string) => void } | undefined;
|
|
51
|
+
let pending: { provider: string; id: string; name: string; notify: (message: string) => void } | undefined;
|
|
37
52
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
53
|
|
|
39
54
|
const flush = () => {
|
|
@@ -43,23 +58,27 @@ export default function modelDefaultExtension(pi: ExtensionAPI) {
|
|
|
43
58
|
pending = undefined;
|
|
44
59
|
if (!choice) return;
|
|
45
60
|
const path = join(getAgentDir(), "settings.json");
|
|
61
|
+
let saved = true;
|
|
46
62
|
try {
|
|
47
63
|
writeSettings(path, applyDefaultModel(readSettingsForWrite(path), choice.provider, choice.id));
|
|
48
64
|
} catch (error) {
|
|
49
65
|
// Persisting a preference must not break the model switch itself, but
|
|
50
66
|
// say why it will be forgotten (typically a malformed settings.json).
|
|
67
|
+
saved = false;
|
|
51
68
|
choice.notify(`Could not save ${choice.provider}/${choice.id} as the default model: ${(error as Error).message}`);
|
|
52
69
|
}
|
|
70
|
+
announceLocalCommand(pi, { name: "model", stdout: modelSwitchStdout(choice.name, saved) });
|
|
53
71
|
};
|
|
54
72
|
|
|
55
73
|
// Debounced: a ctrl+p cycle emits model_select per step, and only the model
|
|
56
74
|
// the user settles on should hit the disk (last one wins).
|
|
57
75
|
pi.on("model_select", (event, ctx) => {
|
|
58
|
-
const model = event.model as { provider?: string; id?: string } | undefined;
|
|
76
|
+
const model = event.model as { provider?: string; id?: string; name?: string } | undefined;
|
|
59
77
|
if (!model?.provider || !model.id) return;
|
|
60
78
|
pending = {
|
|
61
79
|
provider: model.provider,
|
|
62
80
|
id: model.id,
|
|
81
|
+
name: model.name || model.id,
|
|
63
82
|
notify: (message) => {
|
|
64
83
|
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
65
84
|
},
|
|
@@ -22,9 +22,9 @@ import { join } from "node:path";
|
|
|
22
22
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
23
23
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import { type ChildAction, SUBAGENT_ACTIONS_CHANNEL, type SubagentActionsPayload } from "../auto-mode/actions.ts";
|
|
25
|
-
import {
|
|
25
|
+
import type { HandBackVerdict } from "../lib/notifications.ts";
|
|
26
26
|
import { MODEL_UNUSABLE_CHANNEL, type ModelUnusableEvent } from "../lib/model-unusable.ts";
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
import { classify, createClassifierState } from "../auto-mode/classifier.ts";
|
|
29
29
|
import {
|
|
30
30
|
type AutoModeConfig,
|
|
@@ -52,6 +52,9 @@ import { checkRecoverability } from "../auto-mode/recoverability.ts";
|
|
|
52
52
|
import { safetyControlWrite } from "../auto-mode/safety-floor.ts";
|
|
53
53
|
import { isExecutionPrimitivePath, isSensitivePath } from "../auto-mode/sensitive.ts";
|
|
54
54
|
import { analyzeShellCommand, type ShellEvidence } from "../auto-mode/shell-analysis.ts";
|
|
55
|
+
import { isGitStatusCommand, powershellReadOnly } from "./powershell-rules.ts";
|
|
56
|
+
import { isShellTool } from "./matcher.ts";
|
|
57
|
+
import { gitStatusClean } from "../lib/git.ts";
|
|
55
58
|
import { projectMemoryDir } from "../lib/memory.ts";
|
|
56
59
|
import { sessionResultsDir } from "../lib/persisted-output.ts";
|
|
57
60
|
import { sessionScratchpadDir } from "../lib/scratchpad.ts";
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
isPathSubjectTool,
|
|
68
71
|
} from "./matcher.ts";
|
|
69
72
|
import { type SessionGrant, sessionGrant } from "./session-grant.ts";
|
|
70
|
-
import { modeBadge, nextMode, PERMISSION_STATUS_CHANNEL, type PermissionStatus } from "./modes.ts";
|
|
73
|
+
import { modeBadge, nextMode, PERMISSION_STATUS_CHANNEL, type PermissionStatus, CYCLE_KEY } from "./modes.ts";
|
|
71
74
|
import { type ChildToolCall, type ChildGateDecision, SUBAGENT_GATE_CHANNEL } from "./subagent-gate.ts";
|
|
72
75
|
import { trackOriginalCommands } from "../lib/original-command.ts";
|
|
73
76
|
import { MODE_CHANNEL, PLAN_FILE_CHANNEL } from "../lib/plan-mode-channels.ts";
|
|
@@ -80,6 +83,7 @@ import { describeProjectAllow, persistProjectAllowApproval, projectAllowApproved
|
|
|
80
83
|
import { findProjectRoot } from "../lib/git.ts";
|
|
81
84
|
import { oneCodeProjectSettingsPath, oneCodeSettingsPath } from "../lib/one-code-settings.ts";
|
|
82
85
|
import { recordUsage } from "../lib/usage-bus.ts";
|
|
86
|
+
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
83
87
|
|
|
84
88
|
const DENIED_BY_USER =
|
|
85
89
|
"The user doesn't want to proceed with this tool use. The tool use was rejected. Adjust your approach based on the user's feedback instead of retrying the same call.";
|
|
@@ -169,12 +173,8 @@ const approved = (choice: string | undefined, grant: SessionGrant | undefined) =
|
|
|
169
173
|
choice === YES || (grant !== undefined && choice === grant.label);
|
|
170
174
|
|
|
171
175
|
export default function permissionsExtension(pi: ExtensionAPI) {
|
|
172
|
-
const notifyTask = createTaskNotifier(pi);
|
|
173
176
|
// The background hand-back review rides the same steered path as agent
|
|
174
177
|
// completions; render it the same compact way (full body on ctrl+o).
|
|
175
|
-
pi.registerMessageRenderer("subagent-review", (message, { expanded }, theme) =>
|
|
176
|
-
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
177
|
-
);
|
|
178
178
|
pi.registerFlag("permission-mode", {
|
|
179
179
|
description:
|
|
180
180
|
"Permission mode: default (alias: manual) | acceptEdits | plan | auto | bypassPermissions | dontAsk",
|
|
@@ -244,6 +244,14 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
244
244
|
let scratchpadDirPath: string | undefined;
|
|
245
245
|
/** Where oversized tool outputs are persisted this session — readable like the cwd. */
|
|
246
246
|
let resultsDirPath: string | undefined;
|
|
247
|
+
/** This project's pi session dir: transcripts + auto-mode-decisions.jsonl (readable, never a write root). */
|
|
248
|
+
let sessionDirPath: string | undefined;
|
|
249
|
+
/**
|
|
250
|
+
* The harness dirs a read may reach besides the cwd, realpath-resolved once
|
|
251
|
+
* per session (they never change after session_start) — one list for both
|
|
252
|
+
* shell pre-gates, which compare against realpaths.
|
|
253
|
+
*/
|
|
254
|
+
let readableRoots: string[] = [];
|
|
247
255
|
/** The session cwd's own realpath (macOS /var → /private/var), for decide()'s containment check. */
|
|
248
256
|
let resolvedCwd: string | undefined;
|
|
249
257
|
/** The per-repo One Code settings file, resolved once per session so the auto-mode floor need not re-walk for the project root on every tool call. */
|
|
@@ -502,7 +510,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
502
510
|
|
|
503
511
|
let evidence: ShellEvidence | undefined;
|
|
504
512
|
if (normalizeToolName(toolName) === "bash" && subject) {
|
|
505
|
-
evidence = analyzeShellCommand({ command: subject, cwd, home, protectedDirs });
|
|
513
|
+
evidence = analyzeShellCommand({ command: subject, cwd, home, protectedDirs, readableRoots });
|
|
506
514
|
if (evidence.verdict === "safe") {
|
|
507
515
|
logDecision(ctx, { tool: toolName, subject, outcome: "allow", source: "pre-gate" });
|
|
508
516
|
return allow();
|
|
@@ -523,6 +531,17 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
523
531
|
}
|
|
524
532
|
evidence.notes.push(`git recoverability: ${rec.reason}`);
|
|
525
533
|
}
|
|
534
|
+
} else if (normalizeToolName(toolName) === "powershell" && subject) {
|
|
535
|
+
// PowerShell has no pre-gate in v1 beyond Claude Code's read-only cmdlet
|
|
536
|
+
// allowlist (powershell-rules.ts): a read-only line runs unclassified,
|
|
537
|
+
// everything else — every write, delete or unknown executable — goes to
|
|
538
|
+
// the classifier. No containment fast path either: the recoverability
|
|
539
|
+
// judge understands bash deletes, not `Remove-Item`, so PowerShell
|
|
540
|
+
// destruction is always classified (docs/decisions/windows.md).
|
|
541
|
+
if (powershellReadOnly(subject, { cwd, home, readableRoots }).readOnly) {
|
|
542
|
+
logDecision(ctx, { tool: toolName, subject, outcome: "allow", source: "pre-gate" });
|
|
543
|
+
return allow();
|
|
544
|
+
}
|
|
526
545
|
} else if (containmentEligible && isWritingTool(normalizeToolName(toolName))) {
|
|
527
546
|
// A write/edit whose target is inside the project and not a credential
|
|
528
547
|
// path is ordinary sandbox work — Claude Code auto-approves it, and so do
|
|
@@ -717,6 +736,8 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
717
736
|
scratchpadDirPath = sessionScratchpadDir(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
718
737
|
// Resolved like the subjects compared against it (a symlinked parent, macOS /var).
|
|
719
738
|
resultsDirPath = resolvedOrSelf(sessionResultsDir(ctx));
|
|
739
|
+
sessionDirPath = resolvedOrSelf(ctx.sessionManager.getSessionDir());
|
|
740
|
+
readableRoots = [memoryDirPath, scratchpadDirPath, resultsDirPath, sessionDirPath].filter((d): d is string => !!d).map(resolvedOrSelf);
|
|
720
741
|
resolvedCwd = resolveForContainment(ctx.cwd);
|
|
721
742
|
oneCodeProjectSettingsFile = oneCodeProjectSettingsPath(ctx.cwd, os.homedir());
|
|
722
743
|
protectedDirs = runtimeProtectedDirs();
|
|
@@ -771,8 +792,10 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
771
792
|
// Claude Code cycles permission modes with shift+tab; pi reserves that key
|
|
772
793
|
// for the thinking dial. ctrl+q is the one ctrl+letter pi leaves unbound
|
|
773
794
|
// that terminals don't already own (ctrl+m *is* Enter's byte, alt needs
|
|
774
|
-
// option-as-meta configured on macOS) —
|
|
775
|
-
pi
|
|
795
|
+
// option-as-meta configured on macOS) — except on Windows and WSL, where
|
|
796
|
+
// pi binds ctrl+q itself and the key is alt+m (lib/keys.ts) — see
|
|
797
|
+
// docs/decisions.md.
|
|
798
|
+
pi.registerShortcut(CYCLE_KEY, {
|
|
776
799
|
description: "Cycle permission mode",
|
|
777
800
|
handler: (ctx) => {
|
|
778
801
|
badgeCtx = ctx;
|
|
@@ -823,7 +846,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
823
846
|
// command that actually runs). The lookup is by toolCallId on purpose: a
|
|
824
847
|
// value inside `event.input` would be the model's to write, and rules would
|
|
825
848
|
// match a string of its choosing.
|
|
826
|
-
const original = normalizedTool
|
|
849
|
+
const original = isShellTool(normalizedTool) ? originalCommands.get(event.toolCallId) : undefined;
|
|
827
850
|
const matchSubject = original?.command ?? subject;
|
|
828
851
|
const callCwd = original?.cwd ?? ctx.cwd;
|
|
829
852
|
|
|
@@ -835,6 +858,14 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
835
858
|
const recordedInput =
|
|
836
859
|
original !== undefined ? { command: original.command } : (event.input as Record<string, unknown>);
|
|
837
860
|
transcript.push({ kind: "tool", tool: normalizedTool, input: recordedInput });
|
|
861
|
+
// Claude Code follows a `git status` call with a ground-truth line the
|
|
862
|
+
// classifier can trust over the model's account of the tree
|
|
863
|
+
// (`{"meta":{"gitStatus":{"clean":…}}}`, captured 2.1.276 — findings §22).
|
|
864
|
+
const recordedCommand = typeof recordedInput.command === "string" ? recordedInput.command : undefined;
|
|
865
|
+
if (isShellTool(normalizedTool) && recordedCommand && isGitStatusCommand(recordedCommand)) {
|
|
866
|
+
const clean = await gitStatusClean(callCwd);
|
|
867
|
+
if (clean !== undefined) transcript.push({ kind: "meta", gitStatus: { clean } });
|
|
868
|
+
}
|
|
838
869
|
capTranscript();
|
|
839
870
|
}
|
|
840
871
|
|
|
@@ -854,6 +885,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
854
885
|
memoryDirPath,
|
|
855
886
|
scratchpadDirPath,
|
|
856
887
|
resultsDirPath,
|
|
888
|
+
sessionDirPath,
|
|
857
889
|
protectedDirs,
|
|
858
890
|
});
|
|
859
891
|
let result = decideWith([...allow, ...activeSessionAllows(), ...(projectAllowTrusted ? projectAllow : [])]);
|
|
@@ -1074,6 +1106,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1074
1106
|
memoryDirPath,
|
|
1075
1107
|
scratchpadDirPath,
|
|
1076
1108
|
resultsDirPath,
|
|
1109
|
+
sessionDirPath,
|
|
1077
1110
|
protectedDirs,
|
|
1078
1111
|
});
|
|
1079
1112
|
|
|
@@ -1111,7 +1144,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1111
1144
|
const appendEntry: TranscriptEntry = {
|
|
1112
1145
|
kind: "tool",
|
|
1113
1146
|
tool: normalizedTool,
|
|
1114
|
-
input: normalizedTool
|
|
1147
|
+
input: isShellTool(normalizedTool) ? { command: subject } : input,
|
|
1115
1148
|
};
|
|
1116
1149
|
const outcome = await runClassifier(toolName, input, subject, ctx, result.cause !== "protected-path", {
|
|
1117
1150
|
cwd,
|
|
@@ -1183,9 +1216,9 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1183
1216
|
};
|
|
1184
1217
|
};
|
|
1185
1218
|
|
|
1186
|
-
pi
|
|
1219
|
+
registerLocalCommand(pi, "permissions", {
|
|
1187
1220
|
description: "Show permission mode and loaded rules",
|
|
1188
|
-
handler: async (
|
|
1221
|
+
handler: async (args, ctx) => {
|
|
1189
1222
|
const fmt = (rules: PermissionRule[]) => (rules.length ? rules.map((r) => r.raw).join(", ") : "(none)");
|
|
1190
1223
|
const denials = pauseTracker.recentDenials();
|
|
1191
1224
|
const autoLines =
|
|
@@ -1249,7 +1282,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1249
1282
|
// there is no harm floor to short-circuit for a run that already happened.
|
|
1250
1283
|
const childEntries: TranscriptEntry[] = actions.map((action) => {
|
|
1251
1284
|
const tool = normalizeToolName(action.toolName);
|
|
1252
|
-
return { kind: "tool", tool, input: tool
|
|
1285
|
+
return { kind: "tool", tool, input: isShellTool(tool) ? { command: action.subject } : { subject: action.subject } };
|
|
1253
1286
|
});
|
|
1254
1287
|
const verdict = await classify(
|
|
1255
1288
|
{
|
|
@@ -1307,16 +1340,12 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1307
1340
|
}
|
|
1308
1341
|
// Background/resident: the spawning call already returned, so there is no
|
|
1309
1342
|
// tool_result to attach to. The emitter holds its completion report until
|
|
1310
|
-
// we answer `onReview`, so the verdict travels with the report
|
|
1311
|
-
// without the callback gets the older standalone notice instead). Answer
|
|
1343
|
+
// we answer `onReview`, so the verdict travels with the report. Answer
|
|
1312
1344
|
// synchronously when no review will run, so the report is never delayed
|
|
1313
1345
|
// for nothing. A review that throws fails closed: the report goes out
|
|
1314
1346
|
// flagged, not clean.
|
|
1315
|
-
const respond =
|
|
1316
|
-
|
|
1317
|
-
((flag: string | undefined) => {
|
|
1318
|
-
if (flag) notifyTask("subagent-review", flag);
|
|
1319
|
-
});
|
|
1347
|
+
const respond = payload.onReview;
|
|
1348
|
+
if (!respond) return; // every background emitter supplies the callback (hand-back-review.ts)
|
|
1320
1349
|
const ctx = lastReviewCtx;
|
|
1321
1350
|
if (mode !== "auto" || payload.actions.length === 0 || pauseTracker.isPaused() || !ctx) {
|
|
1322
1351
|
respond(undefined);
|
|
@@ -1326,12 +1355,12 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1326
1355
|
const epoch = sessionEpoch;
|
|
1327
1356
|
// A review that outlives its session (a /clear mid-review) is dropped: the
|
|
1328
1357
|
// agents were stopped with the old session and its ctx no longer renders.
|
|
1329
|
-
const respondIfCurrent = (
|
|
1330
|
-
if (epoch === sessionEpoch) respond(
|
|
1358
|
+
const respondIfCurrent = (verdict: HandBackVerdict | undefined) => {
|
|
1359
|
+
if (epoch === sessionEpoch) respond(verdict);
|
|
1331
1360
|
};
|
|
1332
1361
|
reviewCompletedRun(payload.actions, ctx, label, new AbortController().signal)
|
|
1333
|
-
.then((reason) => respondIfCurrent(reason ?
|
|
1334
|
-
.catch((error) => respondIfCurrent(
|
|
1362
|
+
.then((reason) => respondIfCurrent(reason ? { kind: "blocked", reason } : undefined))
|
|
1363
|
+
.catch((error) => respondIfCurrent({ kind: "unavailable", reason: `the review itself failed (${(error as Error).message})` }));
|
|
1335
1364
|
});
|
|
1336
1365
|
|
|
1337
1366
|
pi.on("tool_result", async (event, ctx) => {
|
|
@@ -1533,7 +1562,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1533
1562
|
}
|
|
1534
1563
|
};
|
|
1535
1564
|
|
|
1536
|
-
pi
|
|
1565
|
+
registerLocalCommand(pi, "auto-mode", {
|
|
1537
1566
|
description: "Auto-mode classifier: /auto-mode [setup|defaults|config|model [provider/model-id|clear]]",
|
|
1538
1567
|
getArgumentCompletions: () =>
|
|
1539
1568
|
[
|
|
@@ -1619,7 +1648,7 @@ export default function permissionsExtension(pi: ExtensionAPI) {
|
|
|
1619
1648
|
},
|
|
1620
1649
|
});
|
|
1621
1650
|
|
|
1622
|
-
pi
|
|
1651
|
+
registerLocalCommand(pi, "allow", {
|
|
1623
1652
|
description: 'Persist an allow rule: /allow Bash(npm test:*) [global]',
|
|
1624
1653
|
handler: async (args, ctx) => {
|
|
1625
1654
|
const global = /\s+global$/.test(args.trim());
|