jeo-code 0.12.13 → 0.12.16

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 (64) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.ja.md +33 -9
  3. package/README.ko.md +33 -9
  4. package/README.md +33 -9
  5. package/README.zh.md +33 -9
  6. package/package.json +4 -2
  7. package/src/agent/browser-tool.ts +9 -1
  8. package/src/agent/config-schema.ts +8 -0
  9. package/src/agent/context-files.ts +19 -0
  10. package/src/agent/engine.ts +25 -6
  11. package/src/agent/eval-tool.ts +6 -0
  12. package/src/agent/goal-verifier.ts +30 -6
  13. package/src/agent/hooks.ts +52 -4
  14. package/src/agent/job-registry.ts +110 -18
  15. package/src/agent/job-tool.ts +43 -9
  16. package/src/agent/loop-guards.ts +17 -2
  17. package/src/agent/memory.ts +151 -112
  18. package/src/agent/monitor-registry.ts +142 -30
  19. package/src/agent/monitor-tool.ts +34 -6
  20. package/src/agent/notify/discord-daemon.ts +135 -42
  21. package/src/agent/notify/session-endpoint.ts +27 -5
  22. package/src/agent/sandbox/assets.d.ts +4 -0
  23. package/src/agent/sandbox/authority.ts +358 -0
  24. package/src/agent/sandbox/bundle.ts +40 -0
  25. package/src/agent/sandbox/control.ts +49 -0
  26. package/src/agent/sandbox/docker.ts +293 -0
  27. package/src/agent/sandbox/hands-source.text +14548 -0
  28. package/src/agent/sandbox/hands-worker.ts +168 -0
  29. package/src/agent/sandbox/policy.ts +172 -0
  30. package/src/agent/sandbox/rpc.ts +90 -0
  31. package/src/agent/sandbox/supervisor.ts +210 -0
  32. package/src/agent/sandbox/types.ts +77 -0
  33. package/src/agent/shell-lease.ts +190 -0
  34. package/src/agent/state.ts +167 -0
  35. package/src/agent/subagents.ts +4 -3
  36. package/src/agent/task-tool.ts +8 -0
  37. package/src/agent/todo-tool.ts +30 -1
  38. package/src/agent/tool-schemas.ts +13 -0
  39. package/src/ai/model-discovery.ts +12 -0
  40. package/src/ai/providers/anthropic.ts +27 -2
  41. package/src/ai/providers/antigravity.ts +10 -2
  42. package/src/ai/providers/gemini.ts +20 -16
  43. package/src/ai/providers/ollama.ts +14 -0
  44. package/src/ai/providers/openai-responses.ts +22 -5
  45. package/src/ai/providers/openai.ts +23 -40
  46. package/src/cli/runner.ts +3 -0
  47. package/src/commands/approve.ts +37 -13
  48. package/src/commands/launch/flags.ts +2 -1
  49. package/src/commands/launch/jobs-slash.ts +1 -1
  50. package/src/commands/launch.ts +275 -35
  51. package/src/commands/messenger.ts +107 -39
  52. package/src/commands/notify.ts +2 -2
  53. package/src/commands/team.ts +44 -12
  54. package/src/commands/ultragoal.ts +6 -16
  55. package/src/messenger/activity.ts +2 -1
  56. package/src/messenger/config.ts +78 -10
  57. package/src/messenger/discord-setup.ts +216 -34
  58. package/src/messenger/discord.ts +37 -43
  59. package/src/messenger/gateway.ts +85 -37
  60. package/src/messenger/model-selection.ts +101 -0
  61. package/src/messenger/session-pool.ts +189 -35
  62. package/src/messenger/types.ts +5 -2
  63. package/src/skills/catalog.ts +207 -0
  64. package/src/util/retry.ts +12 -0
@@ -1,3 +1,5 @@
1
+ import type { SandboxConfig } from "../agent/sandbox/types";
2
+
1
3
  export type MessengerPlatform = "discord" | "telegram" | "slack";
2
4
 
