@atlaso-labs/opencode 0.2.0 → 0.2.1

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1] — 2026-09-21
4
+
5
+ Preserve nearest-package identity while recognizing managed worktrees, rejecting untrusted directories, and retaining exact long project keys. This updates capture/recall identity; it does not add SessionStart Ambient delivery.
6
+
3
7
  ## 0.2.0
4
8
 
5
9
  **Your memories now survive a bad connection.** Capture used to send a memory once
package/lib/project.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import { createHash } from "node:crypto";
13
13
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
- import { basename, dirname, join, parse, resolve } from "node:path";
15
+ import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
16
16
 
17
17
  const MARKERS = [
18
18
  ".git", "pyproject.toml", "package.json", "Cargo.toml", "go.mod",
@@ -46,7 +46,25 @@ function pathParts(p: string): Set<string> {
46
46
  function garbageRoot(root: string): boolean {
47
47
  try {
48
48
  const parts = pathParts(root);
49
- if (intersects(parts, TOOL_DOT_DIRS)) return true;
49
+ const blocked = new Set([...parts].filter((part) => TOOL_DOT_DIRS.has(part)));
50
+ // Match Python: only proven managed worktrees exempt .claude/.codex.
51
+ // A nested plugin/cache directory must still fail the ancestry guard.
52
+ const sequence = root.split(/[/\\]+/).filter(Boolean);
53
+ for (const name of [".claude", ".codex"]) {
54
+ const positions = sequence.flatMap((part, i) => part === name ? [i] : []);
55
+ if (positions.length && positions.every((i) => {
56
+ if (sequence[i + 1] !== "worktrees") return false;
57
+ let ancestor = root;
58
+ while (ancestor.split(/[/\\]+/).filter(Boolean).length > i + 2) {
59
+ if (existsSync(join(ancestor, ".git"))) return true;
60
+ const parent = dirname(ancestor);
61
+ if (parent === ancestor) break;
62
+ ancestor = parent;
63
+ }
64
+ return false;
65
+ })) blocked.delete(name);
66
+ }
67
+ if (blocked.size) return true;
50
68
  // plugin caches that hide under non-dot dirs (marketplaces/cache/repos
51
69
  // layouts, e.g. "…/plugins/marketplaces/atlaso/atlaso/runtime")
52
70
  if (parts.has("plugins") && intersects(parts, ["cache", "marketplaces", "repos"])) return true;
@@ -83,27 +101,18 @@ function noProjectRoot(root: string): boolean {
83
101
  }
84
102
 
85
103
  export function projectRoot(start?: string): string {
86
- let cur: string;
87
- try {
88
- cur = resolve(start || process.cwd());
89
- } catch {
90
- return process.cwd();
91
- }
104
+ const cur = realpathSync(resolve(start || process.cwd()));
92
105
  let d = cur;
93
- // walk up to the filesystem root looking for a project marker
94
106
  while (true) {
95
- for (const m of MARKERS) {
96
- try {
97
- if (existsSync(join(d, m))) return d;
98
- } catch {
99
- /* ignore */
100
- }
101
- }
107
+ // Preserve existing package identities, and do not inherit a home dotfiles
108
+ // repository when the opened directory is below the home boundary.
109
+ if (noProjectRoot(d)) return cur;
110
+ if (MARKERS.some((m) => existsSync(join(d, m)))) return d;
102
111
  const parent = dirname(d);
103
112
  if (parent === d) break;
104
113
  d = parent;
105
114
  }
106
- return cur; // no markers → the cwd itself is the "project"
115
+ return cur;
107
116
  }
108
117
 
109
118
  /** Read remote.origin.url straight from .git/config (no subprocess). Handles a
@@ -213,25 +222,30 @@ export interface ProjectResolution {
213
222
  * memories became indistinguishable from "no project" and disappeared. */
214
223
  export function projectResolution(start?: string): ProjectResolution {
215
224
  try {
216
- let root = projectRoot(start);
217
- try {
218
- root = realpathSync(root); // canonicalize BEFORE the garbage/none checks
219
- } catch {
220
- /* keep the path.resolve()'d root */
225
+ const supplied = start ?? process.cwd();
226
+ if (!isAbsolute(supplied) || !statSync(supplied).isDirectory()) {
227
+ return { status: "unknown", key: null };
221
228
  }
229
+ const current = realpathSync(supplied);
230
+ if (garbageRoot(current)) return { status: "unknown", key: null };
231
+ const root = projectRoot(current);
222
232
  if (garbageRoot(root)) return { status: "unknown", key: null };
223
233
  if (noProjectRoot(root)) return { status: "none", key: null };
224
234
  const origin = gitOrigin(root);
225
- if (origin) {
226
- const key = normalizeRemote(origin);
227
- if (key) return { status: "ok", key: key.slice(0, 120) };
228
- }
229
- return { status: "ok", key: fallbackKey(root) };
235
+ const key = origin ? normalizeRemote(origin) : fallbackKey(root);
236
+ return validProjectKey(key) ? { status: "ok", key } : { status: "unknown", key: null };
230
237
  } catch {
231
238
  return { status: "unknown", key: null };
232
239
  }
233
240
  }
234
241
 
242
+ /** Exact project identities match the shared Python client; never truncate. */
243
+ function validProjectKey(key: string): boolean {
244
+ return key.length > 0 && [...key].length <= 512 && key === key.trim()
245
+ && !/[\s\x00-\x1f\x7f]/u.test(key)
246
+ && key !== "unknown" && key !== "project-unknown";
247
+ }
248
+
235
249
  /** A stable identity for the current project. null → personal-only (both the
236
250
  * 'none' and 'unknown' cases — recall treats them the same). Mirrors Python
237
251
  * `project_key`. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlaso-labs/opencode",
3
- "version": "0.2.0",
4
- "description": "Long-term memory for OpenCode \u2014 recalls what you've decided and remembers what matters, across sessions, projects, and tools. A pure-TypeScript OpenCode plugin (no engine; HTTP to the Atlaso brain only).",
3
+ "version": "0.2.1",
4
+ "description": "Long-term memory for OpenCode recalls what you've decided and remembers what matters, across sessions, projects, and tools. A pure-TypeScript OpenCode plugin (no engine; HTTP to the Atlaso brain only).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Atlaso Labs Inc. <hello@atlaso.ai>",