jeo-code 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.
Files changed (93) hide show
  1. package/README.md +342 -0
  2. package/package.json +57 -0
  3. package/scripts/install.sh +322 -0
  4. package/scripts/uninstall.sh +30 -0
  5. package/src/agent/compaction.ts +75 -0
  6. package/src/agent/config-schema.ts +87 -0
  7. package/src/agent/context-files.ts +51 -0
  8. package/src/agent/engine.ts +208 -0
  9. package/src/agent/json.ts +87 -0
  10. package/src/agent/loop.ts +22 -0
  11. package/src/agent/session.ts +198 -0
  12. package/src/agent/state.ts +199 -0
  13. package/src/agent/subagents.ts +149 -0
  14. package/src/agent/tools.ts +355 -0
  15. package/src/ai/index.ts +11 -0
  16. package/src/ai/model-catalog-compat.ts +119 -0
  17. package/src/ai/model-catalog.ts +97 -0
  18. package/src/ai/model-discovery.ts +148 -0
  19. package/src/ai/model-enrich.ts +75 -0
  20. package/src/ai/model-manager.ts +178 -0
  21. package/src/ai/model-picker.ts +73 -0
  22. package/src/ai/model-registry.ts +83 -0
  23. package/src/ai/provider-status.ts +77 -0
  24. package/src/ai/providers/anthropic.ts +87 -0
  25. package/src/ai/providers/errors.ts +47 -0
  26. package/src/ai/providers/gemini.ts +77 -0
  27. package/src/ai/providers/ollama.ts +54 -0
  28. package/src/ai/providers/openai.ts +67 -0
  29. package/src/ai/sse.ts +46 -0
  30. package/src/ai/types.ts +37 -0
  31. package/src/auth/callback-server.ts +195 -0
  32. package/src/auth/flows/anthropic.ts +114 -0
  33. package/src/auth/flows/google.ts +120 -0
  34. package/src/auth/flows/index.ts +50 -0
  35. package/src/auth/flows/openai.ts +130 -0
  36. package/src/auth/index.ts +23 -0
  37. package/src/auth/oauth.ts +80 -0
  38. package/src/auth/pkce.ts +24 -0
  39. package/src/auth/refresh.ts +60 -0
  40. package/src/auth/storage.ts +113 -0
  41. package/src/auth/types.ts +26 -0
  42. package/src/cli/index.ts +1 -0
  43. package/src/cli/runner.ts +245 -0
  44. package/src/cli.ts +17 -0
  45. package/src/commands/approve.ts +63 -0
  46. package/src/commands/auth.ts +144 -0
  47. package/src/commands/chat.ts +37 -0
  48. package/src/commands/deep-interview.ts +239 -0
  49. package/src/commands/doctor.ts +250 -0
  50. package/src/commands/evolve.ts +191 -0
  51. package/src/commands/launch.ts +745 -0
  52. package/src/commands/mcp.ts +18 -0
  53. package/src/commands/models.ts +104 -0
  54. package/src/commands/ralplan.ts +86 -0
  55. package/src/commands/resume.ts +6 -0
  56. package/src/commands/setup-helpers.ts +93 -0
  57. package/src/commands/setup.ts +190 -0
  58. package/src/commands/skills.ts +38 -0
  59. package/src/commands/team.ts +337 -0
  60. package/src/commands/ultragoal.ts +102 -0
  61. package/src/index.ts +31 -0
  62. package/src/mcp/index.ts +3 -0
  63. package/src/mcp/protocol.ts +45 -0
  64. package/src/mcp/server.ts +97 -0
  65. package/src/mcp/tools.ts +156 -0
  66. package/src/skills/catalog.ts +61 -0
  67. package/src/tui/app.ts +297 -0
  68. package/src/tui/components/ascii-art.ts +340 -0
  69. package/src/tui/components/autocomplete.ts +165 -0
  70. package/src/tui/components/capability.ts +29 -0
  71. package/src/tui/components/code-view.ts +146 -0
  72. package/src/tui/components/color.ts +172 -0
  73. package/src/tui/components/config-panel.ts +193 -0
  74. package/src/tui/components/evolution.ts +305 -0
  75. package/src/tui/components/footer.ts +95 -0
  76. package/src/tui/components/forge.ts +167 -0
  77. package/src/tui/components/index.ts +7 -0
  78. package/src/tui/components/layout.ts +105 -0
  79. package/src/tui/components/meter.ts +61 -0
  80. package/src/tui/components/model-picker.ts +82 -0
  81. package/src/tui/components/provider-picker.ts +42 -0
  82. package/src/tui/components/select-list.ts +199 -0
  83. package/src/tui/components/slash.ts +34 -0
  84. package/src/tui/components/spinner.ts +49 -0
  85. package/src/tui/components/status.ts +45 -0
  86. package/src/tui/components/stream.ts +36 -0
  87. package/src/tui/components/themes.ts +86 -0
  88. package/src/tui/components/tool-list.ts +67 -0
  89. package/src/tui/index.ts +2 -0
  90. package/src/tui/renderer.ts +70 -0
  91. package/src/tui/terminal.ts +78 -0
  92. package/src/util/retry.ts +108 -0
  93. package/tsconfig.json +18 -0
