@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.
- package/dist/agent-launch.d.ts +1 -0
- package/dist/agent-launch.js +48 -0
- package/dist/auth/profiles.d.ts +58 -0
- package/dist/auth/profiles.js +238 -0
- package/dist/cli/cli.js +2 -8
- package/dist/entrypoint.d.ts +21 -1
- package/dist/entrypoint.js +123 -19
- package/dist/index.js +24 -12
- package/dist/session/artifacts.d.ts +44 -0
- package/dist/session/artifacts.js +259 -0
- package/dist/session/utils.d.ts +3 -0
- package/dist/session/utils.js +6 -1
- package/dist/status.js +1 -1
- package/dist/tools/session.js +9 -30
- package/package.json +9 -1
|
@@ -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
|
-
|
|
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.`);
|
package/dist/entrypoint.d.ts
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
export declare
|
|
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>;
|
package/dist/entrypoint.js
CHANGED
|
@@ -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 -
|
|
10
|
+
const HELP_TEXT = `phren manage - memory, setup, and store operations
|
|
10
11
|
|
|
11
|
-
phren
|
|
12
|
-
phren init
|
|
13
|
-
phren
|
|
14
|
-
phren
|
|
15
|
-
phren
|
|
16
|
-
phren
|
|
17
|
-
phren
|
|
18
|
-
phren
|
|
19
|
-
phren
|
|
20
|
-
phren
|
|
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
|
-
|
|
25
|
-
phren
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
}
|
package/dist/session/utils.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/session/utils.js
CHANGED
|
@@ -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() || !
|
|
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, "..", "
|
|
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
|
}
|
package/dist/tools/session.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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"
|