@gaia-ai/core 0.6.1 → 0.6.3

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.
@@ -0,0 +1,47 @@
1
+ /** Skip a `//`-line comment; return the index of the terminating newline (or EOF). */
2
+ export declare function skipLineComment(src: string, i: number): number;
3
+ /** Skip a block comment; return the index just past it. */
4
+ export declare function skipBlockComment(src: string, i: number): number;
5
+ /** Skip a string/template literal (honouring `\` escapes and `${ … }`
6
+ * interpolations in backtick strings); return the index just past the close. */
7
+ export declare function skipString(src: string, i: number): number;
8
+ /** Given the index of an opening `{`/`[`/`(`, return the index of its matching
9
+ * close, skipping nested delimiters, strings and comments. -1 if unbalanced. */
10
+ export declare function matchDelimiter(src: string, openIdx: number): number;
11
+ /** Skip whitespace + comments starting at `i` (bounded by `limit`). */
12
+ export declare function skipTrivia(src: string, i: number, limit: number): number;
13
+ /** Read a property key (bare identifier or quoted string) at `i`. */
14
+ export declare function readKey(src: string, i: number): {
15
+ name?: string;
16
+ end: number;
17
+ };
18
+ /** From `from`, scan to just past the next top-level `,` (or to `limit` when the
19
+ * value runs to the object close), skipping nested delimiters/strings/comments. */
20
+ export declare function scanToTopLevelComma(src: string, from: number, limit: number): number;
21
+ /** One top-level property of a config's default-export object literal. */
22
+ export interface PropEntry {
23
+ key: string | undefined;
24
+ /** Start of the entry INCLUDING its leading trivia (comments/whitespace). */
25
+ start: number;
26
+ /** End of the entry (just past its trailing comma, or the object close). */
27
+ end: number;
28
+ }
29
+ /** Locate the `export default { … }` object; return the body span (exclusive of
30
+ * the braces) or undefined when the default export is not an object literal. */
31
+ export declare function findDefaultExportObject(src: string): {
32
+ bodyStart: number;
33
+ bodyEnd: number;
34
+ } | undefined;
35
+ /** Enumerate the top-level properties of the object body [bodyStart, bodyEnd). */
36
+ export declare function scanTopLevelProperties(src: string, bodyStart: number, bodyEnd: number): PropEntry[];
37
+ /**
38
+ * Does this config SOURCE declare `key` as a real top-level property of its
39
+ * `export default { … }` object?
40
+ *
41
+ * Deliberately stricter than a `/site\s*:/` regex, which would also fire on the
42
+ * word in a comment, in a string, or on a NESTED `site:` inside some slot's
43
+ * `with: { … }`. `false` when the default export is not an object literal at all
44
+ * (a `defineConfig(…)` call, a re-export shim) — such a file cannot be read as a
45
+ * connection source by inspection, and guessing is worse than falling through.
46
+ */
47
+ export declare function declaresTopLevelKey(src: string, key: string): boolean;
@@ -0,0 +1,197 @@
1
+ // GAIA-230: the balanced-delimiter, comment-aware SOURCE scanner for a config
2
+ // module's `export default { … }` object — hoisted here from the conductor's
3
+ // `config.ts` (where GAIA-218 AC-7's `stripLegacyConnectionFromConfigSource`
4
+ // introduced it) so both users share one source of truth for "which top-level
5
+ // properties does this config source declare?":
6
+ //
7
+ // - `@gaia-ai/core` — `findGaiaConfig` gates the LEGACY `conductor.config.js`
8
+ // connection read on CONTENT: a config declaring no `site` is not a
9
+ // connection source (root cause 2). Core is layer 0 with zero `@gaia-ai/*`
10
+ // edges, so it cannot import the conductor's copy — hence the hoist.
11
+ // - `@gaia-ai/conductor` — the strip pass re-uses these primitives.
12
+ //
13
+ // Everything here is a PURE TEXT read. Nothing executes the config module: a
14
+ // config may import a secret-bearing machine context and have side effects, and
15
+ // both callers (a sync path resolver, an idempotent `--dry-run` migration) must
16
+ // stay safe to run on an untrusted tree.
17
+ const CLOSER = { '{': '}', '[': ']', '(': ')' };
18
+ /** Skip a `//`-line comment; return the index of the terminating newline (or EOF). */
19
+ export function skipLineComment(src, i) {
20
+ const nl = src.indexOf('\n', i);
21
+ return nl === -1 ? src.length : nl;
22
+ }
23
+ /** Skip a block comment; return the index just past it. */
24
+ export function skipBlockComment(src, i) {
25
+ const end = src.indexOf('*/', i + 2);
26
+ return end === -1 ? src.length : end + 2;
27
+ }
28
+ /** Skip a string/template literal (honouring `\` escapes and `${ … }`
29
+ * interpolations in backtick strings); return the index just past the close. */
30
+ export function skipString(src, i) {
31
+ const quote = src[i];
32
+ i++;
33
+ while (i < src.length) {
34
+ const c = src[i];
35
+ if (c === '\\') {
36
+ i += 2;
37
+ continue;
38
+ }
39
+ if (quote === '`' && c === '$' && src[i + 1] === '{') {
40
+ i = matchDelimiter(src, i + 1) + 1;
41
+ continue;
42
+ }
43
+ if (c === quote)
44
+ return i + 1;
45
+ i++;
46
+ }
47
+ return i;
48
+ }
49
+ /** Given the index of an opening `{`/`[`/`(`, return the index of its matching
50
+ * close, skipping nested delimiters, strings and comments. -1 if unbalanced. */
51
+ export function matchDelimiter(src, openIdx) {
52
+ const stack = [CLOSER[src[openIdx]]];
53
+ let i = openIdx + 1;
54
+ while (i < src.length && stack.length > 0) {
55
+ const c = src[i];
56
+ if (c === '/' && src[i + 1] === '/') {
57
+ i = skipLineComment(src, i);
58
+ continue;
59
+ }
60
+ if (c === '/' && src[i + 1] === '*') {
61
+ i = skipBlockComment(src, i);
62
+ continue;
63
+ }
64
+ if (c === "'" || c === '"' || c === '`') {
65
+ i = skipString(src, i);
66
+ continue;
67
+ }
68
+ if (c === '{' || c === '[' || c === '(') {
69
+ stack.push(CLOSER[c]);
70
+ i++;
71
+ continue;
72
+ }
73
+ if (c === '}' || c === ']' || c === ')') {
74
+ if (c === stack[stack.length - 1])
75
+ stack.pop();
76
+ i++;
77
+ continue;
78
+ }
79
+ i++;
80
+ }
81
+ return stack.length === 0 ? i - 1 : -1;
82
+ }
83
+ /** Skip whitespace + comments starting at `i` (bounded by `limit`). */
84
+ export function skipTrivia(src, i, limit) {
85
+ while (i < limit) {
86
+ const c = src[i];
87
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
88
+ i++;
89
+ continue;
90
+ }
91
+ if (c === '/' && src[i + 1] === '/') {
92
+ i = skipLineComment(src, i);
93
+ continue;
94
+ }
95
+ if (c === '/' && src[i + 1] === '*') {
96
+ i = skipBlockComment(src, i);
97
+ continue;
98
+ }
99
+ break;
100
+ }
101
+ return i;
102
+ }
103
+ /** Read a property key (bare identifier or quoted string) at `i`. */
104
+ export function readKey(src, i) {
105
+ const c = src[i];
106
+ if (c === "'" || c === '"') {
107
+ const end = skipString(src, i);
108
+ return { name: src.slice(i + 1, end - 1), end };
109
+ }
110
+ const m = /^[A-Za-z0-9_$]+/.exec(src.slice(i));
111
+ if (m)
112
+ return { name: m[0], end: i + m[0].length };
113
+ return { end: i };
114
+ }
115
+ /** From `from`, scan to just past the next top-level `,` (or to `limit` when the
116
+ * value runs to the object close), skipping nested delimiters/strings/comments. */
117
+ export function scanToTopLevelComma(src, from, limit) {
118
+ let i = from;
119
+ while (i < limit) {
120
+ const c = src[i];
121
+ if (c === '/' && src[i + 1] === '/') {
122
+ i = skipLineComment(src, i);
123
+ continue;
124
+ }
125
+ if (c === '/' && src[i + 1] === '*') {
126
+ i = skipBlockComment(src, i);
127
+ continue;
128
+ }
129
+ if (c === "'" || c === '"' || c === '`') {
130
+ i = skipString(src, i);
131
+ continue;
132
+ }
133
+ if (c === '{' || c === '[' || c === '(') {
134
+ i = matchDelimiter(src, i) + 1;
135
+ continue;
136
+ }
137
+ if (c === ',')
138
+ return i + 1;
139
+ i++;
140
+ }
141
+ return limit;
142
+ }
143
+ /** Locate the `export default { … }` object; return the body span (exclusive of
144
+ * the braces) or undefined when the default export is not an object literal. */
145
+ export function findDefaultExportObject(src) {
146
+ const m = /export\s+default\s*/.exec(src);
147
+ if (!m)
148
+ return undefined;
149
+ const braceIdx = m.index + m[0].length;
150
+ if (src[braceIdx] !== '{')
151
+ return undefined;
152
+ const end = matchDelimiter(src, braceIdx);
153
+ if (end === -1)
154
+ return undefined;
155
+ return { bodyStart: braceIdx + 1, bodyEnd: end };
156
+ }
157
+ /** Enumerate the top-level properties of the object body [bodyStart, bodyEnd). */
158
+ export function scanTopLevelProperties(src, bodyStart, bodyEnd) {
159
+ const entries = [];
160
+ let i = bodyStart;
161
+ while (i < bodyEnd) {
162
+ const entryStart = i;
163
+ const keyPos = skipTrivia(src, i, bodyEnd);
164
+ if (keyPos >= bodyEnd)
165
+ break;
166
+ const key = readKey(src, keyPos);
167
+ let j = skipTrivia(src, key.end, bodyEnd);
168
+ if (src[j] !== ':') {
169
+ // Not a `key: value` property (spread/computed/shorthand) — keep it.
170
+ const term = scanToTopLevelComma(src, keyPos, bodyEnd);
171
+ entries.push({ key: undefined, start: entryStart, end: term });
172
+ i = term;
173
+ continue;
174
+ }
175
+ j++;
176
+ const term = scanToTopLevelComma(src, j, bodyEnd);
177
+ entries.push({ key: key.name, start: entryStart, end: term });
178
+ i = term;
179
+ }
180
+ return entries;
181
+ }
182
+ /**
183
+ * Does this config SOURCE declare `key` as a real top-level property of its
184
+ * `export default { … }` object?
185
+ *
186
+ * Deliberately stricter than a `/site\s*:/` regex, which would also fire on the
187
+ * word in a comment, in a string, or on a NESTED `site:` inside some slot's
188
+ * `with: { … }`. `false` when the default export is not an object literal at all
189
+ * (a `defineConfig(…)` call, a re-export shim) — such a file cannot be read as a
190
+ * connection source by inspection, and guessing is worse than falling through.
191
+ */
192
+ export function declaresTopLevelKey(src, key) {
193
+ const obj = findDefaultExportObject(src);
194
+ if (!obj)
195
+ return false;
196
+ return scanTopLevelProperties(src, obj.bodyStart, obj.bodyEnd).some((e) => e.key === key);
197
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The user-global `~/.gaia` dir. Core owns where the split-config files live,
3
+ * so a consumer that must recognise the home dir (e.g. to exclude it from a
4
+ * project walk-up, GAIA-219 AC-9) asks for it rather than joining it itself.
5
+ */
6
+ export declare function homeGaiaDir(home?: string): string;
1
7
  /**
2
8
  * Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
3
9
  * `.gaia/` dir holds at least one conductor config; return that `.gaia/` dir, or
@@ -17,6 +23,12 @@ export declare function findGaiaDir(cwd: string): string | undefined;
17
23
  * 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error.
18
24
  */
19
25
  export declare function resolveConfigPath(override?: string, cwd?: string, conductorName?: string): string;
26
+ /**
27
+ * Which leg of the connection precedence produced the resolved path. Purely
28
+ * diagnostic (GAIA-219 D4): it lets a consumer report project-vs-home without
29
+ * re-deriving the rule, which would be a second precedence implementation.
30
+ */
31
+ export type GaiaConfigSource = 'explicit' | 'project' | 'home' | 'fallback';
20
32
  /** A resolved connection-config file: its path, whether it is the shipped
21
33
  * machine-context fallback, and whether it is a legacy `conductor.config.js`. */
22
34
  export interface GaiaConfigResolution {
@@ -25,23 +37,56 @@ export interface GaiaConfigResolution {
25
37
  fallback: boolean;
26
38
  /** true when the resolved file is a legacy `conductor.config.js` connection. */
27
39
  legacy: boolean;
40
+ /** Which leg produced `path` (GAIA-219 D4). */
41
+ source: GaiaConfigSource;
28
42
  }
29
43
  /**
30
44
  * Walk root-ward for the nearest `.gaia/` connection config: prefer
31
45
  * `gaia.config.js`; fall back to a legacy `conductor.config.js` in the same dir
32
- * (back-compat). Returns `undefined` when neither is found up the tree.
46
+ * **only when it really declares a connection** (a top-level `site` — GAIA-230).
47
+ * An engine-only `conductor.config.js` is skipped, and the walk continues, so the
48
+ * home `~/.gaia/gaia.config.js` every repo inherits is genuinely reachable.
49
+ * Returns `undefined` when no connection source is found up the tree.
33
50
  */
34
51
  export declare function findGaiaConfig(cwd: string): {
35
52
  path: string;
36
53
  legacy: boolean;
37
54
  } | undefined;
55
+ /**
56
+ * Walk root-ward for the nearest `.gaia/` dir that marks a **project** — one
57
+ * holding any gaia config at all, a `gaia.config.js` or at least one conductor
58
+ * config. Returns that `.gaia/` dir, or `undefined` up to the filesystem root.
59
+ *
60
+ * Deliberately a different question from `findGaiaConfig` (GAIA-219 AC-9):
61
+ * "which project am I standing in" is not "where does my connection come from".
62
+ * Since GAIA-230 the connection walk-up skips an engine-only
63
+ * `conductor.config.js`, and GAIA-218 makes exactly that the canonical repo
64
+ * shape — so keying project detection on the connection would stop recognising
65
+ * the most common project as a project. It is content-agnostic for the same
66
+ * reason `findGaiaDir` is: a `.gaia/` dir is the marker.
67
+ *
68
+ * It is a superset of `findGaiaDir`, which requires a *conductor* config
69
+ * because it resolves the engine config; a repo carrying only a project
70
+ * connection override is a project here and not there.
71
+ */
72
+ export declare function findProjectGaiaDir(cwd: string): string | undefined;
38
73
  /**
39
74
  * Resolve the CONNECTION (`gaia.config.js`) config path. Precedence (AC-4):
40
75
  * 1. `explicit` (`--config` / `$GAIA_CONFIG` / `$DROPSH_CONFIG`) wins verbatim.
41
- * 2. Project `./.gaia/gaia.config.js` found by walk-up (legacy
42
- * `conductor.config.js` accepted as a connection source).
76
+ * 2. Project `./.gaia/gaia.config.js` found by walk-up (a legacy
77
+ * `conductor.config.js` accepted as a connection source only when it declares
78
+ * a top-level `site` — GAIA-230).
43
79
  * 3. Home `~/.gaia/gaia.config.js` when present.
44
80
  * 4. The shipped fallback (`homeFallbackGaiaConfigPath`, reads the machine context).
81
+ *
82
+ * `source` (GAIA-219 D4) names the leg that won. One correction applies **to
83
+ * the project leg only**: the walk-up in step 2 climbs to the filesystem root,
84
+ * so from any cwd under `$HOME` it reaches `~/.gaia/gaia.config.js` and would
85
+ * return it as `project`. That is the same file step 3 would return, so
86
+ * resolution is unaffected — but the label would be a lie, hence step 2
87
+ * re-labels the home candidate `home`. An **explicit** path that happens to
88
+ * name the home config still reports `explicit`: there the user did choose it
89
+ * by name, and which leg won is exactly what the report is for.
45
90
  */
46
91
  export declare function resolveGaiaConfigPath(opts?: {
47
92
  cwd?: string;
@@ -1,6 +1,7 @@
1
- import { existsSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
+ import { declaresTopLevelKey } from './config-source.js';
4
5
  import { homeFallbackGaiaConfigPath } from './paths.js';
5
6
  // GAIA-201: the pure `.gaia/` walk-up, hoisted from the conductor's `config.ts`.
6
7
  // Two config kinds now live under `.gaia/`:
@@ -11,6 +12,16 @@ import { homeFallbackGaiaConfigPath } from './paths.js';
11
12
  // `conductor.config.js` still carrying `site`/`plugins` is accepted
12
13
  // as a connection source (back-compat) when no `gaia.config.js` is
13
14
  // found, so an un-migrated repo keeps working.
15
+ //
16
+ // GAIA-230 (root cause 2): that back-compat read is gated on CONTENT, not on file
17
+ // existence. `gaia upgrade` step 0 (GAIA-218 AC-7) deliberately STRIPS
18
+ // `site`/`plugins` from every engine config, and step 1 defaults the project
19
+ // connection override to skip — because the global `~/.gaia/gaia.config.js` is the
20
+ // default connection every repo inherits. An existence-only read let the
21
+ // engine-only file it leaves behind keep precedence 2 and shadow that inherited
22
+ // default, so `loadGaiaConfig` died on `requires site.base_url`. A
23
+ // `conductor.config.js` declaring no top-level `site` is therefore NOT a
24
+ // connection source and falls through to the home config.
14
25
  /** A repo's config files live in this dir, one conductor config per conductor. */
15
26
  const GAIA_DIR = '.gaia';
16
27
  /** The default conductor's file name; its stem is `conductor`. */
@@ -40,6 +51,14 @@ function configStems(gaiaDir) {
40
51
  function fileForStem(stem) {
41
52
  return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
42
53
  }
54
+ /**
55
+ * The user-global `~/.gaia` dir. Core owns where the split-config files live,
56
+ * so a consumer that must recognise the home dir (e.g. to exclude it from a
57
+ * project walk-up, GAIA-219 AC-9) asks for it rather than joining it itself.
58
+ */
59
+ export function homeGaiaDir(home = homedir()) {
60
+ return join(home, GAIA_DIR);
61
+ }
43
62
  /**
44
63
  * Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
45
64
  * `.gaia/` dir holds at least one conductor config; return that `.gaia/` dir, or
@@ -98,10 +117,28 @@ export function resolveConfigPath(override, cwd = process.cwd(), conductorName)
98
117
  throw new Error(`${stems.length} conductors in ${gaiaDir} (${stems.join(', ')}); ` +
99
118
  'select one with --conductor <name> or $GAIA_CONDUCTOR');
100
119
  }
120
+ /**
121
+ * GAIA-230: is this `conductor.config.js` a legacy CONNECTION source, i.e. does
122
+ * its source still declare a top-level `site`? A pure source read — a config may
123
+ * import a secret-bearing machine context, and this resolver is sync and must
124
+ * never execute the module. An unreadable file, or one whose default export is
125
+ * not an object literal, is not a connection source.
126
+ */
127
+ function declaresConnection(path) {
128
+ try {
129
+ return declaresTopLevelKey(readFileSync(path, 'utf8'), 'site');
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
101
135
  /**
102
136
  * Walk root-ward for the nearest `.gaia/` connection config: prefer
103
137
  * `gaia.config.js`; fall back to a legacy `conductor.config.js` in the same dir
104
- * (back-compat). Returns `undefined` when neither is found up the tree.
138
+ * **only when it really declares a connection** (a top-level `site` — GAIA-230).
139
+ * An engine-only `conductor.config.js` is skipped, and the walk continues, so the
140
+ * home `~/.gaia/gaia.config.js` every repo inherits is genuinely reachable.
141
+ * Returns `undefined` when no connection source is found up the tree.
105
142
  */
106
143
  export function findGaiaConfig(cwd) {
107
144
  let dir = resolve(cwd);
@@ -112,8 +149,41 @@ export function findGaiaConfig(cwd) {
112
149
  if (existsSync(gaiaCfg))
113
150
  return { path: gaiaCfg, legacy: false };
114
151
  const legacy = join(gaiaDir, DEFAULT_CONFIG);
115
- if (existsSync(legacy))
152
+ if (existsSync(legacy) && declaresConnection(legacy)) {
116
153
  return { path: legacy, legacy: true };
154
+ }
155
+ }
156
+ const parent = dirname(dir);
157
+ if (parent === dir)
158
+ return undefined;
159
+ dir = parent;
160
+ }
161
+ }
162
+ /**
163
+ * Walk root-ward for the nearest `.gaia/` dir that marks a **project** — one
164
+ * holding any gaia config at all, a `gaia.config.js` or at least one conductor
165
+ * config. Returns that `.gaia/` dir, or `undefined` up to the filesystem root.
166
+ *
167
+ * Deliberately a different question from `findGaiaConfig` (GAIA-219 AC-9):
168
+ * "which project am I standing in" is not "where does my connection come from".
169
+ * Since GAIA-230 the connection walk-up skips an engine-only
170
+ * `conductor.config.js`, and GAIA-218 makes exactly that the canonical repo
171
+ * shape — so keying project detection on the connection would stop recognising
172
+ * the most common project as a project. It is content-agnostic for the same
173
+ * reason `findGaiaDir` is: a `.gaia/` dir is the marker.
174
+ *
175
+ * It is a superset of `findGaiaDir`, which requires a *conductor* config
176
+ * because it resolves the engine config; a repo carrying only a project
177
+ * connection override is a project here and not there.
178
+ */
179
+ export function findProjectGaiaDir(cwd) {
180
+ let dir = resolve(cwd);
181
+ for (;;) {
182
+ const gaiaDir = join(dir, GAIA_DIR);
183
+ if (existsSync(gaiaDir) &&
184
+ (existsSync(join(gaiaDir, GAIA_CONFIG)) ||
185
+ configStems(gaiaDir).length > 0)) {
186
+ return gaiaDir;
117
187
  }
118
188
  const parent = dirname(dir);
119
189
  if (parent === dir)
@@ -124,29 +194,49 @@ export function findGaiaConfig(cwd) {
124
194
  /**
125
195
  * Resolve the CONNECTION (`gaia.config.js`) config path. Precedence (AC-4):
126
196
  * 1. `explicit` (`--config` / `$GAIA_CONFIG` / `$DROPSH_CONFIG`) wins verbatim.
127
- * 2. Project `./.gaia/gaia.config.js` found by walk-up (legacy
128
- * `conductor.config.js` accepted as a connection source).
197
+ * 2. Project `./.gaia/gaia.config.js` found by walk-up (a legacy
198
+ * `conductor.config.js` accepted as a connection source only when it declares
199
+ * a top-level `site` — GAIA-230).
129
200
  * 3. Home `~/.gaia/gaia.config.js` when present.
130
201
  * 4. The shipped fallback (`homeFallbackGaiaConfigPath`, reads the machine context).
202
+ *
203
+ * `source` (GAIA-219 D4) names the leg that won. One correction applies **to
204
+ * the project leg only**: the walk-up in step 2 climbs to the filesystem root,
205
+ * so from any cwd under `$HOME` it reaches `~/.gaia/gaia.config.js` and would
206
+ * return it as `project`. That is the same file step 3 would return, so
207
+ * resolution is unaffected — but the label would be a lie, hence step 2
208
+ * re-labels the home candidate `home`. An **explicit** path that happens to
209
+ * name the home config still reports `explicit`: there the user did choose it
210
+ * by name, and which leg won is exactly what the report is for.
131
211
  */
132
212
  export function resolveGaiaConfigPath(opts = {}) {
213
+ const homeCfg = join(homeGaiaDir(opts.home), GAIA_CONFIG);
133
214
  const explicit = opts.explicit;
134
215
  if (explicit !== undefined && explicit.trim() !== '') {
135
- return { path: explicit, fallback: false, legacy: false };
216
+ return {
217
+ path: explicit,
218
+ fallback: false,
219
+ legacy: false,
220
+ source: 'explicit',
221
+ };
136
222
  }
137
223
  const cwd = opts.cwd ?? process.cwd();
138
224
  const project = findGaiaConfig(cwd);
139
225
  if (project !== undefined) {
140
- return { path: project.path, fallback: false, legacy: project.legacy };
226
+ return {
227
+ path: project.path,
228
+ fallback: false,
229
+ legacy: project.legacy,
230
+ source: project.path === homeCfg ? 'home' : 'project',
231
+ };
141
232
  }
142
- const home = opts.home ?? homedir();
143
- const homeCfg = join(home, GAIA_DIR, GAIA_CONFIG);
144
233
  if (existsSync(homeCfg)) {
145
- return { path: homeCfg, fallback: false, legacy: false };
234
+ return { path: homeCfg, fallback: false, legacy: false, source: 'home' };
146
235
  }
147
236
  return {
148
237
  path: opts.shippedFallback ?? homeFallbackGaiaConfigPath(),
149
238
  fallback: true,
150
239
  legacy: false,
240
+ source: 'fallback',
151
241
  };
152
242
  }
@@ -1,5 +1,6 @@
1
1
  import type { DropSHPlugin } from 'dropsh/plugin';
2
2
  import type { GaiaCommandHost } from './commands.js';
3
+ import { type GaiaConfigSource } from './gaia-dir.js';
3
4
  /** The resolved control-plane connection: site + constructed auth plugins. */
4
5
  export interface GaiaConnectionConfig {
5
6
  site: {
@@ -11,6 +12,10 @@ export interface GaiaConnectionConfig {
11
12
  config_path: string;
12
13
  /** true when this is the shipped machine-context fallback. */
13
14
  fallback: boolean;
15
+ /** Which leg of the precedence produced `config_path` (GAIA-219 D4). */
16
+ source: GaiaConfigSource;
17
+ /** true when the loaded file is a legacy `conductor.config.js` connection. */
18
+ legacy: boolean;
14
19
  }
15
20
  /**
16
21
  * Load the connection config for `gaia ui` / `gaia dropsh` / conductor-auth.
@@ -113,5 +113,7 @@ export async function loadGaiaConfig(host, opts = {}) {
113
113
  plugins,
114
114
  config_path: configPath,
115
115
  fallback: resolution.fallback,
116
+ source: resolution.source,
117
+ legacy: resolution.legacy,
116
118
  };
117
119
  }
@@ -1,6 +1,7 @@
1
1
  export type { GaiaCommandHost, GaiaCommandPlugin } from './cli/commands.js';
2
- export type { GaiaConfigResolution } from './cli/gaia-dir.js';
3
- export { findGaiaConfig, findGaiaDir, resolveConfigPath, resolveGaiaConfigPath, } from './cli/gaia-dir.js';
2
+ export { declaresTopLevelKey, findDefaultExportObject, matchDelimiter, type PropEntry, readKey, scanTopLevelProperties, scanToTopLevelComma, skipBlockComment, skipLineComment, skipString, skipTrivia, } from './cli/config-source.js';
3
+ export type { GaiaConfigResolution, GaiaConfigSource, } from './cli/gaia-dir.js';
4
+ export { findGaiaConfig, findGaiaDir, findProjectGaiaDir, homeGaiaDir, resolveConfigPath, resolveGaiaConfigPath, } from './cli/gaia-dir.js';
4
5
  export { type GaiaConnectionConfig, loadGaiaConfig, } from './cli/load-gaia-config.js';
5
6
  export { legacyMachineContextPath, type MachineContext, machineContextPath, readMachineContext, resolveMachineContextPath, } from './cli/machine-context.js';
6
7
  export { corePackageRoot, homeFallbackGaiaConfigPath, } from './cli/paths.js';
package/dist/src/index.js CHANGED
@@ -1,4 +1,9 @@
1
- export { findGaiaConfig, findGaiaDir, resolveConfigPath, resolveGaiaConfigPath, } from './cli/gaia-dir.js';
1
+ // GAIA-230: the comment-aware config-SOURCE scanner. Owned by the kernel so the
2
+ // content-gated legacy connection read (`findGaiaConfig`) and the conductor's
3
+ // GAIA-218 strip pass share one answer to "which top-level properties does this
4
+ // config source declare?" — core is layer 0 and cannot import the conductor's copy.
5
+ export { declaresTopLevelKey, findDefaultExportObject, matchDelimiter, readKey, scanTopLevelProperties, scanToTopLevelComma, skipBlockComment, skipLineComment, skipString, skipTrivia, } from './cli/config-source.js';
6
+ export { findGaiaConfig, findGaiaDir, findProjectGaiaDir, homeGaiaDir, resolveConfigPath, resolveGaiaConfigPath, } from './cli/gaia-dir.js';
2
7
  export { loadGaiaConfig, } from './cli/load-gaia-config.js';
3
8
  export { legacyMachineContextPath, machineContextPath, readMachineContext, resolveMachineContextPath, } from './cli/machine-context.js';
4
9
  export { corePackageRoot, homeFallbackGaiaConfigPath, } from './cli/paths.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/core",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "GAIA surface-agnostic kernel: host contract, split-config helpers, addon preset/discovery machinery, shared primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",