@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 +4 -0
- package/lib/project.ts +41 -27
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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.
|
|
4
|
-
"description": "Long-term memory for OpenCode
|
|
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>",
|