coder-config 0.42.30 → 0.42.31
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/README.md +3 -0
- package/hooks/session-end.sh +4 -25
- package/hooks/session-start.sh +6 -28
- package/lib/constants.js +1 -1
- package/lib/sessions.js +34 -148
- package/package.json +1 -1
- package/templates/commands/flush.md +5 -3
- package/ui/dist/assets/index-B0EJaqXM.css +32 -0
- package/ui/dist/assets/{index-CeRWlhrc.js → index-BmOt8KXX.js} +89 -86
- package/ui/dist/index.html +2 -2
- package/ui/routes/index.js +2 -0
- package/ui/routes/sessions.js +192 -0
- package/ui/server.cjs +25 -0
- package/ui/dist/assets/index-CHTUXM93.css +0 -32
package/README.md
CHANGED
package/hooks/session-end.sh
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Session End Hook -
|
|
3
|
-
#
|
|
2
|
+
# Session End Hook - Log session info
|
|
3
|
+
# Context is saved by /flush to .claude/session-context.md (project-local)
|
|
4
4
|
|
|
5
5
|
# Read hook input
|
|
6
6
|
INPUT=$(cat)
|
|
7
7
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
8
|
-
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
|
|
9
|
-
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
|
|
10
|
-
REASON=$(echo "$INPUT" | jq -r '.reason // empty')
|
|
11
8
|
|
|
12
9
|
# Skip if no session ID
|
|
13
10
|
[ -z "$SESSION_ID" ] && exit 0
|
|
14
11
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
mkdir -p "$STATE_DIR"
|
|
18
|
-
|
|
19
|
-
# Save session metadata
|
|
20
|
-
cat > "$STATE_DIR/last-session.json" << EOF
|
|
21
|
-
{
|
|
22
|
-
"session_id": "$SESSION_ID",
|
|
23
|
-
"transcript_path": "$TRANSCRIPT_PATH",
|
|
24
|
-
"cwd": "$CWD",
|
|
25
|
-
"reason": "$REASON",
|
|
26
|
-
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
27
|
-
}
|
|
28
|
-
EOF
|
|
29
|
-
|
|
30
|
-
# If there's a flushed context file, preserve it
|
|
31
|
-
FLUSH_FILE="$STATE_DIR/flushed-context.md"
|
|
32
|
-
if [ -f "$FLUSH_FILE" ]; then
|
|
33
|
-
cp "$FLUSH_FILE" "$STATE_DIR/last-flushed-context.md"
|
|
34
|
-
fi
|
|
12
|
+
# Log session end (optional, for debugging)
|
|
13
|
+
# echo "Session $SESSION_ID ended" >> /tmp/claude-sessions.log
|
|
35
14
|
|
|
36
15
|
exit 0
|
package/hooks/session-start.sh
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Session Start Hook - Restore context from previous session
|
|
3
|
-
#
|
|
3
|
+
# Context is stored in .claude/session-context.md (project-local)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
STATE_DIR="$HOME/.coder-config/sessions"
|
|
5
|
+
PROJECT_CONTEXT=".claude/session-context.md"
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Only inject if we have flushed context
|
|
13
|
-
if [ -f "$LAST_CONTEXT" ]; then
|
|
14
|
-
# Check if context is recent (within 24 hours)
|
|
15
|
-
if [ "$(uname)" = "Darwin" ]; then
|
|
16
|
-
# macOS
|
|
17
|
-
FILE_AGE=$(( $(date +%s) - $(stat -f %m "$LAST_CONTEXT") ))
|
|
18
|
-
else
|
|
19
|
-
# Linux
|
|
20
|
-
FILE_AGE=$(( $(date +%s) - $(stat -c %Y "$LAST_CONTEXT") ))
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
# 24 hours = 86400 seconds
|
|
24
|
-
if [ "$FILE_AGE" -lt 86400 ]; then
|
|
25
|
-
# Output context - Claude will see this in the session
|
|
26
|
-
echo "<session-context source=\"previous-session\">"
|
|
27
|
-
echo "The following context was saved from a previous session:"
|
|
28
|
-
echo ""
|
|
29
|
-
cat "$LAST_CONTEXT"
|
|
30
|
-
echo ""
|
|
31
|
-
echo "</session-context>"
|
|
32
|
-
fi
|
|
7
|
+
if [ -f "$PROJECT_CONTEXT" ]; then
|
|
8
|
+
echo "<session-context source=\"project\">"
|
|
9
|
+
cat "$PROJECT_CONTEXT"
|
|
10
|
+
echo "</session-context>"
|
|
33
11
|
fi
|
|
34
12
|
|
|
35
13
|
exit 0
|
package/lib/constants.js
CHANGED
package/lib/sessions.js
CHANGED
|
@@ -1,56 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session persistence management
|
|
3
|
-
*
|
|
3
|
+
* Context is stored in project-local .claude/session-context.md
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
const SESSION_DIR = path.join(os.homedir(), '.coder-config', 'sessions');
|
|
11
|
-
const LAST_SESSION_FILE = path.join(SESSION_DIR, 'last-session.json');
|
|
12
|
-
const FLUSHED_CONTEXT_FILE = path.join(SESSION_DIR, 'flushed-context.md');
|
|
13
|
-
const LAST_FLUSHED_FILE = path.join(SESSION_DIR, 'last-flushed-context.md');
|
|
14
|
-
|
|
15
10
|
/**
|
|
16
|
-
*
|
|
11
|
+
* Get session status for a project
|
|
17
12
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get session status
|
|
26
|
-
*/
|
|
27
|
-
function getSessionStatus() {
|
|
28
|
-
ensureSessionDir();
|
|
13
|
+
function getSessionStatus(projectDir = process.cwd()) {
|
|
14
|
+
const contextFile = path.join(projectDir, '.claude', 'session-context.md');
|
|
29
15
|
|
|
30
16
|
const status = {
|
|
31
17
|
hasSavedContext: false,
|
|
32
|
-
|
|
18
|
+
contextPath: contextFile,
|
|
33
19
|
contextAge: null,
|
|
34
20
|
};
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
if (fs.existsSync(LAST_FLUSHED_FILE)) {
|
|
22
|
+
if (fs.existsSync(contextFile)) {
|
|
38
23
|
status.hasSavedContext = true;
|
|
39
|
-
const stat = fs.statSync(
|
|
24
|
+
const stat = fs.statSync(contextFile);
|
|
40
25
|
status.contextAge = Math.floor((Date.now() - stat.mtimeMs) / 1000 / 60); // minutes
|
|
41
|
-
} else if (fs.existsSync(FLUSHED_CONTEXT_FILE)) {
|
|
42
|
-
status.hasSavedContext = true;
|
|
43
|
-
const stat = fs.statSync(FLUSHED_CONTEXT_FILE);
|
|
44
|
-
status.contextAge = Math.floor((Date.now() - stat.mtimeMs) / 1000 / 60);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check for last session metadata
|
|
48
|
-
if (fs.existsSync(LAST_SESSION_FILE)) {
|
|
49
|
-
try {
|
|
50
|
-
status.lastSession = JSON.parse(fs.readFileSync(LAST_SESSION_FILE, 'utf8'));
|
|
51
|
-
} catch (e) {
|
|
52
|
-
// Ignore parse errors
|
|
53
|
-
}
|
|
54
26
|
}
|
|
55
27
|
|
|
56
28
|
return status;
|
|
@@ -66,74 +38,41 @@ function showSessionStatus() {
|
|
|
66
38
|
|
|
67
39
|
if (status.hasSavedContext) {
|
|
68
40
|
console.log(` Saved context: Yes`);
|
|
41
|
+
console.log(` Location: ${status.contextPath}`);
|
|
69
42
|
if (status.contextAge !== null) {
|
|
70
43
|
if (status.contextAge < 60) {
|
|
71
|
-
console.log(`
|
|
44
|
+
console.log(` Age: ${status.contextAge} minutes`);
|
|
72
45
|
} else if (status.contextAge < 1440) {
|
|
73
|
-
console.log(`
|
|
46
|
+
console.log(` Age: ${Math.floor(status.contextAge / 60)} hours`);
|
|
74
47
|
} else {
|
|
75
|
-
console.log(`
|
|
48
|
+
console.log(` Age: ${Math.floor(status.contextAge / 1440)} days`);
|
|
76
49
|
}
|
|
77
50
|
}
|
|
78
51
|
} else {
|
|
79
52
|
console.log(' Saved context: None');
|
|
53
|
+
console.log(` Expected at: ${status.contextPath}`);
|
|
80
54
|
}
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
console.log(`\n Last session:`);
|
|
84
|
-
console.log(` ID: ${status.lastSession.session_id || 'unknown'}`);
|
|
85
|
-
console.log(` CWD: ${status.lastSession.cwd || 'unknown'}`);
|
|
86
|
-
console.log(` Ended: ${status.lastSession.timestamp || 'unknown'}`);
|
|
87
|
-
console.log(` Reason: ${status.lastSession.reason || 'unknown'}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log(`\n Storage: ${SESSION_DIR}`);
|
|
91
|
-
|
|
92
|
-
if (!status.hasSavedContext) {
|
|
93
|
-
console.log('\nTo save context, use /flush in Claude Code or:');
|
|
94
|
-
console.log(' coder-config session flush');
|
|
95
|
-
}
|
|
56
|
+
console.log('\nTo save context, use /flush in Claude Code.');
|
|
96
57
|
}
|
|
97
58
|
|
|
98
59
|
/**
|
|
99
|
-
* Print instructions for
|
|
100
|
-
* (Actual flush happens in Claude Code via /flush command)
|
|
60
|
+
* Print instructions for flush
|
|
101
61
|
*/
|
|
102
62
|
function flushContext() {
|
|
103
63
|
console.log('Session context flush\n');
|
|
104
|
-
console.log('
|
|
105
|
-
console.log('
|
|
106
|
-
console.log(` ${FLUSHED_CONTEXT_FILE}\n`);
|
|
107
|
-
console.log('The context will be automatically restored on the next session start');
|
|
108
|
-
console.log('(if session hooks are installed).\n');
|
|
109
|
-
console.log('Install hooks with:');
|
|
110
|
-
console.log(' coder-config session install-hooks');
|
|
64
|
+
console.log('Use /flush in Claude Code to save session context.');
|
|
65
|
+
console.log('Context is saved to: .claude/session-context.md');
|
|
111
66
|
}
|
|
112
67
|
|
|
113
68
|
/**
|
|
114
69
|
* Clear saved session context
|
|
115
70
|
*/
|
|
116
|
-
function clearContext() {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let cleared = false;
|
|
71
|
+
function clearContext(projectDir = process.cwd()) {
|
|
72
|
+
const contextFile = path.join(projectDir, '.claude', 'session-context.md');
|
|
120
73
|
|
|
121
|
-
if (fs.existsSync(
|
|
122
|
-
fs.unlinkSync(
|
|
123
|
-
cleared = true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (fs.existsSync(LAST_FLUSHED_FILE)) {
|
|
127
|
-
fs.unlinkSync(LAST_FLUSHED_FILE);
|
|
128
|
-
cleared = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (fs.existsSync(LAST_SESSION_FILE)) {
|
|
132
|
-
fs.unlinkSync(LAST_SESSION_FILE);
|
|
133
|
-
cleared = true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (cleared) {
|
|
74
|
+
if (fs.existsSync(contextFile)) {
|
|
75
|
+
fs.unlinkSync(contextFile);
|
|
137
76
|
console.log('Session context cleared.');
|
|
138
77
|
} else {
|
|
139
78
|
console.log('No session context to clear.');
|
|
@@ -147,12 +86,10 @@ function installHooks() {
|
|
|
147
86
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
148
87
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
149
88
|
|
|
150
|
-
// Ensure .claude directory exists
|
|
151
89
|
if (!fs.existsSync(claudeDir)) {
|
|
152
90
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
153
91
|
}
|
|
154
92
|
|
|
155
|
-
// Load existing settings
|
|
156
93
|
let settings = {};
|
|
157
94
|
if (fs.existsSync(settingsFile)) {
|
|
158
95
|
try {
|
|
@@ -163,31 +100,22 @@ function installHooks() {
|
|
|
163
100
|
}
|
|
164
101
|
}
|
|
165
102
|
|
|
166
|
-
// Initialize hooks if needed
|
|
167
103
|
if (!settings.hooks) {
|
|
168
104
|
settings.hooks = {};
|
|
169
105
|
}
|
|
170
106
|
|
|
171
|
-
// Find coder-config hooks directory
|
|
172
107
|
const hooksSrcDir = path.join(__dirname, '..', 'hooks');
|
|
173
108
|
const sessionStartHook = path.join(hooksSrcDir, 'session-start.sh');
|
|
174
|
-
const sessionEndHook = path.join(hooksSrcDir, 'session-end.sh');
|
|
175
109
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
console.error('Session hooks not found in coder-config package.');
|
|
179
|
-
console.log('Expected locations:');
|
|
180
|
-
console.log(` ${sessionStartHook}`);
|
|
181
|
-
console.log(` ${sessionEndHook}`);
|
|
110
|
+
if (!fs.existsSync(sessionStartHook)) {
|
|
111
|
+
console.error('Session hook not found:', sessionStartHook);
|
|
182
112
|
return;
|
|
183
113
|
}
|
|
184
114
|
|
|
185
|
-
// Make hooks executable
|
|
186
115
|
try {
|
|
187
116
|
fs.chmodSync(sessionStartHook, '755');
|
|
188
|
-
fs.chmodSync(sessionEndHook, '755');
|
|
189
117
|
} catch (e) {
|
|
190
|
-
|
|
118
|
+
// Continue even if chmod fails
|
|
191
119
|
}
|
|
192
120
|
|
|
193
121
|
// Add SessionStart hook
|
|
@@ -198,54 +126,30 @@ function installHooks() {
|
|
|
198
126
|
settings.hooks.SessionStart = [settings.hooks.SessionStart];
|
|
199
127
|
}
|
|
200
128
|
|
|
201
|
-
// Check if our hook is already installed
|
|
202
|
-
const startHookEntry = { type: 'command', command: sessionStartHook };
|
|
203
129
|
const hasStartHook = settings.hooks.SessionStart.some(h =>
|
|
204
130
|
typeof h === 'object' && h.command === sessionStartHook
|
|
205
131
|
);
|
|
206
132
|
if (!hasStartHook) {
|
|
207
|
-
settings.hooks.SessionStart.push(
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Add SessionEnd hook
|
|
211
|
-
if (!settings.hooks.SessionEnd) {
|
|
212
|
-
settings.hooks.SessionEnd = [];
|
|
213
|
-
}
|
|
214
|
-
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
215
|
-
settings.hooks.SessionEnd = [settings.hooks.SessionEnd];
|
|
133
|
+
settings.hooks.SessionStart.push({ type: 'command', command: sessionStartHook });
|
|
216
134
|
}
|
|
217
135
|
|
|
218
|
-
const endHookEntry = { type: 'command', command: sessionEndHook };
|
|
219
|
-
const hasEndHook = settings.hooks.SessionEnd.some(h =>
|
|
220
|
-
typeof h === 'object' && h.command === sessionEndHook
|
|
221
|
-
);
|
|
222
|
-
if (!hasEndHook) {
|
|
223
|
-
settings.hooks.SessionEnd.push(endHookEntry);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Save settings
|
|
227
136
|
try {
|
|
228
137
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
229
|
-
console.log('Session
|
|
230
|
-
console.log('
|
|
231
|
-
console.log('
|
|
232
|
-
console.log(' - SessionEnd: Preserves flushed context\n');
|
|
233
|
-
console.log('To save context before exiting, use /flush in Claude Code.');
|
|
234
|
-
console.log('The saved context will be restored on the next session start.');
|
|
138
|
+
console.log('Session hook installed.\n');
|
|
139
|
+
console.log('SessionStart hook restores context from .claude/session-context.md');
|
|
140
|
+
console.log('Use /flush in Claude Code to save context.');
|
|
235
141
|
} catch (e) {
|
|
236
142
|
console.error('Error writing settings.json:', e.message);
|
|
237
143
|
}
|
|
238
144
|
}
|
|
239
145
|
|
|
240
146
|
/**
|
|
241
|
-
* Get flushed context content
|
|
147
|
+
* Get flushed context content
|
|
242
148
|
*/
|
|
243
|
-
function getFlushedContext() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (fs.existsSync(FLUSHED_CONTEXT_FILE)) {
|
|
248
|
-
return fs.readFileSync(FLUSHED_CONTEXT_FILE, 'utf8');
|
|
149
|
+
function getFlushedContext(projectDir = process.cwd()) {
|
|
150
|
+
const contextFile = path.join(projectDir, '.claude', 'session-context.md');
|
|
151
|
+
if (fs.existsSync(contextFile)) {
|
|
152
|
+
return fs.readFileSync(contextFile, 'utf8');
|
|
249
153
|
}
|
|
250
154
|
return null;
|
|
251
155
|
}
|
|
@@ -258,21 +162,17 @@ function installFlushCommand() {
|
|
|
258
162
|
const commandsDir = path.join(claudeDir, 'commands');
|
|
259
163
|
const destFile = path.join(commandsDir, 'flush.md');
|
|
260
164
|
|
|
261
|
-
// Find the template
|
|
262
165
|
const templateFile = path.join(__dirname, '..', 'templates', 'commands', 'flush.md');
|
|
263
166
|
|
|
264
167
|
if (!fs.existsSync(templateFile)) {
|
|
265
168
|
console.error('Flush command template not found.');
|
|
266
|
-
console.log(`Expected: ${templateFile}`);
|
|
267
169
|
return false;
|
|
268
170
|
}
|
|
269
171
|
|
|
270
|
-
// Ensure commands directory exists
|
|
271
172
|
if (!fs.existsSync(commandsDir)) {
|
|
272
173
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
273
174
|
}
|
|
274
175
|
|
|
275
|
-
// Check if command already exists
|
|
276
176
|
if (fs.existsSync(destFile)) {
|
|
277
177
|
const existing = fs.readFileSync(destFile, 'utf8');
|
|
278
178
|
const template = fs.readFileSync(templateFile, 'utf8');
|
|
@@ -280,15 +180,10 @@ function installFlushCommand() {
|
|
|
280
180
|
console.log('/flush command already installed.');
|
|
281
181
|
return true;
|
|
282
182
|
}
|
|
283
|
-
// Backup existing
|
|
284
|
-
const backupFile = path.join(commandsDir, 'flush.md.bak');
|
|
285
|
-
fs.copyFileSync(destFile, backupFile);
|
|
286
|
-
console.log(`Backed up existing /flush to ${backupFile}`);
|
|
287
183
|
}
|
|
288
184
|
|
|
289
|
-
// Copy template
|
|
290
185
|
fs.copyFileSync(templateFile, destFile);
|
|
291
|
-
console.log('/flush command installed
|
|
186
|
+
console.log('/flush command installed.');
|
|
292
187
|
return true;
|
|
293
188
|
}
|
|
294
189
|
|
|
@@ -297,17 +192,10 @@ function installFlushCommand() {
|
|
|
297
192
|
*/
|
|
298
193
|
function installAll() {
|
|
299
194
|
console.log('Installing session persistence...\n');
|
|
300
|
-
|
|
301
|
-
// Install hooks
|
|
302
195
|
installHooks();
|
|
303
|
-
|
|
304
196
|
console.log('');
|
|
305
|
-
|
|
306
|
-
// Install command
|
|
307
197
|
installFlushCommand();
|
|
308
|
-
|
|
309
|
-
console.log('\nSession persistence setup complete!');
|
|
310
|
-
console.log('Use /flush in Claude Code to save context before exiting.');
|
|
198
|
+
console.log('\nUse /flush in Claude Code to save context before exiting.');
|
|
311
199
|
}
|
|
312
200
|
|
|
313
201
|
module.exports = {
|
|
@@ -319,6 +207,4 @@ module.exports = {
|
|
|
319
207
|
getFlushedContext,
|
|
320
208
|
installFlushCommand,
|
|
321
209
|
installAll,
|
|
322
|
-
SESSION_DIR,
|
|
323
|
-
FLUSHED_CONTEXT_FILE,
|
|
324
210
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coder-config",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.31",
|
|
4
4
|
"description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
|
|
5
5
|
"author": "regression.io",
|
|
6
6
|
"main": "config-loader.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Flush Context to Resumable Doc
|
|
2
2
|
|
|
3
|
-
Save all current session context to a resumable document
|
|
3
|
+
Save all current session context to a resumable document in the current project's `.claude/` directory.
|
|
4
4
|
|
|
5
5
|
## Instructions
|
|
6
6
|
|
|
@@ -12,9 +12,11 @@ Save all current session context to a resumable document that will be automatica
|
|
|
12
12
|
- **Pending Work**: What still needs to be done
|
|
13
13
|
- **Important Context**: Any critical information needed to continue
|
|
14
14
|
|
|
15
|
-
2.
|
|
15
|
+
2. Determine the project root (where `.git/` or `.claude/` exists) and write this summary to: `<project-root>/.claude/session-context.md`
|
|
16
|
+
- If `.claude/` directory doesn't exist, create it
|
|
17
|
+
- This keeps the context local to this specific project
|
|
16
18
|
|
|
17
|
-
3. Confirm to the user that context has been saved and will be
|
|
19
|
+
3. Confirm to the user that context has been saved to the project directory and will be available on the next session in this project.
|
|
18
20
|
|
|
19
21
|
## Output Format
|
|
20
22
|
|