forge-cw 0.1.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/LICENSE +21 -0
- package/console/assets/addon-fit-BlxrFPDK.js +1 -0
- package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
- package/console/assets/index-Digctq9W.css +32 -0
- package/console/assets/index-ItVUTuQf.js +31 -0
- package/console/assets/xterm-DlVFs1Kw.js +9 -0
- package/console/index.html +13 -0
- package/cw/.bundle.json +4 -0
- package/cw/agents/presentation-curator.md +119 -0
- package/cw/agents/weather-agent.md +67 -0
- package/cw/cw +6188 -0
- package/cw/cw-shell-integration.sh +132 -0
- package/cw/hooks/HOOKS-README.md +471 -0
- package/cw/hooks/config/hooks-config.json +24 -0
- package/cw/hooks/scripts/auto-compact.py +133 -0
- package/cw/hooks/scripts/hooks.py +472 -0
- package/cw/hooks/scripts/review-autoclose.py +171 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
- package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
- package/cw/hooks/sounds/configchange/configchange.wav +0 -0
- package/cw/hooks/sounds/notification/notification.mp3 +0 -0
- package/cw/hooks/sounds/notification/notification.wav +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
- package/cw/hooks/sounds/precompact/precompact.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
- package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
- package/cw/hooks/sounds/setup/Setup.wav +0 -0
- package/cw/hooks/sounds/stop/stop.mp3 +0 -0
- package/cw/hooks/sounds/stop/stop.wav +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
- package/cw/install.sh +243 -0
- package/cw/lib/context/github.sh +51 -0
- package/cw/lib/context/linear.sh +49 -0
- package/cw/lib/context/notion.sh +38 -0
- package/cw/lib/dashboard/activity-hook.py +134 -0
- package/cw/lib/dashboard/index.html +1515 -0
- package/cw/lib/dashboard/server.py +379 -0
- package/cw/lib/dashboard/start.sh +15 -0
- package/cw/lib/harnesses/claude.sh +92 -0
- package/cw/lib/harnesses/codex.sh +245 -0
- package/cw/lib/harnesses/opencode.sh +120 -0
- package/cw/lib/harnesses/pi.sh +66 -0
- package/cw/lib/iterm2.sh +140 -0
- package/cw/templates/CLAUDE.template.md +48 -0
- package/dist/index.js +4376 -0
- package/package.json +51 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# codex cli driver
|
|
2
|
+
# unverified: no skip-permissions equivalent flag confirmed, so it stays unsupported
|
|
3
|
+
codex_supports() {
|
|
4
|
+
local cap
|
|
5
|
+
for cap in continue_last non_interactive_prompt model_flag custom_provider \
|
|
6
|
+
instructions_file api_key_login headless_login; do
|
|
7
|
+
[[ "$1" == "$cap" ]] && return 0
|
|
8
|
+
done
|
|
9
|
+
return 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
codex_config_env() {
|
|
13
|
+
printf 'CODEX_HOME=%s\n' "$CW_HARNESS_DIR"
|
|
14
|
+
}
|
|
15
|
+
# unverified: no model_providers table is written for a custom endpoint, only the provider name
|
|
16
|
+
|
|
17
|
+
# lists the rollout files codex has written for this account
|
|
18
|
+
_codex_rollouts() {
|
|
19
|
+
[[ -d "$CW_HARNESS_DIR/sessions" ]] || return 0
|
|
20
|
+
find "$CW_HARNESS_DIR/sessions" -type f -name 'rollout-*.jsonl' 2>/dev/null | sort
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# remembers which rollouts existed before a launch, so the new one can be told apart
|
|
24
|
+
_codex_snapshot() {
|
|
25
|
+
_CW_CODEX_BEFORE="$(_codex_rollouts)"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# unverified rollout layout: prints the id of the one new rollout naming the notes file, else nothing
|
|
29
|
+
codex_session_ref() {
|
|
30
|
+
local marker="${CW_NOTES_FILE:-}"
|
|
31
|
+
[[ -n "$marker" && "${CW_PROMPT:-}" == *"$marker"* ]] || return 0
|
|
32
|
+
CW_CX_BEFORE="${_CW_CODEX_BEFORE:-}" CW_CX_AFTER="$(_codex_rollouts)" CW_CX_MARKER="$marker" \
|
|
33
|
+
python3 - <<'PY' 2>/dev/null
|
|
34
|
+
import json, os, re
|
|
35
|
+
before = set(filter(None, os.environ["CW_CX_BEFORE"].split("\n")))
|
|
36
|
+
after = [p for p in os.environ["CW_CX_AFTER"].split("\n") if p and p not in before]
|
|
37
|
+
marker = os.environ["CW_CX_MARKER"]
|
|
38
|
+
def mentions(value):
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
return marker in value
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
return any(mentions(v) for v in value.values())
|
|
43
|
+
if isinstance(value, list):
|
|
44
|
+
return any(mentions(v) for v in value)
|
|
45
|
+
return False
|
|
46
|
+
hits = []
|
|
47
|
+
for path in after:
|
|
48
|
+
try:
|
|
49
|
+
with open(path) as f:
|
|
50
|
+
if any(mentions(json.loads(line)) for line in f if line.strip()):
|
|
51
|
+
hits.append(path)
|
|
52
|
+
except Exception:
|
|
53
|
+
continue
|
|
54
|
+
uuid = r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$"
|
|
55
|
+
m = re.search(uuid, hits[0]) if len(hits) == 1 else None
|
|
56
|
+
if m:
|
|
57
|
+
print(m.group(1), end="")
|
|
58
|
+
PY
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# sets only the top-level model keys in config.toml, refusing any file it cannot edit safely
|
|
63
|
+
_codex_write_provider() {
|
|
64
|
+
local provider="$1" model="$2"
|
|
65
|
+
[[ -n "$provider" && "$provider" != "native" ]] || return 0
|
|
66
|
+
# said once per command, however many launch attempts ask again
|
|
67
|
+
[[ -z "${_CW_CODEX_REFUSED:-}" ]] || return 1
|
|
68
|
+
mkdir -p "$CW_HARNESS_DIR"
|
|
69
|
+
local why
|
|
70
|
+
why=$(CW_TOML="$CW_HARNESS_DIR/config.toml" CW_TOML_PROVIDER="$provider" CW_TOML_MODEL="$model" \
|
|
71
|
+
python3 - <<'PY'
|
|
72
|
+
import os, re, shutil, sys, tempfile
|
|
73
|
+
path = os.environ["CW_TOML"]
|
|
74
|
+
want = [("model_provider", os.environ["CW_TOML_PROVIDER"]), ("model", os.environ["CW_TOML_MODEL"])]
|
|
75
|
+
|
|
76
|
+
def refuse(msg):
|
|
77
|
+
print(msg, end="")
|
|
78
|
+
sys.exit(3)
|
|
79
|
+
|
|
80
|
+
# a toml basic string; every control character and DEL goes out as a \u escape
|
|
81
|
+
def enc(v):
|
|
82
|
+
out = []
|
|
83
|
+
for ch in v:
|
|
84
|
+
o = ord(ch)
|
|
85
|
+
if ch == '"':
|
|
86
|
+
out.append('\\"')
|
|
87
|
+
elif ch == "\\":
|
|
88
|
+
out.append("\\\\")
|
|
89
|
+
elif o < 0x20 or o == 0x7F:
|
|
90
|
+
out.append("\\u%04x" % o)
|
|
91
|
+
else:
|
|
92
|
+
out.append(ch)
|
|
93
|
+
return '"' + "".join(out) + '"'
|
|
94
|
+
|
|
95
|
+
# bracket depth left open by these lines, ignoring strings and comments
|
|
96
|
+
def depth(lines):
|
|
97
|
+
d = 0
|
|
98
|
+
for line in lines:
|
|
99
|
+
i = 0
|
|
100
|
+
while i < len(line):
|
|
101
|
+
c = line[i]
|
|
102
|
+
if c == "#":
|
|
103
|
+
break
|
|
104
|
+
if c == '"':
|
|
105
|
+
i += 1
|
|
106
|
+
while i < len(line) and line[i] != '"':
|
|
107
|
+
i += 2 if line[i] == "\\" else 1
|
|
108
|
+
elif c == "'":
|
|
109
|
+
i += 1
|
|
110
|
+
while i < len(line) and line[i] != "'":
|
|
111
|
+
i += 1
|
|
112
|
+
elif c in "[{":
|
|
113
|
+
d += 1
|
|
114
|
+
elif c in "]}":
|
|
115
|
+
d -= 1
|
|
116
|
+
i += 1
|
|
117
|
+
return d
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
with open(path) as f:
|
|
121
|
+
original = f.read()
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
original = None
|
|
124
|
+
except Exception as e:
|
|
125
|
+
refuse(f"cannot read it ({e})")
|
|
126
|
+
|
|
127
|
+
lines = [] if original is None else original.splitlines(keepends=True)
|
|
128
|
+
first_table = next((i for i, l in enumerate(lines) if l.lstrip().startswith("[")), len(lines))
|
|
129
|
+
top = lines[:first_table]
|
|
130
|
+
if any(q in l for l in top for q in ('"' * 3, "'" * 3)):
|
|
131
|
+
refuse("its top level holds a multi-line string")
|
|
132
|
+
if depth(top) != 0:
|
|
133
|
+
refuse("its top level holds a multi-line array or table")
|
|
134
|
+
|
|
135
|
+
plain = re.compile(r"""^\s*[^=]+=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(#.*)?$""")
|
|
136
|
+
for key, value in want:
|
|
137
|
+
if not value:
|
|
138
|
+
continue
|
|
139
|
+
new_line = f"{key} = {enc(value)}\n"
|
|
140
|
+
key_re = re.compile(r'^\s*(%s|"%s")\s*=' % (re.escape(key), re.escape(key)))
|
|
141
|
+
hits = [i for i, l in enumerate(lines[:first_table]) if key_re.match(l)]
|
|
142
|
+
if len(hits) > 1:
|
|
143
|
+
refuse(f"it sets {key} more than once")
|
|
144
|
+
if hits:
|
|
145
|
+
if not plain.match(lines[hits[0]].rstrip("\r\n")):
|
|
146
|
+
refuse(f"its {key} is not a plain one-line string")
|
|
147
|
+
lines[hits[0]] = new_line
|
|
148
|
+
else:
|
|
149
|
+
lines.insert(0, new_line)
|
|
150
|
+
first_table += 1
|
|
151
|
+
|
|
152
|
+
result = "".join(lines)
|
|
153
|
+
if result == original:
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
try:
|
|
156
|
+
import tomllib
|
|
157
|
+
except ImportError:
|
|
158
|
+
tomllib = None
|
|
159
|
+
if tomllib is not None and original is not None:
|
|
160
|
+
try:
|
|
161
|
+
before, after = tomllib.loads(original), tomllib.loads(result)
|
|
162
|
+
except Exception:
|
|
163
|
+
refuse("it does not parse as toml")
|
|
164
|
+
for key, value in want:
|
|
165
|
+
if value:
|
|
166
|
+
before[key] = value
|
|
167
|
+
if before != after:
|
|
168
|
+
refuse("the edit would change more than the keys cw owns")
|
|
169
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".config.toml.")
|
|
170
|
+
with os.fdopen(fd, "w") as f:
|
|
171
|
+
f.write(result)
|
|
172
|
+
if original is not None:
|
|
173
|
+
shutil.copymode(path, tmp)
|
|
174
|
+
os.replace(tmp, path)
|
|
175
|
+
PY
|
|
176
|
+
) && return 0
|
|
177
|
+
_CW_CODEX_REFUSED=1
|
|
178
|
+
_err "Cannot apply provider '$provider' to $CW_HARNESS_DIR/config.toml: ${why:-unknown error}."
|
|
179
|
+
_err "Set model_provider and model in that file yourself; cw left it untouched."
|
|
180
|
+
return 1
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_codex_base() {
|
|
184
|
+
_codex_write_provider "${CW_PROVIDER:-}" "${CW_MODEL:-}" || return 1
|
|
185
|
+
HARNESS_ENV=("CODEX_HOME=$CW_HARNESS_DIR")
|
|
186
|
+
if [[ -f "$CW_HARNESS_DIR/env" ]]; then
|
|
187
|
+
local -a extra_env=()
|
|
188
|
+
mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
|
|
189
|
+
[[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
|
|
190
|
+
fi
|
|
191
|
+
HARNESS_ARGV=(codex)
|
|
192
|
+
local f
|
|
193
|
+
# unverified: flag ordering around the resume subcommand (before vs after)
|
|
194
|
+
for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# unverified: interactive launch argv beyond the bare binary
|
|
199
|
+
codex_launch() {
|
|
200
|
+
_codex_base || return 1
|
|
201
|
+
[[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
|
|
202
|
+
[[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
|
|
203
|
+
_codex_snapshot
|
|
204
|
+
return 0
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# unverified --last scope: offers the recorded id, then --last only where cw vouches for the dir
|
|
208
|
+
codex_resume() {
|
|
209
|
+
local attempt="$1"
|
|
210
|
+
local -a plan=()
|
|
211
|
+
[[ -n "$CW_SESSION_REF" ]] && plan+=(id)
|
|
212
|
+
[[ -n "${CW_CONTINUE_LAST_SAFE:-}" ]] && plan+=(last)
|
|
213
|
+
local step="${plan[$((attempt - 1))]:-}"
|
|
214
|
+
[[ -n "$step" ]] || return 1
|
|
215
|
+
_codex_base || return 1
|
|
216
|
+
case "$step" in
|
|
217
|
+
id) HARNESS_ARGV+=(resume "$CW_SESSION_REF") ;;
|
|
218
|
+
last) HARNESS_ARGV+=(resume --last) ;;
|
|
219
|
+
*) return 1 ;;
|
|
220
|
+
esac
|
|
221
|
+
[[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
|
|
222
|
+
[[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
|
|
223
|
+
_codex_snapshot
|
|
224
|
+
return 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
codex_doctor() {
|
|
228
|
+
local status detail="null"
|
|
229
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
230
|
+
status="not_installed"; detail='"codex not found on PATH"'
|
|
231
|
+
elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
|
|
232
|
+
status="connected"
|
|
233
|
+
else
|
|
234
|
+
status="not_logged_in"
|
|
235
|
+
fi
|
|
236
|
+
printf '{"harness":"codex","status":"%s","detail":%s}\n' "$status" "$detail"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
codex_login() {
|
|
240
|
+
HARNESS_ENV=("CODEX_HOME=$CW_HARNESS_DIR")
|
|
241
|
+
HARNESS_ARGV=(codex login)
|
|
242
|
+
[[ -n "${CW_LOGIN_NO_BROWSER:-}" ]] && HARNESS_ARGV+=(--device-auth)
|
|
243
|
+
[[ -n "${CW_LOGIN_API_KEY_STDIN:-}" ]] && HARNESS_ARGV=(codex login --with-api-key)
|
|
244
|
+
return 0
|
|
245
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# opencode driver
|
|
2
|
+
# continue_last is not claimed: its --continue has no confirmed per-worktree scope
|
|
3
|
+
opencode_supports() {
|
|
4
|
+
local cap
|
|
5
|
+
for cap in non_interactive_prompt model_flag custom_provider; do
|
|
6
|
+
[[ "$1" == "$cap" ]] && return 0
|
|
7
|
+
done
|
|
8
|
+
return 1
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
opencode_config_env() {
|
|
12
|
+
printf 'OPENCODE_DATA_DIR=%s\n' "$CW_HARNESS_DIR"
|
|
13
|
+
printf 'OPENCODE_CONFIG=%s\n' "$CW_HARNESS_DIR/opencode.json"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# opencode has no name cw controls; no confirmed way to read one back either
|
|
17
|
+
opencode_session_ref() {
|
|
18
|
+
printf '%s' ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_opencode_base() {
|
|
22
|
+
HARNESS_ENV=("OPENCODE_DATA_DIR=$CW_HARNESS_DIR"
|
|
23
|
+
"OPENCODE_CONFIG=$CW_HARNESS_DIR/opencode.json")
|
|
24
|
+
if [[ -f "$CW_HARNESS_DIR/env" ]]; then
|
|
25
|
+
local -a extra_env=()
|
|
26
|
+
mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
|
|
27
|
+
[[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
|
|
28
|
+
fi
|
|
29
|
+
HARNESS_ARGV=(opencode)
|
|
30
|
+
local f
|
|
31
|
+
# unverified: flag ordering, and whether these flags exist on opencode at all
|
|
32
|
+
for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# prints the provider/model ref opencode expects; a native provider adds no prefix
|
|
37
|
+
_opencode_model_ref() {
|
|
38
|
+
local provider="${CW_PROVIDER:-native}" model="$CW_MODEL"
|
|
39
|
+
if [[ -z "$provider" || "$provider" == "native" || "$model" == "$provider/"* ]]; then
|
|
40
|
+
printf '%s' "$model"
|
|
41
|
+
else
|
|
42
|
+
printf '%s/%s' "$provider" "$model"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# writes the provider and model into the account's opencode config
|
|
47
|
+
_opencode_write_config() {
|
|
48
|
+
[[ -n "$CW_PROVIDER" && "$CW_PROVIDER" != "native" ]] || return 0
|
|
49
|
+
mkdir -p "$CW_HARNESS_DIR"
|
|
50
|
+
local why
|
|
51
|
+
why=$(CW_OC_FILE="$CW_HARNESS_DIR/opencode.json" CW_OC_MODEL="$(_opencode_model_ref)" \
|
|
52
|
+
python3 - <<'PY'
|
|
53
|
+
import json, os, sys
|
|
54
|
+
p = os.environ["CW_OC_FILE"]
|
|
55
|
+
model = os.environ["CW_OC_MODEL"]
|
|
56
|
+
try:
|
|
57
|
+
with open(p) as f:
|
|
58
|
+
cfg = json.load(f)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
cfg = {}
|
|
61
|
+
except Exception:
|
|
62
|
+
print("it is not plain json cw can edit", end="")
|
|
63
|
+
sys.exit(3)
|
|
64
|
+
if not isinstance(cfg, dict):
|
|
65
|
+
print("it is not a json object", end="")
|
|
66
|
+
sys.exit(3)
|
|
67
|
+
if not model or cfg.get("model") == model:
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
cfg["model"] = model
|
|
70
|
+
with open(p, "w") as f:
|
|
71
|
+
json.dump(cfg, f, indent=2)
|
|
72
|
+
PY
|
|
73
|
+
) && return 0
|
|
74
|
+
_err "Cannot apply model '$(_opencode_model_ref)' to $CW_HARNESS_DIR/opencode.json: ${why:-unknown error}."
|
|
75
|
+
_err "Set \"model\" in that file yourself; cw left it untouched."
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# unverified: --model flag ordering, whether the prompt can be passed on argv for a bare launch
|
|
80
|
+
opencode_launch() {
|
|
81
|
+
_opencode_write_config || return 1
|
|
82
|
+
_opencode_base
|
|
83
|
+
if [[ -n "$CW_PROMPT" ]]; then
|
|
84
|
+
HARNESS_ARGV=(opencode run "$CW_PROMPT")
|
|
85
|
+
[[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
|
|
86
|
+
elif [[ -n "$CW_MODEL" ]]; then
|
|
87
|
+
HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
|
|
88
|
+
fi
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# only a recorded id is attributable; --continue has no confirmed worktree scope, so it is unused
|
|
93
|
+
opencode_resume() {
|
|
94
|
+
local attempt="$1"
|
|
95
|
+
[[ "$attempt" == "1" && -n "$CW_SESSION_REF" ]] || return 1
|
|
96
|
+
_opencode_write_config || return 1
|
|
97
|
+
_opencode_base
|
|
98
|
+
HARNESS_ARGV=(opencode run "$CW_PROMPT" --session "$CW_SESSION_REF")
|
|
99
|
+
[[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
opencode_doctor() {
|
|
104
|
+
local status detail="null"
|
|
105
|
+
if ! command -v opencode >/dev/null 2>&1; then
|
|
106
|
+
status="not_installed"; detail='"opencode not found on PATH"'
|
|
107
|
+
elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
|
|
108
|
+
status="connected"
|
|
109
|
+
else
|
|
110
|
+
status="not_logged_in"
|
|
111
|
+
fi
|
|
112
|
+
printf '{"harness":"opencode","status":"%s","detail":%s}\n' "$status" "$detail"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# unverified: no confirmed --no-browser/device-code flag or api-key import path for opencode
|
|
116
|
+
opencode_login() {
|
|
117
|
+
HARNESS_ENV=("OPENCODE_DATA_DIR=$CW_HARNESS_DIR")
|
|
118
|
+
HARNESS_ARGV=(opencode auth login)
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# pi coding agent driver
|
|
2
|
+
# unverified: entire capability set below is inferred from docs, no binary to check against
|
|
3
|
+
# no custom_provider: this driver does not apply CW_PROVIDER yet
|
|
4
|
+
pi_supports() {
|
|
5
|
+
local cap
|
|
6
|
+
for cap in model_flag instructions_file; do
|
|
7
|
+
[[ "$1" == "$cap" ]] && return 0
|
|
8
|
+
done
|
|
9
|
+
return 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pi_config_env() {
|
|
13
|
+
printf 'PI_CODING_AGENT_DIR=%s\n' "$CW_HARNESS_DIR"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# pi has no name cw controls; no confirmed way to read one back either
|
|
17
|
+
pi_session_ref() {
|
|
18
|
+
printf '%s' ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_pi_base() {
|
|
22
|
+
HARNESS_ENV=("PI_CODING_AGENT_DIR=$CW_HARNESS_DIR")
|
|
23
|
+
if [[ -f "$CW_HARNESS_DIR/env" ]]; then
|
|
24
|
+
local -a extra_env=()
|
|
25
|
+
mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
|
|
26
|
+
[[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
|
|
27
|
+
fi
|
|
28
|
+
HARNESS_ARGV=(pi)
|
|
29
|
+
local f
|
|
30
|
+
# unverified: flag ordering and whether these flags exist on pi at all
|
|
31
|
+
for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# unverified: --model flag, and whether a prompt can be passed on argv at all
|
|
36
|
+
pi_launch() {
|
|
37
|
+
_pi_base
|
|
38
|
+
[[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
|
|
39
|
+
[[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
|
|
40
|
+
return 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# unverified resume mechanism, so no attempt is offered and cw starts fresh from the notes
|
|
44
|
+
pi_resume() {
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pi_doctor() {
|
|
49
|
+
local status detail="null"
|
|
50
|
+
if ! command -v pi >/dev/null 2>&1; then
|
|
51
|
+
status="not_installed"; detail='"pi not found on PATH"'
|
|
52
|
+
elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
|
|
53
|
+
status="connected"
|
|
54
|
+
else
|
|
55
|
+
status="not_logged_in"
|
|
56
|
+
fi
|
|
57
|
+
printf '{"harness":"pi","status":"%s","detail":%s}\n' "$status" "$detail"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# unverified: login is documented as an in-session /login command, not a subcommand or flag
|
|
61
|
+
# cw can only drop the user into a bare interactive session to run it themselves
|
|
62
|
+
pi_login() {
|
|
63
|
+
HARNESS_ENV=("PI_CODING_AGENT_DIR=$CW_HARNESS_DIR")
|
|
64
|
+
HARNESS_ARGV=(pi)
|
|
65
|
+
return 0
|
|
66
|
+
}
|
package/cw/lib/iterm2.sh
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# CW iTerm2 Helpers — AppleScript + escape sequences
|
|
4
|
+
# ============================================================================
|
|
5
|
+
|
|
6
|
+
# ── Color presets per mode ─────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
_mode_color() {
|
|
9
|
+
case "$1" in
|
|
10
|
+
code) echo "50 120 220" ;;
|
|
11
|
+
review) echo "180 80 200" ;;
|
|
12
|
+
research) echo "40 180 160" ;;
|
|
13
|
+
docs) echo "220 160 40" ;;
|
|
14
|
+
planning) echo "60 180 75" ;;
|
|
15
|
+
comms) echo "200 60 80" ;;
|
|
16
|
+
full) echo "100 100 100" ;;
|
|
17
|
+
shell) echo "80 80 80" ;;
|
|
18
|
+
git) echo "230 120 50" ;;
|
|
19
|
+
feature) echo "50 150 250" ;;
|
|
20
|
+
bug) echo "230 60 60" ;;
|
|
21
|
+
*) echo "100 100 100" ;;
|
|
22
|
+
esac
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# ── Core AppleScript functions ─────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
# Create a new iTerm2 window, cd to dir
|
|
28
|
+
_iterm_new_window() {
|
|
29
|
+
local dir="$1"
|
|
30
|
+
osascript <<EOF
|
|
31
|
+
tell application "iTerm2"
|
|
32
|
+
set newWindow to (create window with default profile)
|
|
33
|
+
tell current session of current tab of newWindow
|
|
34
|
+
write text "cd ${dir}"
|
|
35
|
+
end tell
|
|
36
|
+
end tell
|
|
37
|
+
EOF
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Create a new tab in current window, cd to dir
|
|
41
|
+
_iterm_new_tab() {
|
|
42
|
+
local dir="$1"
|
|
43
|
+
osascript <<EOF
|
|
44
|
+
tell application "iTerm2"
|
|
45
|
+
tell current window
|
|
46
|
+
set newTab to (create tab with default profile)
|
|
47
|
+
tell current session of newTab
|
|
48
|
+
write text "cd ${dir}"
|
|
49
|
+
end tell
|
|
50
|
+
end tell
|
|
51
|
+
end tell
|
|
52
|
+
EOF
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Send a command to current session
|
|
56
|
+
_iterm_send() {
|
|
57
|
+
local cmd="$1"
|
|
58
|
+
osascript <<EOF
|
|
59
|
+
tell application "iTerm2"
|
|
60
|
+
tell current session of current tab of current window
|
|
61
|
+
write text "${cmd}"
|
|
62
|
+
end tell
|
|
63
|
+
end tell
|
|
64
|
+
EOF
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Select tab by index (1-based)
|
|
68
|
+
_iterm_select_tab() {
|
|
69
|
+
local idx="$1"
|
|
70
|
+
osascript <<EOF
|
|
71
|
+
tell application "iTerm2"
|
|
72
|
+
tell current window
|
|
73
|
+
select tab ${idx}
|
|
74
|
+
end tell
|
|
75
|
+
end tell
|
|
76
|
+
EOF
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Focus iTerm2
|
|
80
|
+
_iterm_focus() {
|
|
81
|
+
osascript -e 'tell application "iTerm2" to activate'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ── Tab coloring via escape sequences ──────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
# Set tab color on current session (must be called AFTER the tab exists)
|
|
87
|
+
_iterm_set_tab_color() {
|
|
88
|
+
local r="$1" g="$2" b="$3"
|
|
89
|
+
_iterm_send "printf '\\\\033]6;1;bg;red;brightness;${r}\\\\a\\\\033]6;1;bg;green;brightness;${g}\\\\a\\\\033]6;1;bg;blue;brightness;${b}\\\\a'"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Set badge on current session
|
|
93
|
+
_iterm_set_badge() {
|
|
94
|
+
local text="$1"
|
|
95
|
+
local b64
|
|
96
|
+
b64=$(printf '%s' "$text" | base64)
|
|
97
|
+
_iterm_send "printf '\\\\033]1337;SetBadgeFormat=${b64}\\\\a'"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Set tab title via escape sequence
|
|
101
|
+
_iterm_set_title() {
|
|
102
|
+
local title="$1"
|
|
103
|
+
_iterm_send "printf '\\\\033]0;${title}\\\\007'"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ── Compound: configured window and tabs ──────────────────────────────────
|
|
107
|
+
|
|
108
|
+
# Create a fully configured window: dir, command, badge, mode
|
|
109
|
+
_iterm_configured_window() {
|
|
110
|
+
local title="$1" dir="$2" cmd="$3" badge="$4" mode="$5"
|
|
111
|
+
local r g b
|
|
112
|
+
read -r r g b <<< "$(_mode_color "$mode")"
|
|
113
|
+
|
|
114
|
+
_iterm_new_window "$dir"
|
|
115
|
+
sleep 0.4
|
|
116
|
+
|
|
117
|
+
_iterm_set_title "$title"
|
|
118
|
+
_iterm_set_tab_color "$r" "$g" "$b"
|
|
119
|
+
[[ -n "$badge" ]] && _iterm_set_badge "$badge"
|
|
120
|
+
|
|
121
|
+
sleep 0.2
|
|
122
|
+
[[ -n "$cmd" ]] && _iterm_send "$cmd"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Create a fully configured tab: dir, command, badge, mode
|
|
126
|
+
_iterm_configured_tab() {
|
|
127
|
+
local title="$1" dir="$2" cmd="$3" badge="$4" mode="$5"
|
|
128
|
+
local r g b
|
|
129
|
+
read -r r g b <<< "$(_mode_color "$mode")"
|
|
130
|
+
|
|
131
|
+
_iterm_new_tab "$dir"
|
|
132
|
+
sleep 0.4
|
|
133
|
+
|
|
134
|
+
_iterm_set_title "$title"
|
|
135
|
+
_iterm_set_tab_color "$r" "$g" "$b"
|
|
136
|
+
[[ -n "$badge" ]] && _iterm_set_badge "$badge"
|
|
137
|
+
|
|
138
|
+
sleep 0.2
|
|
139
|
+
[[ -n "$cmd" ]] && _iterm_send "$cmd"
|
|
140
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# [Project Name]
|
|
2
|
+
|
|
3
|
+
## What is this
|
|
4
|
+
<!-- One line: what this project does -->
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
<!-- Tech stack as table or short list -->
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
```
|
|
11
|
+
src/
|
|
12
|
+
├── ...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Development
|
|
16
|
+
```bash
|
|
17
|
+
# Setup
|
|
18
|
+
# Dev
|
|
19
|
+
# Test
|
|
20
|
+
# Build
|
|
21
|
+
# Deploy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Conventions
|
|
25
|
+
- Conventional commits: `feat:`, `fix:`, `chore:`
|
|
26
|
+
- Tests required for all new features
|
|
27
|
+
- No direct pushes to main
|
|
28
|
+
- Comments & notes: single line only, no docstring-style or multi-line blocks — in code and in TASK_NOTES.md alike; never reference task IDs, branch names, or GitHub/Linear issue numbers
|
|
29
|
+
<!-- Add project-specific conventions -->
|
|
30
|
+
|
|
31
|
+
## Working with me (Claude)
|
|
32
|
+
- If you don't know something about the project, ask. Don't make it up.
|
|
33
|
+
- Before major changes, explain your plan first.
|
|
34
|
+
- Run tests after every change.
|
|
35
|
+
- If you see a TASK_NOTES.md or REVIEW_NOTES.md in the working directory, read it first — it has context from previous sessions. The actual file lives in `~/.cw/sessions/` and is symlinked here.
|
|
36
|
+
- Never touch `.env`, credentials, or secrets. Only reference them.
|
|
37
|
+
|
|
38
|
+
## Do NOT
|
|
39
|
+
- Do not change infrastructure config without confirmation
|
|
40
|
+
- Do not modify existing migrations
|
|
41
|
+
- Do not delete tests
|
|
42
|
+
<!-- Add project-specific restrictions -->
|
|
43
|
+
|
|
44
|
+
## Integrations
|
|
45
|
+
<!-- MCPs, APIs, external services -->
|
|
46
|
+
|
|
47
|
+
## Additional context
|
|
48
|
+
<!-- Links to docs, ADRs, important decisions -->
|