3
5
  export interface MessengerMessage {
@@ -48,12 +50,13 @@ export interface MessengerPlatformConfig {
48
50
  }
49
51
 
50
52
  export interface MessengerConfig {
53
+ /** Directory holding the loaded messenger.json, token file and run script; set by `loadMessengerConfig`. */
54
+ profileDir?: string;
51
55
  cwd: string;
52
56
  sessionDir: string;
53
57
  toolNames: string[];
58
+ sandbox?: SandboxConfig;
54
59
  platforms: Partial<Record<MessengerPlatform, MessengerPlatformConfig>>;
55
- /** Whether each message invocation runs in an isolated independent session. Defaults to true. */
56
- independentSessions?: boolean;
57
60
  }
58
61
 
59
62
  export interface MessengerTransportDeps {
@@ -209,6 +209,17 @@ import * as path from "node:path";
209
209
  import * as os from "node:os";
210
210
 
211
211
  import { jeoEnv } from "../util/env";
212
+ import { currentSandboxExecution } from "../agent/hooks";
213
+ import type { SandboxSupervisor } from "../agent/sandbox/types";
214
+ import { canonicalSandboxPath, nativeSandboxHome } from "../agent/sandbox/policy";
215
+
216
+ async function sandboxSkillCatalog<T>(action: string, cwd: string, args: Record<string, unknown> = {}): Promise<T> {
217
+ const sandbox = currentSandboxExecution();
218
+ if (!sandbox) throw new Error("SANDBOX_SKILLS: missing execution scope");
219
+ const result = await sandbox.supervisor.call("__workflow", { operation: "skillCatalog", action, ...args }, { cwd, signal: sandbox.signal });
220
+ if (!result.success) throw new Error(result.error || "SANDBOX_SKILLS: operation failed");
221
+ return JSON.parse(result.output) as T;
222
+ }
212
223
 
213
224
  const BUILTIN_SLASH_ALIASES = new Set([
214
225
  "/help", "/clear", "/compact", "/model", "/fast", "/provider", "/logout",
@@ -479,8 +490,204 @@ async function resolveSkillEntry(dir: string, entry: Dirent): Promise<SkillDoc |
479
490
  * batches racing across the `skillDirs` loop below. */
480
491
  const SKILL_ENTRY_LOAD_BATCH = 32;
481
492
 
493
+ interface CatalogPath { path: string; traversal: string[]; visited?: string[]; guestPath?: string }
494
+ interface CatalogEntry { location: CatalogPath; skillName: string; doc?: SkillDoc }
495
+ interface CatalogDirectory { location: CatalogPath; entries: CatalogEntry[] }
496
+ interface SandboxSkillCatalog { cwd: string; roots: string[]; readableRoots: string[]; directories: CatalogDirectory[] }
497
+ const sandboxSkillCatalogs = new WeakMap<SandboxSupervisor, SandboxSkillCatalog>();
498
+
499
+ function withinCatalogRoot(file: string, root: string): boolean {
500
+ const relative = path.relative(root, file);
501
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
502
+ }
503
+
504
+ function guestCatalogPath(location: CatalogPath, roots: string[]): string | undefined {
505
+ if (location.guestPath) return location.guestPath;
506
+ const route = location.traversal.find(file => roots.some(root => withinCatalogRoot(file, root)));
507
+ if (!route && location.visited?.some(file => roots.some(root => withinCatalogRoot(file, root)))) {
508
+ throw new Error("SANDBOX_SKILLS: catalog traversal leaves a writable root; explicit guest access required");
509
+ }
510
+ return route;
511
+ }
512
+
513
+ /** Metadata only: stop BEFORE traversing a workspace-controlled symlink. Called
514
+ * only while constructing the pre-execution snapshot, never for live host reads. */
515
+ async function traceCatalogPath(file: string, roots: string[], protectedRoots: string[]): Promise<CatalogPath> {
516
+ const absolute = path.resolve(file);
517
+ const parsed = path.parse(absolute);
518
+ let current = parsed.root;
519
+ let parts = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
520
+ const traversal = [absolute];
521
+ if (!protectedRoots.some(root => withinCatalogRoot(absolute, root))) return { path: absolute, traversal, guestPath: absolute };
522
+ const visited: string[] = [];
523
+ let links = 0;
524
+ while (parts.length > 0) {
525
+ const part = parts.shift()!;
526
+ if (part === ".") continue;
527
+ current = part === ".." ? path.dirname(current) : path.join(current, part);
528
+ visited.push(current);
529
+ const candidate = path.join(current, ...parts);
530
+ if (!protectedRoots.some(root => withinCatalogRoot(current, root) || withinCatalogRoot(root, current))) {
531
+ return { path: candidate, traversal, visited, guestPath: candidate };
532
+ }
533
+ if (roots.some(root => withinCatalogRoot(current, root))) {
534
+ traversal.push(candidate);
535
+ const location = { path: candidate, traversal, visited };
536
+ guestCatalogPath(location, roots); // Reject '..' escape, never normalize it into a trusted host read.
537
+ return location;
538
+ }
539
+ if (part !== ".." && (await fs.lstat(current)).isSymbolicLink()) {
540
+ if (++links > 40) throw new Error("Too many skill catalog symlinks");
541
+ const target = await fs.readlink(current);
542
+ const targetRoot = path.parse(target).root;
543
+ current = targetRoot || path.dirname(current);
544
+ // Retain '..' until its preceding components and symlinks have been visited.
545
+ parts = target.slice(targetRoot.length).split(path.sep).filter(Boolean).concat(parts);
546
+ traversal.push(path.join(current, ...parts));
547
+ }
548
+ }
549
+ return { path: current, traversal, visited };
550
+ }
551
+
552
+ /** Must run before the supervisor executes any model-controlled guest code.
553
+ * Parent skill bytes are frozen in memory, not reread after tools can mutate paths. */
554
+ export async function prepareSandboxSkillCatalog(supervisor: SandboxSupervisor, cwd: string, writableRoots: readonly string[] = [], readableRoots: readonly string[] = writableRoots): Promise<void> {
555
+ if (currentSandboxExecution()) throw new Error("SANDBOX_SKILLS: snapshot requires trusted initialization");
556
+ if (sandboxSkillCatalogs.has(supervisor)) throw new Error("SANDBOX_SKILLS: catalog already initialized");
557
+ const roots = [...new Set([path.resolve(cwd), await fs.realpath(cwd), ...writableRoots.map(root => path.resolve(root))])];
558
+ // These roots (and their canonical aliases) are forbidden to EVERY sandbox
559
+ // profile, unlike runtime HOME/config directories or another session's grants.
560
+ const nativeHome = await nativeSandboxHome();
561
+ const nativeRoots = [".agents", ".claude", ".jeo", ".jeopi"].map(name => path.join(nativeHome, name));
562
+ const protectedRoots = [...new Set([...nativeRoots, ...await Promise.all(nativeRoots.map(root => canonicalSandboxPath(root, false)))])];
563
+ const authorizedRoots = [...new Set([path.resolve(cwd), await fs.realpath(cwd), ...readableRoots.map(root => path.resolve(root))])];
564
+ const directories: CatalogDirectory[] = [];
565
+ for (const dir of skillDirs(cwd)) {
566
+ let location: CatalogPath;
567
+ try { location = await traceCatalogPath(dir, roots, protectedRoots); }
568
+ catch {
569
+ directories.push({ location: { path: path.resolve(dir), traversal: [path.resolve(dir)] }, entries: [] });
570
+ continue;
571
+ }
572
+ const directory: CatalogDirectory = { location, entries: [] };
573
+ directories.push(directory);
574
+ if (guestCatalogPath(location, roots)) continue;
575
+ let entries: Dirent[];
576
+ try { entries = await fs.readdir(location.path, { withFileTypes: true }); } catch { continue; }
577
+ for (const entry of entries) {
578
+ if (entry.name.startsWith(".")) continue;
579
+ try {
580
+ let resolved = await traceCatalogPath(path.join(location.path, entry.name), roots, protectedRoots);
581
+ resolved.traversal.unshift(...location.traversal.map(root => path.join(root, entry.name)));
582
+ resolved.visited = [...(location.visited ?? []), ...(resolved.visited ?? [])];
583
+ let isDirectory = false;
584
+ if (!guestCatalogPath(resolved, roots)) {
585
+ const stat = await fs.stat(resolved.path);
586
+ if (stat.isDirectory()) {
587
+ isDirectory = true;
588
+ const skillFile = await traceCatalogPath(path.join(resolved.path, "SKILL.md"), roots, protectedRoots);
589
+ skillFile.traversal.unshift(...resolved.traversal.map(root => path.join(root, "SKILL.md")));
590
+ skillFile.visited = [...(resolved.visited ?? []), ...(skillFile.visited ?? [])];
591
+ resolved = skillFile;
592
+ } else if (!stat.isFile() || !entry.name.endsWith(".md")) continue;
593
+ }
594
+ const item: CatalogEntry = { location: resolved, skillName: entry.name.replace(/\.md$/, "") };
595
+ directory.entries.push(item);
596
+ if (guestCatalogPath(resolved, roots)) continue;
597
+ const doc = parseSkillMarkdown(entry.name.replace(/\.md$/, ""), await fs.readFile(resolved.path, "utf-8"), { preferMetaName: true });
598
+ doc.sourcePath = path.join(dir, entry.name, ...(isDirectory ? ["SKILL.md"] : []));
599
+ if (isSupportedExternalSkill(doc)) item.doc = doc;
600
+ } catch {
601
+ // Absent/unreadable global docs are not part of this session's snapshot.
602
+ }
603
+ }
604
+ }
605
+ sandboxSkillCatalogs.set(supervisor, { cwd: path.resolve(cwd), roots, readableRoots: authorizedRoots, directories });
606
+ }
607
+
608
+ /** Grant revisions may taint formerly trusted paths. Reclassify captured path
609
+ * provenance only; never refresh content from a now-running model's host paths. */
610
+ export function updateSandboxSkillCatalogRoots(supervisor: SandboxSupervisor, writableRoots: readonly string[], readableRoots: readonly string[] = writableRoots): void {
611
+ const catalog = sandboxSkillCatalogs.get(supervisor);
612
+ if (!catalog) throw new Error("SANDBOX_SKILLS: catalog not initialized");
613
+ catalog.roots = [...new Set([...catalog.roots, ...writableRoots.map(root => path.resolve(root))])];
614
+ catalog.readableRoots = [...new Set([catalog.cwd, ...readableRoots.map(root => path.resolve(root))])];
615
+ }
616
+
617
+ async function loadSandboxSkills(cwd: string): Promise<SkillDoc[]> {
618
+ const sandbox = currentSandboxExecution();
619
+ if (!sandbox) throw new Error("SANDBOX_SKILLS: missing execution scope");
620
+ const catalog = sandboxSkillCatalogs.get(sandbox.supervisor);
621
+ if (!catalog) throw new Error("SANDBOX_SKILLS: trusted catalog not initialized");
622
+ const byName = new Map<string, SkillDoc>(SKILLS.map(skill => [skill.name.toLowerCase(), skill]));
623
+ for (const name of await sandboxSkillCatalog<string[]>("lock", cwd)) ALLOWED_SKILL_NAMES.add(name.toLowerCase());
624
+ // A subagent may use another approved cwd: reuse only trusted global bytes,
625
+ // and derive its project directories without touching host filesystem state.
626
+ const directories = path.resolve(cwd) === catalog.cwd
627
+ ? catalog.directories
628
+ : catalog.directories.map((directory, index) => index >= 5 && index < 10
629
+ ? { location: { path: skillDirs(cwd)[index]!, traversal: [skillDirs(cwd)[index]!] }, entries: [] }
630
+ : directory);
631
+ for (const directory of directories) {
632
+ const guestDir = guestCatalogPath(directory.location, catalog.roots)
633
+ ?? (path.resolve(cwd) !== catalog.cwd && withinCatalogRoot(directory.location.path, path.resolve(cwd)) ? directory.location.path : undefined);
634
+ if (guestDir) {
635
+ if (!catalog.readableRoots.some(root => withinCatalogRoot(guestDir, root))) continue;
636
+ for (const doc of await sandboxSkillCatalog<SkillDoc[]>("scan", guestDir)) byName.set(doc.name.toLowerCase(), doc);
637
+ continue;
638
+ }
639
+ for (const entry of directory.entries) {
640
+ const guestFile = guestCatalogPath(entry.location, catalog.roots);
641
+ if (guestFile && !catalog.readableRoots.some(root => withinCatalogRoot(guestFile, root))) continue;
642
+ const doc = guestFile
643
+ ? await sandboxSkillCatalog<SkillDoc | null>("entry", path.dirname(guestFile), { name: path.basename(guestFile), skillName: entry.skillName })
644
+ : entry.doc;
645
+ if (doc) byName.set(doc.name.toLowerCase(), { ...doc });
646
+ }
647
+ }
648
+ return [...byName.values()];
649
+ }
650
+
651
+ async function scanSkillDirectory(dir: string): Promise<SkillDoc[]> {
652
+ let entries: Dirent[];
653
+ try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return []; }
654
+ const skills: SkillDoc[] = [];
655
+ for (let i = 0; i < entries.length; i += SKILL_ENTRY_LOAD_BATCH) {
656
+ const resolved = await Promise.all(entries.slice(i, i + SKILL_ENTRY_LOAD_BATCH).map(entry => resolveSkillEntry(dir, entry)));
657
+ for (const skill of resolved) if (skill) skills.push(skill);
658
+ }
659
+ return skills;
660
+ }
661
+
662
+ /** Only workspace catalog effects execute in hands; trusted global docs stay in brain. */
663
+ export async function executeSkillCatalogOperation(args: Record<string, unknown>, cwd: string): Promise<unknown> {
664
+ if (currentSandboxExecution()) throw new Error("SANDBOX_SKILLS: guest handler cannot run in the brain");
665
+ if (args.action === "lock") {
666
+ try {
667
+ const lockData: unknown = JSON.parse(await fs.readFile(path.join(cwd, "skills-lock.json"), "utf-8"));
668
+ if (lockData && typeof lockData === "object" && "skills" in lockData && lockData.skills && typeof lockData.skills === "object") return Object.keys(lockData.skills);
669
+ } catch {}
670
+ return [];
671
+ }
672
+ if (args.action === "scan") return scanSkillDirectory(cwd);
673
+ if (args.action === "entry") {
674
+ if (typeof args.name !== "string" || args.name !== path.basename(args.name)) throw new Error("Invalid skill entry name");
675
+ const entryPath = path.join(cwd, args.name);
676
+ try {
677
+ const stat = await fs.stat(entryPath);
678
+ const file = stat.isDirectory() ? path.join(entryPath, "SKILL.md") : entryPath;
679
+ if (!stat.isDirectory() && (!stat.isFile() || !args.name.endsWith(".md"))) return null;
680
+ const doc = parseSkillMarkdown(typeof args.skillName === "string" ? args.skillName : args.name.replace(/\.md$/, ""), await fs.readFile(file, "utf-8"), { preferMetaName: true });
681
+ doc.sourcePath = file;
682
+ return isSupportedExternalSkill(doc) ? doc : null;
683
+ } catch { return null; }
684
+ }
685
+ throw new Error("Unknown skill catalog operation");
686
+ }
687
+
482
688
  /** Bundled skills merged with user skill docs from {@link skillDirs} (user overrides by name). */
483
689
  export async function loadSkills(cwd: string = process.cwd()): Promise<SkillDoc[]> {
690
+ if (currentSandboxExecution()) return loadSandboxSkills(cwd);
484
691
  try {
485
692
  const lockPath = path.join(cwd, "skills-lock.json");
486
693
  const lockContent = await fs.readFile(lockPath, "utf-8");
package/src/util/retry.ts CHANGED
@@ -64,6 +64,18 @@ export function defaultRetryable(err: unknown): boolean {
64
64
  if (isRefusalError(err)) {
65
65
  return false;
66
66
  }
67
+ // An Antigravity/Gemini 3.x `finishReason=MALFORMED_FUNCTION_CALL` on an otherwise-200
68
+ // response that already carried partial text/functionCall parts is rejected by
69
+ // antigravity.ts as "…response incomplete (finishReason=MALFORMED_FUNCTION_CALL)."
70
+ // (call AND stream). It is an intermittent model fault, not a deterministic classifier
71
+ // verdict: the adapter throws BEFORE serializing the buffered functionCalls, so a resend
72
+ // executes no unvalidated call. Retry it within the normal attempt budget. Scoped to this
73
+ // exact shape — other `response incomplete (…)` reasons (MAX_TOKENS, missing finishReason)
74
+ // stay non-retryable; the empty-candidate variant ("returned no content (…)") is already
75
+ // retried below. A stream that has yielded a chunk is never re-run by retryableStream.
76
+ if (/Antigravity Cloud Code Assist response incomplete \(finishReason=MALFORMED_FUNCTION_CALL\)/i.test(errorMessageOf(err))) {
77
+ return true;
78
+ }
67
79
  // A pre-response connection failure (refused / unresolved host) — retryable: a
68
80
  // local provider that is mid-restart, or a transient DNS blip, can clear within
69
81
  // the backoff budget. A provider that is simply never running exhausts the