@phren/cli 0.0.28 → 0.0.33

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.
Files changed (153) hide show
  1. package/mcp/dist/capabilities/cli.js +2 -5
  2. package/mcp/dist/capabilities/mcp.js +5 -8
  3. package/mcp/dist/capabilities/types.js +2 -5
  4. package/mcp/dist/capabilities/vscode.js +2 -5
  5. package/mcp/dist/capabilities/web-ui.js +2 -5
  6. package/mcp/dist/{cli-actions.js → cli/actions.js} +25 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
  11. package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
  12. package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
  13. package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
  14. package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
  15. package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
  16. package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +58 -117
  17. package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
  18. package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
  19. package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
  20. package/mcp/dist/{cli-search.js → cli/search.js} +12 -11
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +323 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +337 -0
  24. package/mcp/dist/cli-hooks-stop.js +519 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
  26. package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
  27. package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
  28. package/mcp/dist/{content-learning.js → content/learning.js} +41 -20
  29. package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
  30. package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
  31. package/mcp/dist/{core-project.js → core/project.js} +4 -4
  32. package/mcp/dist/{core-search.js → core/search.js} +2 -2
  33. package/mcp/dist/{data-access.js → data/access.js} +142 -15
  34. package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
  35. package/mcp/dist/embedding.js +9 -14
  36. package/mcp/dist/entrypoint.js +11 -11
  37. package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
  38. package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
  39. package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
  40. package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +13 -7
  41. package/mcp/dist/governance/audit.js +30 -0
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
  46. package/mcp/dist/hooks.js +53 -37
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +54 -30
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +80 -69
  51. package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
  52. package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
  53. package/mcp/dist/{init-shared.js → init/shared.js} +4 -4
  54. package/mcp/dist/init-bootstrap.js +21 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-env.js +114 -0
  57. package/mcp/dist/init-fresh.js +234 -0
  58. package/mcp/dist/init-hooks.js +26 -0
  59. package/mcp/dist/init-mcp.js +65 -0
  60. package/mcp/dist/init-modes.js +135 -0
  61. package/mcp/dist/init-npm.js +37 -0
  62. package/mcp/dist/init-project-local.js +99 -0
  63. package/mcp/dist/init-semantic.js +48 -0
  64. package/mcp/dist/init-types.js +1 -0
  65. package/mcp/dist/init-uninstall.js +504 -0
  66. package/mcp/dist/init-update.js +96 -0
  67. package/mcp/dist/init-walkthrough.js +524 -0
  68. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  69. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  70. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  71. package/mcp/dist/{link.js → link/link.js} +26 -31
  72. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  73. package/mcp/dist/logger.js +11 -3
  74. package/mcp/dist/package-metadata.js +1 -1
  75. package/mcp/dist/phren-art.js +4 -126
  76. package/mcp/dist/phren-paths.js +30 -12
  77. package/mcp/dist/proactivity.js +3 -3
  78. package/mcp/dist/profile-store.js +5 -6
  79. package/mcp/dist/project-config.js +2 -2
  80. package/mcp/dist/project-topics.js +17 -47
  81. package/mcp/dist/provider-adapters.js +1 -1
  82. package/mcp/dist/query-correlation.js +1 -1
  83. package/mcp/dist/runtime-profile.js +1 -1
  84. package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
  85. package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
  86. package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
  87. package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +28 -3
  88. package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
  89. package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +19 -42
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
  93. package/mcp/dist/shared/process.js +24 -0
  94. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
  95. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
  96. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  97. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  98. package/mcp/dist/shared.js +6 -60
  99. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  100. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  101. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  102. package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
  103. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  104. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  105. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  106. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  107. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  108. package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
  109. package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
  110. package/mcp/dist/startup-embedding.js +2 -2
  111. package/mcp/dist/status.js +15 -14
  112. package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
  113. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  114. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
  115. package/mcp/dist/telemetry.js +3 -4
  116. package/mcp/dist/tool-registry.js +29 -17
  117. package/mcp/dist/tools/config.js +530 -0
  118. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  119. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  120. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  121. package/mcp/dist/tools/finding.js +584 -0
  122. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  123. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  124. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  125. package/mcp/dist/tools/ops.js +468 -0
  126. package/mcp/dist/tools/search.js +672 -0
  127. package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
  128. package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
  129. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  130. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  131. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  132. package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
  133. package/mcp/dist/ui/server.js +1024 -0
  134. package/mcp/dist/update.js +2 -2
  135. package/mcp/dist/utils.js +63 -19
  136. package/package.json +2 -2
  137. package/scripts/preuninstall.mjs +31 -0
  138. package/starter/global/CLAUDE.md +3 -2
  139. package/mcp/dist/governance-audit.js +0 -22
  140. package/mcp/dist/mcp-config.js +0 -551
  141. package/mcp/dist/mcp-finding.js +0 -594
  142. package/mcp/dist/mcp-ops.js +0 -363
  143. package/mcp/dist/mcp-search.js +0 -668
  144. package/mcp/dist/memory-ui-server.js +0 -1411
  145. package/mcp/dist/shared-governance.js +0 -4
  146. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  147. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  148. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  149. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  150. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  151. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  152. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  153. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * SessionStart handler and onboarding helpers.
