pushary 1.8.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/CHANGELOG.md +1943 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/data/SKILL.md +591 -0
- package/data/cowork/SKILL.md +77 -0
- package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
- package/data/cursor-plugin/CHANGELOG.md +70 -0
- package/data/cursor-plugin/CONTRIBUTING.md +40 -0
- package/data/cursor-plugin/LICENSE +21 -0
- package/data/cursor-plugin/README.md +113 -0
- package/data/cursor-plugin/SECURITY.md +33 -0
- package/data/cursor-plugin/assets/logo.png +0 -0
- package/data/cursor-plugin/commands/notify-when-done.md +14 -0
- package/data/cursor-plugin/commands/pushary-test.md +13 -0
- package/data/cursor-plugin/hooks/hooks.json +75 -0
- package/data/cursor-plugin/mcp.json +11 -0
- package/data/cursor-plugin/rules/pushary.mdc +40 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
- package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
- package/data/cursor-plugin/scripts/redaction.mjs +51 -0
- package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
- package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
- package/data/vscode-plugin/.mcp.json +11 -0
- package/data/vscode-plugin/CHANGELOG.md +49 -0
- package/data/vscode-plugin/CONTRIBUTING.md +40 -0
- package/data/vscode-plugin/LICENSE +21 -0
- package/data/vscode-plugin/README.md +128 -0
- package/data/vscode-plugin/SECURITY.md +56 -0
- package/data/vscode-plugin/assets/logo.png +0 -0
- package/data/vscode-plugin/commands/notify-when-done.md +14 -0
- package/data/vscode-plugin/commands/pushary-test.md +13 -0
- package/data/vscode-plugin/hooks/hooks.json +60 -0
- package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
- package/data/vscode-plugin/scripts/redaction.mjs +51 -0
- package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
- package/dist/bin/pushary-bell-hook.d.ts +1 -0
- package/dist/bin/pushary-bell-hook.js +46 -0
- package/dist/bin/pushary-bell.d.ts +1 -0
- package/dist/bin/pushary-bell.js +146 -0
- package/dist/bin/pushary-claude.d.ts +1 -0
- package/dist/bin/pushary-claude.js +1916 -0
- package/dist/bin/pushary-clean.d.ts +1 -0
- package/dist/bin/pushary-clean.js +1300 -0
- package/dist/bin/pushary-codex-bridge.d.ts +1 -0
- package/dist/bin/pushary-codex-bridge.js +599 -0
- package/dist/bin/pushary-codex-hook.d.ts +1 -0
- package/dist/bin/pushary-codex-hook.js +886 -0
- package/dist/bin/pushary-codex.d.ts +1 -0
- package/dist/bin/pushary-codex.js +125 -0
- package/dist/bin/pushary-connect.d.ts +1 -0
- package/dist/bin/pushary-connect.js +100 -0
- package/dist/bin/pushary-cowork.d.ts +1 -0
- package/dist/bin/pushary-cowork.js +36 -0
- package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
- package/dist/bin/pushary-daemon-supervisor.js +27 -0
- package/dist/bin/pushary-daemon.d.ts +1 -0
- package/dist/bin/pushary-daemon.js +878 -0
- package/dist/bin/pushary-disconnect.d.ts +1 -0
- package/dist/bin/pushary-disconnect.js +264 -0
- package/dist/bin/pushary-doctor.d.ts +1 -0
- package/dist/bin/pushary-doctor.js +1433 -0
- package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
- package/dist/bin/pushary-elicitation-hook.js +149 -0
- package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
- package/dist/bin/pushary-gemini-bridge.js +339 -0
- package/dist/bin/pushary-gemini-hook.d.ts +1 -0
- package/dist/bin/pushary-gemini-hook.js +381 -0
- package/dist/bin/pushary-hook.d.ts +1 -0
- package/dist/bin/pushary-hook.js +95 -0
- package/dist/bin/pushary-login.d.ts +1 -0
- package/dist/bin/pushary-login.js +198 -0
- package/dist/bin/pushary-logout.d.ts +1 -0
- package/dist/bin/pushary-logout.js +147 -0
- package/dist/bin/pushary-mcp.d.ts +1 -0
- package/dist/bin/pushary-mcp.js +114 -0
- package/dist/bin/pushary-mode.d.ts +1 -0
- package/dist/bin/pushary-mode.js +130 -0
- package/dist/bin/pushary-notification-hook.d.ts +1 -0
- package/dist/bin/pushary-notification-hook.js +56 -0
- package/dist/bin/pushary-opencode-hook.d.ts +1 -0
- package/dist/bin/pushary-opencode-hook.js +338 -0
- package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
- package/dist/bin/pushary-permission-denied-hook.js +60 -0
- package/dist/bin/pushary-permission-hook.d.ts +1 -0
- package/dist/bin/pushary-permission-hook.js +65 -0
- package/dist/bin/pushary-post-hook.d.ts +1 -0
- package/dist/bin/pushary-post-hook.js +70 -0
- package/dist/bin/pushary-prompt-hook.d.ts +1 -0
- package/dist/bin/pushary-prompt-hook.js +61 -0
- package/dist/bin/pushary-session-end-hook.d.ts +1 -0
- package/dist/bin/pushary-session-end-hook.js +59 -0
- package/dist/bin/pushary-session-start-hook.d.ts +1 -0
- package/dist/bin/pushary-session-start-hook.js +72 -0
- package/dist/bin/pushary-setup.d.ts +1 -0
- package/dist/bin/pushary-setup.js +2660 -0
- package/dist/bin/pushary-stats.d.ts +1 -0
- package/dist/bin/pushary-stats.js +44 -0
- package/dist/bin/pushary-status.d.ts +1 -0
- package/dist/bin/pushary-status.js +262 -0
- package/dist/bin/pushary-stop-hook.d.ts +1 -0
- package/dist/bin/pushary-stop-hook.js +63 -0
- package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
- package/dist/bin/pushary-stopfailure-hook.js +56 -0
- package/dist/bin/pushary-suggestions.d.ts +1 -0
- package/dist/bin/pushary-suggestions.js +100 -0
- package/dist/bin/pushary-transcript-register.d.ts +1 -0
- package/dist/bin/pushary-transcript-register.js +40 -0
- package/dist/bin/pushary-transcripts.d.ts +1 -0
- package/dist/bin/pushary-transcripts.js +77 -0
- package/dist/bin/pushary-upgrade.d.ts +1 -0
- package/dist/bin/pushary-upgrade.js +599 -0
- package/dist/bin/pushary-wait.d.ts +1 -0
- package/dist/bin/pushary-wait.js +122 -0
- package/dist/bin/pushary.d.ts +1 -0
- package/dist/bin/pushary.js +74 -0
- package/dist/chunk-2ABTSGFT.js +173 -0
- package/dist/chunk-2OY3ZZ5N.js +124 -0
- package/dist/chunk-3EGEA4KH.js +44 -0
- package/dist/chunk-3EVPNIBE.js +185 -0
- package/dist/chunk-5OP5MQI7.js +45 -0
- package/dist/chunk-6J2JC2RT.js +149 -0
- package/dist/chunk-7QLSKOSU.js +19 -0
- package/dist/chunk-7Z7Q4GGS.js +88 -0
- package/dist/chunk-A2A2YTMG.js +835 -0
- package/dist/chunk-AHV4LNB5.js +377 -0
- package/dist/chunk-AXPCESYO.js +197 -0
- package/dist/chunk-C2WCPBF7.js +86 -0
- package/dist/chunk-CJVVKOSR.js +73 -0
- package/dist/chunk-CULUZTWQ.js +2117 -0
- package/dist/chunk-CVQYGMSR.js +142 -0
- package/dist/chunk-CVWG2N3Y.js +16 -0
- package/dist/chunk-D2FX3EPW.js +15 -0
- package/dist/chunk-DB3ONZB4.js +44 -0
- package/dist/chunk-DE4Y6TCC.js +160 -0
- package/dist/chunk-E4FJTUH4.js +249 -0
- package/dist/chunk-EPCXUP3P.js +41 -0
- package/dist/chunk-EX3GAPA4.js +44 -0
- package/dist/chunk-EYK3GSRH.js +143 -0
- package/dist/chunk-GMXKITVA.js +70 -0
- package/dist/chunk-GPEGOGRJ.js +49 -0
- package/dist/chunk-GU3EJCVV.js +43 -0
- package/dist/chunk-HQ6G3B5R.js +59 -0
- package/dist/chunk-HZVVNGEW.js +22 -0
- package/dist/chunk-IPVKH2ST.js +231 -0
- package/dist/chunk-J5TIV3J6.js +238 -0
- package/dist/chunk-JU6W3XAU.js +64 -0
- package/dist/chunk-JYG4B33G.js +227 -0
- package/dist/chunk-JZTHGAPA.js +503 -0
- package/dist/chunk-KGDWDSNL.js +102 -0
- package/dist/chunk-KVQBWI5T.js +99 -0
- package/dist/chunk-KZERVKTD.js +16 -0
- package/dist/chunk-LXQ5FBBD.js +47 -0
- package/dist/chunk-M5ICYGHT.js +138 -0
- package/dist/chunk-M5JDZ45W.js +943 -0
- package/dist/chunk-MDEPU45E.js +1226 -0
- package/dist/chunk-MPWXGONV.js +157 -0
- package/dist/chunk-N6IOSZOE.js +185 -0
- package/dist/chunk-NGDD6TXY.js +192 -0
- package/dist/chunk-NQTHFF4W.js +2830 -0
- package/dist/chunk-OKXS7WDZ.js +68 -0
- package/dist/chunk-OLQRRFBX.js +21 -0
- package/dist/chunk-P7WQEVWA.js +307 -0
- package/dist/chunk-POFBE6ZY.js +12 -0
- package/dist/chunk-PQ4K74WL.js +30 -0
- package/dist/chunk-PUZZELE6.js +37 -0
- package/dist/chunk-Q7E4I6UA.js +767 -0
- package/dist/chunk-QCGNGEGE.js +15 -0
- package/dist/chunk-R7437YI3.js +151 -0
- package/dist/chunk-RHR6NARM.js +305 -0
- package/dist/chunk-SAXBC4AZ.js +60 -0
- package/dist/chunk-SGZJ5LYH.js +76 -0
- package/dist/chunk-SIFVFP6T.js +70 -0
- package/dist/chunk-SLFABHW3.js +150 -0
- package/dist/chunk-T7E75EKA.js +67 -0
- package/dist/chunk-TY4JT7W3.js +38 -0
- package/dist/chunk-U4TMAANR.js +111 -0
- package/dist/chunk-UI36QSBI.js +177 -0
- package/dist/chunk-VQKRG3AC.js +339 -0
- package/dist/chunk-WE2J62NC.js +1131 -0
- package/dist/chunk-XHKBHWLX.js +14 -0
- package/dist/chunk-ZSZSSJCV.js +275 -0
- package/dist/chunk-ZVSKMPBZ.js +135 -0
- package/dist/reapply-CW3MA66V.js +34 -0
- package/dist/src/index.d.ts +204 -0
- package/dist/src/index.js +64 -0
- package/package.json +108 -0
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_BRIDGE_BINARY
|
|
3
|
+
} from "./chunk-SLFABHW3.js";
|
|
4
|
+
import {
|
|
5
|
+
openCodeConfigDirFrom
|
|
6
|
+
} from "./chunk-AHV4LNB5.js";
|
|
7
|
+
import {
|
|
8
|
+
KEY_FILE_MODE
|
|
9
|
+
} from "./chunk-P7WQEVWA.js";
|
|
10
|
+
import {
|
|
11
|
+
NpmMissingError,
|
|
12
|
+
codexHomeFrom,
|
|
13
|
+
resolvePathBinary
|
|
14
|
+
} from "./chunk-A2A2YTMG.js";
|
|
15
|
+
import {
|
|
16
|
+
readJsonSafe,
|
|
17
|
+
writeFileAtomic,
|
|
18
|
+
writeJsonAtomic
|
|
19
|
+
} from "./chunk-T7E75EKA.js";
|
|
20
|
+
import {
|
|
21
|
+
NPX_LATEST_COMMAND
|
|
22
|
+
} from "./chunk-E4FJTUH4.js";
|
|
23
|
+
import {
|
|
24
|
+
HOOK_COMPATIBLE_PROFILES
|
|
25
|
+
} from "./chunk-NQTHFF4W.js";
|
|
26
|
+
|
|
27
|
+
// src/hermes-config.ts
|
|
28
|
+
import { execFileSync } from "child_process";
|
|
29
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
30
|
+
import { homedir, tmpdir } from "os";
|
|
31
|
+
import { join } from "path";
|
|
32
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
33
|
+
var isSnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
|
|
34
|
+
var isHermesConfigReceipt = (value) => {
|
|
35
|
+
if (!isRecord(value)) return false;
|
|
36
|
+
return ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"].every((key) => isSnapshot(value[key])) && typeof value.pushary_enabled === "boolean" && typeof value.clarify_disabled === "boolean";
|
|
37
|
+
};
|
|
38
|
+
var HERMES_PIP_PACKAGE = "hermes-plugin-pushary";
|
|
39
|
+
var HERMES_PLUGIN_NAME = "pushary";
|
|
40
|
+
var HERMES_CONFIG_SCRIPT = String.raw`
|
|
41
|
+
import copy, importlib.util, json, os, sys
|
|
42
|
+
from hermes_cli.config import load_config, save_config
|
|
43
|
+
|
|
44
|
+
PUSHARY_TRANSPORT = "pushary"
|
|
45
|
+
PUSHARY_FALLBACK = "builtin"
|
|
46
|
+
PUSHARY_PLUGIN = "pushary"
|
|
47
|
+
PUSHARY_PLUGIN_MODULE = "pushary_plugin"
|
|
48
|
+
QUESTION_TOOLSET = "clarify"
|
|
49
|
+
|
|
50
|
+
mode = sys.argv[1]
|
|
51
|
+
receipt_path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
52
|
+
c = load_config()
|
|
53
|
+
original = copy.deepcopy(c)
|
|
54
|
+
|
|
55
|
+
def section(obj, key):
|
|
56
|
+
value = obj.get(key)
|
|
57
|
+
return value if isinstance(value, dict) else {}
|
|
58
|
+
|
|
59
|
+
def members(obj, key):
|
|
60
|
+
value = obj.get(key)
|
|
61
|
+
return value if isinstance(value, list) else []
|
|
62
|
+
|
|
63
|
+
if mode == "status":
|
|
64
|
+
print(json.dumps({
|
|
65
|
+
"plugin_installed": importlib.util.find_spec(PUSHARY_PLUGIN_MODULE) is not None,
|
|
66
|
+
"plugin_enabled": PUSHARY_PLUGIN in members(section(c, "plugins"), "enabled"),
|
|
67
|
+
"approvals_routed": section(section(c, "security"), "approval").get("transport") == PUSHARY_TRANSPORT,
|
|
68
|
+
"questions_routed": QUESTION_TOOLSET in members(section(c, "agent"), "disabled_toolsets"),
|
|
69
|
+
}))
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
def recorded(snapshot, value):
|
|
73
|
+
return bool(snapshot.get("present")) and snapshot.get("value") == value
|
|
74
|
+
|
|
75
|
+
def pop_if(obj, key, value):
|
|
76
|
+
if obj.get(key) == value:
|
|
77
|
+
obj.pop(key, None)
|
|
78
|
+
|
|
79
|
+
def snap(obj, key):
|
|
80
|
+
return {"present": key in obj, "value": obj.get(key)}
|
|
81
|
+
|
|
82
|
+
restore_conflicts = []
|
|
83
|
+
|
|
84
|
+
def restore(obj, key, before, ours):
|
|
85
|
+
if before.get("present") and before.get("value") == ours:
|
|
86
|
+
return
|
|
87
|
+
if before.get("present"):
|
|
88
|
+
if key in obj and obj.get(key) == before.get("value"):
|
|
89
|
+
return
|
|
90
|
+
elif key not in obj:
|
|
91
|
+
return
|
|
92
|
+
if obj.get(key) != ours:
|
|
93
|
+
restore_conflicts.append(key)
|
|
94
|
+
return
|
|
95
|
+
if before.get("present"):
|
|
96
|
+
obj[key] = before.get("value")
|
|
97
|
+
else:
|
|
98
|
+
obj.pop(key, None)
|
|
99
|
+
|
|
100
|
+
def restore_container(obj, key, before, current, changed):
|
|
101
|
+
raw = obj.get(key)
|
|
102
|
+
if isinstance(before.get("value"), dict):
|
|
103
|
+
if not isinstance(raw, dict):
|
|
104
|
+
if changed:
|
|
105
|
+
restore_conflicts.append(key)
|
|
106
|
+
return
|
|
107
|
+
if current or before.get("present"):
|
|
108
|
+
obj[key] = current
|
|
109
|
+
else:
|
|
110
|
+
obj.pop(key, None)
|
|
111
|
+
elif isinstance(raw, dict) and current:
|
|
112
|
+
restore_conflicts.append(key)
|
|
113
|
+
else:
|
|
114
|
+
restore(obj, key, before, {})
|
|
115
|
+
|
|
116
|
+
def restore_list_member(obj, key, before, ours, ours_before):
|
|
117
|
+
raw = obj.get(key)
|
|
118
|
+
if isinstance(before.get("value"), list):
|
|
119
|
+
if ours_before:
|
|
120
|
+
return
|
|
121
|
+
if not isinstance(raw, list):
|
|
122
|
+
restore_conflicts.append(key)
|
|
123
|
+
return
|
|
124
|
+
remaining = [item for item in raw if item != ours]
|
|
125
|
+
if remaining or before.get("present"):
|
|
126
|
+
obj[key] = remaining
|
|
127
|
+
else:
|
|
128
|
+
obj.pop(key, None)
|
|
129
|
+
return
|
|
130
|
+
if before.get("present") and raw == before.get("value"):
|
|
131
|
+
return
|
|
132
|
+
if not before.get("present") and key not in obj:
|
|
133
|
+
return
|
|
134
|
+
if not isinstance(raw, list):
|
|
135
|
+
restore_conflicts.append(key)
|
|
136
|
+
return
|
|
137
|
+
remaining = [item for item in raw if item != ours]
|
|
138
|
+
if remaining:
|
|
139
|
+
restore_conflicts.append(key)
|
|
140
|
+
else:
|
|
141
|
+
restore(obj, key, before, raw)
|
|
142
|
+
|
|
143
|
+
def valid_snapshot(value):
|
|
144
|
+
return isinstance(value, dict) and isinstance(value.get("present"), bool) and "value" in value
|
|
145
|
+
|
|
146
|
+
def valid_receipt(value):
|
|
147
|
+
snapshots = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"]
|
|
148
|
+
return (isinstance(value, dict)
|
|
149
|
+
and all(valid_snapshot(value.get(key)) for key in snapshots)
|
|
150
|
+
and isinstance(value.get("pushary_enabled"), bool)
|
|
151
|
+
and isinstance(value.get("clarify_disabled"), bool))
|
|
152
|
+
|
|
153
|
+
if mode == "setup":
|
|
154
|
+
security_before = snap(c, "security")
|
|
155
|
+
s = section(c, "security")
|
|
156
|
+
approval_before = snap(s, "approval")
|
|
157
|
+
ap = section(s, "approval")
|
|
158
|
+
plugins_before = snap(c, "plugins")
|
|
159
|
+
p = section(c, "plugins")
|
|
160
|
+
agent_before = snap(c, "agent")
|
|
161
|
+
a = section(c, "agent")
|
|
162
|
+
enabled = members(p, "enabled")
|
|
163
|
+
disabled = members(a, "disabled_toolsets")
|
|
164
|
+
receipt = {
|
|
165
|
+
"security": security_before,
|
|
166
|
+
"approval": approval_before,
|
|
167
|
+
"transport": snap(ap, "transport"),
|
|
168
|
+
"fallback": snap(ap, "transport_fallback"),
|
|
169
|
+
"plugins": plugins_before,
|
|
170
|
+
"enabled": snap(p, "enabled"),
|
|
171
|
+
"pushary_enabled": PUSHARY_PLUGIN in enabled,
|
|
172
|
+
"agent": agent_before,
|
|
173
|
+
"disabled": snap(a, "disabled_toolsets"),
|
|
174
|
+
"clarify_disabled": QUESTION_TOOLSET in disabled,
|
|
175
|
+
}
|
|
176
|
+
if os.path.exists(receipt_path):
|
|
177
|
+
with open(receipt_path, encoding="utf-8") as handle:
|
|
178
|
+
existing_receipt = json.load(handle)
|
|
179
|
+
if not valid_receipt(existing_receipt):
|
|
180
|
+
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
181
|
+
else:
|
|
182
|
+
os.makedirs(os.path.dirname(receipt_path), exist_ok=True)
|
|
183
|
+
temp = receipt_path + ".tmp"
|
|
184
|
+
with open(temp, "x", encoding="utf-8") as handle:
|
|
185
|
+
json.dump(receipt, handle)
|
|
186
|
+
os.chmod(temp, 0o600)
|
|
187
|
+
os.replace(temp, receipt_path)
|
|
188
|
+
ap["transport"] = PUSHARY_TRANSPORT
|
|
189
|
+
ap["transport_fallback"] = PUSHARY_FALLBACK
|
|
190
|
+
s["approval"] = ap
|
|
191
|
+
c["security"] = s
|
|
192
|
+
p["enabled"] = enabled if PUSHARY_PLUGIN in enabled else enabled + [PUSHARY_PLUGIN]
|
|
193
|
+
c["plugins"] = p
|
|
194
|
+
a["disabled_toolsets"] = disabled if QUESTION_TOOLSET in disabled else disabled + [QUESTION_TOOLSET]
|
|
195
|
+
c["agent"] = a
|
|
196
|
+
else:
|
|
197
|
+
receipt_exists = os.path.exists(receipt_path)
|
|
198
|
+
try:
|
|
199
|
+
with open(receipt_path, encoding="utf-8") as handle:
|
|
200
|
+
receipt = json.load(handle)
|
|
201
|
+
except (OSError, ValueError):
|
|
202
|
+
if receipt_exists:
|
|
203
|
+
raise
|
|
204
|
+
receipt = None
|
|
205
|
+
s = section(c, "security")
|
|
206
|
+
ap = section(s, "approval")
|
|
207
|
+
if receipt:
|
|
208
|
+
if not valid_receipt(receipt):
|
|
209
|
+
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
210
|
+
approval_changed = (not receipt["transport"].get("present")
|
|
211
|
+
or receipt["transport"].get("value") != PUSHARY_TRANSPORT
|
|
212
|
+
or not receipt["fallback"].get("present")
|
|
213
|
+
or receipt["fallback"].get("value") != PUSHARY_FALLBACK)
|
|
214
|
+
plugins_changed = not (isinstance(receipt["enabled"].get("value"), list)
|
|
215
|
+
and receipt["pushary_enabled"])
|
|
216
|
+
agent_changed = not (isinstance(receipt["disabled"].get("value"), list)
|
|
217
|
+
and receipt["clarify_disabled"])
|
|
218
|
+
transport_left_over = recorded(receipt["transport"], PUSHARY_TRANSPORT)
|
|
219
|
+
if transport_left_over:
|
|
220
|
+
pop_if(ap, "transport", PUSHARY_TRANSPORT)
|
|
221
|
+
else:
|
|
222
|
+
restore(ap, "transport", receipt["transport"], PUSHARY_TRANSPORT)
|
|
223
|
+
if transport_left_over and recorded(receipt["fallback"], PUSHARY_FALLBACK):
|
|
224
|
+
pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
|
|
225
|
+
else:
|
|
226
|
+
restore(ap, "transport_fallback", receipt["fallback"], PUSHARY_FALLBACK)
|
|
227
|
+
else:
|
|
228
|
+
pop_if(ap, "transport", PUSHARY_TRANSPORT)
|
|
229
|
+
pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
|
|
230
|
+
if receipt:
|
|
231
|
+
restore_container(s, "approval", receipt["approval"], ap, approval_changed)
|
|
232
|
+
restore_container(c, "security", receipt["security"], s, approval_changed)
|
|
233
|
+
else:
|
|
234
|
+
if ap: s["approval"] = ap
|
|
235
|
+
else: s.pop("approval", None)
|
|
236
|
+
if s: c["security"] = s
|
|
237
|
+
else: c.pop("security", None)
|
|
238
|
+
|
|
239
|
+
p = section(c, "plugins")
|
|
240
|
+
if receipt:
|
|
241
|
+
restore_list_member(p, "enabled", receipt["enabled"], PUSHARY_PLUGIN, False)
|
|
242
|
+
restore_container(c, "plugins", receipt["plugins"], p, plugins_changed)
|
|
243
|
+
else:
|
|
244
|
+
enabled = [item for item in members(p, "enabled") if item != PUSHARY_PLUGIN]
|
|
245
|
+
if enabled: p["enabled"] = enabled
|
|
246
|
+
else: p.pop("enabled", None)
|
|
247
|
+
if p: c["plugins"] = p
|
|
248
|
+
else: c.pop("plugins", None)
|
|
249
|
+
|
|
250
|
+
a = section(c, "agent")
|
|
251
|
+
if receipt:
|
|
252
|
+
clarify_was_users = receipt["clarify_disabled"] and not recorded(receipt["transport"], PUSHARY_TRANSPORT)
|
|
253
|
+
restore_list_member(a, "disabled_toolsets", receipt["disabled"], QUESTION_TOOLSET, clarify_was_users)
|
|
254
|
+
restore_container(c, "agent", receipt["agent"], a, agent_changed)
|
|
255
|
+
else:
|
|
256
|
+
disabled = [item for item in members(a, "disabled_toolsets") if item != QUESTION_TOOLSET]
|
|
257
|
+
if disabled: a["disabled_toolsets"] = disabled
|
|
258
|
+
else: a.pop("disabled_toolsets", None)
|
|
259
|
+
if a: c["agent"] = a
|
|
260
|
+
else: c.pop("agent", None)
|
|
261
|
+
|
|
262
|
+
if receipt and restore_conflicts:
|
|
263
|
+
raise ValueError("Hermes config changed after Pushary setup")
|
|
264
|
+
|
|
265
|
+
changed = c != original
|
|
266
|
+
if changed and mode != "clean-preview":
|
|
267
|
+
save_config(c)
|
|
268
|
+
print(json.dumps({"changed": changed}))
|
|
269
|
+
`;
|
|
270
|
+
var resolveHermesPython = () => {
|
|
271
|
+
const launcher = resolvePathBinary("hermes");
|
|
272
|
+
if (launcher && process.platform !== "win32") {
|
|
273
|
+
try {
|
|
274
|
+
const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
|
|
275
|
+
const interpreter = shebang?.startsWith("#!") ? shebang.slice(2).trim().split(/\s+/)[0] : void 0;
|
|
276
|
+
if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const root = join(homedir(), ".hermes", "hermes-agent", "venv");
|
|
281
|
+
const candidates = process.platform === "win32" ? [join(root, "Scripts", "python.exe"), join(root, "Scripts", "python3.exe")] : [join(root, "bin", "python3"), join(root, "bin", "python")];
|
|
282
|
+
return candidates.find(existsSync) ?? null;
|
|
283
|
+
};
|
|
284
|
+
var HERMES_SCRIPT_TIMEOUT_MS = 15e3;
|
|
285
|
+
var runPythonScript = (python, script, args) => {
|
|
286
|
+
const path = join(tmpdir(), `pushary-hermes-${process.pid}-${Date.now()}.py`);
|
|
287
|
+
try {
|
|
288
|
+
writeFileSync(path, script, { encoding: "utf-8", mode: 384 });
|
|
289
|
+
return execFileSync(python, [path, ...args], {
|
|
290
|
+
env: { ...process.env },
|
|
291
|
+
encoding: "utf-8",
|
|
292
|
+
stdio: "pipe",
|
|
293
|
+
timeout: HERMES_SCRIPT_TIMEOUT_MS
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
} finally {
|
|
298
|
+
try {
|
|
299
|
+
rmSync(path, { force: true });
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var lastReport = (stdout) => {
|
|
305
|
+
const line = stdout.trim().split("\n").at(-1) ?? "";
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(line);
|
|
308
|
+
} catch {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var configRunFrom = (stdout) => {
|
|
313
|
+
const report = stdout === null ? void 0 : lastReport(stdout);
|
|
314
|
+
if (!isRecord(report) || typeof report.changed !== "boolean") return "failed";
|
|
315
|
+
return report.changed ? "changed" : "unchanged";
|
|
316
|
+
};
|
|
317
|
+
var runHermesConfigScript = (python, mode, receiptPath) => configRunFrom(runPythonScript(python, HERMES_CONFIG_SCRIPT, [mode, receiptPath]));
|
|
318
|
+
var HERMES_STATUS_FIELDS = ["plugin_installed", "plugin_enabled", "approvals_routed", "questions_routed"];
|
|
319
|
+
var isHermesStatusReport = (value) => isRecord(value) && HERMES_STATUS_FIELDS.every((field) => typeof value[field] === "boolean");
|
|
320
|
+
var hermesWiredByPushary = (status, setupRecorded) => setupRecorded || status.pluginEnabled || status.approvalsRouted;
|
|
321
|
+
var readHermesStatus = (python) => {
|
|
322
|
+
const stdout = runPythonScript(python, HERMES_CONFIG_SCRIPT, ["status"]);
|
|
323
|
+
const report = stdout === null ? void 0 : lastReport(stdout);
|
|
324
|
+
if (!isHermesStatusReport(report)) return null;
|
|
325
|
+
return {
|
|
326
|
+
pluginInstalled: report.plugin_installed,
|
|
327
|
+
pluginEnabled: report.plugin_enabled,
|
|
328
|
+
approvalsRouted: report.approvals_routed,
|
|
329
|
+
questionsRouted: report.questions_routed
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/fx-config.ts
|
|
334
|
+
var FX_SERVER_NAME = "pushary";
|
|
335
|
+
var FX_OPERATION_TIMEOUT_MS = 75e3;
|
|
336
|
+
var FX_PROFILE_MAX_BYTES = 1024 * 1024 - 1;
|
|
337
|
+
var FX_SETTINGS_MAX_BYTES = 64 * 1024;
|
|
338
|
+
var FX_PUSHARY_TOOLS_RULE = "mcp_pushary_*";
|
|
339
|
+
var FX_TOOL_DISCOVERY_NOTE = 'In fx, these tools are named mcp_pushary_ask_user, mcp_pushary_send_notification, mcp_pushary_wait_for_answer, mcp_pushary_cancel_question and mcp_pushary_propose_scope. fx does not load them at the start of a turn: call mcp_select_tool with the exact name, for example {"name": "mcp_pushary_ask_user"}, then call the tool. For fx this replaces the guidance above to ask the whole round in the terminal while the user is there: ask every question through mcp_pushary_ask_user, even while the user is at this terminal, rather than through ask_user_question. fx cannot show its own question prompt in the Mac notch or on the phone, so a question asked there waits unseen once the user steps away. Ask in your reply instead only when a Pushary handoffAction tells you to ask here, or when mcp_pushary_ask_user is unavailable or fails without one.';
|
|
340
|
+
var ALLOW_ACTION = "allow";
|
|
341
|
+
var PERMISSION_KEY = "permission";
|
|
342
|
+
var WORKSPACES_KEY = "workspaces";
|
|
343
|
+
var CANONICAL_SERVERS_KEY = "mcp";
|
|
344
|
+
var ALIAS_SERVERS_KEY = "mcpServers";
|
|
345
|
+
var LOCAL_TRANSPORT = "local";
|
|
346
|
+
var SERVER_TRANSPORTS = /* @__PURE__ */ new Set(["local", "stdio", "http", "sse"]);
|
|
347
|
+
var STDIO_TRANSPORTS = /* @__PURE__ */ new Set(["local", "stdio"]);
|
|
348
|
+
var AMBIGUOUS_SERVER_KEYS = /* @__PURE__ */ new Set([
|
|
349
|
+
"mcpserver",
|
|
350
|
+
"mcpservers",
|
|
351
|
+
"modelcontextprotocol",
|
|
352
|
+
"modelcontextprotocolserver",
|
|
353
|
+
"modelcontextprotocolservers",
|
|
354
|
+
"servers"
|
|
355
|
+
]);
|
|
356
|
+
var PROFILE_SCAN_LIMIT = 64;
|
|
357
|
+
var PROFILE_KEY_BYTES_LIMIT = 128;
|
|
358
|
+
var FX_JSON_REFUSALS = {
|
|
359
|
+
"too-large": "is larger than fx reads",
|
|
360
|
+
"unparseable": "is not strict JSON (fx rejects comments, trailing commas and a byte-order mark)",
|
|
361
|
+
"duplicate-key": "repeats a key, which fx rejects",
|
|
362
|
+
"root-not-object": "is not a JSON object"
|
|
363
|
+
};
|
|
364
|
+
var FX_PROFILE_REFUSALS = {
|
|
365
|
+
...FX_JSON_REFUSALS,
|
|
366
|
+
"servers-not-object": 'has an "mcp" or "mcpServers" value that is not an object',
|
|
367
|
+
"ambiguous-server-key": "holds servers under a key fx does not read, so fx refuses to edit it too",
|
|
368
|
+
"too-many-keys": "has more top-level keys than fx checks for misplaced servers, so fx refuses to edit it too"
|
|
369
|
+
};
|
|
370
|
+
var FX_SETTINGS_REFUSALS = {
|
|
371
|
+
...FX_JSON_REFUSALS,
|
|
372
|
+
"permission-not-object": 'has a "permission" value that is not an object',
|
|
373
|
+
"invalid-permission-rule": "has a permission rule fx rejects, so fx ignores the whole file",
|
|
374
|
+
"invalid-permission-mode": "has a permission_mode fx does not recognise, so fx ignores the whole file"
|
|
375
|
+
};
|
|
376
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
377
|
+
var stringTokenEnd = (text, start) => {
|
|
378
|
+
let index = start + 1;
|
|
379
|
+
while (index < text.length && text[index] !== '"') index += text[index] === "\\" ? 2 : 1;
|
|
380
|
+
return index;
|
|
381
|
+
};
|
|
382
|
+
var hasDuplicateObjectKeys = (json) => {
|
|
383
|
+
const scopes = [];
|
|
384
|
+
let expectingKey = false;
|
|
385
|
+
for (let index = 0; index < json.length; index += 1) {
|
|
386
|
+
const char = json[index];
|
|
387
|
+
if (char === '"') {
|
|
388
|
+
const end = stringTokenEnd(json, index);
|
|
389
|
+
const scope = scopes.at(-1);
|
|
390
|
+
if (expectingKey && scope) {
|
|
391
|
+
const key = JSON.parse(json.slice(index, end + 1));
|
|
392
|
+
if (typeof key === "string" && scope.has(key)) return true;
|
|
393
|
+
if (typeof key === "string") scope.add(key);
|
|
394
|
+
expectingKey = false;
|
|
395
|
+
}
|
|
396
|
+
index = end;
|
|
397
|
+
} else if (char === "{") {
|
|
398
|
+
scopes.push(/* @__PURE__ */ new Set());
|
|
399
|
+
expectingKey = true;
|
|
400
|
+
} else if (char === "[") {
|
|
401
|
+
scopes.push(null);
|
|
402
|
+
expectingKey = false;
|
|
403
|
+
} else if (char === "}" || char === "]") {
|
|
404
|
+
scopes.pop();
|
|
405
|
+
expectingKey = false;
|
|
406
|
+
} else if (char === ",") {
|
|
407
|
+
expectingKey = scopes.at(-1) instanceof Set;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
};
|
|
412
|
+
var normalizedServerKey = (key) => key.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
413
|
+
var looksLikeServerEntry = (value) => {
|
|
414
|
+
const entry = asRecord(value);
|
|
415
|
+
if (!entry) return false;
|
|
416
|
+
return typeof entry.command === "string" || Array.isArray(entry.command) || typeof entry.url === "string" || typeof entry.type === "string" && SERVER_TRANSPORTS.has(entry.type);
|
|
417
|
+
};
|
|
418
|
+
var holdsServerEntries = (value) => Object.values(asRecord(value) ?? {}).some(looksLikeServerEntry);
|
|
419
|
+
var isServerMapKey = (key) => key === CANONICAL_SERVERS_KEY || key === ALIAS_SERVERS_KEY;
|
|
420
|
+
var exceedsScanLimit = (value) => Object.keys(asRecord(value) ?? {}).length > PROFILE_SCAN_LIMIT;
|
|
421
|
+
var isAmbiguousServerKey = (key, value) => {
|
|
422
|
+
if (isServerMapKey(key)) return false;
|
|
423
|
+
const scanned = Buffer.byteLength(key) > PROFILE_KEY_BYTES_LIMIT || AMBIGUOUS_SERVER_KEYS.has(normalizedServerKey(key));
|
|
424
|
+
return scanned && (exceedsScanLimit(value) || holdsServerEntries(value));
|
|
425
|
+
};
|
|
426
|
+
var misplacedServersRefusal = (profile) => {
|
|
427
|
+
if (exceedsScanLimit(profile)) return "too-many-keys";
|
|
428
|
+
return Object.keys(profile).some((key) => isAmbiguousServerKey(key, profile[key])) ? "ambiguous-server-key" : null;
|
|
429
|
+
};
|
|
430
|
+
var fxServersKeyOf = (profile) => !(CANONICAL_SERVERS_KEY in profile) && ALIAS_SERVERS_KEY in profile ? ALIAS_SERVERS_KEY : CANONICAL_SERVERS_KEY;
|
|
431
|
+
var refused = (reason) => ({ kind: "refused", reason });
|
|
432
|
+
var parseJson = (text) => {
|
|
433
|
+
try {
|
|
434
|
+
return { ok: true, value: JSON.parse(text) };
|
|
435
|
+
} catch {
|
|
436
|
+
return { ok: false };
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var readFxJsonObject = (text, maxBytes) => {
|
|
440
|
+
if (Buffer.byteLength(text) > maxBytes) return { kind: "refused", reason: "too-large" };
|
|
441
|
+
if (!text.trim()) return { kind: "object", value: {} };
|
|
442
|
+
const parsed = parseJson(text);
|
|
443
|
+
if (!parsed.ok) return { kind: "refused", reason: "unparseable" };
|
|
444
|
+
if (hasDuplicateObjectKeys(text)) return { kind: "refused", reason: "duplicate-key" };
|
|
445
|
+
const value = asRecord(parsed.value);
|
|
446
|
+
return value ? { kind: "object", value } : { kind: "refused", reason: "root-not-object" };
|
|
447
|
+
};
|
|
448
|
+
var readFxProfile = (text) => {
|
|
449
|
+
const read = readFxJsonObject(text, FX_PROFILE_MAX_BYTES);
|
|
450
|
+
if (read.kind === "refused") return refused(read.reason);
|
|
451
|
+
const profile = read.value;
|
|
452
|
+
const serversKey = fxServersKeyOf(profile);
|
|
453
|
+
if (serversKey in profile && !asRecord(profile[serversKey])) return refused("servers-not-object");
|
|
454
|
+
const misplaced = misplacedServersRefusal(profile);
|
|
455
|
+
return misplaced ? refused(misplaced) : { kind: "profile", profile };
|
|
456
|
+
};
|
|
457
|
+
var fxPusharyEntry = (command) => ({
|
|
458
|
+
type: LOCAL_TRANSPORT,
|
|
459
|
+
command,
|
|
460
|
+
operation_timeout_ms: FX_OPERATION_TIMEOUT_MS
|
|
461
|
+
});
|
|
462
|
+
var withFxPusharyServer = (profile, entry) => {
|
|
463
|
+
const serversKey = fxServersKeyOf(profile);
|
|
464
|
+
return {
|
|
465
|
+
...profile,
|
|
466
|
+
[serversKey]: { ...asRecord(profile[serversKey]), [FX_SERVER_NAME]: { ...entry, command: [...entry.command] } }
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
var emptiedMapCanGo = (profile, serversKey) => serversKey === ALIAS_SERVERS_KEY || !(ALIAS_SERVERS_KEY in profile);
|
|
470
|
+
var withoutFxPusharyServer = (profile) => {
|
|
471
|
+
const serversKey = fxServersKeyOf(profile);
|
|
472
|
+
const servers = asRecord(profile[serversKey]);
|
|
473
|
+
if (!servers || !(FX_SERVER_NAME in servers)) return { profile, removed: false };
|
|
474
|
+
const remaining = Object.fromEntries(Object.entries(servers).filter(([name]) => name !== FX_SERVER_NAME));
|
|
475
|
+
const withoutServers = Object.fromEntries(Object.entries(profile).filter(([key]) => key !== serversKey));
|
|
476
|
+
const dropsMap = Object.keys(remaining).length === 0 && emptiedMapCanGo(profile, serversKey);
|
|
477
|
+
return {
|
|
478
|
+
profile: dropsMap ? withoutServers : { ...profile, [serversKey]: remaining },
|
|
479
|
+
removed: true
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
var namesBridge = (part) => typeof part === "string" && part.includes(MCP_BRIDGE_BINARY);
|
|
483
|
+
var commandNamesBridge = (command) => Array.isArray(command) ? command.some(namesBridge) : namesBridge(command);
|
|
484
|
+
var isFxPusharyEntry = (value) => {
|
|
485
|
+
const entry = asRecord(value);
|
|
486
|
+
if (!entry || entry.enabled === false) return false;
|
|
487
|
+
const transport = typeof entry.type === "string" ? entry.type : LOCAL_TRANSPORT;
|
|
488
|
+
return STDIO_TRANSPORTS.has(transport) && (commandNamesBridge(entry.command) || commandNamesBridge(entry.args));
|
|
489
|
+
};
|
|
490
|
+
var fxPusharyEntryOf = (profile) => asRecord(profile[fxServersKeyOf(profile)])?.[FX_SERVER_NAME];
|
|
491
|
+
var isBridgeTarget = (part) => namesBridge(part);
|
|
492
|
+
var fxBridgeTargetOf = (value) => {
|
|
493
|
+
const entry = asRecord(value);
|
|
494
|
+
const parts = [entry?.command, entry?.args].flatMap((part) => Array.isArray(part) ? part : [part]);
|
|
495
|
+
return parts.find(isBridgeTarget);
|
|
496
|
+
};
|
|
497
|
+
var fxFileText = (value, maxBytes) => {
|
|
498
|
+
const text = JSON.stringify(value);
|
|
499
|
+
return Buffer.byteLength(text) > maxBytes ? { kind: "refused", reason: "too-large" } : { kind: "text", text };
|
|
500
|
+
};
|
|
501
|
+
var fxProfileText = (profile) => fxFileText(profile, FX_PROFILE_MAX_BYTES);
|
|
502
|
+
var fxSettingsText = (settings) => fxFileText(settings, FX_SETTINGS_MAX_BYTES);
|
|
503
|
+
var readFxSettings = (text) => {
|
|
504
|
+
const read = readFxJsonObject(text, FX_SETTINGS_MAX_BYTES);
|
|
505
|
+
if (read.kind === "refused") return read;
|
|
506
|
+
const invalid = fxSettingsRejection(read.value);
|
|
507
|
+
if (invalid) return { kind: "refused", reason: invalid };
|
|
508
|
+
const permission = read.value[PERMISSION_KEY];
|
|
509
|
+
if (permission !== void 0 && !asRecord(permission)) return { kind: "refused", reason: "permission-not-object" };
|
|
510
|
+
return { kind: "settings", settings: read.value };
|
|
511
|
+
};
|
|
512
|
+
var withPusharyToolsAllowed = (settings) => {
|
|
513
|
+
const permission = asRecord(settings[PERMISSION_KEY]) ?? {};
|
|
514
|
+
const existing = permission[FX_PUSHARY_TOOLS_RULE];
|
|
515
|
+
if (existing === ALLOW_ACTION) return { settings, change: "present" };
|
|
516
|
+
if (existing !== void 0) return { settings, change: "user-rule-kept" };
|
|
517
|
+
return {
|
|
518
|
+
settings: { ...settings, [PERMISSION_KEY]: { ...permission, [FX_PUSHARY_TOOLS_RULE]: ALLOW_ACTION } },
|
|
519
|
+
change: "added"
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
var withoutPermissionRule = (settings, permission, rule) => {
|
|
523
|
+
const remaining = Object.fromEntries(Object.entries(permission).filter(([name]) => name !== rule));
|
|
524
|
+
const withoutPermission = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== PERMISSION_KEY));
|
|
525
|
+
return Object.keys(remaining).length === 0 ? withoutPermission : { ...settings, [PERMISSION_KEY]: remaining };
|
|
526
|
+
};
|
|
527
|
+
var withoutPusharyToolsAllowed = (settings) => {
|
|
528
|
+
const permission = asRecord(settings[PERMISSION_KEY]);
|
|
529
|
+
if (!permission || permission[FX_PUSHARY_TOOLS_RULE] !== ALLOW_ACTION) return { settings, removed: false };
|
|
530
|
+
return { settings: withoutPermissionRule(settings, permission, FX_PUSHARY_TOOLS_RULE), removed: true };
|
|
531
|
+
};
|
|
532
|
+
var pusharyToolsAllowed = (settings) => asRecord(settings[PERMISSION_KEY])?.[FX_PUSHARY_TOOLS_RULE] === ALLOW_ACTION;
|
|
533
|
+
var overridesPusharyRule = (workspace) => {
|
|
534
|
+
const permission = asRecord(asRecord(workspace)?.[PERMISSION_KEY]);
|
|
535
|
+
return permission !== void 0 && !(FX_PUSHARY_TOOLS_RULE in permission);
|
|
536
|
+
};
|
|
537
|
+
var workspacesOverridingPusharyRule = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => overridesPusharyRule(workspace)).map(([path]) => path);
|
|
538
|
+
var FX_QUESTION_TOOL = "ask_user_question";
|
|
539
|
+
var DENY_ACTION = "deny";
|
|
540
|
+
var GLOBAL_TARGET_PATTERN = "*";
|
|
541
|
+
var PERMISSION_MODE_KEY = "permission_mode";
|
|
542
|
+
var FX_PERMISSION_MODE_ENV = "FX_PERMISSION_MODE";
|
|
543
|
+
var FX_DEFAULT_PERMISSION_MODE = "auto";
|
|
544
|
+
var FX_PERMISSION_MODE_SPELLINGS = {
|
|
545
|
+
ask: "ask",
|
|
546
|
+
auto: "auto",
|
|
547
|
+
"full-access": "full-access",
|
|
548
|
+
"full access": "full-access",
|
|
549
|
+
yolo: "full-access"
|
|
550
|
+
};
|
|
551
|
+
var FX_RULE_WHITESPACE = /^[ \t\r\n]+|[ \t\r\n]+$/g;
|
|
552
|
+
var FX_QUESTION_RULE_RECEIPT = { rule: FX_QUESTION_TOOL, action: DENY_ACTION };
|
|
553
|
+
var isFxQuestionRuleReceipt = (value) => {
|
|
554
|
+
const receipt = asRecord(value);
|
|
555
|
+
return receipt?.rule === FX_QUESTION_TOOL && receipt.action === DENY_ACTION;
|
|
556
|
+
};
|
|
557
|
+
var isFxRuleAction = (value) => value === "allow" || value === "ask" || value === DENY_ACTION;
|
|
558
|
+
var fxRuleAction = (value) => {
|
|
559
|
+
if (typeof value !== "string") return null;
|
|
560
|
+
const action = value.toLowerCase();
|
|
561
|
+
return isFxRuleAction(action) ? action : null;
|
|
562
|
+
};
|
|
563
|
+
var parseFxPermissionMode = (value) => typeof value === "string" ? FX_PERMISSION_MODE_SPELLINGS[value.toLowerCase()] ?? null : null;
|
|
564
|
+
var trimFxRuleText = (text) => text.replace(FX_RULE_WHITESPACE, "");
|
|
565
|
+
var isValidPatternMap = (value) => {
|
|
566
|
+
const patterns = asRecord(value);
|
|
567
|
+
return patterns !== void 0 && Object.values(patterns).every((action) => fxRuleAction(action) !== null);
|
|
568
|
+
};
|
|
569
|
+
var isValidPermissionValue = (value) => {
|
|
570
|
+
if (fxRuleAction(value) !== null) return true;
|
|
571
|
+
const permission = asRecord(value);
|
|
572
|
+
return permission !== void 0 && Object.entries(permission).every(([name, rule]) => trimFxRuleText(name).length > 0 && (fxRuleAction(rule) !== null || isValidPatternMap(rule)));
|
|
573
|
+
};
|
|
574
|
+
var permissionScopeRejection = (scope) => {
|
|
575
|
+
if (scope[PERMISSION_KEY] !== void 0 && !isValidPermissionValue(scope[PERMISSION_KEY])) return "invalid-permission-rule";
|
|
576
|
+
if (scope[PERMISSION_MODE_KEY] !== void 0 && parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) === null) return "invalid-permission-mode";
|
|
577
|
+
return null;
|
|
578
|
+
};
|
|
579
|
+
var workspaceScopes = (settings) => Object.values(asRecord(settings[WORKSPACES_KEY]) ?? {}).flatMap((workspace) => {
|
|
580
|
+
const scope = asRecord(workspace);
|
|
581
|
+
return scope ? [scope] : [];
|
|
582
|
+
});
|
|
583
|
+
var fxSettingsRejection = (settings) => [settings, ...workspaceScopes(settings)].map(permissionScopeRejection).find((rejection) => rejection !== null) ?? null;
|
|
584
|
+
var rulesOfPermissionEntry = ([name, value]) => {
|
|
585
|
+
const permission = trimFxRuleText(name);
|
|
586
|
+
const action = fxRuleAction(value);
|
|
587
|
+
if (action) return [{ permission, pattern: GLOBAL_TARGET_PATTERN, action }];
|
|
588
|
+
return Object.entries(asRecord(value) ?? {}).flatMap(([pattern, target]) => {
|
|
589
|
+
const targetAction = fxRuleAction(target);
|
|
590
|
+
return targetAction ? [{ permission, pattern: trimFxRuleText(pattern), action: targetAction }] : [];
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
var fxPermissionRules = (permission) => {
|
|
594
|
+
const everything = fxRuleAction(permission);
|
|
595
|
+
if (everything) return [{ permission: GLOBAL_TARGET_PATTERN, pattern: GLOBAL_TARGET_PATTERN, action: everything }];
|
|
596
|
+
return Object.entries(asRecord(permission) ?? {}).flatMap(rulesOfPermissionEntry);
|
|
597
|
+
};
|
|
598
|
+
var fxWildcardMatch = (pattern, candidate) => {
|
|
599
|
+
let patternIndex = 0;
|
|
600
|
+
let candidateIndex = 0;
|
|
601
|
+
let starIndex = null;
|
|
602
|
+
let starCandidateIndex = 0;
|
|
603
|
+
while (candidateIndex < candidate.length) {
|
|
604
|
+
if (patternIndex < pattern.length && (pattern[patternIndex] === "?" || pattern[patternIndex] === candidate[candidateIndex])) {
|
|
605
|
+
patternIndex += 1;
|
|
606
|
+
candidateIndex += 1;
|
|
607
|
+
} else if (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
608
|
+
starIndex = patternIndex;
|
|
609
|
+
patternIndex += 1;
|
|
610
|
+
starCandidateIndex = candidateIndex;
|
|
611
|
+
} else if (starIndex === null) {
|
|
612
|
+
return false;
|
|
613
|
+
} else {
|
|
614
|
+
starCandidateIndex += 1;
|
|
615
|
+
candidateIndex = starCandidateIndex;
|
|
616
|
+
patternIndex = starIndex + 1;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
while (pattern[patternIndex] === "*") patternIndex += 1;
|
|
620
|
+
return patternIndex === pattern.length;
|
|
621
|
+
};
|
|
622
|
+
var DIRECTORY_TREE_SUFFIX = "/**";
|
|
623
|
+
var directoryTreeMatches = (pattern, candidate) => {
|
|
624
|
+
if (!pattern.endsWith(DIRECTORY_TREE_SUFFIX)) return false;
|
|
625
|
+
const directory = pattern.slice(0, -DIRECTORY_TREE_SUFFIX.length);
|
|
626
|
+
return candidate === directory || candidate.startsWith(directory) && candidate[directory.length] === "/";
|
|
627
|
+
};
|
|
628
|
+
var targetMatches = (pattern, candidate) => directoryTreeMatches(pattern, candidate) || fxWildcardMatch(pattern, candidate);
|
|
629
|
+
var isGlobalTargetPattern = (pattern) => /^\*+$/.test(pattern);
|
|
630
|
+
var decisionForTarget = (rules, target) => rules.filter((rule) => targetMatches(rule.pattern, target)).at(-1)?.action ?? null;
|
|
631
|
+
var reopensTool = (rules, rule) => rule.action !== DENY_ACTION && decisionForTarget(rules, rule.pattern) !== DENY_ACTION && decisionForTarget(rules, rule.pattern) !== null;
|
|
632
|
+
var fxRulesHideTool = (rules, toolName) => {
|
|
633
|
+
const matching = rules.filter((rule) => fxWildcardMatch(rule.permission, toolName));
|
|
634
|
+
const lastGlobalRule = matching.filter((rule) => isGlobalTargetPattern(rule.pattern)).at(-1);
|
|
635
|
+
if (lastGlobalRule?.action !== DENY_ACTION) return false;
|
|
636
|
+
return !matching.slice(matching.lastIndexOf(lastGlobalRule) + 1).some((rule) => reopensTool(matching, rule));
|
|
637
|
+
};
|
|
638
|
+
var effectivePermissionMode = (scope, environment) => parseFxPermissionMode(environment[FX_PERMISSION_MODE_ENV]) ?? parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) ?? FX_DEFAULT_PERMISSION_MODE;
|
|
639
|
+
var fxRunsWithoutRules = (settings, environment) => effectivePermissionMode(settings, environment) === "full-access";
|
|
640
|
+
var fxQuestionToolHidden = (permission) => fxRulesHideTool(fxPermissionRules(permission), FX_QUESTION_TOOL);
|
|
641
|
+
var fxQuestionRouting = (settings, environment) => {
|
|
642
|
+
if (fxRunsWithoutRules(settings, environment)) return "full-access";
|
|
643
|
+
return fxQuestionToolHidden(settings[PERMISSION_KEY]) ? "routed" : "overridden";
|
|
644
|
+
};
|
|
645
|
+
var withQuestionsRoutedToPushary = (settings) => {
|
|
646
|
+
const permission = asRecord(settings[PERMISSION_KEY]) ?? {};
|
|
647
|
+
const existing = permission[FX_QUESTION_TOOL];
|
|
648
|
+
if (existing === void 0) {
|
|
649
|
+
return { settings: { ...settings, [PERMISSION_KEY]: { ...permission, [FX_QUESTION_TOOL]: DENY_ACTION } }, change: "added" };
|
|
650
|
+
}
|
|
651
|
+
return { settings, change: fxRuleAction(existing) === DENY_ACTION ? "present" : "user-rule-kept" };
|
|
652
|
+
};
|
|
653
|
+
var withoutQuestionsRoutedToPushary = (settings, receipt) => {
|
|
654
|
+
const permission = asRecord(settings[PERMISSION_KEY]);
|
|
655
|
+
if (!permission || permission[FX_QUESTION_TOOL] === void 0) return { settings, outcome: "absent" };
|
|
656
|
+
if (!receipt) return { settings, outcome: "not-ours" };
|
|
657
|
+
if (fxRuleAction(permission[FX_QUESTION_TOOL]) !== DENY_ACTION) return { settings, outcome: "changed-after-setup" };
|
|
658
|
+
return { settings: withoutPermissionRule(settings, permission, FX_QUESTION_TOOL), outcome: "removed" };
|
|
659
|
+
};
|
|
660
|
+
var workspaceShowsQuestionTool = (workspace) => {
|
|
661
|
+
const scope = asRecord(workspace);
|
|
662
|
+
if (!scope) return false;
|
|
663
|
+
if (parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) === "full-access") return true;
|
|
664
|
+
return scope[PERMISSION_KEY] !== void 0 && !fxQuestionToolHidden(scope[PERMISSION_KEY]);
|
|
665
|
+
};
|
|
666
|
+
var workspacesShowingQuestionTool = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => workspaceShowsQuestionTool(workspace)).map(([path]) => path);
|
|
667
|
+
|
|
668
|
+
// src/fx-files.ts
|
|
669
|
+
import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
670
|
+
import { relative, isAbsolute } from "path";
|
|
671
|
+
var readFxFile = (path, read) => {
|
|
672
|
+
if (!existsSync2(path)) return { kind: "missing" };
|
|
673
|
+
let text;
|
|
674
|
+
try {
|
|
675
|
+
text = readFileSync2(path, "utf-8");
|
|
676
|
+
} catch {
|
|
677
|
+
return { kind: "unreadable" };
|
|
678
|
+
}
|
|
679
|
+
return read(text);
|
|
680
|
+
};
|
|
681
|
+
var isInside = (child, parent) => {
|
|
682
|
+
const path = relative(parent, child);
|
|
683
|
+
return path !== "" && !path.startsWith("..") && !isAbsolute(path);
|
|
684
|
+
};
|
|
685
|
+
var fxIgnoresInstructionsFile = (path, fxDir) => {
|
|
686
|
+
const link = lstatSync(path, { throwIfNoEntry: false });
|
|
687
|
+
if (!link?.isSymbolicLink()) return false;
|
|
688
|
+
try {
|
|
689
|
+
return !isInside(realpathSync(path), realpathSync(fxDir));
|
|
690
|
+
} catch {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/setup/agent-failure.ts
|
|
696
|
+
var AgentConfigError = class extends Error {
|
|
697
|
+
kind;
|
|
698
|
+
filePath;
|
|
699
|
+
constructor(kind, filePath, message) {
|
|
700
|
+
super(message);
|
|
701
|
+
this.name = "AgentConfigError";
|
|
702
|
+
this.kind = kind;
|
|
703
|
+
this.filePath = filePath;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var configFailureKind = (readFailure) => readFailure === "unparseable" ? "config-unparseable" : "config-unreadable";
|
|
707
|
+
var AGENT_FAILURE_DETAILS = {
|
|
708
|
+
"not-wired": "reported success but its config is not there.",
|
|
709
|
+
"app-account-mismatch": "Mac app hooks kept, but their Pushary account could not be matched to this setup."
|
|
710
|
+
};
|
|
711
|
+
var describedAgentFailure = (agent, kind) => ({
|
|
712
|
+
agent,
|
|
713
|
+
kind,
|
|
714
|
+
detail: AGENT_FAILURE_DETAILS[kind],
|
|
715
|
+
remedy: "retry-agent"
|
|
716
|
+
});
|
|
717
|
+
var agentFailureFromError = (agent, error, describeError) => ({
|
|
718
|
+
agent,
|
|
719
|
+
kind: error instanceof AgentConfigError ? error.kind : "installer-error",
|
|
720
|
+
detail: describeError(error),
|
|
721
|
+
remedy: error instanceof NpmMissingError ? "install-node" : "retry-agent"
|
|
722
|
+
});
|
|
723
|
+
var retryAgentCommand = (agent) => `${NPX_LATEST_COMMAND} setup --agents ${agent}`;
|
|
724
|
+
var REMAINING_AGENTS_CONTINUE = "Other agents will continue. Re-run setup to retry.";
|
|
725
|
+
var afterAgentFailureNote = (agentsStillToSetUp) => agentsStillToSetUp > 0 ? REMAINING_AGENTS_CONTINUE : null;
|
|
726
|
+
var failedAgentsContext = (configuredAgentNames) => configuredAgentNames.length > 0 ? `(configured: ${configuredAgentNames.join(", ")})` : null;
|
|
727
|
+
var agentFailureWire = (failures) => failures.map(({ agent, kind }) => ({ agent, kind }));
|
|
728
|
+
|
|
729
|
+
// src/fx-lifecycle.ts
|
|
730
|
+
import { existsSync as existsSync3 } from "fs";
|
|
731
|
+
var fxRetryHint = () => `Fix it, then run ${retryAgentCommand("fx")}.`;
|
|
732
|
+
var readFxProfileForSetup = (filePath) => {
|
|
733
|
+
const read = readFxFile(filePath, readFxProfile);
|
|
734
|
+
if (read.kind === "profile") return read.profile;
|
|
735
|
+
if (read.kind === "missing") return {};
|
|
736
|
+
if (read.kind === "unreadable") {
|
|
737
|
+
throw new AgentConfigError(configFailureKind("unreadable"), filePath, `${filePath} could not be read, so it was left untouched. ${fxRetryHint()}`);
|
|
738
|
+
}
|
|
739
|
+
throw new AgentConfigError(
|
|
740
|
+
configFailureKind("unparseable"),
|
|
741
|
+
filePath,
|
|
742
|
+
`${filePath} ${FX_PROFILE_REFUSALS[read.reason]}, so it was left untouched. ${fxRetryHint()}`
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
var writeFxProfile = (filePath, profile) => {
|
|
746
|
+
const rendered = fxProfileText(profile);
|
|
747
|
+
if (rendered.kind === "refused") {
|
|
748
|
+
throw new Error(`${filePath} would grow past the size fx reads, so it was left untouched. ${fxRetryHint()}`);
|
|
749
|
+
}
|
|
750
|
+
writeFileAtomic(filePath, rendered.text, KEY_FILE_MODE);
|
|
751
|
+
};
|
|
752
|
+
var fxSettingsReadRefusal = (filePath, read) => {
|
|
753
|
+
if (read.kind === "unreadable") return `${filePath} could not be read`;
|
|
754
|
+
if (read.kind === "refused") return `${filePath} ${FX_SETTINGS_REFUSALS[read.reason]}`;
|
|
755
|
+
return null;
|
|
756
|
+
};
|
|
757
|
+
var recordFxQuestionRule = (receiptPath) => {
|
|
758
|
+
try {
|
|
759
|
+
if (!existsSync3(receiptPath)) writeJsonAtomic(receiptPath, FX_QUESTION_RULE_RECEIPT, 384);
|
|
760
|
+
return true;
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var configureFxSettings = (filePath, receiptPath, environment) => {
|
|
766
|
+
const read = readFxFile(filePath, readFxSettings);
|
|
767
|
+
const refusal = fxSettingsReadRefusal(filePath, read);
|
|
768
|
+
if (refusal) return { kind: "refused", detail: refusal };
|
|
769
|
+
const allowed = withPusharyToolsAllowed(read.kind === "settings" ? read.settings : {});
|
|
770
|
+
const routed = withQuestionsRoutedToPushary(allowed.settings);
|
|
771
|
+
const written = {
|
|
772
|
+
kind: "written",
|
|
773
|
+
toolsChange: allowed.change,
|
|
774
|
+
questionChange: routed.change,
|
|
775
|
+
questionRouting: fxQuestionRouting(routed.settings, environment),
|
|
776
|
+
workspacesWithOwnRules: [.../* @__PURE__ */ new Set([...workspacesOverridingPusharyRule(routed.settings), ...workspacesShowingQuestionTool(routed.settings)])],
|
|
777
|
+
ruleRecorded: true
|
|
778
|
+
};
|
|
779
|
+
if (allowed.change !== "added" && routed.change !== "added") return written;
|
|
780
|
+
const rendered = fxSettingsText(routed.settings);
|
|
781
|
+
if (rendered.kind === "refused") return { kind: "refused", detail: `${filePath} would grow past the 64 KiB fx reads` };
|
|
782
|
+
try {
|
|
783
|
+
writeFileAtomic(filePath, rendered.text, KEY_FILE_MODE);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return { kind: "refused", detail: `${filePath} could not be written (${err instanceof Error ? err.message : String(err)})` };
|
|
786
|
+
}
|
|
787
|
+
return { ...written, ruleRecorded: routed.change !== "added" || recordFxQuestionRule(receiptPath) };
|
|
788
|
+
};
|
|
789
|
+
var inspectFxInstallation = (profilePath, settingsPath, environment) => {
|
|
790
|
+
const profile = readFxFile(profilePath, readFxProfile);
|
|
791
|
+
const entry = profile.kind === "profile" ? fxPusharyEntryOf(profile.profile) : void 0;
|
|
792
|
+
const settingsRead = readFxFile(settingsPath, readFxSettings);
|
|
793
|
+
const settings = settingsRead.kind === "settings" ? settingsRead.settings : {};
|
|
794
|
+
const settingsProblem = settingsRead.kind === "missing" ? `${settingsPath} is missing, so fx runs with no Pushary rules; re-run ${retryAgentCommand("fx")}` : fxSettingsReadRefusal(settingsPath, settingsRead);
|
|
795
|
+
return {
|
|
796
|
+
profile,
|
|
797
|
+
configured: isFxPusharyEntry(entry),
|
|
798
|
+
bridge: fxBridgeTargetOf(entry),
|
|
799
|
+
settingsProblem,
|
|
800
|
+
toolsAllowed: pusharyToolsAllowed(settings),
|
|
801
|
+
questionRouting: fxQuestionRouting(settings, environment),
|
|
802
|
+
workspacesShowingQuestions: workspacesShowingQuestionTool(settings)
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
var removalWrite = (path, rendered, label) => rendered.kind === "refused" ? { kind: "kept", label, detail: `${path} would grow past the size fx reads; left untouched` } : { kind: "write", path, text: rendered.text, label };
|
|
806
|
+
var FX_QUESTION_RULE_KEPT = {
|
|
807
|
+
"not-ours": "no setup record says Pushary added it, so it may be yours; left as it is",
|
|
808
|
+
"changed-after-setup": "changed after setup; left as it is"
|
|
809
|
+
};
|
|
810
|
+
var planFxRemoval = (profilePath, settingsPath, receiptPath) => {
|
|
811
|
+
const actions = [];
|
|
812
|
+
const profile = readFxFile(profilePath, readFxProfile);
|
|
813
|
+
if (profile.kind === "profile") {
|
|
814
|
+
const result = withoutFxPusharyServer(profile.profile);
|
|
815
|
+
actions.push(result.removed ? removalWrite(profilePath, fxProfileText(result.profile), "fx MCP") : { kind: "kept", label: "fx MCP", detail: "no pushary entry" });
|
|
816
|
+
} else {
|
|
817
|
+
let detail = "not found";
|
|
818
|
+
if (profile.kind === "unreadable") detail = `${profilePath} could not be read; left untouched`;
|
|
819
|
+
if (profile.kind === "refused") detail = `${profilePath} ${FX_PROFILE_REFUSALS[profile.reason]}; left untouched`;
|
|
820
|
+
actions.push({ kind: "kept", label: "fx MCP", detail });
|
|
821
|
+
}
|
|
822
|
+
const read = readFxFile(settingsPath, readFxSettings);
|
|
823
|
+
const problem = fxSettingsReadRefusal(settingsPath, read);
|
|
824
|
+
if (problem) return [...actions, { kind: "kept", label: "fx settings", detail: `${problem}; left untouched` }];
|
|
825
|
+
if (read.kind === "settings") {
|
|
826
|
+
const rawReceipt = readJsonSafe(receiptPath);
|
|
827
|
+
const receipt = rawReceipt.kind === "ok" && isFxQuestionRuleReceipt(rawReceipt.value) ? rawReceipt.value : null;
|
|
828
|
+
const tools = withoutPusharyToolsAllowed(read.settings);
|
|
829
|
+
const question = withoutQuestionsRoutedToPushary(tools.settings, receipt);
|
|
830
|
+
if (tools.removed || question.outcome === "removed") {
|
|
831
|
+
const write = removalWrite(settingsPath, fxSettingsText(question.settings), "fx permission rules");
|
|
832
|
+
actions.push(write);
|
|
833
|
+
if (write.kind === "kept") return actions;
|
|
834
|
+
}
|
|
835
|
+
if (question.outcome === "not-ours" || question.outcome === "changed-after-setup") {
|
|
836
|
+
actions.push({ kind: "kept", label: "fx question rule", detail: FX_QUESTION_RULE_KEPT[question.outcome] });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (existsSync3(receiptPath)) actions.push({ kind: "remove-receipt", path: receiptPath });
|
|
840
|
+
return actions;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// src/vscode-config.ts
|
|
844
|
+
import { homedir as homedir2 } from "os";
|
|
845
|
+
import { join as join2 } from "path";
|
|
846
|
+
var PLUGIN_LOCATIONS_KEY = "chat.pluginLocations";
|
|
847
|
+
var blankJsonComments = (text) => {
|
|
848
|
+
let out = "";
|
|
849
|
+
let i = 0;
|
|
850
|
+
let inString = false;
|
|
851
|
+
let escaped = false;
|
|
852
|
+
while (i < text.length) {
|
|
853
|
+
const ch = text[i];
|
|
854
|
+
if (inString) {
|
|
855
|
+
out += ch;
|
|
856
|
+
if (escaped) escaped = false;
|
|
857
|
+
else if (ch === "\\") escaped = true;
|
|
858
|
+
else if (ch === '"') inString = false;
|
|
859
|
+
i += 1;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (ch === '"') {
|
|
863
|
+
inString = true;
|
|
864
|
+
out += ch;
|
|
865
|
+
i += 1;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
869
|
+
while (i < text.length && text[i] !== "\n") {
|
|
870
|
+
out += " ";
|
|
871
|
+
i += 1;
|
|
872
|
+
}
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
876
|
+
const end = text.indexOf("*/", i + 2);
|
|
877
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
878
|
+
while (i < stop) {
|
|
879
|
+
out += text[i] === "\n" ? "\n" : " ";
|
|
880
|
+
i += 1;
|
|
881
|
+
}
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
out += ch;
|
|
885
|
+
i += 1;
|
|
886
|
+
}
|
|
887
|
+
return out;
|
|
888
|
+
};
|
|
889
|
+
var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
890
|
+
var parseJsonc = (text) => {
|
|
891
|
+
const blanked = blankJsonComments(text);
|
|
892
|
+
const withoutTrailingCommas = blanked.replace(/,(\s*[}\]])/g, "$1");
|
|
893
|
+
try {
|
|
894
|
+
return asRecord2(JSON.parse(withoutTrailingCommas));
|
|
895
|
+
} catch {
|
|
896
|
+
return void 0;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var rootBraceIndex = (text) => blankJsonComments(text).indexOf("{");
|
|
900
|
+
var detectIndent = (text) => {
|
|
901
|
+
const match = /\n([ \t]+)"/.exec(text);
|
|
902
|
+
return match?.[1] ?? " ";
|
|
903
|
+
};
|
|
904
|
+
var freshDocument = (pluginDir, indent = " ") => `${JSON.stringify({ [PLUGIN_LOCATIONS_KEY]: { [pluginDir]: true } }, null, indent)}
|
|
905
|
+
`;
|
|
906
|
+
var registerPluginLocation = (existing, pluginDir) => {
|
|
907
|
+
if (existing === null || existing.trim() === "") {
|
|
908
|
+
return { kind: "created", content: freshDocument(pluginDir) };
|
|
909
|
+
}
|
|
910
|
+
const indent = detectIndent(existing);
|
|
911
|
+
try {
|
|
912
|
+
const parsed2 = asRecord2(JSON.parse(existing));
|
|
913
|
+
if (parsed2) {
|
|
914
|
+
const locations2 = asRecord2(parsed2[PLUGIN_LOCATIONS_KEY]) ?? {};
|
|
915
|
+
if (locations2[pluginDir] === true) return { kind: "already" };
|
|
916
|
+
parsed2[PLUGIN_LOCATIONS_KEY] = { ...locations2, [pluginDir]: true };
|
|
917
|
+
return { kind: "merged", content: `${JSON.stringify(parsed2, null, indent)}
|
|
918
|
+
` };
|
|
919
|
+
}
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
const parsed = parseJsonc(existing);
|
|
923
|
+
if (!parsed) return { kind: "manual" };
|
|
924
|
+
const locations = asRecord2(parsed[PLUGIN_LOCATIONS_KEY]);
|
|
925
|
+
if (locations?.[pluginDir] === true) return { kind: "already" };
|
|
926
|
+
if (parsed[PLUGIN_LOCATIONS_KEY] !== void 0) return { kind: "manual" };
|
|
927
|
+
const brace = rootBraceIndex(existing);
|
|
928
|
+
if (brace === -1) return { kind: "manual" };
|
|
929
|
+
const needsComma = Object.keys(parsed).length > 0;
|
|
930
|
+
const line = `
|
|
931
|
+
${indent}${JSON.stringify(PLUGIN_LOCATIONS_KEY)}: { ${JSON.stringify(pluginDir)}: true }${needsComma ? "," : ""}`;
|
|
932
|
+
return {
|
|
933
|
+
kind: "inserted",
|
|
934
|
+
content: `${existing.slice(0, brace + 1)}${line}${existing.slice(brace + 1)}`
|
|
935
|
+
};
|
|
936
|
+
};
|
|
937
|
+
var unregisterPluginLocation = (existing, pluginDir) => {
|
|
938
|
+
if (existing === null || existing.trim() === "") return { kind: "absent" };
|
|
939
|
+
const indent = detectIndent(existing);
|
|
940
|
+
try {
|
|
941
|
+
const parsed2 = asRecord2(JSON.parse(existing));
|
|
942
|
+
if (parsed2) {
|
|
943
|
+
const locations2 = asRecord2(parsed2[PLUGIN_LOCATIONS_KEY]);
|
|
944
|
+
if (!locations2 || !(pluginDir in locations2)) return { kind: "absent" };
|
|
945
|
+
const remaining = { ...locations2 };
|
|
946
|
+
delete remaining[pluginDir];
|
|
947
|
+
if (Object.keys(remaining).length === 0) delete parsed2[PLUGIN_LOCATIONS_KEY];
|
|
948
|
+
else parsed2[PLUGIN_LOCATIONS_KEY] = remaining;
|
|
949
|
+
return { kind: "removed", content: `${JSON.stringify(parsed2, null, indent)}
|
|
950
|
+
` };
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
const parsed = parseJsonc(existing);
|
|
955
|
+
if (!parsed) return { kind: "manual" };
|
|
956
|
+
const locations = asRecord2(parsed[PLUGIN_LOCATIONS_KEY]);
|
|
957
|
+
if (!locations || !(pluginDir in locations)) return { kind: "absent" };
|
|
958
|
+
const line = new RegExp(
|
|
959
|
+
`\\n[ \\t]*${escapeRegExp(JSON.stringify(PLUGIN_LOCATIONS_KEY))}\\s*:\\s*\\{\\s*${escapeRegExp(JSON.stringify(pluginDir))}\\s*:\\s*true\\s*\\},?`
|
|
960
|
+
);
|
|
961
|
+
if (!line.test(existing)) return { kind: "manual" };
|
|
962
|
+
return { kind: "removed", content: existing.replace(line, "") };
|
|
963
|
+
};
|
|
964
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
965
|
+
var isPluginRegistered = (existing, pluginDir) => {
|
|
966
|
+
if (!existing) return false;
|
|
967
|
+
const parsed = parseJsonc(existing);
|
|
968
|
+
return asRecord2(parsed?.[PLUGIN_LOCATIONS_KEY])?.[pluginDir] === true;
|
|
969
|
+
};
|
|
970
|
+
var pluginLocationSnippet = (pluginDir) => `"${PLUGIN_LOCATIONS_KEY}": {
|
|
971
|
+
${JSON.stringify(pluginDir)}: true
|
|
972
|
+
}`;
|
|
973
|
+
var VSCODE_PRODUCTS = ["Code", "Code - Insiders"];
|
|
974
|
+
var settingsRoot = (home, platform) => {
|
|
975
|
+
if (platform === "darwin") return join2(home, "Library", "Application Support");
|
|
976
|
+
if (platform === "win32") return process.env.APPDATA?.trim() || join2(home, "AppData", "Roaming");
|
|
977
|
+
return process.env.XDG_CONFIG_HOME?.trim() || join2(home, ".config");
|
|
978
|
+
};
|
|
979
|
+
var vscodeSettingsCandidates = (home = homedir2(), platform = process.platform) => {
|
|
980
|
+
const root = settingsRoot(home, platform);
|
|
981
|
+
return VSCODE_PRODUCTS.map((product) => join2(root, product, "User", "settings.json"));
|
|
982
|
+
};
|
|
983
|
+
var vscodeSettingsTargets = (exists, home, platform) => {
|
|
984
|
+
const candidates = vscodeSettingsCandidates(home, platform);
|
|
985
|
+
const present = candidates.filter((path) => exists(path));
|
|
986
|
+
return present.length > 0 ? present : candidates.slice(0, 1);
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// src/setup/detect.ts
|
|
990
|
+
import { execSync } from "child_process";
|
|
991
|
+
import { existsSync as existsSync4 } from "fs";
|
|
992
|
+
import { homedir as homedir3 } from "os";
|
|
993
|
+
import { dirname, join as join3 } from "path";
|
|
994
|
+
var whichCommand = () => process.platform === "win32" ? "where" : "which";
|
|
995
|
+
var binaryOnPath = (binary) => {
|
|
996
|
+
try {
|
|
997
|
+
execSync(`${whichCommand()} ${binary}`, { stdio: "ignore", timeout: 5e3 });
|
|
998
|
+
return true;
|
|
999
|
+
} catch {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var defaultDeps = { onPath: binaryOnPath, exists: existsSync4 };
|
|
1004
|
+
var detectAgent = (probe, deps = defaultDeps) => {
|
|
1005
|
+
const checked = [];
|
|
1006
|
+
if (probe.binary) {
|
|
1007
|
+
checked.push(`${whichCommand()} ${probe.binary}`);
|
|
1008
|
+
if (deps.onPath(probe.binary)) {
|
|
1009
|
+
return { kind: "installed", evidence: `${probe.binary} on PATH`, strong: true };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const path of probe.paths) {
|
|
1013
|
+
checked.push(path);
|
|
1014
|
+
if (deps.exists(path)) return { kind: "installed", evidence: path, strong: true };
|
|
1015
|
+
}
|
|
1016
|
+
for (const marker of probe.configMarkers) {
|
|
1017
|
+
checked.push(marker);
|
|
1018
|
+
if (deps.exists(marker)) return { kind: "installed", evidence: marker, strong: false };
|
|
1019
|
+
}
|
|
1020
|
+
if (probe.binary === null && probe.paths.length === 0 && probe.configMarkers.length === 0) {
|
|
1021
|
+
return { kind: "unknown" };
|
|
1022
|
+
}
|
|
1023
|
+
return { kind: "not-found", checked };
|
|
1024
|
+
};
|
|
1025
|
+
var agentProbes = (home = homedir3()) => ({
|
|
1026
|
+
claude_code: {
|
|
1027
|
+
binary: "claude",
|
|
1028
|
+
// The native installer puts it here, outside a default non-login PATH.
|
|
1029
|
+
paths: [join3(home, ".local", "bin", "claude")],
|
|
1030
|
+
configMarkers: [join3(home, ".claude")]
|
|
1031
|
+
},
|
|
1032
|
+
codex: {
|
|
1033
|
+
binary: "codex",
|
|
1034
|
+
paths: [],
|
|
1035
|
+
configMarkers: [codexHomeFrom(home)]
|
|
1036
|
+
},
|
|
1037
|
+
gemini_cli: {
|
|
1038
|
+
binary: "gemini",
|
|
1039
|
+
paths: [],
|
|
1040
|
+
configMarkers: [join3(home, ".gemini")]
|
|
1041
|
+
},
|
|
1042
|
+
hermes: {
|
|
1043
|
+
binary: "hermes",
|
|
1044
|
+
paths: [],
|
|
1045
|
+
configMarkers: [join3(home, ".hermes")]
|
|
1046
|
+
},
|
|
1047
|
+
cursor: {
|
|
1048
|
+
// GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
|
|
1049
|
+
// command in PATH" from the command palette, which most never do.
|
|
1050
|
+
binary: "cursor",
|
|
1051
|
+
paths: process.platform === "darwin" ? ["/Applications/Cursor.app"] : process.platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join3(home, ".local", "share", "cursor")],
|
|
1052
|
+
configMarkers: [join3(home, ".cursor")]
|
|
1053
|
+
},
|
|
1054
|
+
vscode: {
|
|
1055
|
+
// Like Cursor, a GUI editor: `code` on PATH only exists if the user ran
|
|
1056
|
+
// "Shell Command: Install 'code' command in PATH", which on macOS is opt-in.
|
|
1057
|
+
binary: "code",
|
|
1058
|
+
paths: process.platform === "darwin" ? ["/Applications/Visual Studio Code.app"] : process.platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "Microsoft VS Code")] : ["/usr/share/code", join3(home, ".local", "share", "code")],
|
|
1059
|
+
// The settings directory rather than ~/.vscode, because the settings
|
|
1060
|
+
// directory is what this installer actually writes to. Insiders counts:
|
|
1061
|
+
// someone running only Insiders still has VS Code.
|
|
1062
|
+
configMarkers: vscodeSettingsCandidates(home).map((path) => dirname(path))
|
|
1063
|
+
},
|
|
1064
|
+
opencode: {
|
|
1065
|
+
binary: "opencode",
|
|
1066
|
+
paths: [],
|
|
1067
|
+
// Resolved rather than spelled: OpenCode honours OPENCODE_CONFIG_DIR and then
|
|
1068
|
+
// XDG_CONFIG_HOME, and `~/.opencode` — the obvious guess — is a path it has
|
|
1069
|
+
// never used.
|
|
1070
|
+
configMarkers: [openCodeConfigDirFrom(home)]
|
|
1071
|
+
},
|
|
1072
|
+
fx: {
|
|
1073
|
+
binary: null,
|
|
1074
|
+
paths: [join3(home, ".fx", "settings.json"), join3(home, ".fx", "sessions"), join3(home, ".fx", "history.jsonl")],
|
|
1075
|
+
configMarkers: []
|
|
1076
|
+
},
|
|
1077
|
+
...compatibleProbes(),
|
|
1078
|
+
// "Other" is a set of printed instructions for any MCP or HTTP client. There is
|
|
1079
|
+
// nothing to detect, and it must never be auto-selected.
|
|
1080
|
+
custom: { binary: null, paths: [], configMarkers: [] }
|
|
1081
|
+
});
|
|
1082
|
+
var compatibleProbes = () => HOOK_COMPATIBLE_PROFILES.reduce((probes, profile) => ({
|
|
1083
|
+
...probes,
|
|
1084
|
+
[profile.agentType]: {
|
|
1085
|
+
binary: profile.binary,
|
|
1086
|
+
paths: [],
|
|
1087
|
+
configMarkers: []
|
|
1088
|
+
}
|
|
1089
|
+
}), {});
|
|
1090
|
+
var detectAllAgents = (deps = defaultDeps, home) => {
|
|
1091
|
+
const probes = agentProbes(home);
|
|
1092
|
+
return Object.keys(probes).map((agent) => ({
|
|
1093
|
+
agent,
|
|
1094
|
+
result: detectAgent(probes[agent], deps)
|
|
1095
|
+
}));
|
|
1096
|
+
};
|
|
1097
|
+
var isDetected = (result) => result.kind === "installed";
|
|
1098
|
+
var shortenHome = (path, home = homedir3()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
1099
|
+
var describeDetection = (result, home) => {
|
|
1100
|
+
if (result.kind !== "installed") return null;
|
|
1101
|
+
const evidence = shortenHome(result.evidence, home);
|
|
1102
|
+
return result.strong ? evidence : `${evidence}, config only`;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// src/instruction-file.ts
|
|
1106
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, rmSync as rmSync2 } from "fs";
|
|
1107
|
+
import { dirname as dirname2 } from "path";
|
|
1108
|
+
var PUSHARY_BEGIN = "<!-- BEGIN PUSHARY (managed by @pushary/agent-hooks; do not edit this block) -->";
|
|
1109
|
+
var PUSHARY_END = "<!-- END PUSHARY -->";
|
|
1110
|
+
var instructionBody = (label) => `## Pushary notifications and approvals
|
|
1111
|
+
|
|
1112
|
+
You have Pushary MCP tools that reach the user on their phone. Use them proactively. Do not wait for the user to ask.
|
|
1113
|
+
|
|
1114
|
+
- When you need the user to make a decision or answer a clarifying question, call \`ask_user\` instead of guessing or stalling. Use type "confirm" for yes/no, "select" for a fixed set of options, and "input" for free text. Read \`answered\`, \`status\` and the returned handoff instead of assuming the tool blocked. Silence is not consent; choice/text answers do not authorize a separate action.
|
|
1115
|
+
- Ask once, on one channel. If you also have a built-in way to prompt the user, use that or \`ask_user\` for a given question, never both. \`ask_user\` is the one that reaches the user when they are away from this machine, so prefer it. Raising both strands the user in whichever prompt you are not waiting on.
|
|
1116
|
+
- Honor authorization already granted in this session. Ask before a risky or irreversible action outside that authorization, or when an enforced host policy requires it. Do not ask again for an already authorized step, and never bypass an enforced gate. When a Pushary hook result says the user approved a specific command after a denial, rerunning that exact command is the approved path and not a bypass of that gate.
|
|
1117
|
+
- Call \`propose_scope\` only for an unresolved or requested file boundary. A ratified scope narrows what may proceed; it does not bypass other approval rules. Do not add a plan approval merely because work spans several files, and do not propose a new scope mid-run just to widen it. Let the installed gate request any scope expansion before the edit executes. Only file paths are checked, and only on tool calls that carry one, so put a boundary that is not a path (recipients, channels, spend, systems you will not open) in \`promises\` and never in \`allowedPaths\`. Read the \`enforces\` field that comes back: an empty array means nothing in that contract is checked automatically, and you must say so rather than report that a scope is in force. \`hookSeen: false\` means no hook has reported that sessionId, so the gate will never find the contract: fix the session id rather than proceeding as if a scope were in force.
|
|
1118
|
+
- Before a task of more than two steps, break it into steps and find the forks. Settle by yourself every fork that a file, a command or a tool call can answer, then ask the rest as one numbered round with your recommended answer against each. A question in the terminal is free; a push is not. When the user is at the terminal, ask the whole round there. When the user is away, send only the fork that stops the run, and decide the rest on your recommendation.
|
|
1119
|
+
- When you are blocked, stuck, or hit an error you cannot resolve on your own, call \`send_notification\` so the user knows, and call \`ask_user\` if you need a decision to continue.
|
|
1120
|
+
- When meaningful work finishes while the user is away, or they requested an alert, call \`send_notification\` so the user knows it is done. Skip trivial completions and duplicate reports to someone already reading. Always pass \`context\` with a \`type\` of "task_complete", "error" or "info": it is what tells Pushary this is a task update, so the user's own setting for where updates land can route it.
|
|
1121
|
+
- Confirm notifications can offer lock-screen actions; arbitrary choices and text open the app. The Mac notch handles personal account questions, not Partner customer enrollment.
|
|
1122
|
+
- Use \`cancel_question\` to retract a pending question once it is no longer needed.
|
|
1123
|
+
- Respect the user's Pushary delivery mode: it decides where an approval is surfaced (phone, terminal, or awareness only), not whether it is required. In Terminal mode do not expect a phone answer; the approval waits in the terminal. If the Pushary hook already gated an action, a single approval is enough, do not ask again.
|
|
1124
|
+
|
|
1125
|
+
Pass \`agentName\` as "${label} - <project folder name>" so the user knows which session is asking. If a question times out, follow \`handoffAction\` when present, otherwise \`nextAction\`: poll once, then cancel a live phone question before asking in the current client. If cancellation returns \`handoffAction: "stop"\`, stop. Otherwise, if cancellation returns false, poll once for 1 second and honor the answer that won the race.`;
|
|
1126
|
+
var SHARED_AGENTS_MD_LABEL = "<your agent's name, such as Codex or Hermes>";
|
|
1127
|
+
var renderAgentInstructions = (label) => instructionBody(label);
|
|
1128
|
+
var KEYLESS_FALLBACK_LINE = "This repo uses Pushary phone approvals. Your teammates get agent questions on their lock screens; you do not have a key yet, so questions will fall back to the terminal. Get your own key (3-day trial): https://pushary.com/sign-up?utm_source=agents-md&utm_medium=teammate";
|
|
1129
|
+
var renderProjectAgentInstructions = (label) => `${instructionBody(label)}
|
|
1130
|
+
|
|
1131
|
+
If this environment has no Pushary API key (no PUSHARY_API_KEY and no key in ~/.pushary/config.json), the tools above are unavailable in this session. In that case, treat the following as a statement of fact about this session and continue in the terminal: ${KEYLESS_FALLBACK_LINE}`;
|
|
1132
|
+
var wrap = (body) => `${PUSHARY_BEGIN}
|
|
1133
|
+
${body}
|
|
1134
|
+
${PUSHARY_END}`;
|
|
1135
|
+
var writeInstructionBlock = (filePath, body) => {
|
|
1136
|
+
const dir = dirname2(filePath);
|
|
1137
|
+
if (!existsSync5(dir)) mkdirSync(dir, { recursive: true });
|
|
1138
|
+
let existing = "";
|
|
1139
|
+
try {
|
|
1140
|
+
existing = readFileSync3(filePath, "utf-8");
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
const block = wrap(body);
|
|
1144
|
+
const start = existing.indexOf(PUSHARY_BEGIN);
|
|
1145
|
+
const end = existing.indexOf(PUSHARY_END);
|
|
1146
|
+
let next;
|
|
1147
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
1148
|
+
next = existing.slice(0, start) + block + existing.slice(end + PUSHARY_END.length);
|
|
1149
|
+
} else {
|
|
1150
|
+
const prefix = existing.trim() ? existing.replace(/\s*$/, "") + "\n\n" : "";
|
|
1151
|
+
next = prefix + block + "\n";
|
|
1152
|
+
}
|
|
1153
|
+
writeFileSync2(filePath, next, "utf-8");
|
|
1154
|
+
};
|
|
1155
|
+
var removeInstructionBlock = (filePath) => {
|
|
1156
|
+
let existing = "";
|
|
1157
|
+
try {
|
|
1158
|
+
existing = readFileSync3(filePath, "utf-8");
|
|
1159
|
+
} catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
const start = existing.indexOf(PUSHARY_BEGIN);
|
|
1163
|
+
const end = existing.indexOf(PUSHARY_END);
|
|
1164
|
+
if (start === -1 || end === -1 || end < start) return false;
|
|
1165
|
+
const remaining = (existing.slice(0, start) + existing.slice(end + PUSHARY_END.length)).trim();
|
|
1166
|
+
if (remaining === "") {
|
|
1167
|
+
rmSync2(filePath, { force: true });
|
|
1168
|
+
} else {
|
|
1169
|
+
writeFileSync2(filePath, remaining + "\n", "utf-8");
|
|
1170
|
+
}
|
|
1171
|
+
return true;
|
|
1172
|
+
};
|
|
1173
|
+
var hasInstructionBlock = (filePath) => {
|
|
1174
|
+
try {
|
|
1175
|
+
return readFileSync3(filePath, "utf-8").includes(PUSHARY_BEGIN);
|
|
1176
|
+
} catch {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
export {
|
|
1182
|
+
isHermesConfigReceipt,
|
|
1183
|
+
HERMES_PIP_PACKAGE,
|
|
1184
|
+
HERMES_PLUGIN_NAME,
|
|
1185
|
+
resolveHermesPython,
|
|
1186
|
+
runHermesConfigScript,
|
|
1187
|
+
hermesWiredByPushary,
|
|
1188
|
+
readHermesStatus,
|
|
1189
|
+
FX_PUSHARY_TOOLS_RULE,
|
|
1190
|
+
FX_TOOL_DISCOVERY_NOTE,
|
|
1191
|
+
FX_PROFILE_REFUSALS,
|
|
1192
|
+
fxPusharyEntry,
|
|
1193
|
+
withFxPusharyServer,
|
|
1194
|
+
FX_QUESTION_TOOL,
|
|
1195
|
+
fxIgnoresInstructionsFile,
|
|
1196
|
+
AgentConfigError,
|
|
1197
|
+
configFailureKind,
|
|
1198
|
+
describedAgentFailure,
|
|
1199
|
+
agentFailureFromError,
|
|
1200
|
+
retryAgentCommand,
|
|
1201
|
+
afterAgentFailureNote,
|
|
1202
|
+
failedAgentsContext,
|
|
1203
|
+
agentFailureWire,
|
|
1204
|
+
readFxProfileForSetup,
|
|
1205
|
+
writeFxProfile,
|
|
1206
|
+
configureFxSettings,
|
|
1207
|
+
inspectFxInstallation,
|
|
1208
|
+
planFxRemoval,
|
|
1209
|
+
registerPluginLocation,
|
|
1210
|
+
unregisterPluginLocation,
|
|
1211
|
+
isPluginRegistered,
|
|
1212
|
+
pluginLocationSnippet,
|
|
1213
|
+
vscodeSettingsTargets,
|
|
1214
|
+
detectAgent,
|
|
1215
|
+
agentProbes,
|
|
1216
|
+
detectAllAgents,
|
|
1217
|
+
isDetected,
|
|
1218
|
+
shortenHome,
|
|
1219
|
+
describeDetection,
|
|
1220
|
+
SHARED_AGENTS_MD_LABEL,
|
|
1221
|
+
renderAgentInstructions,
|
|
1222
|
+
renderProjectAgentInstructions,
|
|
1223
|
+
writeInstructionBlock,
|
|
1224
|
+
removeInstructionBlock,
|
|
1225
|
+
hasInstructionBlock
|
|
1226
|
+
};
|