@@ -0,0 +1,322 @@
1
+ #!/bin/sh
2
+ # joc (jeo-code) installer — gjc-style bun global install.
3
+ #
4
+ # The gjc parity path is a single Bun global install. The published-package form
5
+ # (once jeo-code is on npm) is identical to gajae-code's:
6
+ #
7
+ # bun install -g jeo-code # gjc parity: bun install -g gajae-code
8
+ #
9
+ # Until then this script performs the equivalent global install straight from the
10
+ # GitHub repo, auto-installing Bun if missing. Registry flags are explicit and
11
+ # safe by default: --registry is one-shot for this install; --persist-registry is
12
+ # required before npm's global config is changed.
13
+ #
14
+ # Usage:
15
+ # curl -fsSL <raw-url>/scripts/install.sh | sh # git install from GitHub URL
16
+ # sh scripts/install.sh # same as above
17
+ # sh scripts/install.sh --repo https://github.com/akillness/jeo-code.git
18
+ # sh scripts/install.sh --npm --registry https://registry.npmjs.org/
19
+ # sh scripts/install.sh --npm --registry https://npmjs.co.kr
20
+ # sh scripts/install.sh --registry https://your-company-registry.com --persist-registry
21
+ # sh scripts/install.sh --scope @my-org --registry https://your-company-registry.com --project-npmrc
22
+ # sh scripts/install.sh --ref v0.1.0 # global install of a specific git ref
23
+ # sh scripts/install.sh --local # dev: install from this clone (bun link)
24
+ # sh scripts/install.sh --binary # compile a standalone binary (no bun at runtime)
25
+ set -e
26
+
27
+ DEFAULT_REPO="https://github.com/akillness/jeo-code.git"
28
+ REPO="${JOC_REPO:-${JOC_REPO_URL:-$DEFAULT_REPO}}"
29
+ PKG="${JOC_PKG:-jeo-code}"
30
+ INSTALL_DIR="${JOC_INSTALL_DIR:-$HOME/.local/bin}"
31
+ MIN_BUN_VERSION="1.3.14"
32
+
33
+ MODE="global"
34
+ REF=""
35
+ SRC_DIR=""
36
+ LINKED=""
37
+ REGISTRY="${JOC_REGISTRY:-}"
38
+ SCOPE="${JOC_REGISTRY_SCOPE:-}"
39
+ PERSIST_REGISTRY=0
40
+ PROJECT_NPMRC=0
41
+ DRY_RUN=0
42
+
43
+ usage() {
44
+ cat <<EOF
45
+ joc installer (gjc-style Bun global install)
46
+ (default) bun global install from $REPO → exposes 'joc'
47
+ --repo <url|owner/repo> git source (default $DEFAULT_REPO)
48
+ --npm bun install -g $PKG (npm registry; gjc parity once published)
49
+ --package <name> npm package name for --npm (default $PKG)
50
+ --registry <url> one-shot registry for this install (does not mutate npm config)
51
+ --scope <@scope> registry scope key for persisted/project .npmrc config
52
+ --persist-registry run 'npm config set registry <url>' (or '<scope>:registry')
53
+ --project-npmrc write registry=<url> (or <scope>:registry=<url>) to ./.npmrc
54
+ --print-registry run 'npm config get registry' (or '<scope>:registry') and exit
55
+ --delete-registry run 'npm config delete registry' (or '<scope>:registry') and exit
56
+ --local install from current clone via 'bun link' (dev)
57
+ --binary compile a standalone binary (no bun needed at runtime)
58
+ --ref <ref> install a specific tag/branch/commit
59
+ --dry-run print the bun/npm commands without installing
60
+ Environment:
61
+ JOC_INSTALL_DIR (default \$HOME/.local/bin — compatibility symlink)
62
+ JOC_REPO/JOC_REPO_URL(default $DEFAULT_REPO)
63
+ JOC_PKG (default $PKG)
64
+ JOC_REGISTRY one-shot or persisted registry URL
65
+ JOC_REGISTRY_SCOPE optional scope such as @my-org
66
+ EOF
67
+ }
68
+
69
+ while [ $# -gt 0 ]; do
70
+ case "$1" in
71
+ --global) MODE="global"; shift ;;
72
+ --npm) MODE="npm"; shift ;;
73
+ --local) MODE="local"; shift ;;
74
+ --binary) MODE="binary"; shift ;;
75
+ --repo) shift; REPO="$1"; shift ;;
76
+ --repo=*) REPO="${1#*=}"; shift ;;
77
+ --package) shift; PKG="$1"; shift ;;
78
+ --package=*) PKG="${1#*=}"; shift ;;
79
+ --registry|--npm-registry) shift; REGISTRY="$1"; shift ;;
80
+ --registry=*|--npm-registry=*) REGISTRY="${1#*=}"; shift ;;
81
+ --scope) shift; SCOPE="$1"; shift ;;
82
+ --scope=*) SCOPE="${1#*=}"; shift ;;
83
+ --persist-registry) PERSIST_REGISTRY=1; shift ;;
84
+ --project-npmrc) PROJECT_NPMRC=1; shift ;;
85
+ --print-registry) MODE="registry-print"; shift ;;
86
+ --delete-registry) MODE="registry-delete"; shift ;;
87
+ --dry-run) DRY_RUN=1; shift ;;
88
+ --ref) shift; REF="$1"; shift ;;
89
+ --ref=*) REF="${1#*=}"; shift ;;
90
+ -r) shift; REF="$1"; shift ;;
91
+ -h|--help) usage; exit 0 ;;
92
+ *)
93
+ echo "Unknown option: $1"
94
+ exit 1 ;;
95
+ esac
96
+ done
97
+
98
+ has_bun() { command -v bun >/dev/null 2>&1; }
99
+ has_npm() { command -v npm >/dev/null 2>&1; }
100
+
101
+ version_ge() {
102
+ cur="$1"; min="$2"
103
+ cm="${cur%%.*}"; rest="${cur#*.}"; cn="${rest%%.*}"; cp="${rest#*.}"; cp="${cp%%.*}"
104
+ mm="${min%%.*}"; rest="${min#*.}"; mn="${rest%%.*}"; mp="${rest#*.}"; mp="${mp%%.*}"
105
+ [ "$cm" -ne "$mm" ] && { [ "$cm" -gt "$mm" ]; return $?; }
106
+ [ "$cn" -ne "$mn" ] && { [ "$cn" -gt "$mn" ]; return $?; }
107
+ [ "$cp" -ge "$mp" ]
108
+ }
109
+
110
+ require_bun() {
111
+ if ! has_bun; then
112
+ echo "Installing Bun (required runtime)..."
113
+ curl -fsSL https://bun.sh/install | bash
114
+ export BUN_INSTALL="$HOME/.bun"
115
+ export PATH="$BUN_INSTALL/bin:$PATH"
116
+ fi
117
+ v=$(bun --version 2>/dev/null)
118
+ v_clean=${v%%-*}
119
+ if ! version_ge "$v_clean" "$MIN_BUN_VERSION"; then
120
+ echo "Bun $MIN_BUN_VERSION+ required (found $v_clean). Upgrade: bun upgrade"
121
+ exit 1
122
+ fi
123
+ }
124
+
125
+ bun_bin_dir() { echo "${BUN_INSTALL:-$HOME/.bun}/bin"; }
126
+
127
+ registry_key() {
128
+ if [ -n "$SCOPE" ]; then
129
+ case "$SCOPE" in
130
+ @*) echo "$SCOPE:registry" ;;
131
+ *) echo "@$SCOPE:registry" ;;
132
+ esac
133
+ else
134
+ echo "registry"
135
+ fi
136
+ }
137
+
138
+ validate_registry_url() {
139
+ [ -z "$REGISTRY" ] && return 0
140
+ case "$REGISTRY" in
141
+ http://*|https://*) return 0 ;;
142
+ *) echo "--registry must start with http:// or https:// (got '$REGISTRY')"; exit 1 ;;
143
+ esac
144
+ }
145
+
146
+ require_npm_config() {
147
+ if ! has_npm; then
148
+ echo "npm is required for npm config operations (--persist-registry/--print-registry/--delete-registry)."
149
+ exit 1
150
+ fi
151
+ }
152
+
153
+ print_registry() {
154
+ require_npm_config
155
+ key=$(registry_key)
156
+ npm config get "$key"
157
+ }
158
+
159
+ delete_registry() {
160
+ require_npm_config
161
+ key=$(registry_key)
162
+ if [ "$DRY_RUN" = "1" ]; then
163
+ echo "+ npm config delete $key"
164
+ else
165
+ npm config delete "$key"
166
+ fi
167
+ echo "Deleted npm config key: $key"
168
+ }
169
+
170
+ persist_registry() {
171
+ [ "$PERSIST_REGISTRY" = "1" ] || return 0
172
+ validate_registry_url
173
+ require_npm_config
174
+ key=$(registry_key)
175
+ if [ "$DRY_RUN" = "1" ]; then
176
+ echo "+ npm config set $key $REGISTRY"
177
+ else
178
+ npm config set "$key" "$REGISTRY"
179
+ fi
180
+ echo "Persisted npm config: $key=$REGISTRY"
181
+ }
182
+
183
+ write_project_npmrc() {
184
+ [ "$PROJECT_NPMRC" = "1" ] || return 0
185
+ validate_registry_url
186
+ key=$(registry_key)
187
+ if [ "$DRY_RUN" = "1" ]; then
188
+ echo "+ printf '%s=%s\\n' '$key' '$REGISTRY' > .npmrc"
189
+ else
190
+ printf '%s=%s\n' "$key" "$REGISTRY" > .npmrc
191
+ fi
192
+ echo "Wrote project registry config: .npmrc ($key=$REGISTRY)"
193
+ }
194
+
195
+ normalize_repo_spec() {
196
+ repo="$1"
197
+ case "$repo" in
198
+ github:*|git+*|ssh://*|git@*) spec="$repo" ;;
199
+ http://*|https://*) spec="git+$repo" ;;
200
+ */*) spec="github:$repo" ;;
201
+ *) spec="$repo" ;;
202
+ esac
203
+ [ -n "$REF" ] && spec="$spec#$REF"
204
+ echo "$spec"
205
+ }
206
+
207
+ run_bun_global_add() {
208
+ spec="$1"
209
+ if [ -n "$REGISTRY" ]; then
210
+ validate_registry_url
211
+ if [ -n "$SCOPE" ] && [ "$PERSIST_REGISTRY" != "1" ] && [ "$PROJECT_NPMRC" != "1" ]; then
212
+ echo "Note: scoped registries need --persist-registry or --project-npmrc for npm-compatible scope config; using $REGISTRY as this install's one-shot registry."
213
+ fi
214
+ if [ "$DRY_RUN" = "1" ]; then
215
+ echo "+ NPM_CONFIG_REGISTRY=$REGISTRY npm_config_registry=$REGISTRY bun add -g $spec"
216
+ else
217
+ NPM_CONFIG_REGISTRY="$REGISTRY" npm_config_registry="$REGISTRY" bun add -g "$spec"
218
+ fi
219
+ else
220
+ if [ "$DRY_RUN" = "1" ]; then
221
+ echo "+ bun add -g $spec"
222
+ else
223
+ bun add -g "$spec"
224
+ fi
225
+ fi
226
+ }
227
+
228
+ # Bun global install (the gjc-idiomatic path). Exposes the package's `joc` bin
229
+ # in Bun's global bin dir (~/.bun/bin/joc).
230
+ install_global() {
231
+ spec=$(normalize_repo_spec "$REPO")
232
+ echo "Installing $PKG globally via Bun ($spec)..."
233
+ run_bun_global_add "$spec"
234
+ }
235
+
236
+ install_npm() {
237
+ spec="$PKG"
238
+ [ -n "$REF" ] && spec="$PKG@$REF"
239
+ echo "Installing $PKG globally via Bun ($spec)..."
240
+ run_bun_global_add "$spec"
241
+ }
242
+
243
+ # Dev install from the current clone: register the package globally with
244
+ # `bun link` so source edits are picked up immediately.
245
+ install_local() {
246
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
247
+ SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
248
+ if [ ! -f "$SRC_DIR/src/cli.ts" ]; then
249
+ echo "--local: expected cli.ts at $SRC_DIR/src/cli.ts"
250
+ exit 1
251
+ fi
252
+ if [ "$DRY_RUN" = "1" ]; then
253
+ echo "+ ( cd $SRC_DIR && bun install --silent )"
254
+ echo "+ ( cd $SRC_DIR && bun link )"
255
+ else
256
+ ( cd "$SRC_DIR" && bun install --silent >/dev/null )
257
+ ( cd "$SRC_DIR" && bun link >/dev/null 2>&1 ) || true
258
+ fi
259
+ }
260
+
261
+ install_binary() {
262
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
263
+ SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
264
+ if [ ! -f "$SRC_DIR/src/cli.ts" ]; then
265
+ echo "--binary must be run from a clone (expected $SRC_DIR/src/cli.ts)"
266
+ exit 1
267
+ fi
268
+ if [ "$DRY_RUN" = "1" ]; then
269
+ echo "+ ( cd $SRC_DIR && bun install --silent )"
270
+ echo "+ bun build src/cli.ts --compile --outfile $INSTALL_DIR/joc"
271
+ else
272
+ ( cd "$SRC_DIR" && bun install --silent >/dev/null )
273
+ mkdir -p "$INSTALL_DIR"
274
+ echo "Compiling standalone binary → $INSTALL_DIR/joc ..."
275
+ ( cd "$SRC_DIR" && bun build src/cli.ts --compile --outfile "$INSTALL_DIR/joc" >/dev/null )
276
+ chmod +x "$INSTALL_DIR/joc" 2>/dev/null || true
277
+ fi
278
+ }
279
+
280
+ # Add a compatibility symlink in INSTALL_DIR so the documented location keeps
281
+ # working even when Bun's global bin dir is not on PATH.
282
+ link_compat() {
283
+ [ "$DRY_RUN" = "1" ] && return 0
284
+ BUN_BIN="$(bun_bin_dir)"
285
+ [ -e "$BUN_BIN/joc" ] && LINKED="$BUN_BIN/joc"
286
+ mkdir -p "$INSTALL_DIR"
287
+ if [ -n "$LINKED" ]; then
288
+ ln -sf "$LINKED" "$INSTALL_DIR/joc"
289
+ chmod +x "$INSTALL_DIR/joc" 2>/dev/null || true
290
+ fi
291
+ }
292
+
293
+ print_done() {
294
+ BUN_BIN="$(bun_bin_dir)"
295
+ echo ""
296
+ if [ "$DRY_RUN" = "1" ]; then
297
+ echo "Dry run complete; no install changes were made."
298
+ return 0
299
+ fi
300
+ [ -n "$LINKED" ] && echo "Linked joc via Bun → $LINKED"
301
+ [ -e "$INSTALL_DIR/joc" ] && echo "Compatibility symlink → $INSTALL_DIR/joc"
302
+ case ":$PATH:" in
303
+ *":$BUN_BIN:"*|*":$INSTALL_DIR:"*) echo "Run: joc --help" ;;
304
+ *) echo "Add $BUN_BIN (or $INSTALL_DIR) to PATH, then run: joc --help" ;;
305
+ esac
306
+ }
307
+
308
+ case "$MODE" in
309
+ registry-print) print_registry; exit 0 ;;
310
+ registry-delete) delete_registry; exit 0 ;;
311
+ esac
312
+
313
+ persist_registry
314
+ write_project_npmrc
315
+ require_bun
316
+ case "$MODE" in
317
+ global) install_global; link_compat ;;
318
+ npm) install_npm; link_compat ;;
319
+ local) install_local; link_compat ;;
320
+ binary) install_binary ;;
321
+ esac
322
+ print_done
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ # joc uninstaller — removes binary symlink and optionally global config.
3
+ # Usage:
4
+ # sh scripts/uninstall.sh # remove binary only
5
+ # sh scripts/uninstall.sh --purge # also remove ~/.joc/
6
+ set -e
7
+ INSTALL_DIR="${JOC_INSTALL_DIR:-$HOME/.local/bin}"
8
+ PURGE=0
9
+ [ "$1" = "--purge" ] && PURGE=1
10
+
11
+ if [ -L "$INSTALL_DIR/joc" ] || [ -f "$INSTALL_DIR/joc" ]; then
12
+ rm -f "$INSTALL_DIR/joc"
13
+ echo "Removed $INSTALL_DIR/joc"
14
+ else
15
+ echo "No joc binary at $INSTALL_DIR/joc"
16
+ fi
17
+
18
+ # Remove the bun-native link (bin + global registry entry).
19
+ BUN_BIN="${BUN_INSTALL:-$HOME/.bun}/bin"
20
+ if [ -L "$BUN_BIN/joc" ] || [ -f "$BUN_BIN/joc" ]; then
21
+ rm -f "$BUN_BIN/joc"
22
+ echo "Removed $BUN_BIN/joc (bun link)"
23
+ fi
24
+ GLOBAL_PKG="${BUN_INSTALL:-$HOME/.bun}/install/global/node_modules/jeo-code"
25
+ [ -e "$GLOBAL_PKG" ] && rm -rf "$GLOBAL_PKG" && echo "Unregistered jeo-code from bun global"
26
+
27
+ if [ "$PURGE" = "1" ]; then
28
+ rm -rf "$HOME/.joc"
29
+ echo "Removed ~/.joc/"
30
+ fi
@@ -0,0 +1,75 @@
1
+ import { callLlm, type Message } from "./loop";
2
+
3
+ export interface CompactionOptions {
4
+ maxMessages?: number;
5
+ keepRecent?: number;
6
+ model?: string;
7
+ /** User-initiated `/compact`: lower the trigger floor so it actually compacts a small history. */
8
+ force?: boolean;
9
+ }
10
+
11
+ export interface CompactionResult {
12
+ compacted: boolean;
13
+ removed: number;
14
+ summary?: string;
15
+ }
16
+
17
+ export async function maybeCompact(
18
+ history: Message[],
19
+ opts: CompactionOptions = {}
20
+ ): Promise<CompactionResult> {
21
+ const maxMessages = opts.maxMessages ?? (opts.force ? 1 : 40);
22
+ const keepRecent = opts.keepRecent ?? (opts.force ? 4 : 12);
23
+
24
+ const hasSystem = history.length > 0 && history[0].role === "system";
25
+ const systemCount = hasSystem ? 1 : 0;
26
+ const body = history.slice(systemCount);
27
+
28
+ if (body.length <= maxMessages) {
29
+ return { compacted: false, removed: 0 };
30
+ }
31
+
32
+ const olderCount = body.length - keepRecent;
33
+ if (olderCount <= 0) {
34
+ return { compacted: false, removed: 0 };
35
+ }
36
+
37
+ const older = body.slice(0, olderCount);
38
+ const recent = body.slice(olderCount);
39
+
40
+ const olderFormatted = older
41
+ .map(msg => `[${msg.role}] ${msg.content}`)
42
+ .join("\n");
43
+
44
+ const systemPrompt =
45
+ "Summarize the following coding-agent conversation so work can continue. Capture decisions, files changed, current task state, and open TODOs. Be concise.";
46
+
47
+ try {
48
+ const summary = await callLlm(
49
+ [
50
+ { role: "user", content: olderFormatted }
51
+ ],
52
+ {
53
+ model: opts.model,
54
+ systemPrompt,
55
+ }
56
+ );
57
+
58
+ const systemMessages = hasSystem ? [history[0]] : [];
59
+ const next: Message[] = [
60
+ ...systemMessages,
61
+ { role: "user", content: "[Earlier conversation summary]\n" + summary },
62
+ ...recent
63
+ ];
64
+
65
+ history.splice(0, history.length, ...next);
66
+
67
+ return {
68
+ compacted: true,
69
+ removed: older.length,
70
+ summary,
71
+ };
72
+ } catch {
73
+ return { compacted: false, removed: 0 };
74
+ }
75
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Runtime validation for `~/.joc/config.json`. Previously the file was
5
+ * `JSON.parse`d and cast straight to `Config` — a wrong-typed field (e.g. a
6
+ * numeric `defaultModel`) slipped through untyped and surfaced as a confusing
7
+ * downstream failure. `parseConfig` turns that into a clear, actionable signal.
8
+ */
9
+ const StoredOAuthSchema = z.object({
10
+ access: z.string(),
11
+ refresh: z.string().optional(),
12
+ expires: z.number().optional(),
13
+ accountId: z.string().optional(),
14
+ email: z.string().optional(),
15
+ projectId: z.string().optional(),
16
+ });
17
+
18
+ const OAuthEntry = z.union([z.string(), StoredOAuthSchema]);
19
+
20
+ export const ConfigSchema = z
21
+ .object({
22
+ providers: z
23
+ .object({
24
+ anthropic: z.string().optional(),
25
+ openai: z.string().optional(),
26
+ gemini: z.string().optional(),
27
+ })
28
+ .default({}),
29
+ oauth: z
30
+ .object({
31
+ anthropic: OAuthEntry.optional(),
32
+ openai: OAuthEntry.optional(),
33
+ gemini: OAuthEntry.optional(),
34
+ })
35
+ .optional(),
36
+ ollamaBaseUrl: z.string().optional(),
37
+ openaiBaseUrl: z.string().optional(),
38
+ defaultModel: z.string().min(1),
39
+ thinkingLevel: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
40
+ modelAliases: z.record(z.string()).optional(),
41
+ /**
42
+ * Provider retry budgets (gjc parity). `requestMaxRetries` counts retries
43
+ * (not the initial request) for a provider request; `maxDelayMs` caps backoff.
44
+ * `maxRetries`/`streamMaxRetries` are accepted for gjc-config compatibility.
45
+ */
46
+ retry: z
47
+ .object({
48
+ requestMaxRetries: z.number().int().min(0).optional(),
49
+ streamMaxRetries: z.number().int().min(0).optional(),
50
+ maxRetries: z.number().int().min(0).optional(),
51
+ maxDelayMs: z.number().int().min(0).optional(),
52
+ })
53
+ .optional(),
54
+ /**
55
+ * Per-subagent-role overrides (gjc role-agent parity). Keyed by role id
56
+ * (executor / planner / architect / critic); each may pin a model and/or a
57
+ * step budget. Tolerant of unknown keys.
58
+ */
59
+ subagents: z
60
+ .record(
61
+ z.object({
62
+ model: z.string().optional(),
63
+ maxSteps: z.number().int().min(1).optional(),
64
+ }),
65
+ )
66
+ .optional(),
67
+ /** Model role tiers (smol/slow/plan); each falls back to defaultModel. */
68
+ roles: z
69
+ .object({
70
+ smol: z.string().optional(),
71
+ slow: z.string().optional(),
72
+ plan: z.string().optional(),
73
+ })
74
+ .optional(),
75
+ })
76
+ .passthrough();
77
+
78
+ export type ValidatedConfig = z.infer<typeof ConfigSchema>;
79
+
80
+ /** Validate parsed JSON against the config schema. Returns a tagged result, never throws. */
81
+ export function parseConfig(raw: unknown): { ok: true; config: ValidatedConfig } | { ok: false; message: string } {
82
+ const result = ConfigSchema.safeParse(raw);
83
+ if (result.success) return { ok: true, config: result.data };
84
+ const issue = result.error.issues[0];
85
+ const where = issue?.path?.length ? issue.path.join(".") : "config";
86
+ return { ok: false, message: `${where}: ${issue?.message ?? "invalid"}` };
87
+ }
@@ -0,0 +1,51 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export interface ProjectContextFile {
5
+ path: string;
6
+ content: string;
7
+ }
8
+
9
+ export const CONTEXT_CANDIDATES = ["JEO.md", "AGENTS.md", ".joc/context.md", "CLAUDE.md"];
10
+
11
+ export async function loadProjectContext(cwd = process.cwd()): Promise<ProjectContextFile[]> {
12
+ const result: ProjectContextFile[] = [];
13
+
14
+ for (const candidate of CONTEXT_CANDIDATES) {
15
+ const filePath = path.join(cwd, candidate);
16
+ try {
17
+ const stat = await fs.stat(filePath);
18
+ if (stat.isFile() && stat.size > 0) {
19
+ const content = await fs.readFile(filePath, "utf-8");
20
+ if (content.length > 0) {
21
+ let finalContent = content;
22
+ if (content.length > 16000) {
23
+ finalContent = content.slice(0, 16000) + "\n…(truncated)";
24
+ }
25
+ result.push({
26
+ path: candidate,
27
+ content: finalContent,
28
+ });
29
+ }
30
+ }
31
+ } catch (err) {
32
+ // Skip missing, unreadable, or directories/empty files
33
+ }
34
+ }
35
+
36
+ return result;
37
+ }
38
+
39
+ export function withProjectContext(systemPrompt: string, contextFiles: ProjectContextFile[]): string {
40
+ if (!contextFiles || contextFiles.length === 0) {
41
+ return systemPrompt;
42
+ }
43
+
44
+ let contextBlock = "<project_context>\n\nProject-specific instructions and guidelines:\n";
45
+ for (const file of contextFiles) {
46
+ contextBlock += `\n<project_instructions path="${file.path}">\n${file.content}\n</project_instructions>\n`;
47
+ }
48
+ contextBlock += "</project_context>";
49
+
50
+ return `${systemPrompt}\n\n${contextBlock}`;
51
+ }