3
+ * Extracted from cli-hooks-session.ts for modularity.
4
+ */
5
+ import { buildHookContext, handleGuardSkip, debugLog, sessionMarker, getProjectDirs, findProjectNameCaseInsensitive, updateRuntimeHealth, appendAuditLog, detectProject, isProjectHookEnabled, readProjectConfig, getProjectSourcePath, detectProjectDir, ensureLocalGitRepo, isProjectTracked, repairPreexistingInstall, isFeatureEnabled, errorMessage, runDoctor, resolveRuntimeProfile, } from "./cli/hooks-context.js";
6
+ import { qualityMarkers, } from "./shared.js";
7
+ import { readInstallPreferences } from "./init/preferences.js";
8
+ import { logger } from "./logger.js";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { spawnDetachedChild } from "./shared/process.js";
12
+ import { TASKS_FILENAME } from "./data/tasks.js";
13
+ import { resolveSubprocessArgs as _resolveSubprocessArgs, runBestEffortGit, countUnsyncedCommits, } from "./cli-hooks-git.js";
14
+ const SESSION_START_ONBOARDING_MARKER = "session-start-onboarding-v1";
15
+ const SYNC_WARN_MARKER = "sync-broken-warned-v1";
16
+ function projectHasBootstrapSignals(phrenPath, project) {
17
+ const projectDir = path.join(phrenPath, project);
18
+ const findingsPath = path.join(projectDir, "FINDINGS.md");
19
+ if (fs.existsSync(findingsPath)) {
20
+ const findings = fs.readFileSync(findingsPath, "utf8");
21
+ if (/^-\s+/m.test(findings))
22
+ return true;
23
+ }
24
+ const tasksPath = path.join(projectDir, TASKS_FILENAME);
25
+ if (fs.existsSync(tasksPath)) {
26
+ const tasks = fs.readFileSync(tasksPath, "utf8");
27
+ if (/^-\s+\[(?: |x|X)\]/m.test(tasks))
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ export function getUntrackedProjectNotice(phrenPath, cwd) {
33
+ const profile = resolveRuntimeProfile(phrenPath);
34
+ const projectDir = detectProjectDir(cwd, phrenPath);
35
+ if (!projectDir)
36
+ return null;
37
+ const activeProfile = profile || undefined;
38
+ // Check the exact current working directory against projects in the active profile.
39
+ // This avoids prompting when cwd is already inside a tracked sourcePath.
40
+ if (detectProject(phrenPath, cwd, activeProfile))
41
+ return null;
42
+ if (detectProject(phrenPath, projectDir, activeProfile))
43
+ return null;
44
+ const projectName = path.basename(projectDir).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
45
+ if (isProjectTracked(phrenPath, projectName, activeProfile)) {
46
+ const trackedName = getProjectDirs(phrenPath, activeProfile)
47
+ .map((dir) => path.basename(dir))
48
+ .find((name) => name.toLowerCase() === projectName)
49
+ || findProjectNameCaseInsensitive(phrenPath, projectName)
50
+ || projectName;
51
+ const config = readProjectConfig(phrenPath, trackedName);
52
+ const sourcePath = getProjectSourcePath(phrenPath, trackedName, config);
53
+ if (!sourcePath)
54
+ return null;
55
+ const resolvedProjectDir = path.resolve(projectDir);
56
+ const sameSource = resolvedProjectDir === sourcePath || resolvedProjectDir.startsWith(sourcePath + path.sep);
57
+ if (sameSource)
58
+ return null;
59
+ }
60
+ return [
61
+ "<phren-notice>",
62
+ "This project directory is not tracked by phren yet.",
63
+ "Run `npx phren add` to track it now.",
64
+ `Suggested command: \`npx phren add "${projectDir}"\``,
65
+ "Ask the user whether they want to add it to phren now.",
66
+ "If they say no, tell them they can always run `npx phren add` later.",
67
+ "If they say yes, also ask whether phren should manage repo instruction files or leave their existing repo-owned CLAUDE/AGENTS files alone.",
68
+ `Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`npx phren add\` from that directory.`,
69
+ "After onboarding, run `npx phren doctor` if hooks or MCP tools are not responding.",
70
+ "<phren-notice>",
71
+ "",
72
+ ].join("\n");
73
+ }
74
+ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
75
+ const markerPath = sessionMarker(phrenPath, SESSION_START_ONBOARDING_MARKER);
76
+ if (fs.existsSync(markerPath))
77
+ return null;
78
+ if (getUntrackedProjectNotice(phrenPath, cwd))
79
+ return null;
80
+ const profile = resolveRuntimeProfile(phrenPath);
81
+ const trackedProjects = getProjectDirs(phrenPath, profile).filter((dir) => path.basename(dir) !== "global");
82
+ if (trackedProjects.length === 0) {
83
+ return [
84
+ "<phren-notice>",
85
+ "Phren onboarding: no tracked projects are active for this workspace yet.",
86
+ "Start in a project repo and run `npx phren add` so SessionStart can inject project context.",
87
+ "Run `npx phren doctor` to verify hooks and MCP wiring after setup.",
88
+ "<phren-notice>",
89
+ "",
90
+ ].join("\n");
91
+ }
92
+ if (!activeProject)
93
+ return null;
94
+ if (projectHasBootstrapSignals(phrenPath, activeProject))
95
+ return null;
96
+ return [
97
+ "<phren-notice>",
98
+ `Phren onboarding: project "${activeProject}" is tracked but memory is still empty.`,
99
+ "Capture one finding with `add_finding` and one task with `add_task` to seed future SessionStart context.",
100
+ "Run `npx phren doctor` if setup seems incomplete.",
101
+ "<phren-notice>",
102
+ "",
103
+ ].join("\n");
104
+ }
105
+ function scheduleBackgroundMaintenance(phrenPathLocal, project) {
106
+ if (!isFeatureEnabled("PHREN_FEATURE_DAILY_MAINTENANCE", true))
107
+ return false;
108
+ const markers = qualityMarkers(phrenPathLocal);
109
+ if (fs.existsSync(markers.done))
110
+ return false;
111
+ if (fs.existsSync(markers.lock)) {
112
+ try {
113
+ const ageMs = Date.now() - fs.statSync(markers.lock).mtimeMs;
114
+ if (ageMs <= 2 * 60 * 60 * 1000)
115
+ return false;
116
+ fs.unlinkSync(markers.lock);
117
+ }
118
+ catch (err) {
119
+ debugLog(`maybeRunBackgroundMaintenance: lock check failed: ${errorMessage(err)}`);
120
+ return false;
121
+ }
122
+ }
123
+ const spawnArgs = _resolveSubprocessArgs("background-maintenance");
124
+ if (!spawnArgs)
125
+ return false;
126
+ try {
127
+ // Use exclusive open (O_EXCL) to atomically claim the lock; if another process
128
+ // already holds it this throws and we return false without spawning a duplicate.
129
+ const lockContent = JSON.stringify({
130
+ startedAt: new Date().toISOString(),
131
+ project: project || "all",
132
+ pid: process.pid,
133
+ }) + "\n";
134
+ let fd;
135
+ try {
136
+ fd = fs.openSync(markers.lock, "wx");
137
+ }
138
+ catch (err) {
139
+ // Another process already claimed the lock
140
+ logger.debug("backgroundMaintenance lockClaim", errorMessage(err));
141
+ return false;
142
+ }
143
+ try {
144
+ fs.writeSync(fd, lockContent);
145
+ }
146
+ finally {
147
+ fs.closeSync(fd);
148
+ }
149
+ if (project)
150
+ spawnArgs.push(project);
151
+ const logDir = path.join(phrenPathLocal, ".config");
152
+ fs.mkdirSync(logDir, { recursive: true });
153
+ const logPath = path.join(logDir, "background-maintenance.log");
154
+ const logFd = fs.openSync(logPath, "a");
155
+ fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
156
+ const child = spawnDetachedChild(spawnArgs, { phrenPath: phrenPathLocal, logFd });
157
+ child.on("exit", (code, signal) => {
158
+ const msg = `[${new Date().toISOString()}] exit code=${code ?? "null"} signal=${signal ?? "none"}\n`;
159
+ try {
160
+ fs.appendFileSync(logPath, msg);
161
+ }
162
+ catch (err) {
163
+ logger.debug("backgroundMaintenance exitLog", errorMessage(err));
164
+ }
165
+ if (code === 0) {
166
+ try {
167
+ fs.writeFileSync(markers.done, new Date().toISOString() + "\n");
168
+ }
169
+ catch (err) {
170
+ logger.debug("backgroundMaintenance doneMarker", errorMessage(err));
171
+ }
172
+ }
173
+ try {
174
+ fs.unlinkSync(markers.lock);
175
+ }
176
+ catch (err) {
177
+ logger.debug("backgroundMaintenance unlockOnExit", errorMessage(err));
178
+ }
179
+ });
180
+ child.on("error", (spawnErr) => {
181
+ const msg = `[${new Date().toISOString()}] spawn error: ${spawnErr.message}\n`;
182
+ try {
183
+ fs.appendFileSync(logPath, msg);
184
+ }
185
+ catch (err) {
186
+ logger.debug("backgroundMaintenance errorLog", errorMessage(err));
187
+ }
188
+ try {
189
+ fs.unlinkSync(markers.lock);
190
+ }
191
+ catch (err) {
192
+ logger.debug("backgroundMaintenance unlockOnError", errorMessage(err));
193
+ }
194
+ });
195
+ fs.closeSync(logFd);
196
+ child.unref();
197
+ return true;
198
+ }
199
+ catch (err) {
200
+ const errMsg = errorMessage(err);
201
+ try {
202
+ const logDir = path.join(phrenPathLocal, ".config");
203
+ fs.mkdirSync(logDir, { recursive: true });
204
+ fs.appendFileSync(path.join(logDir, "background-maintenance.log"), `[${new Date().toISOString()}] spawn failed: ${errMsg}\n`);
205
+ }
206
+ catch (err) {
207
+ logger.debug("backgroundMaintenance logSpawnFailure", errorMessage(err));
208
+ }
209
+ try {
210
+ fs.unlinkSync(markers.lock);
211
+ }
212
+ catch (err) {
213
+ logger.debug("backgroundMaintenance unlockOnFailure", errorMessage(err));
214
+ }
215
+ return false;
216
+ }
217
+ }
218
+ export async function handleHookSessionStart() {
219
+ const startedAt = new Date().toISOString();
220
+ const ctx = buildHookContext();
221
+ const { phrenPath, cwd, activeProject, manifest } = ctx;
222
+ // Check common guards (hooks enabled, tool enabled)
223
+ if (!ctx.hooksEnabled) {
224
+ handleGuardSkip(ctx, "hook_session_start", "disabled", { lastSessionStartAt: startedAt });
225
+ return;
226
+ }
227
+ if (!ctx.toolHookEnabled) {
228
+ handleGuardSkip(ctx, "hook_session_start", `tool_disabled tool=${ctx.hookTool}`);
229
+ return;
230
+ }
231
+ try {
232
+ repairPreexistingInstall(phrenPath);
233
+ }
234
+ catch (err) {
235
+ debugLog(`hook-session-start repair failed: ${errorMessage(err)}`);
236
+ }
237
+ if (!isProjectHookEnabled(phrenPath, activeProject, "SessionStart")) {
238
+ handleGuardSkip(ctx, "hook_session_start", `project_disabled project=${activeProject}`, { lastSessionStartAt: startedAt });
239
+ return;
240
+ }
241
+ if (manifest?.installMode === "project-local") {
242
+ updateRuntimeHealth(phrenPath, {
243
+ lastSessionStartAt: startedAt,
244
+ lastSync: {
245
+ lastPullAt: startedAt,
246
+ lastPullStatus: "ok",
247
+ lastPullDetail: "project-local mode does not manage git sync",
248
+ },
249
+ });
250
+ appendAuditLog(phrenPath, "hook_session_start", "status=skipped-local");
251
+ return;
252
+ }
253
+ const gitRepo = ensureLocalGitRepo(phrenPath);
254
+ const remotes = gitRepo.ok ? await runBestEffortGit(["remote"], phrenPath) : { ok: false, error: gitRepo.detail };
255
+ const hasRemote = Boolean(remotes.ok && remotes.output && remotes.output.trim());
256
+ const pull = !gitRepo.ok
257
+ ? { ok: false, error: gitRepo.detail }
258
+ : hasRemote
259
+ ? await runBestEffortGit(["pull", "--rebase", "--quiet"], phrenPath)
260
+ : {
261
+ ok: true,
262
+ output: gitRepo.initialized
263
+ ? "initialized local git repo; no remote configured"
264
+ : "local-only repo; no remote configured",
265
+ };
266
+ const doctor = await runDoctor(phrenPath, false);
267
+ const maintenanceScheduled = scheduleBackgroundMaintenance(phrenPath);
268
+ const unsyncedCommits = hasRemote ? await countUnsyncedCommits(phrenPath) : 0;
269
+ try {
270
+ const { trackSession } = await import("./telemetry.js");
271
+ trackSession(phrenPath);
272
+ }
273
+ catch (err) {
274
+ logger.debug("hookSessionStart trackSession", errorMessage(err));
275
+ }
276
+ updateRuntimeHealth(phrenPath, {
277
+ lastSessionStartAt: startedAt,
278
+ lastSync: {
279
+ lastPullAt: startedAt,
280
+ lastPullStatus: pull.ok ? "ok" : "error",
281
+ lastPullDetail: pull.ok ? (pull.output || "pull ok") : (pull.error || "pull failed"),
282
+ lastSuccessfulPullAt: pull.ok && hasRemote ? startedAt : undefined,
283
+ unsyncedCommits,
284
+ },
285
+ });
286
+ appendAuditLog(phrenPath, "hook_session_start", `pull=${hasRemote ? (pull.ok ? "ok" : "fail") : "skipped-local"} doctor=${doctor.ok ? "ok" : "issues"} maintenance=${maintenanceScheduled ? "scheduled" : "skipped"}`);
287
+ // Sync intent warning: if the user intended sync but remote is missing or pull failed, warn once
288
+ try {
289
+ const syncPrefs = readInstallPreferences(phrenPath);
290
+ const syncBroken = syncPrefs.syncIntent === "sync" && (!hasRemote || !pull.ok);
291
+ if (syncBroken) {
292
+ const syncWarnPath = sessionMarker(phrenPath, SYNC_WARN_MARKER);
293
+ if (!fs.existsSync(syncWarnPath)) {
294
+ const reason = !hasRemote
295
+ ? "no git remote is connected"
296
+ : `pull failed: ${pull.error || "unknown error"}`;
297
+ process.stdout.write([
298
+ "<phren-notice>",
299
+ `Sync is configured but ${reason}. Your phren data is local-only.`,
300
+ `To fix: cd ${phrenPath} && git remote add origin <YOUR_REPO_URL> && git push -u origin main`,
301
+ "<phren-notice>",
302
+ "",
303
+ ].join("\n"));
304
+ try {
305
+ fs.writeFileSync(syncWarnPath, `${startedAt}\n`);
306
+ }
307
+ catch (err) {
308
+ debugLog(`sync-warn marker write failed: ${errorMessage(err)}`);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ catch (err) {
314
+ debugLog(`sync-intent check failed: ${errorMessage(err)}`);
315
+ }
316
+ // Untracked project detection: suggest `phren add` if CWD looks like a project but isn't tracked
317
+ try {
318
+ const notice = getUntrackedProjectNotice(phrenPath, cwd);
319
+ if (notice) {
320
+ process.stdout.write(notice);
321
+ debugLog(`untracked project detected at ${cwd}`);
322
+ }
323
+ const onboarding = getSessionStartOnboardingNotice(phrenPath, cwd, activeProject);
324
+ if (onboarding) {
325
+ process.stdout.write(onboarding);
326
+ try {
327
+ fs.writeFileSync(sessionMarker(phrenPath, SESSION_START_ONBOARDING_MARKER), `${startedAt}\n`);
328
+ }
329
+ catch (err) {
330
+ debugLog(`session-start onboarding marker write failed: ${errorMessage(err)}`);
331
+ }
332
+ }
333
+ }
334
+ catch (err) {
335
+ debugLog(`session-start onboarding detection failed: ${errorMessage(err)}`);
336
+ }
337
+ }