feat: add categories and tasks commands#7
Conversation
WalkthroughReplaces Supabase wiring with a Listee API–first CLI: adds env validation, an API base/client, category/task service modules and CLI commands, refactors auth to use AuthToken shapes and authenticated contexts, updates docs and package deps, and wires new commands into startup. (50 words) Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as "CLI Program"
participant Env as "Env Validator"
participant Auth as "Auth Service"
participant API as "API Client"
participant Listee as "Listee API"
participant Out as "CLI Output"
User->>CLI: run "categories list --email user@example.com"
CLI->>Env: checkEnv()
Env-->>CLI: env validated
CLI->>Auth: getAuthenticatedAccessToken(email)
Auth->>Listee: POST /auth (returns AuthTokenResponse)
Listee-->>Auth: access token (JWT)
Auth-->>API: createAuthenticatedContext(authorizationValue)
API->>Listee: GET /users/{userId}/categories (Bearer ...)
Listee-->>API: 200 + JSON payload
API-->>CLI: parsed categories
CLI->>Out: printCategories (list + pagination)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (8)
src/commands/auth.ts (2)
37-40: Exclude arrays in isRecord.Arrays pass this check but aren’t desired as generic records. Add
!Array.isArray(value).-const isRecord = (value: unknown): value is Record<string, unknown> => { - return typeof value === "object" && value !== null; -}; +const isRecord = (value: unknown): value is Record<string, unknown> => { + return typeof value === "object" && value !== null && !Array.isArray(value); +};
366-368: Use ASCII-only logs for terminal compatibility.Replace emoji with ASCII to meet the “ASCII unless already used” guideline and improve portability.
- console.log( - "📩 Confirmation email sent. Keep this terminal open while you click the link.", - ); + console.log( + "Confirmation email sent. Keep this terminal open while you click the link.", + );As per coding guidelines.
src/commands/tasks.ts (1)
8-29: DRY shared CLI helpers across commands.
executeandensureNonEmptyStringduplicate logic in categories.ts. Extract intosrc/commands/cli-utils.tsand import in both files.+// src/commands/cli-utils.ts +export const execute = <T extends unknown[]>(task: (...args: T) => Promise<void>) => { + return async (...args: T): Promise<void> => { + try { + await task(...args); + } catch (error) { + if (error instanceof Error) console.error(`Error: ${error.message}`); + else console.error("Unknown error occurred."); + process.exitCode = 1; + } + }; +}; + +export const ensureNonEmptyString = (value: string, label: string): string => { + const trimmed = value.trim(); + if (trimmed.length === 0) throw new Error(`${label} must not be empty.`); + return trimmed; +};-const execute = <T extends unknown[]>(task: (...args: T) => Promise<void>) => { ... } -const ensureNonEmptyString = (value: string, label: string): string => { ... } +import { execute, ensureNonEmptyString } from "./cli-utils.js";Apply similarly in
src/commands/categories.ts. As per coding guidelines (module boundaries, avoid duplication).Also applies to: 75-151
src/commands/categories.ts (1)
8-17: Consolidate common helpers with tasks CLI.Move
ensurePositiveIntegerandensureNonEmptyString(and theexecutewrapper) to a sharedsrc/commands/cli-utils.tsto reduce duplication.Also applies to: 19-40
src/services/category-api.ts (3)
21-23: Extract shared isRecord helper to avoid duplication.The
isRecordhelper is duplicated acrosssrc/services/category-api.ts,src/services/task-api.ts, andsrc/services/api-client.ts. Consider extracting it to a shared utility module (e.g.,src/services/type-guards.ts) to follow DRY principles.Example:
// src/services/type-guards.ts export const isRecord = (value: unknown): value is Record<string, unknown> => { return typeof value === "object" && value !== null; };Then import from the shared location:
+import { isRecord } from "./type-guards.js"; + -const isRecord = (value: unknown): value is Record<string, unknown> => { - return typeof value === "object" && value !== null; -};
41-54: Consider simplifying toCategory.After
isCategoryvalidation, the explicit field-by-field mapping is redundant. You could return the value directly for conciseness:const toCategory = (value: unknown): Category => { if (!isCategory(value)) { throw new Error("API response did not include valid category data."); } - return { - id: value.id, - name: value.name, - kind: value.kind, - createdBy: value.createdBy, - updatedBy: value.updatedBy, - createdAt: value.createdAt, - updatedAt: value.updatedAt, - }; + return value; };However, explicit mapping does provide clarity and can be retained if preferred.
63-93: Consider simplifying nextCursor validation.The nextCursor validation is correct but somewhat indirect. Consider a more straightforward approach:
- const nextCursorValue = - metaValue.nextCursor === null || typeof metaValue.nextCursor === "string" - ? metaValue.nextCursor - : undefined; - if (metaValue.hasMore !== true && metaValue.hasMore !== false) { - throw new Error("List categories response has invalid hasMore flag."); - } - - if (nextCursorValue === undefined) { - throw new Error("List categories response has invalid cursor value."); - } + if (metaValue.nextCursor !== null && typeof metaValue.nextCursor !== "string") { + throw new Error("List categories response has invalid cursor value."); + } + if (typeof metaValue.hasMore !== "boolean") { + throw new Error("List categories response has invalid hasMore flag."); + } return { data, meta: { - nextCursor: nextCursorValue, + nextCursor: metaValue.nextCursor, hasMore: metaValue.hasMore, }, };src/services/task-api.ts (1)
17-19: Extract shared isRecord helper to avoid duplication.The
isRecordhelper is duplicated here as well. As noted in the review ofsrc/services/category-api.ts, consider extracting this to a shared utility module.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.env.example(1 hunks)README.md(3 hunks)src/commands/auth.ts(6 hunks)src/commands/categories.ts(1 hunks)src/commands/tasks.ts(1 hunks)src/index.ts(2 hunks)src/services/api-client.ts(1 hunks)src/services/category-api.ts(1 hunks)src/services/task-api.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/services/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
src/services/**/*.ts: Place Supabase-facing logic under src/services/
Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Files:
src/services/api-client.tssrc/services/category-api.tssrc/services/task-api.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript strict practices: forbid any, forbid as assertions, no implicit any; implement type guards for narrowing
Use two-space indentation and LF line endings
Use camelCase for identifiers
Keep comments purposeful; add brief context only for non-trivial flows
Maintain ASCII in files unless the file already uses Unicode
Files:
src/services/api-client.tssrc/index.tssrc/commands/categories.tssrc/services/category-api.tssrc/services/task-api.tssrc/commands/auth.tssrc/commands/tasks.ts
src/**/[a-z0-9-]*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use kebab-case filenames for modules under src/
Files:
src/services/api-client.tssrc/index.tssrc/commands/categories.tssrc/services/category-api.tssrc/services/task-api.tssrc/commands/auth.tssrc/commands/tasks.ts
src/index.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Files:
src/index.ts
src/commands/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place CLI command handlers under src/commands/
Files:
src/commands/categories.tssrc/commands/auth.tssrc/commands/tasks.ts
🧠 Learnings (3)
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
PR: listee-dev/listee-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Applied to files:
README.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
PR: listee-dev/listee-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/commands/**/*.ts : Place CLI command handlers under src/commands/
Applied to files:
README.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
PR: listee-dev/listee-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/index.ts : Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Applied to files:
README.md
🧬 Code graph analysis (6)
src/services/api-client.ts (1)
src/services/auth-service.ts (1)
getAccessToken(423-456)
src/index.ts (2)
src/commands/categories.ts (1)
registerCategoryCommand(78-158)src/commands/tasks.ts (1)
registerTaskCommand(75-152)
src/commands/categories.ts (1)
src/services/category-api.ts (3)
listCategories(112-128)getCategory(135-142)createCategory(150-167)
src/services/category-api.ts (1)
src/services/api-client.ts (2)
createAuthenticatedContext(100-118)requestJson(148-169)
src/services/task-api.ts (1)
src/services/api-client.ts (2)
createAuthenticatedContext(100-118)requestJson(148-169)
src/commands/tasks.ts (1)
src/services/task-api.ts (3)
listTasksByCategory(83-90)getTask(97-104)createTask(114-134)
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 3-3: [UnorderedKey] The LISTEE_API_URL key should go before the SUPABASE_PUBLISHABLE_KEY key
(UnorderedKey)
🔇 Additional comments (32)
src/commands/auth.ts (1)
145-149: Good hardening of JSON body parsing.Validating shape post-parse before accessing fields prevents runtime surprises. LGTM.
src/index.ts (1)
17-18: CLI wiring looks correct.Category and task commands are properly registered before parse. No issues.
src/commands/categories.ts (1)
83-116: List flow and pagination messaging look good.Validation + cursor handling are clear and correct. LGTM.
.env.example (1)
3-5: I need to inspect the actual implementation to confirm whether both keys are intentionally used:Let me inspect the full .env.example file and check the context around the added lines:
Based on my verification using the grep results and Supabase documentation, I need to assess whether the original review comment is accurate.
The script confirmed that:
.env.exampleline 2 containsSUPABASE_PUBLISHABLE_KEYauth-service.tsline 108 readsSUPABASE_PUBLISHABLE_KEYauth-service.tsline 113 readsSUPABASE_ANON_KEYas a legacy fallback- The error message at line 119 explicitly states
SUPABASE_PUBLISHABLE_KEY is not setA single publishable key can be used to replace the anon key, and in the transition period, you can use both the current anon and service_role keys and the new publishable key. The code is correctly using the modern
SUPABASE_PUBLISHABLE_KEYformat.The original review comment claims that
.env.exampleusingSUPABASE_PUBLISHABLE_KEY"will break auth setup" and that the code expectsSUPABASE_ANON_KEY. However, this is incorrect—the code explicitly reads and expectsSUPABASE_PUBLISHABLE_KEY. The README mentioningSUPABASE_ANON_KEYreflects legacy documentation, but the.env.exampleis aligned with the current code implementation.
Likely an incorrect or invalid review comment.
src/services/category-api.ts (7)
1-7: LGTM!The imports and type definition are clean. The explicit remapping of
createdAtandupdatedAtfrom database types to string representation is appropriate for API serialization.
25-39: LGTM!The type guard comprehensively validates all required Category fields with appropriate type checks.
56-61: LGTM!Array validation and transformation logic is correct and follows the established pattern.
95-102: LGTM!The category detail response parser is clean and follows the established validation pattern.
112-128: LGTM!The listCategories implementation correctly handles optional pagination parameters, builds authenticated requests, and transforms responses with appropriate validation.
135-142: LGTM!The getCategory implementation is straightforward and correctly handles authentication and response transformation.
150-167: LGTM!The createCategory implementation correctly defaults
kindto "user", sets appropriate headers, and transforms the response. The POST request is well-structured.src/services/api-client.ts (12)
1-2: LGTM!Imports are appropriate for the module's functionality.
15-17: LGTM!The non-empty string validation correctly trims whitespace and checks length.
23-31: LGTM!Environment variable validation follows the coding guidelines to read from environment variables with clear error messages when missing.
As per coding guidelines.
33-36: LGTM!The URL construction from environment variable is straightforward and will throw on invalid URLs.
38-69: JWT decoding is correct for trusted tokens.The implementation correctly decodes JWT claims without signature verification. This is acceptable since tokens come from
getAccessToken(which obtains them from a trusted auth service), but ensure tokens are always obtained through authenticated flows.
71-82: LGTM!JSON parsing with empty response handling and error wrapping is well-implemented.
84-98: LGTM!Authorization mode configuration with sensible defaults and strict validation is well-implemented.
100-118: LGTM!The authenticated context creation correctly handles token retrieval, claims extraction, and mode-based authorization value selection.
120-125: LGTM!Standard Bearer authentication and JSON Accept headers are correctly formatted.
127-135: LGTM!URL construction with proper path validation and base URL normalization correctly handles various URL formats.
137-146: LGTM!Error message extraction with fallback handling is defensive and correct.
148-169: LGTM!The HTTP request wrapper correctly merges headers, handles errors with meaningful messages, and provides a clean interface for authenticated API calls.
src/services/task-api.ts (9)
1-7: LGTM!The Task type definition follows the same pattern as Category, consistently remapping timestamp fields to strings.
9-15: LGTM!Response types are clean. Note that
TaskListResponsedoes not include pagination metadata, unlikeListCategoriesResponse. This is acceptable if task lists per category are expected to be small, but consider adding pagination if lists could grow large.
21-37: LGTM!The type guard comprehensively validates all Task fields, correctly handling the nullable
descriptionfield and booleanisCheckedfield.
39-54: LGTM!The toTask converter follows the same explicit mapping pattern as
toCategory, maintaining consistency across the codebase.
56-67: LGTM!Task list response parsing is correct and follows the established pattern.
69-76: LGTM!Task detail response parsing is clean and consistent with the category pattern.
83-90: LGTM!The listTasksByCategory implementation correctly scopes tasks to a category with proper authentication and response transformation.
97-104: LGTM!The getTask implementation is straightforward and correct.
114-134: LGTM!The createTask implementation correctly handles optional fields (
descriptiondefaults to null,isCheckedis conditionally included) and constructs a well-formed POST request.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/index.ts (1)
3-20: Catch environment validation errors inside the existing CLI error handler
checkEnv()をトップレベルで呼び出すと、環境変数が欠けている場合に Commander 側のtry/catchに到達する前にモジュールロードで例外が投げられ、スタックトレース付きで CLI が終了してしまいます (listee --helpのような軽量コマンドでも同様)。mainの中で既存のエラーハンドリングに包んで実行すれば、整形済みメッセージと exit code だけを返せるので UX が大きく改善します。-import { registerCategoryCommand } from "./commands/categories.js"; -import { registerTaskCommand } from "./commands/tasks.js"; -import { checkEnv } from "./env.js"; - -checkEnv(); +import { registerCategoryCommand } from "./commands/categories.js"; +import { registerTaskCommand } from "./commands/tasks.js"; +import { checkEnv } from "./env.js"; const program = new Command(); ... const main = async (): Promise<void> => { try { + checkEnv(); await program.parseAsync(process.argv); } catch (error) {
🧹 Nitpick comments (7)
.env.example (1)
1-4: Maintain .env key ordering for lint compliance
.env.exampleのキー順がdotenv-linterの警告対象になっているので、LISTEE_API_URLをSUPABASE_PUBLISHABLE_KEYより前に移動しておきましょう。サンプルを辞書順で揃えておけば、開発者がそのままコピーしても検証ツールでノイズが出ません。SUPABASE_URL= -SUPABASE_PUBLISHABLE_KEY= -LISTEE_API_URL= +LISTEE_API_URL= +SUPABASE_PUBLISHABLE_KEY= # LISTEE_CLI_KEYCHAIN_SERVICE=listee-clisrc/services/api-client.ts (3)
10-12: Consider refining the type guard to exclude arrays.The
isRecordtype guard currently accepts arrays sincetypeof [] === "object". While this doesn't cause immediate issues in the current usage (extracting error messages), it's less precise than necessary.Apply this diff to exclude arrays:
const isRecord = (value: unknown): value is Record<string, unknown> => { - return typeof value === "object" && value !== null; + return typeof value === "object" && value !== null && !Array.isArray(value); };
60-72: Consider simplifying the return type.The
authorizationValuefield is set to the same value asaccessToken, which appears redundant. If the fields serve the same purpose, consider removing one to simplify the context object.If
authorizationValueis intended for future flexibility (e.g., supporting different token formats), the current implementation is fine. Otherwise:type AuthenticatedContext = { readonly accessToken: string; readonly userId: string; - readonly authorizationValue: string; };And update callers to use
accessTokendirectly.
117-144: Consider adding timeout handling for fetch requests.The
fetchcall doesn't specify a timeout, which could lead to indefinitely hanging requests if the API becomes unresponsive. For better user experience in a CLI context, consider adding timeout handling.Example using AbortSignal with timeout:
export const requestJson = async ( path: string, authorizationValue: string, init?: RequestInit, timeoutMs = 30000, ): Promise<unknown> => { const url = buildUrl(path); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...init, signal: controller.signal, headers: { ...buildHeaders(authorizationValue), ...(init?.headers ?? {}), }, }); // ... rest of the function } finally { clearTimeout(timeoutId); } };src/services/category-api.ts (3)
112-128: Consider validating the limit parameter.The
limitparameter is passed to the API without client-side validation. Adding basic validation (e.g., positive integer) would improve user experience by catching invalid input before making the API call.Example validation:
export const listCategories = async ( params: ListCategoriesParams = {}, ): Promise<ListCategoriesResult> => { if (params.limit !== undefined && params.limit <= 0) { throw new Error("limit must be a positive number."); } // ... rest of function };
150-167: Consider validating the name parameter.The
nameparameter is not validated before sending to the API. Adding basic validation (e.g., non-empty, not just whitespace) would provide better user feedback.Example validation:
export const createCategory = async ( params: CreateCategoryParams, ): Promise<Category> => { if (params.name.trim().length === 0) { throw new Error("Category name cannot be empty."); } // ... rest of function };
175-193: Consider validating the name parameter.While the function correctly validates that at least one field is provided, it doesn't validate the
namevalue itself. Adding validation for empty or whitespace-only names would improve error handling.Example validation:
export const updateCategory = async ( params: UpdateCategoryParams, ): Promise<Category> => { const context = await createAuthenticatedContext(params.email); const path = `/categories/${encodeURIComponent(params.categoryId)}`; if (params.name === undefined) { throw new Error("No category fields were provided for update."); } if (params.name.trim().length === 0) { throw new Error("Category name cannot be empty."); } // ... rest of function };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
.env.example(1 hunks)AGENTS.md(1 hunks)README.md(4 hunks)package.json(1 hunks)src/commands/categories.ts(1 hunks)src/commands/tasks.ts(1 hunks)src/env.ts(1 hunks)src/index.ts(2 hunks)src/services/api-client.ts(1 hunks)src/services/auth-service.test.ts(3 hunks)src/services/auth-service.ts(3 hunks)src/services/category-api.ts(1 hunks)src/services/task-api.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/services/task-api.ts
🧰 Additional context used
📓 Path-based instructions (5)
src/services/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
src/services/**/*.ts: Place Supabase-facing logic under src/services/
Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Files:
src/services/auth-service.test.tssrc/services/category-api.tssrc/services/api-client.tssrc/services/auth-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript strict practices: forbid any, forbid as assertions, no implicit any; implement type guards for narrowing
Use two-space indentation and LF line endings
Use camelCase for identifiers
Keep comments purposeful; add brief context only for non-trivial flows
Maintain ASCII in files unless the file already uses Unicode
Files:
src/services/auth-service.test.tssrc/services/category-api.tssrc/commands/tasks.tssrc/services/api-client.tssrc/env.tssrc/index.tssrc/services/auth-service.tssrc/commands/categories.ts
src/**/[a-z0-9-]*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use kebab-case filenames for modules under src/
Files:
src/services/auth-service.test.tssrc/services/category-api.tssrc/commands/tasks.tssrc/services/api-client.tssrc/env.tssrc/index.tssrc/services/auth-service.tssrc/commands/categories.ts
src/commands/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place CLI command handlers under src/commands/
Files:
src/commands/tasks.tssrc/commands/categories.ts
src/index.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Files:
src/index.ts
🧠 Learnings (7)
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Applied to files:
src/services/auth-service.test.tsREADME.mdAGENTS.mdsrc/env.tssrc/services/auth-service.ts.env.example
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Use Bun 1.2.22 and the provided Bun commands; do not switch package managers
Applied to files:
README.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Applied to files:
README.mdAGENTS.mdsrc/services/auth-service.ts.env.example
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/commands/**/*.ts : Place CLI command handlers under src/commands/
Applied to files:
README.mdsrc/commands/tasks.tssrc/index.tssrc/commands/categories.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/index.ts : Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Applied to files:
README.mdsrc/commands/tasks.tssrc/index.tssrc/commands/categories.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to **/*.{ts,tsx} : Keep comments purposeful; add brief context only for non-trivial flows
Applied to files:
AGENTS.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Place Supabase-facing logic under src/services/
Applied to files:
src/services/auth-service.ts
🧬 Code graph analysis (7)
src/services/auth-service.test.ts (3)
src/env.ts (1)
resetEnvCache(85-87)src/types/auth.ts (1)
AccessTokenResult(21-25)src/services/auth-service.ts (1)
toAuthenticatedAccessTokenResult(490-507)
src/services/category-api.ts (1)
src/services/api-client.ts (2)
createAuthenticatedContext(60-72)requestJson(117-144)
src/commands/tasks.ts (1)
src/services/task-api.ts (5)
listTasksByCategory(83-90)getTask(97-104)createTask(114-134)updateTask(144-168)deleteTask(175-181)
src/services/api-client.ts (2)
src/env.ts (2)
getEnv(81-83)EnvValidationError(10-17)src/services/auth-service.ts (1)
getAuthenticatedAccessToken(509-514)
src/index.ts (3)
src/env.ts (1)
checkEnv(107-121)src/commands/categories.ts (1)
registerCategoryCommand(80-208)src/commands/tasks.ts (1)
registerTaskCommand(77-245)
src/services/auth-service.ts (2)
src/env.ts (3)
getEnv(81-83)EnvValidationError(10-17)checkEnv(107-121)src/types/auth.ts (1)
AccessTokenResult(21-25)
src/commands/categories.ts (1)
src/services/category-api.ts (5)
listCategories(112-128)getCategory(135-142)createCategory(150-167)updateCategory(175-193)deleteCategory(200-208)
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 3-3: [UnorderedKey] The LISTEE_API_URL key should go before the SUPABASE_PUBLISHABLE_KEY key
(UnorderedKey)
🔇 Additional comments (10)
src/services/api-client.ts (3)
14-34: LGTM!The error handling provides clear, user-friendly messages when
LISTEE_API_URLis missing or invalid, and properly re-throws unrelated errors.
41-58: LGTM!The payload parsing logic correctly handles JSON, text, and empty responses with robust error handling for JSON parse failures.
91-115: LGTM!The error extraction logic properly handles JSON, text, and empty payloads with sensible fallbacks and response truncation for large text bodies.
src/services/category-api.ts (7)
1-7: LGTM!The
Categorytype correctly transforms the database type for API usage by converting timestamp fields to strings.
9-19: LGTM!Response types are well-structured with proper readonly modifiers and clear pagination metadata.
25-39: LGTM!The
isCategorytype guard thoroughly validates all required category fields with proper type checks.
41-61: LGTM!The conversion functions properly validate and transform API responses with clear error messages.
63-102: LGTM!Response parsing functions include thorough validation with descriptive error messages for malformed API responses.
130-142: LGTM!The
getCategoryfunction properly encodes the category ID and handles the response.
195-208: LGTM!The
deleteCategoryfunction properly implements the DELETE operation with correct return type and URL encoding.
src/services/api-client.ts
Outdated
| const buildUrl = (path: string): URL => { | ||
| if (!path.startsWith("/")) { | ||
| throw new Error("API path must start with '/' ."); | ||
| } | ||
| const base = getApiBaseUrl(); | ||
| const baseHref = base.href.endsWith("/") ? base.href : `${base.href}/`; | ||
| const normalizedPath = path.replace(/^\//u, ""); | ||
| return new URL(normalizedPath, baseHref); | ||
| }; |
There was a problem hiding this comment.
Minor typo in error message.
Line 83 has a space before the period in the error message.
Apply this diff:
if (!path.startsWith("/")) {
- throw new Error("API path must start with '/' .");
+ throw new Error("API path must start with '/'.");
}🤖 Prompt for AI Agents
In src/services/api-client.ts around lines 81 to 89, the thrown Error message on
line 83 contains an extra space before the period ("API path must start with '/'
."); remove the stray space so the message reads "API path must start with '/'."
to fix the minor typo.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/services/api-base.ts (1)
53-63: Handle+jsonmedia types as JSON.Many APIs (including RFC 7807 problem details) return payloads with vendor-specific
application/*+jsontypes. Right now those responses fall through to the text branch, losing structured error details. Consider recognising+jsonsuffixes alongsideapplication/jsonso we still parse them as JSON.- if (contentType.toLowerCase().includes("application/json")) { + const normalizedContentType = contentType.toLowerCase(); + if ( + normalizedContentType.includes("application/json") || + normalizedContentType.includes("+json") + ) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
.env.example(1 hunks)AGENTS.md(2 hunks)README.md(4 hunks)src/commands/auth.ts(8 hunks)src/env.ts(1 hunks)src/services/api-base.ts(1 hunks)src/services/api-client.ts(1 hunks)src/services/auth-service.test.ts(2 hunks)src/services/auth-service.ts(9 hunks)src/types/auth.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- .env.example
- src/env.ts
- README.md
- src/services/api-client.ts
- src/commands/auth.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/services/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
src/services/**/*.ts: Place Supabase-facing logic under src/services/
Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Files:
src/services/auth-service.tssrc/services/api-base.tssrc/services/auth-service.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript strict practices: forbid any, forbid as assertions, no implicit any; implement type guards for narrowing
Use two-space indentation and LF line endings
Use camelCase for identifiers
Keep comments purposeful; add brief context only for non-trivial flows
Maintain ASCII in files unless the file already uses Unicode
Files:
src/services/auth-service.tssrc/types/auth.tssrc/services/api-base.tssrc/services/auth-service.test.ts
src/**/[a-z0-9-]*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use kebab-case filenames for modules under src/
Files:
src/services/auth-service.tssrc/types/auth.tssrc/services/api-base.tssrc/services/auth-service.test.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Place Supabase-facing logic under src/services/
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Applied to files:
src/services/auth-service.tssrc/types/auth.tssrc/services/api-base.tsAGENTS.mdsrc/services/auth-service.test.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Place Supabase-facing logic under src/services/
Applied to files:
src/services/auth-service.tsAGENTS.mdsrc/services/auth-service.test.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Follow Listee org conventions from sibling repos and keep changes minimal and well-justified
Applied to files:
AGENTS.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/index.ts : Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Applied to files:
AGENTS.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/commands/**/*.ts : Place CLI command handlers under src/commands/
Applied to files:
AGENTS.md
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Applied to files:
AGENTS.md
🧬 Code graph analysis (3)
src/services/auth-service.ts (3)
src/types/auth.ts (2)
AuthTokenResponse(3-8)AccessTokenResult(15-19)src/env.ts (2)
checkEnv(97-111)getEnv(79-81)src/services/api-base.ts (3)
buildListeeApiUrl(34-42)readApiPayload(44-64)extractApiErrorMessage(66-90)
src/services/api-base.ts (1)
src/env.ts (2)
getEnv(79-81)EnvValidationError(10-17)
src/services/auth-service.test.ts (3)
src/env.ts (1)
resetEnvCache(83-85)src/services/auth-service.ts (2)
ensureListeeApiConfig(82-85)toAuthenticatedAccessTokenResult(388-405)src/types/auth.ts (1)
AccessTokenResult(15-19)
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/services/auth-service.ts (1)
170-199: Separate expiration validation from structural type narrowing.The expiration check (lines 193-196) inside
isAuthTokenPayloadmixes concerns. Type guards should verify shape and required fields, not enforce business rules like expiration. When a structurally valid but expired token fails this check,decodeAuthTokenthrows "Access token payload structure is invalid" (line 204), which is misleading.Consider extracting the expiration check into a separate validation function:
const isAuthTokenPayload = (payload: unknown): payload is AuthTokenClaims => { if (!isRecord(payload)) { return false; } const subValue = "sub" in payload ? payload.sub : undefined; const emailValue = "email" in payload ? payload.email : undefined; const expValue = "exp" in payload ? payload.exp : undefined; const iatValue = "iat" in payload ? payload.iat : undefined; - if ( - !isString(subValue) || - subValue.trim().length === 0 || - !isString(emailValue) || - emailValue.trim().length === 0 || - !isNumber(expValue) || - expValue <= 0 || - !isNumber(iatValue) || - iatValue <= 0 - ) { - return false; - } - - const currentEpochSeconds = Math.floor(Date.now() / 1000); - if (expValue <= currentEpochSeconds) { - return false; - } - - return true; + return ( + isString(subValue) && + subValue.trim().length > 0 && + isString(emailValue) && + emailValue.trim().length > 0 && + isNumber(expValue) && + expValue > 0 && + isNumber(iatValue) && + iatValue > 0 + ); }; + +const isTokenExpired = (payload: AuthTokenClaims): boolean => { + const currentEpochSeconds = Math.floor(Date.now() / 1000); + return payload.exp <= currentEpochSeconds; +};Then update
decodeAuthToken:const decodeAuthToken = (token: string): AuthTokenClaims => { const payload = decodeJwtPayload(token); if (!isAuthTokenPayload(payload)) { throw new Error("Access token payload structure is invalid."); } if (isTokenExpired(payload)) { throw new Error("Access token has expired."); } return payload; };
🧹 Nitpick comments (1)
src/services/auth-service.ts (1)
82-85: Consider clarifying the validation intent.The
void getEnv().LISTEE_API_URLpattern on line 84 works becausegetEnv()will throw if the environment is invalid, but the intent isn't immediately obvious.Consider adding a brief comment to clarify:
export const ensureListeeApiConfig = (): void => { checkEnv(); + // Ensures LISTEE_API_URL is present; getEnv() throws if validation fails void getEnv().LISTEE_API_URL; };Alternatively, make the validation more explicit:
export const ensureListeeApiConfig = (): void => { checkEnv(); - void getEnv().LISTEE_API_URL; + const env = getEnv(); + if (!env.LISTEE_API_URL) { + throw new Error("LISTEE_API_URL is required"); + } };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/services/auth-service.ts(9 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/services/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
src/services/**/*.ts: Place Supabase-facing logic under src/services/
Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Default Keytar service name to "listee-cli"; allow overrides via env vars or CLI flags
Files:
src/services/auth-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript strict practices: forbid any, forbid as assertions, no implicit any; implement type guards for narrowing
Use two-space indentation and LF line endings
Use camelCase for identifiers
Keep comments purposeful; add brief context only for non-trivial flows
Maintain ASCII in files unless the file already uses Unicode
Files:
src/services/auth-service.ts
src/**/[a-z0-9-]*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use kebab-case filenames for modules under src/
Files:
src/services/auth-service.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Place Supabase-facing logic under src/services/
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Read SUPABASE_URL and SUPABASE_ANON_KEY from environment variables or .env; never hardcode credentials
Applied to files:
src/services/auth-service.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/services/**/*.ts : Place Supabase-facing logic under src/services/
Applied to files:
src/services/auth-service.ts
🧬 Code graph analysis (1)
src/services/auth-service.ts (3)
src/types/auth.ts (2)
AuthTokenResponse(3-8)AccessTokenResult(15-19)src/env.ts (2)
checkEnv(97-111)getEnv(79-81)src/services/api-base.ts (3)
buildListeeApiUrl(34-42)readApiPayload(44-64)extractApiErrorMessage(66-90)
🔇 Additional comments (2)
src/services/auth-service.ts (2)
92-123: Past review comment has been addressed.The implementation now correctly handles empty success responses (lines 113-114), returning
nullinstead of throwing an error. This allows auth endpoints that respond with204 No Contentto succeed as expected.
387-404: LGTM!The function correctly decodes the access token, extracts the user ID, and enriches the token result with authentication context. The defensive trimming and empty-check (lines 390-392) are appropriate safeguards.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/commands/tasks.ts (1)
140-143: Consider normalizing empty descriptions to undefined.After trimming, an empty string is passed to the API, which will store it as-is rather than null. For data consistency, you might want to treat empty trimmed descriptions the same as undefined.
const description = options.description === undefined ? undefined - : options.description.trim(); + : options.description.trim() || undefined;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/commands/categories.ts(1 hunks)src/commands/tasks.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/commands/categories.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/commands/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place CLI command handlers under src/commands/
Files:
src/commands/tasks.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use TypeScript strict practices: forbid any, forbid as assertions, no implicit any; implement type guards for narrowing
Use two-space indentation and LF line endings
Use camelCase for identifiers
Keep comments purposeful; add brief context only for non-trivial flows
Maintain ASCII in files unless the file already uses Unicode
Files:
src/commands/tasks.ts
src/**/[a-z0-9-]*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use kebab-case filenames for modules under src/
Files:
src/commands/tasks.ts
🧠 Learnings (2)
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/index.ts : Keep CLI wiring in src/index.ts using Commander; avoid moving or duplicating the entry wiring
Applied to files:
src/commands/tasks.ts
📚 Learning: 2025-10-07T13:26:37.124Z
Learnt from: CR
Repo: listee-dev/listee-cli PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T13:26:37.124Z
Learning: Applies to src/commands/**/*.ts : Place CLI command handlers under src/commands/
Applied to files:
src/commands/tasks.ts
🧬 Code graph analysis (1)
src/commands/tasks.ts (1)
src/services/task-api.ts (5)
listTasksByCategory(83-90)getTask(97-104)createTask(114-134)updateTask(144-168)deleteTask(175-181)
🔇 Additional comments (4)
src/commands/tasks.ts (4)
96-96: Previous validation concern has been addressed.The earlier review flagged that
options.categoryshould be validated before use. Line 96 now correctly appliesensureNonEmptyStringto guard against empty category IDs.
112-112: Previous validation concern has been addressed.The earlier review flagged that
taskIdshould be validated before use. Line 112 now correctly appliesensureNonEmptyStringto guard against empty task IDs.
157-234: Excellent input validation and conflict detection.The update command properly validates all inputs, detects conflicting flags (--checked/--unchecked, --description/--clear-description), and ensures at least one update field is provided. The error messages are clear and actionable.
10-23: Clean error handling pattern.The
executewrapper provides consistent error formatting and exit code handling across all commands. Settingprocess.exitCoderather than callingprocess.exit()allows the action handler to complete gracefully while ensuring the process exits with the correct code.
Summary
Testing
Summary by CodeRabbit
New Features
Configuration
Improvements
Tests