edsger 0.71.0 → 0.72.0

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/index.js CHANGED
@@ -58,6 +58,7 @@ import { runWorkflow } from './commands/workflow/index.js';
58
58
  import { DEFAULT_MAX_FILES as FIND_ARCHITECTURE_DEFAULT_MAX_FILES } from './phases/find-architecture/index.js';
59
59
  import { DEFAULT_MAX_FILES as FIND_SMELLS_DEFAULT_MAX_FILES } from './phases/find-smells/index.js';
60
60
  import { SMELL_CATEGORIES, } from './phases/find-smells/types.js';
61
+ import { deregisterSession, registerSession, } from './system/session-manager.js';
61
62
  import { logError, logInfo } from './utils/logger.js';
62
63
  // Get package.json version dynamically
63
64
  // eslint-disable-next-line @typescript-eslint/naming-convention -- ESM __filename/__dirname polyfill
@@ -77,6 +78,24 @@ program
77
78
  .name('edsger')
78
79
  .description('AI-powered workflow automation and code review CLI tool')
79
80
  .version(version);
81
+ // When the desktop app spawns a streamed CLI it sets EDSGER_PROCESS_KEY (its
82
+ // process-registry key with the `:output` marker stripped, e.g.
83
+ // `sync-terraform` or `recipes:<productId>`). Open a cli_sessions row whose
84
+ // `command` is that key so the originating tab can find the still-running
85
+ // session and replay its logs across tab switches / restarts / devices.
86
+ // registerSession is idempotent: commands that open their own (richer) session
87
+ // just enrich the row started here (the process key stays in `command`), and
88
+ // deregisterSession is a no-op once they've closed it.
89
+ program.hook('preAction', async (_thisCommand, actionCommand) => {
90
+ if (process.env.EDSGER_PROCESS_KEY) {
91
+ await registerSession({ command: actionCommand.name() });
92
+ }
93
+ });
94
+ program.hook('postAction', async () => {
95
+ if (process.env.EDSGER_PROCESS_KEY) {
96
+ await deregisterSession();
97
+ }
98
+ });
80
99
  // ============================================================
81
100
  // Subcommand: edsger login
82
101
  // ============================================================
@@ -20,6 +20,14 @@ export interface CliSession {
20
20
  /**
21
21
  * Register this CLI session with the server.
22
22
  * Optionally accepts command name and product ID for filtering on detail pages.
23
+ *
24
+ * Idempotent: if a session is already active (e.g. registered by the top-level
25
+ * command hook), this enriches that row with the command/product instead of
26
+ * creating a second session, and returns the existing id.
27
+ *
28
+ * The stored `command` is the desktop process key when the CLI was spawned by
29
+ * the app (EDSGER_PROCESS_KEY), so a feature/tab can find this session; see
30
+ * `resolvedCommand`.
23
31
  */
24
32
  export declare function registerSession(options?: {
25
33
  command?: string;
@@ -25,13 +25,39 @@ function generateSessionId() {
25
25
  const random = Math.random().toString(36).substring(2, 8);
26
26
  return `cli-${timestamp}-${random}`;
27
27
  }
28
+ /**
29
+ * The value stored in `cli_sessions.command`.
30
+ *
31
+ * When the desktop app spawns a streamed CLI it injects EDSGER_PROCESS_KEY --
32
+ * its process-registry key (`:output` channel marker already stripped), e.g.
33
+ * `sync-terraform`, `recipes:<productId>`, `pr-review:<prId>`. That key is what
34
+ * the originating tab looks up by, so it takes precedence over the bare command
35
+ * name; standalone runs fall back to the supplied command name.
36
+ */
37
+ function resolvedCommand(options) {
38
+ return process.env.EDSGER_PROCESS_KEY || options?.command;
39
+ }
28
40
  /**
29
41
  * Register this CLI session with the server.
30
42
  * Optionally accepts command name and product ID for filtering on detail pages.
43
+ *
44
+ * Idempotent: if a session is already active (e.g. registered by the top-level
45
+ * command hook), this enriches that row with the command/product instead of
46
+ * creating a second session, and returns the existing id.
47
+ *
48
+ * The stored `command` is the desktop process key when the CLI was spawned by
49
+ * the app (EDSGER_PROCESS_KEY), so a feature/tab can find this session; see
50
+ * `resolvedCommand`.
31
51
  */
32
52
  export async function registerSession(options) {
53
+ // Already in a session for this process — enrich, don't double-register.
54
+ if (currentSessionId) {
55
+ await enrichSession(options);
56
+ return currentSessionId;
57
+ }
33
58
  const sessionId = generateSessionId();
34
59
  currentSessionId = sessionId;
60
+ const command = resolvedCommand(options);
35
61
  try {
36
62
  const userId = getUserId();
37
63
  if (hasSupabaseSession() && userId) {
@@ -44,8 +70,8 @@ export async function registerSession(options) {
44
70
  started_at: new Date().toISOString(),
45
71
  last_heartbeat: new Date().toISOString(),
46
72
  };
47
- if (options?.command) {
48
- row.command = options.command;
73
+ if (command) {
74
+ row.command = command;
49
75
  }
50
76
  if (options?.productId) {
51
77
  row.product_id = options.productId;
@@ -64,8 +90,8 @@ export async function registerSession(options) {
64
90
  version: getVersion(),
65
91
  status: 'running',
66
92
  };
67
- if (options?.command) {
68
- payload.command = options.command;
93
+ if (command) {
94
+ payload.command = command;
69
95
  }
70
96
  if (options?.productId) {
71
97
  payload.product_id = options.productId;
@@ -82,6 +108,41 @@ export async function registerSession(options) {
82
108
  initLogSync(sessionId);
83
109
  return sessionId;
84
110
  }
111
+ /**
112
+ * Best-effort enrichment of the already-active session row with command /
113
+ * product info (e.g. when a command registers itself after the top-level hook
114
+ * already opened the session). Direct-SDK path only; the MCP heartbeat path
115
+ * keeps whatever the initial register supplied.
116
+ */
117
+ async function enrichSession(options) {
118
+ const command = resolvedCommand(options);
119
+ if (!currentSessionId || (!command && !options?.productId)) {
120
+ return;
121
+ }
122
+ try {
123
+ const userId = getUserId();
124
+ if (!hasSupabaseSession() || !userId) {
125
+ return;
126
+ }
127
+ const patch = {};
128
+ // With a desktop process key present, `command` resolves to that key, so
129
+ // a self-registering command can't clobber it with its bare name.
130
+ if (command) {
131
+ patch.command = command;
132
+ }
133
+ if (options?.productId) {
134
+ patch.product_id = options.productId;
135
+ }
136
+ await getSupabase()
137
+ .from('cli_sessions')
138
+ .update(patch)
139
+ .eq('session_id', currentSessionId)
140
+ .eq('user_id', userId);
141
+ }
142
+ catch {
143
+ // best-effort
144
+ }
145
+ }
85
146
  /**
86
147
  * Send a heartbeat to keep the session alive and check for commands
87
148
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"