@phren/cli 0.1.8 → 0.1.9

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 @@
1
+ export declare function runBundledAgentCli(args: string[]): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { pathToFileURL } from "url";
5
+ import { ROOT } from "./package-metadata.js";
6
+ async function tryImport(candidate) {
7
+ try {
8
+ return await import(candidate);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ async function loadBundledAgentModule() {
15
+ const workspaceCandidates = [
16
+ path.join(ROOT, "..", "agent", "src", "index.ts"),
17
+ path.join(ROOT, "..", "agent", "dist", "index.js"),
18
+ ];
19
+ for (const candidate of workspaceCandidates) {
20
+ if (!fs.existsSync(candidate))
21
+ continue;
22
+ const mod = await tryImport(pathToFileURL(candidate).href);
23
+ if (mod?.runAgentCli)
24
+ return mod;
25
+ }
26
+ const packageModule = await tryImport("@phren/agent");
27
+ if (packageModule?.runAgentCli)
28
+ return packageModule;
29
+ return null;
30
+ }
31
+ export async function runBundledAgentCli(args) {
32
+ const mod = await loadBundledAgentModule();
33
+ if (mod?.runAgentCli) {
34
+ await mod.runAgentCli(args);
35
+ return;
36
+ }
37
+ try {
38
+ execFileSync("phren-agent", args, { stdio: "inherit" });
39
+ return;
40
+ }
41
+ catch (err) {
42
+ if (err && typeof err === "object" && "status" in err) {
43
+ const status = err.status;
44
+ process.exit(status ?? 1);
45
+ }
46
+ throw new Error("Integrated agent runtime is unavailable. Install or build @phren/agent, or use `phren manage ...` for memory operations.");
47
+ }
48
+ }
@@ -0,0 +1,58 @@
1
+ export type ApiKeyProvider = "openai" | "openrouter" | "anthropic";
2
+ export type AuthProvider = ApiKeyProvider | "openai-codex";
3
+ export interface ApiKeyProfile {
4
+ id: string;
5
+ kind: "api-key";
6
+ provider: ApiKeyProvider;
7
+ label: string;
8
+ apiKey: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
12
+ export interface CodexAuthProfile {
13
+ id: string;
14
+ kind: "codex-subscription";
15
+ provider: "openai-codex";
16
+ label: string;
17
+ accessToken: string;
18
+ refreshToken?: string;
19
+ expiresAt: number;
20
+ accountId?: string;
21
+ lastRefresh?: string;
22
+ source: "phren-oauth" | "codex-cli-import";
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+ export type AuthProfile = ApiKeyProfile | CodexAuthProfile;
27
+ export declare function authProfilesPath(): string;
28
+ export declare function getAuthProfiles(): AuthProfile[];
29
+ export declare function getApiKeyProfile(provider: ApiKeyProvider): ApiKeyProfile | null;
30
+ export declare function hasApiKeyProfile(provider: ApiKeyProvider): boolean;
31
+ export declare function upsertApiKeyProfile(provider: ApiKeyProvider, apiKey: string): ApiKeyProfile;
32
+ export declare function removeApiKeyProfile(provider: ApiKeyProvider): boolean;
33
+ export declare function resolveApiKey(provider: ApiKeyProvider, envVar: string): string | null;
34
+ export declare function hasCodexCliAuth(): boolean;
35
+ export declare function getCodexAuthProfile(opts?: {
36
+ allowCliImport?: boolean;
37
+ }): CodexAuthProfile | null;
38
+ export declare function hasCodexAuthProfile(opts?: {
39
+ allowCliImport?: boolean;
40
+ }): boolean;
41
+ export declare function upsertCodexAuthProfile(data: {
42
+ accessToken: string;
43
+ refreshToken?: string;
44
+ expiresAt: number;
45
+ accountId?: string;
46
+ lastRefresh?: string;
47
+ source?: "phren-oauth" | "codex-cli-import";
48
+ }): CodexAuthProfile;
49
+ export declare function removeCodexAuthProfile(): boolean;
50
+ export interface AuthStatusEntry {
51
+ provider: AuthProvider;
52
+ configured: boolean;
53
+ source: "env" | "profile" | "codex-cli" | "none";
54
+ label: string;
55
+ expiresAt?: number;
56
+ accountId?: string;
57
+ }
58
+ export declare function getAuthStatusEntries(): AuthStatusEntry[];
@@ -0,0 +1,238 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { atomicWriteText, homePath } from "../phren-paths.js";
4
+ const DEFAULT_PROFILE_IDS = {
5
+ openai: "openai-default",
6
+ openrouter: "openrouter-default",
7
+ anthropic: "anthropic-default",
8
+ "openai-codex": "openai-codex-default",
9
+ };
10
+ function defaultLabel(provider) {
11
+ switch (provider) {
12
+ case "openai":
13
+ return "OpenAI API";
14
+ case "openrouter":
15
+ return "OpenRouter API";
16
+ case "anthropic":
17
+ return "Anthropic API";
18
+ case "openai-codex":
19
+ return "OpenAI Codex Subscription";
20
+ }
21
+ }
22
+ function authProfileDir() {
23
+ return homePath(".phren", ".config");
24
+ }
25
+ function authProfilesFilePath() {
26
+ return path.join(authProfileDir(), "auth-profiles.json");
27
+ }
28
+ function codexCliAuthPath() {
29
+ return homePath(".codex", "auth.json");
30
+ }
31
+ function ensureAuthProfileDir() {
32
+ fs.mkdirSync(authProfileDir(), { recursive: true, mode: 0o700 });
33
+ }
34
+ function persistProfiles(data) {
35
+ ensureAuthProfileDir();
36
+ const filePath = authProfilesFilePath();
37
+ atomicWriteText(filePath, JSON.stringify(data, null, 2) + "\n");
38
+ try {
39
+ fs.chmodSync(filePath, 0o600);
40
+ }
41
+ catch { /* best effort */ }
42
+ }
43
+ function normalizeStore(raw) {
44
+ if (!raw || typeof raw !== "object") {
45
+ return { schemaVersion: 1, profiles: [] };
46
+ }
47
+ const record = raw;
48
+ if (record.schemaVersion !== 1 || !Array.isArray(record.profiles)) {
49
+ return { schemaVersion: 1, profiles: [] };
50
+ }
51
+ return {
52
+ schemaVersion: 1,
53
+ profiles: record.profiles.filter((profile) => {
54
+ if (!profile || typeof profile !== "object")
55
+ return false;
56
+ const p = profile;
57
+ return typeof p.id === "string" && typeof p.kind === "string" && typeof p.provider === "string";
58
+ }),
59
+ };
60
+ }
61
+ function loadStore() {
62
+ try {
63
+ return normalizeStore(JSON.parse(fs.readFileSync(authProfilesFilePath(), "utf8")));
64
+ }
65
+ catch {
66
+ return { schemaVersion: 1, profiles: [] };
67
+ }
68
+ }
69
+ function upsertProfile(profile) {
70
+ const store = loadStore();
71
+ store.profiles = store.profiles.filter((entry) => entry.id !== profile.id);
72
+ store.profiles.push(profile);
73
+ persistProfiles(store);
74
+ return profile;
75
+ }
76
+ export function authProfilesPath() {
77
+ return authProfilesFilePath();
78
+ }
79
+ export function getAuthProfiles() {
80
+ return loadStore().profiles;
81
+ }
82
+ export function getApiKeyProfile(provider) {
83
+ return loadStore().profiles.find((profile) => profile.kind === "api-key" && profile.provider === provider && profile.id === DEFAULT_PROFILE_IDS[provider]) ?? null;
84
+ }
85
+ export function hasApiKeyProfile(provider) {
86
+ return Boolean(getApiKeyProfile(provider));
87
+ }
88
+ export function upsertApiKeyProfile(provider, apiKey) {
89
+ const now = new Date().toISOString();
90
+ const existing = getApiKeyProfile(provider);
91
+ const profile = {
92
+ id: DEFAULT_PROFILE_IDS[provider],
93
+ kind: "api-key",
94
+ provider,
95
+ label: defaultLabel(provider),
96
+ apiKey,
97
+ createdAt: existing?.createdAt ?? now,
98
+ updatedAt: now,
99
+ };
100
+ return upsertProfile(profile);
101
+ }
102
+ export function removeApiKeyProfile(provider) {
103
+ const store = loadStore();
104
+ const before = store.profiles.length;
105
+ store.profiles = store.profiles.filter((profile) => !(profile.kind === "api-key" && profile.provider === provider));
106
+ if (store.profiles.length === before)
107
+ return false;
108
+ persistProfiles(store);
109
+ return true;
110
+ }
111
+ export function resolveApiKey(provider, envVar) {
112
+ const envValue = process.env[envVar];
113
+ if (typeof envValue === "string" && envValue.trim())
114
+ return envValue.trim();
115
+ return getApiKeyProfile(provider)?.apiKey ?? null;
116
+ }
117
+ function inferCodexExpiry(lastRefresh) {
118
+ const refreshedAt = lastRefresh ? Date.parse(lastRefresh) : NaN;
119
+ if (!Number.isNaN(refreshedAt))
120
+ return refreshedAt + 60 * 60 * 1000;
121
+ return Date.now() + 60 * 60 * 1000;
122
+ }
123
+ function readCodexCliAuthFile() {
124
+ try {
125
+ const parsed = JSON.parse(fs.readFileSync(codexCliAuthPath(), "utf8"));
126
+ const accessToken = typeof parsed.tokens?.access_token === "string" ? parsed.tokens.access_token : null;
127
+ if (!accessToken)
128
+ return null;
129
+ const now = new Date().toISOString();
130
+ return {
131
+ id: DEFAULT_PROFILE_IDS["openai-codex"],
132
+ kind: "codex-subscription",
133
+ provider: "openai-codex",
134
+ label: defaultLabel("openai-codex"),
135
+ accessToken,
136
+ refreshToken: typeof parsed.tokens?.refresh_token === "string" ? parsed.tokens.refresh_token : undefined,
137
+ accountId: typeof parsed.tokens?.account_id === "string" ? parsed.tokens.account_id : undefined,
138
+ expiresAt: inferCodexExpiry(typeof parsed.last_refresh === "string" ? parsed.last_refresh : undefined),
139
+ lastRefresh: typeof parsed.last_refresh === "string" ? parsed.last_refresh : undefined,
140
+ source: "codex-cli-import",
141
+ createdAt: now,
142
+ updatedAt: now,
143
+ };
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ export function hasCodexCliAuth() {
150
+ return Boolean(readCodexCliAuthFile());
151
+ }
152
+ export function getCodexAuthProfile(opts = {}) {
153
+ const localProfile = loadStore().profiles.find((profile) => profile.kind === "codex-subscription" && profile.provider === "openai-codex") ?? null;
154
+ if (localProfile)
155
+ return localProfile;
156
+ if (!opts.allowCliImport)
157
+ return null;
158
+ const imported = readCodexCliAuthFile();
159
+ if (!imported)
160
+ return null;
161
+ return upsertCodexAuthProfile({
162
+ accessToken: imported.accessToken,
163
+ refreshToken: imported.refreshToken,
164
+ expiresAt: imported.expiresAt,
165
+ accountId: imported.accountId,
166
+ lastRefresh: imported.lastRefresh,
167
+ source: "codex-cli-import",
168
+ });
169
+ }
170
+ export function hasCodexAuthProfile(opts = {}) {
171
+ if (getCodexAuthProfile({ allowCliImport: false }))
172
+ return true;
173
+ return Boolean(opts.allowCliImport && readCodexCliAuthFile());
174
+ }
175
+ export function upsertCodexAuthProfile(data) {
176
+ const now = new Date().toISOString();
177
+ const existing = getCodexAuthProfile({ allowCliImport: false });
178
+ const profile = {
179
+ id: DEFAULT_PROFILE_IDS["openai-codex"],
180
+ kind: "codex-subscription",
181
+ provider: "openai-codex",
182
+ label: defaultLabel("openai-codex"),
183
+ accessToken: data.accessToken,
184
+ refreshToken: data.refreshToken,
185
+ expiresAt: data.expiresAt,
186
+ accountId: data.accountId,
187
+ lastRefresh: data.lastRefresh ?? now,
188
+ source: data.source ?? "phren-oauth",
189
+ createdAt: existing?.createdAt ?? now,
190
+ updatedAt: now,
191
+ };
192
+ return upsertProfile(profile);
193
+ }
194
+ export function removeCodexAuthProfile() {
195
+ const store = loadStore();
196
+ const before = store.profiles.length;
197
+ store.profiles = store.profiles.filter((profile) => !(profile.kind === "codex-subscription" && profile.provider === "openai-codex"));
198
+ if (store.profiles.length === before)
199
+ return false;
200
+ persistProfiles(store);
201
+ return true;
202
+ }
203
+ export function getAuthStatusEntries() {
204
+ const apiProviders = [
205
+ { provider: "openrouter", envVar: "OPENROUTER_API_KEY" },
206
+ { provider: "anthropic", envVar: "ANTHROPIC_API_KEY" },
207
+ { provider: "openai", envVar: "OPENAI_API_KEY" },
208
+ ];
209
+ const apiEntries = apiProviders.map(({ provider, envVar }) => {
210
+ const envValue = process.env[envVar];
211
+ const profile = getApiKeyProfile(provider);
212
+ return {
213
+ provider,
214
+ configured: Boolean((typeof envValue === "string" && envValue.trim()) || profile),
215
+ source: (typeof envValue === "string" && envValue.trim())
216
+ ? "env"
217
+ : profile
218
+ ? "profile"
219
+ : "none",
220
+ label: defaultLabel(provider),
221
+ };
222
+ });
223
+ const localCodex = getCodexAuthProfile({ allowCliImport: false });
224
+ const cliCodex = localCodex ? null : readCodexCliAuthFile();
225
+ const codexEntry = {
226
+ provider: "openai-codex",
227
+ configured: Boolean(localCodex || cliCodex),
228
+ source: localCodex
229
+ ? (localCodex.source === "codex-cli-import" ? "codex-cli" : "profile")
230
+ : cliCodex
231
+ ? "codex-cli"
232
+ : "none",
233
+ label: defaultLabel("openai-codex"),
234
+ expiresAt: localCodex?.expiresAt ?? cliCodex?.expiresAt,
235
+ accountId: localCodex?.accountId ?? cliCodex?.accountId,
236
+ };
237
+ return [...apiEntries, codexEntry];
238
+ }
package/dist/cli/cli.js CHANGED
@@ -13,6 +13,7 @@ import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInject
13
13
  import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handleTruths, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./actions.js";
14
14
  import { handleGraphNamespace } from "./graph.js";
15
15
  import { resolveRuntimeProfile } from "../runtime-profile.js";
16
+ import { runBundledAgentCli } from "../agent-launch.js";
16
17
  // ── CLI router ───────────────────────────────────────────────────────────────
17
18
  export async function runCliCommand(command, args) {
18
19
  const getProfile = () => resolveRuntimeProfile(getPhrenPath());
@@ -119,14 +120,7 @@ export async function runCliCommand(command, args) {
119
120
  case "promote":
120
121
  return handlePromoteNamespace(args);
121
122
  case "agent": {
122
- try {
123
- const { runAgentCli } = await import("@phren/agent");
124
- return runAgentCli(args);
125
- }
126
- catch {
127
- console.error("@phren/agent is not installed. Install it with: npm i -g @phren/agent");
128
- process.exit(1);
129
- }
123
+ return runBundledAgentCli(args);
130
124
  }
131
125
  default:
132
126
  console.error(`Unknown command: ${command}\nRun 'phren --help' for available commands.`);
@@ -1 +1,21 @@
1
- export declare function runTopLevelCommand(argv: string[]): Promise<boolean>;
1
+ export declare const CLI_COMMANDS: string[];
2
+ export type TopLevelInvocation = {
3
+ kind: "agent";
4
+ argv: string[];
5
+ } | {
6
+ kind: "manage";
7
+ argv: string[];
8
+ } | {
9
+ kind: "mcp";
10
+ phrenArg: string;
11
+ } | {
12
+ kind: "help";
13
+ } | {
14
+ kind: "version";
15
+ };
16
+ export declare function resolveTopLevelInvocation(argv: string[]): TopLevelInvocation;
17
+ export declare function printIntegratedHelp(): void;
18
+ export declare function printIntegratedVersion(): void;
19
+ export declare function runTopLevelCommand(argv: string[], opts?: {
20
+ allowDefaultShell?: boolean;
21
+ }): Promise<boolean>;
@@ -3,31 +3,67 @@ import * as path from "path";
3
3
  import { parseMcpMode, runInit } from "./init/init.js";
4
4
  import { errorMessage } from "./utils.js";
5
5
  import { logger } from "./logger.js";
6
- import { defaultPhrenPath, findPhrenPath } from "./shared.js";
6
+ import { defaultPhrenPath, expandHomePath, findPhrenPath, readRootManifest } from "./shared.js";
7
+ import { VERSION } from "./package-metadata.js";
7
8
  import { addProjectFromPath } from "./core/project.js";
8
9
  import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, } from "./project-config.js";
9
- const HELP_TEXT = `phren - persistent knowledge for your agents
10
+ const HELP_TEXT = `phren manage - memory, setup, and store operations
10
11
 
11
- phren Interactive shell
12
- phren init Set up phren
13
- phren quickstart Quick setup: init + project scaffold
14
- phren add [path] Register a project
15
- phren search <query> Search what phren knows
16
- phren status Health check
17
- phren doctor [--fix] Diagnose and repair
18
- phren web-ui Open the knowledge graph
19
- phren tasks Cross-project task view
20
- phren graph Fragment knowledge graph
21
- phren agent <task> Run the coding agent
22
- phren agent -i Interactive agent TUI
12
+ phren manage init Set up phren
13
+ phren manage quickstart Quick setup: init + project scaffold
14
+ phren manage add [path] Register a project
15
+ phren manage search <query> Search what phren knows
16
+ phren manage status Health check
17
+ phren manage doctor [--fix] Diagnose and repair
18
+ phren manage web-ui Open the knowledge graph
19
+ phren manage tasks Cross-project task view
20
+ phren manage graph Fragment knowledge graph
21
+ phren manage shell Interactive memory shell
23
22
 
24
- phren store list List registered stores
25
- phren team init <name> Create a team store
23
+ Agent shortcuts:
24
+ phren Interactive coding agent
25
+ phren "fix the login bug" One-shot coding task
26
+ phren --provider openai-codex -i Interactive agent with explicit provider
27
+ phren --reasoning high "..." Run GPT-5.4 with higher reasoning
26
28
 
27
- phren help <topic> Detailed help for a topic
29
+ Legacy shortcuts still work:
30
+ phren init, phren search, phren task, phren config, ...
28
31
 
32
+ phren manage help <topic> Detailed management help
29
33
  Topics: projects, skills, hooks, config, maintain, setup, stores, team, env, all
30
34
  `;
35
+ const INTEGRATED_HELP_TEXT = `phren - persistent memory with an integrated coding agent
36
+
37
+ Agent:
38
+ phren Interactive coding agent
39
+ phren "fix the login bug" One-shot coding task
40
+ phren -i Interactive agent TUI
41
+ phren --provider openai-codex "..." Run the agent with an explicit provider
42
+ phren --reasoning high "..." Override the default medium thinking level
43
+ phren agent ... Explicit alias for agent mode
44
+ phren auth status Show configured provider auth
45
+ phren auth login Sign in with your Codex subscription
46
+
47
+ Memory and management:
48
+ phren manage <command> Memory/store/setup/config operations
49
+ phren mem <command> Alias for \`phren manage\`
50
+ phren manage shell Interactive memory shell
51
+ phren manage search <query> Search what phren knows
52
+ phren manage task add <project> "..."
53
+ phren manage config ...
54
+ phren manage init
55
+
56
+ Legacy shortcuts still work:
57
+ phren init
58
+ phren search <query>
59
+ phren task ...
60
+ phren config ...
61
+ phren store ...
62
+
63
+ Machine-facing mode:
64
+ phren <phren-root-path> Start the MCP server over stdio
65
+
66
+ Use \`phren manage help\` for the full management command reference.`;
31
67
  const HELP_TOPICS = {
32
68
  projects: `Projects:
33
69
  phren add [path] [--ownership <mode>] Register a project
@@ -145,7 +181,7 @@ Usage:
145
181
 
146
182
  ${Object.values(HELP_TOPICS).join("\n")}`;
147
183
  }
148
- const CLI_COMMANDS = [
184
+ export const CLI_COMMANDS = [
149
185
  "search",
150
186
  "shell",
151
187
  "update",
@@ -191,6 +227,70 @@ const CLI_COMMANDS = [
191
227
  "promote",
192
228
  "agent",
193
229
  ];
230
+ const DIRECT_MANAGE_COMMANDS = new Set([
231
+ "add",
232
+ "init",
233
+ "uninstall",
234
+ "status",
235
+ "verify",
236
+ "mcp-mode",
237
+ "hooks-mode",
238
+ "link",
239
+ "--health",
240
+ ...CLI_COMMANDS,
241
+ ]);
242
+ function looksLikePhrenRootArg(arg) {
243
+ if (!arg || arg.startsWith("-"))
244
+ return false;
245
+ try {
246
+ const resolved = path.resolve(expandHomePath(arg));
247
+ return fs.existsSync(resolved) && Boolean(readRootManifest(resolved));
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
253
+ function firstPositionalArg(argv) {
254
+ return argv.find((arg) => !arg.startsWith("-"));
255
+ }
256
+ export function resolveTopLevelInvocation(argv) {
257
+ const argvCommand = argv[0];
258
+ const positional = firstPositionalArg(argv);
259
+ if (looksLikePhrenRootArg(positional)) {
260
+ return { kind: "mcp", phrenArg: positional };
261
+ }
262
+ if (argvCommand === "--help" || argvCommand === "-h") {
263
+ return { kind: "help" };
264
+ }
265
+ if (argvCommand === "--version" || argvCommand === "-v" || argvCommand === "version") {
266
+ return { kind: "version" };
267
+ }
268
+ if (argvCommand === "help") {
269
+ return argv.length > 1 ? { kind: "manage", argv } : { kind: "help" };
270
+ }
271
+ if (argvCommand === "manage" || argvCommand === "mem") {
272
+ return { kind: "manage", argv: argv.slice(1).length > 0 ? argv.slice(1) : ["help"] };
273
+ }
274
+ if (argvCommand === "agent") {
275
+ return { kind: "agent", argv: argv.slice(1) };
276
+ }
277
+ if (argvCommand === "auth") {
278
+ return { kind: "agent", argv };
279
+ }
280
+ if (!argvCommand) {
281
+ return { kind: "agent", argv: ["-i"] };
282
+ }
283
+ if (DIRECT_MANAGE_COMMANDS.has(argvCommand)) {
284
+ return { kind: "manage", argv };
285
+ }
286
+ return { kind: "agent", argv };
287
+ }
288
+ export function printIntegratedHelp() {
289
+ console.log(INTEGRATED_HELP_TEXT);
290
+ }
291
+ export function printIntegratedVersion() {
292
+ console.log(`phren v${VERSION}`);
293
+ }
194
294
  async function flushTopLevelOutput() {
195
295
  await Promise.all([
196
296
  new Promise((resolve) => process.stdout.write("", () => resolve())),
@@ -254,7 +354,7 @@ async function promptProjectOwnership(phrenPath, fallback) {
254
354
  });
255
355
  });
256
356
  }
257
- export async function runTopLevelCommand(argv) {
357
+ export async function runTopLevelCommand(argv, opts = {}) {
258
358
  const argvCommand = argv[0];
259
359
  if (argvCommand === "--help" || argvCommand === "-h" || argvCommand === "help") {
260
360
  const topic = argv[1]?.toLowerCase();
@@ -429,6 +529,10 @@ export async function runTopLevelCommand(argv) {
429
529
  return finish();
430
530
  }
431
531
  if (!argvCommand && process.stdin.isTTY && process.stdout.isTTY) {
532
+ if (opts.allowDefaultShell === false) {
533
+ console.log(HELP_TEXT);
534
+ return finish();
535
+ }
432
536
  const { runCliCommand } = await import("./cli/cli.js");
433
537
  await runCliCommand("shell", []);
434
538
  return finish();
package/dist/index.js CHANGED
@@ -21,15 +21,29 @@ import { register as registerExtract } from "./tools/extract.js";
21
21
  import { register as registerConfig } from "./tools/config.js";
22
22
  import { mcpResponse } from "./tools/types.js";
23
23
  import { errorMessage } from "./utils.js";
24
- import { runTopLevelCommand } from "./entrypoint.js";
24
+ import { printIntegratedHelp, printIntegratedVersion, resolveTopLevelInvocation, runTopLevelCommand, } from "./entrypoint.js";
25
25
  import { startEmbeddingWarmup } from "./startup-embedding.js";
26
26
  import { resolveRuntimeProfile } from "./runtime-profile.js";
27
27
  import { VERSION as PACKAGE_VERSION } from "./package-metadata.js";
28
- const handledTopLevelCommand = await runTopLevelCommand(process.argv.slice(2));
29
- // MCP mode: first non-flag arg is the phren path. Resolve it lazily so CLI commands
30
- // like `maintain` are not misinterpreted as a filesystem path after the command has run.
31
- const phrenArg = handledTopLevelCommand ? undefined : process.argv.find((a, i) => i >= 2 && !a.startsWith("-"));
32
- const phrenPath = handledTopLevelCommand ? "" : findPhrenPathWithArg(phrenArg);
28
+ import { runBundledAgentCli } from "./agent-launch.js";
29
+ const invocation = resolveTopLevelInvocation(process.argv.slice(2));
30
+ if (invocation.kind === "help") {
31
+ printIntegratedHelp();
32
+ process.exit(0);
33
+ }
34
+ if (invocation.kind === "version") {
35
+ printIntegratedVersion();
36
+ process.exit(0);
37
+ }
38
+ if (invocation.kind === "manage") {
39
+ await runTopLevelCommand(invocation.argv, { allowDefaultShell: false });
40
+ process.exit(process.exitCode ?? 0);
41
+ }
42
+ if (invocation.kind === "agent") {
43
+ await runBundledAgentCli(invocation.argv);
44
+ process.exit(process.exitCode ?? 0);
45
+ }
46
+ const phrenPath = findPhrenPathWithArg(invocation.phrenArg);
33
47
  const STALE_LOCK_MS = 120_000; // 2 min — slightly above EXEC_TIMEOUT_MS (30s) to avoid blocking healthy writers
34
48
  function cleanStaleLocks(phrenPath) {
35
49
  const dir = runtimeDir(phrenPath);
@@ -222,9 +236,7 @@ async function main() {
222
236
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
223
237
  process.on("SIGINT", () => void shutdown("SIGINT"));
224
238
  }
225
- if (!handledTopLevelCommand) {
226
- main().catch((err) => {
227
- console.error("Failed to start phren-mcp:", err);
228
- process.exit(1);
229
- });
230
- }
239
+ main().catch((err) => {
240
+ console.error("Failed to start phren-mcp:", err);
241
+ process.exit(1);
242
+ });
@@ -0,0 +1,44 @@
1
+ import { type SessionState } from "./utils.js";
2
+ export type SessionCounterField = "findingsAdded" | "tasksCompleted";
3
+ export interface SessionSummaryRecord {
4
+ summary: string;
5
+ sessionId: string;
6
+ project?: string;
7
+ endedAt?: string;
8
+ }
9
+ export interface SessionSummaryLookup {
10
+ summary: string | null;
11
+ sessionId?: string;
12
+ project?: string;
13
+ endedAt?: string;
14
+ }
15
+ export interface SerializedSessionMessage {
16
+ role: string;
17
+ content: unknown;
18
+ }
19
+ export interface SessionMessagesSnapshot {
20
+ schemaVersion: 1;
21
+ sessionId: string;
22
+ project?: string;
23
+ savedAt: string;
24
+ messages: SerializedSessionMessage[];
25
+ }
26
+ interface StartSessionOptions {
27
+ sessionId?: string;
28
+ project?: string;
29
+ agentScope?: string;
30
+ hookCreated?: boolean;
31
+ agentCreated?: boolean;
32
+ }
33
+ export declare function lastSummaryPath(phrenPath: string): string;
34
+ export declare function readLastSummary(phrenPath: string): SessionSummaryRecord | null;
35
+ export declare function writeLastSummary(phrenPath: string, record: SessionSummaryRecord): void;
36
+ export declare function findMostRecentSummaryWithProject(phrenPath: string, project?: string): SessionSummaryLookup;
37
+ export declare function startSessionRecord(phrenPath: string, options?: StartSessionOptions): string;
38
+ export declare function readSessionState(phrenPath: string, sessionId: string): SessionState | null;
39
+ export declare function endSessionRecord(phrenPath: string, sessionId: string, summary?: string): void;
40
+ export declare function incrementSessionStateCounter(phrenPath: string, sessionId: string, field: SessionCounterField, count?: number): void;
41
+ export declare function saveSessionMessages(phrenPath: string, sessionId: string, messages: SerializedSessionMessage[], project?: string): void;
42
+ export declare function loadLastSessionSnapshot(phrenPath: string, project?: string): SessionMessagesSnapshot | null;
43
+ export declare function loadLastSessionMessages(phrenPath: string, project?: string): SerializedSessionMessage[] | null;
44
+ export {};
@@ -0,0 +1,259 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { withFileLock } from "../shared/governance.js";
5
+ import { atomicWriteJson, debugError, readSessionStateFile, scanSessionFiles, sessionFileForId, sessionsDir, writeSessionStateFile, } from "./utils.js";
6
+ function normalizeSummary(summary) {
7
+ const trimmed = summary?.trim();
8
+ return trimmed ? trimmed : undefined;
9
+ }
10
+ function sessionMessagesFileForId(phrenPath, sessionId) {
11
+ return path.join(sessionsDir(phrenPath), `session-${sessionId}-messages.json`);
12
+ }
13
+ function inferProjectForSession(phrenPath, sessionId) {
14
+ return readSessionState(phrenPath, sessionId)?.project;
15
+ }
16
+ function inferSavedAt(filePath) {
17
+ try {
18
+ return new Date(fs.statSync(filePath).mtimeMs).toISOString();
19
+ }
20
+ catch {
21
+ return new Date(0).toISOString();
22
+ }
23
+ }
24
+ function extractSessionIdFromMessageFile(filePath) {
25
+ const match = path.basename(filePath).match(/^session-(.+)-messages\.json$/);
26
+ return match?.[1] ?? "unknown";
27
+ }
28
+ function parseSessionMessagesSnapshot(filePath) {
29
+ try {
30
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
31
+ if (Array.isArray(parsed)) {
32
+ return {
33
+ schemaVersion: 1,
34
+ sessionId: extractSessionIdFromMessageFile(filePath),
35
+ savedAt: inferSavedAt(filePath),
36
+ messages: parsed,
37
+ };
38
+ }
39
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.messages)) {
40
+ return null;
41
+ }
42
+ return {
43
+ schemaVersion: 1,
44
+ sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : extractSessionIdFromMessageFile(filePath),
45
+ project: typeof parsed.project === "string" ? parsed.project : undefined,
46
+ savedAt: typeof parsed.savedAt === "string" ? parsed.savedAt : inferSavedAt(filePath),
47
+ messages: parsed.messages,
48
+ };
49
+ }
50
+ catch (err) {
51
+ debugError("parseSessionMessagesSnapshot", err);
52
+ return null;
53
+ }
54
+ }
55
+ function listSessionMessageSnapshots(phrenPath) {
56
+ const dir = sessionsDir(phrenPath);
57
+ let entries;
58
+ try {
59
+ entries = fs.readdirSync(dir, { withFileTypes: true });
60
+ }
61
+ catch (err) {
62
+ debugError("listSessionMessageSnapshots readdir", err);
63
+ return [];
64
+ }
65
+ const snapshots = [];
66
+ for (const entry of entries) {
67
+ if (!entry.isFile() || !entry.name.startsWith("session-") || !entry.name.endsWith("-messages.json"))
68
+ continue;
69
+ const fullPath = path.join(dir, entry.name);
70
+ const snapshot = parseSessionMessagesSnapshot(fullPath);
71
+ if (!snapshot)
72
+ continue;
73
+ let mtimeMs = 0;
74
+ try {
75
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
76
+ }
77
+ catch {
78
+ mtimeMs = 0;
79
+ }
80
+ snapshots.push({ snapshot, mtimeMs });
81
+ }
82
+ snapshots.sort((a, b) => {
83
+ const bySavedAt = Date.parse(b.snapshot.savedAt) - Date.parse(a.snapshot.savedAt);
84
+ if (!Number.isNaN(bySavedAt) && bySavedAt !== 0)
85
+ return bySavedAt;
86
+ return b.mtimeMs - a.mtimeMs;
87
+ });
88
+ return snapshots;
89
+ }
90
+ export function lastSummaryPath(phrenPath) {
91
+ return path.join(sessionsDir(phrenPath), "last-summary.json");
92
+ }
93
+ export function readLastSummary(phrenPath) {
94
+ try {
95
+ const parsed = JSON.parse(fs.readFileSync(lastSummaryPath(phrenPath), "utf8"));
96
+ if (!parsed || typeof parsed !== "object")
97
+ return null;
98
+ if (typeof parsed.summary !== "string" || typeof parsed.sessionId !== "string")
99
+ return null;
100
+ return {
101
+ summary: parsed.summary,
102
+ sessionId: parsed.sessionId,
103
+ project: typeof parsed.project === "string" ? parsed.project : undefined,
104
+ endedAt: typeof parsed.endedAt === "string" ? parsed.endedAt : undefined,
105
+ };
106
+ }
107
+ catch (err) {
108
+ if (err.code !== "ENOENT") {
109
+ debugError("readLastSummary", err);
110
+ }
111
+ return null;
112
+ }
113
+ }
114
+ export function writeLastSummary(phrenPath, record) {
115
+ try {
116
+ atomicWriteJson(lastSummaryPath(phrenPath), {
117
+ summary: record.summary,
118
+ sessionId: record.sessionId,
119
+ project: record.project,
120
+ endedAt: record.endedAt ?? new Date().toISOString(),
121
+ });
122
+ }
123
+ catch (err) {
124
+ debugError("writeLastSummary", err);
125
+ }
126
+ }
127
+ export function findMostRecentSummaryWithProject(phrenPath, project) {
128
+ const fastPath = readLastSummary(phrenPath);
129
+ if (fastPath && (!project || fastPath.project === project)) {
130
+ return fastPath;
131
+ }
132
+ const dir = sessionsDir(phrenPath);
133
+ const results = scanSessionFiles(dir, readSessionStateFile, (state) => typeof state.summary === "string" && state.summary.trim().length > 0, { errorScope: "findMostRecentSummaryWithProject" });
134
+ if (!project) {
135
+ if (results.length === 0)
136
+ return { summary: null };
137
+ const best = results[0].data;
138
+ return {
139
+ summary: best.summary ?? null,
140
+ sessionId: best.sessionId,
141
+ project: best.project,
142
+ endedAt: best.endedAt,
143
+ };
144
+ }
145
+ const projectMatch = results.find(({ data }) => data.project === project);
146
+ if (projectMatch) {
147
+ return {
148
+ summary: projectMatch.data.summary ?? null,
149
+ sessionId: projectMatch.data.sessionId,
150
+ project: projectMatch.data.project,
151
+ endedAt: projectMatch.data.endedAt,
152
+ };
153
+ }
154
+ if (results.length === 0)
155
+ return { summary: null };
156
+ const fallback = results[0].data;
157
+ return {
158
+ summary: fallback.summary ?? null,
159
+ sessionId: fallback.sessionId,
160
+ project: fallback.project,
161
+ endedAt: fallback.endedAt,
162
+ };
163
+ }
164
+ export function startSessionRecord(phrenPath, options = {}) {
165
+ const sessionId = options.sessionId ?? crypto.randomUUID();
166
+ const state = {
167
+ sessionId,
168
+ project: options.project,
169
+ agentScope: options.agentScope,
170
+ startedAt: new Date().toISOString(),
171
+ findingsAdded: 0,
172
+ tasksCompleted: 0,
173
+ hookCreated: options.hookCreated,
174
+ agentCreated: options.agentCreated,
175
+ };
176
+ writeSessionStateFile(sessionFileForId(phrenPath, sessionId), state);
177
+ return sessionId;
178
+ }
179
+ export function readSessionState(phrenPath, sessionId) {
180
+ return readSessionStateFile(sessionFileForId(phrenPath, sessionId));
181
+ }
182
+ export function endSessionRecord(phrenPath, sessionId, summary) {
183
+ const file = sessionFileForId(phrenPath, sessionId);
184
+ if (!fs.existsSync(file))
185
+ return;
186
+ const normalizedSummary = normalizeSummary(summary);
187
+ try {
188
+ withFileLock(file, () => {
189
+ const current = readSessionStateFile(file);
190
+ if (!current)
191
+ return;
192
+ const nextState = {
193
+ ...current,
194
+ endedAt: new Date().toISOString(),
195
+ summary: normalizedSummary ?? current.summary,
196
+ findingsAdded: Number.isFinite(current.findingsAdded) ? current.findingsAdded : 0,
197
+ tasksCompleted: Number.isFinite(current.tasksCompleted) ? current.tasksCompleted : 0,
198
+ };
199
+ writeSessionStateFile(file, nextState);
200
+ if (normalizedSummary) {
201
+ writeLastSummary(phrenPath, {
202
+ summary: normalizedSummary,
203
+ sessionId,
204
+ project: nextState.project,
205
+ endedAt: nextState.endedAt,
206
+ });
207
+ }
208
+ });
209
+ }
210
+ catch (err) {
211
+ debugError("endSessionRecord", err);
212
+ }
213
+ }
214
+ export function incrementSessionStateCounter(phrenPath, sessionId, field, count = 1) {
215
+ const file = sessionFileForId(phrenPath, sessionId);
216
+ if (!fs.existsSync(file))
217
+ return;
218
+ try {
219
+ withFileLock(file, () => {
220
+ const current = readSessionStateFile(file);
221
+ if (!current)
222
+ return;
223
+ const currentValue = Number.isFinite(current[field]) ? current[field] : 0;
224
+ writeSessionStateFile(file, {
225
+ ...current,
226
+ findingsAdded: Number.isFinite(current.findingsAdded) ? current.findingsAdded : 0,
227
+ tasksCompleted: Number.isFinite(current.tasksCompleted) ? current.tasksCompleted : 0,
228
+ [field]: currentValue + count,
229
+ });
230
+ });
231
+ }
232
+ catch (err) {
233
+ debugError(`incrementSessionStateCounter(${field})`, err);
234
+ }
235
+ }
236
+ export function saveSessionMessages(phrenPath, sessionId, messages, project) {
237
+ const snapshot = {
238
+ schemaVersion: 1,
239
+ sessionId,
240
+ project: project ?? inferProjectForSession(phrenPath, sessionId),
241
+ savedAt: new Date().toISOString(),
242
+ messages,
243
+ };
244
+ atomicWriteJson(sessionMessagesFileForId(phrenPath, sessionId), snapshot);
245
+ }
246
+ export function loadLastSessionSnapshot(phrenPath, project) {
247
+ const snapshots = listSessionMessageSnapshots(phrenPath);
248
+ if (snapshots.length === 0)
249
+ return null;
250
+ if (project) {
251
+ const projectSnapshot = snapshots.find(({ snapshot }) => snapshot.project === project);
252
+ if (projectSnapshot)
253
+ return projectSnapshot.snapshot;
254
+ }
255
+ return snapshots[0]?.snapshot ?? null;
256
+ }
257
+ export function loadLastSessionMessages(phrenPath, project) {
258
+ return loadLastSessionSnapshot(phrenPath, project)?.messages ?? null;
259
+ }
@@ -14,9 +14,12 @@ export interface SessionState {
14
14
  tasksCompleted: number;
15
15
  /** When true, this session was created by a lifecycle hook, not an explicit MCP call. */
16
16
  hookCreated?: boolean;
17
+ /** When true, this session was created by the coding agent runtime. */
18
+ agentCreated?: boolean;
17
19
  }
18
20
  export declare function sessionsDir(phrenPath: string): string;
19
21
  export declare function sessionFileForId(phrenPath: string, sessionId: string): string;
22
+ export declare function isSessionStateFileName(name: string): boolean;
20
23
  export declare function readSessionStateFile(file: string): SessionState | null;
21
24
  export declare function writeSessionStateFile(file: string, state: SessionState): void;
22
25
  /**
@@ -19,6 +19,11 @@ export function sessionsDir(phrenPath) {
19
19
  export function sessionFileForId(phrenPath, sessionId) {
20
20
  return path.join(sessionsDir(phrenPath), `session-${sessionId}.json`);
21
21
  }
22
+ export function isSessionStateFileName(name) {
23
+ return name.startsWith("session-") &&
24
+ name.endsWith(".json") &&
25
+ !name.endsWith("-messages.json");
26
+ }
22
27
  export function readSessionStateFile(file) {
23
28
  try {
24
29
  return JSON.parse(fs.readFileSync(file, "utf-8"));
@@ -63,7 +68,7 @@ export function scanSessionFiles(dir, parse, filter, opts) {
63
68
  }
64
69
  const results = [];
65
70
  for (const entry of entries) {
66
- if (!entry.isFile() || !entry.name.startsWith("session-") || !entry.name.endsWith(".json"))
71
+ if (!entry.isFile() || !isSessionStateFileName(entry.name))
67
72
  continue;
68
73
  const fullPath = path.join(dir, entry.name);
69
74
  try {
package/dist/status.js CHANGED
@@ -15,7 +15,7 @@ import { RESET, BOLD, DIM, GREEN, YELLOW, RED, CYAN } from "./shell/render.js";
15
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
16
  function readPackageVersion() {
17
17
  try {
18
- const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
18
+ const pkgPath = path.resolve(__dirname, "..", "package.json");
19
19
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
20
20
  return typeof pkg.version === "string" ? pkg.version : "unknown";
21
21
  }
@@ -14,8 +14,9 @@ import { readTasks } from "../data/tasks.js";
14
14
  import { readFindings } from "../data/access.js";
15
15
  import { getActiveTaskForSession } from "../task/lifecycle.js";
16
16
  import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints.js";
17
+ import { findMostRecentSummaryWithProject as findMostRecentSummaryRecord, writeLastSummary as writeLastSummaryRecord, } from "../session/artifacts.js";
17
18
  import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
18
- import { atomicWriteJson, debugError, scanSessionFiles, sessionsDir, sessionFileForId, readSessionStateFile, writeSessionStateFile, } from "../session/utils.js";
19
+ import { debugError, scanSessionFiles, sessionsDir, sessionFileForId, readSessionStateFile, writeSessionStateFile, } from "../session/utils.js";
19
20
  import { getRuntimeHealth } from "../governance/policy.js";
20
21
  import { getProjectSourcePath, readProjectConfig } from "../project-config.js";
21
22
  const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -127,18 +128,9 @@ export function resolveActiveSessionScope(phrenPath, project) {
127
128
  }
128
129
  return normalizeMemoryScope(bestState?.agentScope);
129
130
  }
130
- /** Path for the last-summary fast-path file. */
131
- function lastSummaryPath(phrenPath) {
132
- return path.join(sessionsDir(phrenPath), "last-summary.json");
133
- }
134
131
  /** Write the last summary for fast retrieval by next session_start. */
135
132
  function writeLastSummary(phrenPath, summary, sessionId, project) {
136
- try {
137
- atomicWriteJson(lastSummaryPath(phrenPath), { summary, sessionId, project, endedAt: new Date().toISOString() });
138
- }
139
- catch (err) {
140
- debugError("writeLastSummary", err);
141
- }
133
+ writeLastSummaryRecord(phrenPath, { summary, sessionId, project, endedAt: new Date().toISOString() });
142
134
  }
143
135
  /** Find the most recent session with a summary (including ended sessions).
144
136
  * @internal Exported for tests. */
@@ -147,25 +139,12 @@ export function findMostRecentSummary(phrenPath) {
147
139
  }
148
140
  /** Find the most recent session with a summary and project context. */
149
141
  function findMostRecentSummaryWithProject(phrenPath) {
150
- // Fast path: read from dedicated last-summary file
151
- try {
152
- const data = JSON.parse(fs.readFileSync(lastSummaryPath(phrenPath), "utf-8"));
153
- if (data.summary)
154
- return { summary: data.summary, project: data.project, endedAt: data.endedAt };
155
- }
156
- catch (err) {
157
- // ENOENT is expected when no summary has been written yet
158
- if (err.code !== "ENOENT") {
159
- debugError("findMostRecentSummaryWithProject fastPath", err);
160
- }
161
- }
162
- // Slow path: scan all session files
163
- const dir = sessionsDir(phrenPath);
164
- const results = scanSessionFiles(dir, readSessionStateFile, (state) => !!state.summary, { errorScope: "findMostRecentSummaryWithProject" });
165
- if (results.length === 0)
166
- return { summary: null };
167
- const best = results[0]; // already sorted newest-mtime-first
168
- return { summary: best.data.summary, project: best.data.project, endedAt: best.data.endedAt };
142
+ const result = findMostRecentSummaryRecord(phrenPath);
143
+ return {
144
+ summary: result.summary,
145
+ project: result.project,
146
+ endedAt: result.endedAt,
147
+ };
169
148
  }
170
149
  /** Resolve session file from an explicit sessionId or a previously-bound connectionId. */
171
150
  function resolveSessionFile(phrenPath, sessionId, connectionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Knowledge layer for AI agents — CLI, MCP server, and data layer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,10 +39,18 @@
39
39
  "types": "./dist/core/finding.d.ts",
40
40
  "default": "./dist/core/finding.js"
41
41
  },
42
+ "./auth/profiles": {
43
+ "types": "./dist/auth/profiles.d.ts",
44
+ "default": "./dist/auth/profiles.js"
45
+ },
42
46
  "./session/utils": {
43
47
  "types": "./dist/session/utils.d.ts",
44
48
  "default": "./dist/session/utils.js"
45
49
  },
50
+ "./session/artifacts": {
51
+ "types": "./dist/session/artifacts.d.ts",
52
+ "default": "./dist/session/artifacts.js"
53
+ },
46
54
  "./shell/render-api": {
47
55
  "types": "./dist/shell/render-api.d.ts",
48
56
  "default": "./dist/shell/render-api.js"