Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

## 0.1.1 - 2026-05-14

### Fixed

- Fixed interactive dashboard commands reusing a provider-free startup state after launching from a bare `atomicmemory` invocation. Dashboard sessions now hydrate the saved profile and scope at startup, while plain provider-free commands such as `atomicmemory help` remain provider-free.

## 0.1.0 - 2026-05-14

### Added

- Initial public release of the AtomicMemory CLI.
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicmemory/cli",
"version": "0.1.0",
"version": "0.1.1",
"description": "AtomicMemory CLI for memory operations, config, status, and agent-friendly JSON output.",
"type": "module",
"publishConfig": {
Expand All @@ -19,6 +19,7 @@
"atomicmemory": "dist/bin.js"
},
"files": [
"CHANGELOG.md",
"cli-spec.json",
"config.schema.json",
"dist",
Expand Down
28 changes: 27 additions & 1 deletion packages/cli/src/__tests__/interactive-dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
createInteractiveRuntimeSession,
mergeInteractiveFlags,
} from '../cli/interactive-session.js';
import { shouldLaunchDashboard, type RuntimeState } from '../cli/runtime.js';
import {
resolveRuntimeProfile,
resolveRuntimeScope,
shouldLaunchDashboard,
type RuntimeState,
} from '../cli/runtime.js';

test('splitShellWords supports quoted dashboard command input', () => {
assert.deepEqual(splitShellWords('search "release policy" --namespace docs'), [
Expand Down Expand Up @@ -86,6 +91,27 @@ test('shouldLaunchDashboard only launches for bare or explicit interactive invoc
assert.equal(shouldLaunchDashboard({ path: 'doctor', positional: [], flags: {} }), false);
});

test('bare dashboard hydrates saved profile and scope for cached commands', () => {
const config = sessionConfig('user-from-profile');
const invocation = { path: 'help', positional: [], flags: {}, source: 'bare' } as const;
const profile = resolveRuntimeProfile(invocation, config, 'default', {});
const scope = resolveRuntimeScope(invocation, profile, {});

assert.equal(profile?.provider, 'atomicmemory');
assert.equal(profile?.apiUrl, 'http://localhost:3050');
assert.deepEqual(scope, { user: 'user-from-profile' });
});

test('plain help stays provider-free outside dashboard mode', () => {
const config = sessionConfig('user-from-profile');
const invocation = { path: 'help', positional: [], flags: {}, source: 'help_flag' } as const;
const profile = resolveRuntimeProfile(invocation, config, 'default', {});
const scope = resolveRuntimeScope(invocation, profile, {});

assert.equal(profile, null);
assert.deepEqual(scope, {});
});

test('mergeInteractiveFlags inherits only profile, provider, scope, and config flags', () => {
const merged = mergeInteractiveFlags(
{
Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/__tests__/subprocess-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,37 @@ import { dirname, resolve } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const cliRoot = resolve(here, '..', '..');
const binPath = resolve(cliRoot, 'dist', 'bin.js');
const missingConfigPath = resolve(cliRoot, '.subprocess-smoke-missing-config.json');
const ATOMICMEMORY_ENV_KEYS = [
'ATOMICMEMORY_API_KEY',
'ATOMICMEMORY_API_URL',
'ATOMICMEMORY_CONFIG',
'ATOMICMEMORY_PROVIDER',
'ATOMICMEMORY_SCOPE_AGENT_ID',
'ATOMICMEMORY_SCOPE_NAMESPACE',
'ATOMICMEMORY_SCOPE_THREAD',
'ATOMICMEMORY_SCOPE_USER',
'ATOMICMEMORY_TRUST_SURFACE',
] as const;

function isolatedSubprocessEnv(overrides: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const env = { ...process.env };
for (const key of ATOMICMEMORY_ENV_KEYS) delete env[key];
return {
...env,
ATOMICMEMORY_CONFIG: missingConfigPath,
...overrides,
NO_COLOR: '1',
};
}

function runBin(
args: readonly string[],
env: NodeJS.ProcessEnv = process.env,
env: NodeJS.ProcessEnv = {},
): { stdout: string; stderr: string; code: number } {
const r = spawnSync(process.execPath, [binPath, ...args], {
encoding: 'utf8',
env: { ...env, NO_COLOR: '1' },
env: isolatedSubprocessEnv(env),
});
return {
stdout: r.stdout ?? '',
Expand Down Expand Up @@ -76,10 +99,7 @@ test('subprocess: --json help emits exactly one parseable JSON document, no prea
});

test('subprocess: hooks install is provider-free even with partial provider env', { skip: skipIfUnbuilt }, () => {
const env = { ...process.env };
delete env.ATOMICMEMORY_TRUST_SURFACE;
const r = runBin(['--json', 'hooks', 'install', '--host', 'codex'], {
...env,
ATOMICMEMORY_PROVIDER: 'atomicmemory',
ATOMICMEMORY_API_URL: 'https://core.example.invalid',
});
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/cli/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ function loadRuntimeConfig(
return { paths, config, profileName };
}

function resolveRuntimeProfile(
export function resolveRuntimeProfile(
invocation: Invocation,
config: CliConfigShape,
profileName: string,
Expand All @@ -171,7 +171,7 @@ function resolveRuntimeProfile(
if (invocation.path === 'init') {
return config.profiles[profileName] ?? null;
}
if (isProviderFreeInvocation(invocation)) {
if (isProviderFreeInvocation(invocation) && !isDashboardInvocation(invocation)) {
return null;
}
const baseProfile = resolveBaseProfile(invocation.flags, config, profileName, env);
Expand All @@ -183,7 +183,11 @@ function isProviderFreeInvocation(invocation: Invocation): boolean {
return invocation.path === 'hooks' && invocation.positional[0] === 'install';
}

function resolveRuntimeScope(
function isDashboardInvocation(invocation: Invocation): boolean {
return invocation.source === 'bare' || invocation.flags.interactive === true;
}

export function resolveRuntimeScope(
invocation: Invocation,
profile: CliProfileShape | null,
env: NodeJS.ProcessEnv,
Expand Down
Loading