ledgit-cli 0.2.0 → 0.2.2

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 (2) hide show
  1. package/dist/index.js +462 -157
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24666,21 +24666,29 @@ When you run \`create-entry\` on main branch, a new git branch is automatically
24666
24666
 
24667
24667
  ## Bookkeeping New Entries
24668
24668
 
24669
- **Important:** Always ask the user for clarification if anything is unclear about the transaction before creating an entry.
24670
-
24671
24669
  To create a journal entry from an inbox document:
24672
24670
 
24673
- 1. **Read the document** - Use \`npx ledgit-cli parse-pdf\` for PDFs to understand the content
24674
- 2. **Update documents.yaml** - After parsing, populate missing metadata in \`documents.yaml\`:
24671
+ 1. **Read the document** - You can read PDFs directly, or use \`npx ledgit-cli parse-pdf\` to extract text
24672
+ 2. **Update documents.yaml** - Populate missing metadata in \`documents.yaml\`:
24675
24673
  - \`documentDate\`: Invoice/receipt date (YYYY-MM-DD)
24676
24674
  - \`description\`: Vendor or document name
24677
24675
  - \`totalAmount\`: With \`amount\` (string) and \`currency\` (e.g., EUR, SEK)
24678
24676
  3. **Look for similar entries** - Search \`journal-entries/\` for entries from the same vendor or similar transaction types to learn which accounts and tax codes were used previously
24679
24677
  4. **Check accounts** - Review \`accounts.yaml\` for appropriate expense/revenue accounts
24680
- 5. **Determine tax code** - Based on vendor location and transaction type (see Tax Codes below). Explain your reasoning and describe the tax code meaning to the user
24681
- 6. **Ask for confirmation** - Before creating, confirm with the user: vendor, amount, accounts (with names), and tax code (with description)
24682
- 7. **Create entry** - Run \`npx ledgit-cli create-entry\` with the appropriate parameters
24683
- 8. **Review and sync** - The entry is created in \`journal-entries/\`. Review with \`git diff\`, commit, and run \`npx ledgit-cli sync-journal\` to post to {{PROVIDER}}
24678
+ 5. **Determine tax code** - Based on vendor location and transaction type (see Tax Codes below)
24679
+ 6. **Create entry** - Run \`npx ledgit-cli create-entry\` with the appropriate parameters
24680
+ 7. **Review and sync** - The entry is created in \`journal-entries/\`. Review with \`git diff\`, commit, and run \`npx ledgit-cli sync-journal\` to post to {{PROVIDER}}
24681
+
24682
+ ### GitHub Actions / CI Mode
24683
+
24684
+ When running in GitHub Actions (automated bookkeeping):
24685
+ - **Always proceed** with creating entries using your best judgment
24686
+ - **Always end** by creating or updating a PR
24687
+ - If anything is unclear, include your questions in the PR description
24688
+ - The user will respond via PR comments, which triggers another workflow run
24689
+ - Group related entries (same vendor, same week) in one PR when sensible
24690
+
24691
+ **Never stop and wait for confirmation in CI mode** - the user can't respond until you create a PR.
24684
24692
 
24685
24693
  To discard unwanted inbox items:
24686
24694
  \`\`\`bash
@@ -24788,12 +24796,12 @@ function renderAgentsTemplate(options) {
24788
24796
  }
24789
24797
  // ../bookkeeping/dist/utils/paths.js
24790
24798
  import * as path2 from "node:path";
24791
- var KVITTON_DIR = ".kvitton";
24799
+ var LEDGIT_DIR = ".ledgit";
24792
24800
  function getTokensDir(cwd) {
24793
- return path2.join(cwd, KVITTON_DIR, "tokens");
24801
+ return path2.join(cwd, LEDGIT_DIR, "tokens");
24794
24802
  }
24795
24803
  function getCacheDir(cwd) {
24796
- return path2.join(cwd, KVITTON_DIR, "cache");
24804
+ return path2.join(cwd, LEDGIT_DIR, "cache");
24797
24805
  }
24798
24806
  // ../bookkeeping/dist/services/journal.service.js
24799
24807
  class JournalService {
@@ -26931,7 +26939,7 @@ async function loginFortnox(options) {
26931
26939
  onStatus("Exchanging authorization code for token...");
26932
26940
  const tokenResponse = await exchangeCodeForToken(config2, callbackResult.code);
26933
26941
  const storedToken = saveFortnoxToken(cwd, tokenResponse);
26934
- onStatus("Token saved to .kvitton/tokens/fortnox.json");
26942
+ onStatus("Token saved to .ledgit/tokens/fortnox.json");
26935
26943
  return storedToken;
26936
26944
  } finally {
26937
26945
  stop();
@@ -27792,6 +27800,22 @@ async function syncFortnoxJournal(cwd, options) {
27792
27800
  import ora2 from "ora";
27793
27801
  import * as fs12 from "node:fs/promises";
27794
27802
  import * as path8 from "node:path";
27803
+ var MANIFEST_PATH = ".ledgit/synced-inbox.json";
27804
+ async function loadManifest(cwd) {
27805
+ const manifestPath = path8.join(cwd, MANIFEST_PATH);
27806
+ try {
27807
+ const content = await fs12.readFile(manifestPath, "utf-8");
27808
+ const ids = JSON.parse(content);
27809
+ return new Set(ids);
27810
+ } catch {
27811
+ return new Set;
27812
+ }
27813
+ }
27814
+ async function saveManifest(cwd, syncedIds) {
27815
+ const manifestPath = path8.join(cwd, MANIFEST_PATH);
27816
+ await fs12.mkdir(path8.dirname(manifestPath), { recursive: true });
27817
+ await fs12.writeFile(manifestPath, JSON.stringify([...syncedIds], null, 2));
27818
+ }
27795
27819
  function slugify4(text, maxLength = 30) {
27796
27820
  return text.toLowerCase().slice(0, maxLength).replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
27797
27821
  }
@@ -27815,6 +27839,7 @@ async function syncInboxCommand() {
27815
27839
  async function syncBokioInbox(cwd) {
27816
27840
  const config3 = loadConfig(cwd);
27817
27841
  const client2 = createBokioClient(config3);
27842
+ const syncedIds = await loadManifest(cwd);
27818
27843
  const validateSpinner = ora2("Connecting to Bokio...").start();
27819
27844
  try {
27820
27845
  const companyInfo = await client2.getCompanyInformation();
@@ -27867,8 +27892,13 @@ async function syncBokioInbox(cwd) {
27867
27892
  for (const upload of unprocessedUploads) {
27868
27893
  const slug = upload.description ? slugify4(upload.description) : upload.id.slice(0, 8);
27869
27894
  let dirName = `${today}-${slug}`;
27895
+ if (syncedIds.has(upload.id)) {
27896
+ existingCount++;
27897
+ continue;
27898
+ }
27870
27899
  const alreadyDownloaded = await isAlreadyDownloaded2(cwd, upload.id);
27871
27900
  if (alreadyDownloaded) {
27901
+ syncedIds.add(upload.id);
27872
27902
  existingCount++;
27873
27903
  continue;
27874
27904
  }
@@ -27896,6 +27926,7 @@ async function syncBokioInbox(cwd) {
27896
27926
  ];
27897
27927
  const documentsPath = path8.join(uploadDir, "documents.yaml");
27898
27928
  await fs12.writeFile(documentsPath, toYaml2(documents));
27929
+ syncedIds.add(upload.id);
27899
27930
  newCount++;
27900
27931
  } catch (error) {
27901
27932
  console.error(`
@@ -27906,20 +27937,13 @@ async function syncBokioInbox(cwd) {
27906
27937
  }
27907
27938
  }
27908
27939
  }
27940
+ await saveManifest(cwd, syncedIds);
27909
27941
  console.log(`
27910
27942
 
27911
27943
  Sync complete!
27912
27944
  - ${newCount} new inbox items downloaded
27913
27945
  - ${existingCount} already existed
27914
27946
  `);
27915
- if (newCount > 0) {
27916
- const commitMessage = `Sync inbox: ${newCount} items downloaded`;
27917
- const committed = await commitAll(cwd, commitMessage);
27918
- if (committed) {
27919
- console.log(` Committed: "${commitMessage}"
27920
- `);
27921
- }
27922
- }
27923
27947
  }
27924
27948
  function getExtensionFromContentType(contentType) {
27925
27949
  const map = {
@@ -28006,14 +28030,6 @@ async function syncFortnoxInbox2(cwd) {
28006
28030
  - ${result.newCount} new inbox items downloaded
28007
28031
  - ${result.existingCount} already existed
28008
28032
  `);
28009
- if (result.newCount > 0) {
28010
- const commitMessage = `Sync inbox: ${result.newCount} items downloaded from Fortnox`;
28011
- const committed = await commitAll(cwd, commitMessage);
28012
- if (committed) {
28013
- console.log(` Committed: "${commitMessage}"
28014
- `);
28015
- }
28016
- }
28017
28033
  }
28018
28034
 
28019
28035
  // src/commands/company-info.ts
@@ -28941,6 +28957,8 @@ async function updateEnvFile(cwd) {
28941
28957
  // src/commands/update.ts
28942
28958
  import * as fs17 from "node:fs/promises";
28943
28959
  import * as path13 from "node:path";
28960
+ import { exec as exec6 } from "node:child_process";
28961
+ import { promisify as promisify6 } from "node:util";
28944
28962
  import ora6 from "ora";
28945
28963
 
28946
28964
  // ../../node_modules/ansi-regex/index.js
@@ -34021,83 +34039,6 @@ var dist_default8 = createPrompt4((config3, done) => {
34021
34039
  `).trimEnd();
34022
34040
  return `${lines}${cursorHide4}`;
34023
34041
  });
34024
- // src/commands/update.ts
34025
- async function updateCommand(options = {}) {
34026
- const cwd = process.cwd();
34027
- const agentsPath = path13.join(cwd, "AGENTS.md");
34028
- let companyName;
34029
- let provider;
34030
- try {
34031
- const content = await fs17.readFile(agentsPath, "utf-8");
34032
- const match = content.match(/repository for (.+?) \(Swedish company\).+?integrates with (Fortnox|Bokio)/s);
34033
- if (!match || !match[1] || !match[2]) {
34034
- console.error("Could not parse company info from AGENTS.md");
34035
- process.exit(1);
34036
- }
34037
- companyName = match[1];
34038
- provider = match[2];
34039
- console.log(` Company: ${companyName}`);
34040
- console.log(` Provider: ${provider}`);
34041
- } catch {
34042
- console.error("AGENTS.md not found. Run this command in a ledgit repository.");
34043
- process.exit(1);
34044
- }
34045
- const newContent = renderAgentsTemplate({
34046
- companyName,
34047
- provider
34048
- });
34049
- if (!options.force) {
34050
- const currentContent = await fs17.readFile(agentsPath, "utf-8");
34051
- const hasCustomChanges = currentContent !== newContent;
34052
- if (hasCustomChanges) {
34053
- const proceed = await dist_default2({
34054
- message: "AGENTS.md has been modified. Overwrite with latest template?",
34055
- default: true
34056
- });
34057
- if (!proceed) {
34058
- console.log(" Update cancelled.");
34059
- return;
34060
- }
34061
- }
34062
- }
34063
- const updateSpinner = ora6("Updating AGENTS.md...").start();
34064
- try {
34065
- await fs17.writeFile(agentsPath, newContent, "utf-8");
34066
- const claudePath = path13.join(cwd, "CLAUDE.md");
34067
- try {
34068
- const stat2 = await fs17.lstat(claudePath);
34069
- if (!stat2.isSymbolicLink()) {
34070
- await fs17.unlink(claudePath);
34071
- await fs17.symlink("AGENTS.md", claudePath);
34072
- }
34073
- } catch {
34074
- await fs17.symlink("AGENTS.md", claudePath);
34075
- }
34076
- updateSpinner.succeed("Updated AGENTS.md");
34077
- } catch (error) {
34078
- updateSpinner.fail("Failed to update AGENTS.md");
34079
- console.error(error instanceof Error ? error.message : "Unknown error");
34080
- process.exit(1);
34081
- }
34082
- const commitMessage = "Update AGENTS.md to latest version";
34083
- const committed = await commitAll(cwd, commitMessage);
34084
- if (committed) {
34085
- console.log(`
34086
- Committed: "${commitMessage}"
34087
- `);
34088
- } else {
34089
- console.log(`
34090
- No changes to commit.
34091
- `);
34092
- }
34093
- }
34094
-
34095
- // src/commands/setup-github.ts
34096
- import * as fs18 from "node:fs/promises";
34097
- import * as path14 from "node:path";
34098
- import { exec as exec6 } from "node:child_process";
34099
- import { promisify as promisify6 } from "node:util";
34100
-
34101
34042
  // src/templates/workflows.ts
34102
34043
  var SYNC_INBOX_WORKFLOW = `name: Sync Inbox
34103
34044
  on:
@@ -34112,18 +34053,30 @@ jobs:
34112
34053
  contents: write
34113
34054
  pull-requests: write
34114
34055
  issues: write
34115
- id-token: write
34116
34056
  steps:
34117
34057
  - uses: actions/checkout@v5
34118
34058
 
34119
34059
  - uses: oven-sh/setup-bun@v2
34120
34060
 
34061
+ - name: Configure git
34062
+ run: |
34063
+ git config user.name "ledgit[bot]"
34064
+ git config user.email "bot@ledgit.se"
34065
+
34121
34066
  - name: Sync inbox
34122
34067
  run: npx ledgit-cli sync-inbox
34123
34068
  env:
34124
34069
  BOKIO_TOKEN: \${{ secrets.BOKIO_TOKEN }}
34125
34070
  BOKIO_COMPANY_ID: \${{ secrets.BOKIO_COMPANY_ID }}
34126
34071
 
34072
+ - name: Commit sync manifest
34073
+ run: |
34074
+ if [ -f .ledgit/synced-inbox.json ]; then
34075
+ git add .ledgit/synced-inbox.json
34076
+ git diff --cached --quiet || git commit -m "chore: update synced inbox manifest"
34077
+ git push
34078
+ fi
34079
+
34127
34080
  - name: Check for new items
34128
34081
  id: check
34129
34082
  run: |
@@ -34133,32 +34086,89 @@ jobs:
34133
34086
  echo "has_new=false" >> $GITHUB_OUTPUT
34134
34087
  fi
34135
34088
 
34089
+ - name: Install Claude Code
34090
+ if: steps.check.outputs.has_new == 'true'
34091
+ run: |
34092
+ curl -fsSL https://claude.ai/install.sh | bash
34093
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
34094
+
34136
34095
  - name: Bookkeep with Claude
34137
34096
  if: steps.check.outputs.has_new == 'true'
34138
- uses: anthropics/claude-code-action@v1
34097
+ env:
34098
+ CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
34099
+ GH_TOKEN: \${{ github.token }}
34100
+ run: |
34101
+ claude -p "New documents synced to inbox/ (unstaged).
34102
+
34103
+ YOU handle all git operations.
34104
+
34105
+ FIRST: Check for existing open bookkeeping PRs with gh pr list.
34106
+ - If there's an active PR, checkout that branch and add to it
34107
+ - Otherwise, create a new branch: book/{date}-{description}
34108
+ - Group related entries (same vendor, same week) in one PR when sensible
34109
+
34110
+ THEN:
34111
+ 1. Check each new item in inbox/
34112
+ 2. Find similar entries in journal-entries/ for reference
34113
+ 3. Create entries with npx ledgit-cli create-entry
34114
+ 4. Stage and commit all changes (inbox files + entries)
34115
+ 5. Push and create/update PR
34116
+
34117
+ PR FORMAT:
34118
+ ## Summary
34119
+ Brief description (e.g., 'Book 2 invoices from Anthropic and AWS')
34120
+
34121
+ ## Entries
34122
+ | Entry | Vendor | Amount | Date |
34123
+ |-------|--------|--------|------|
34124
+ | V-XXX | Vendor | Amount SEK | YYYY-MM-DD |
34125
+
34126
+ ### V-XXX: Description
34127
+ - **Account**: XXXX (Name) - why this account
34128
+ - **Tax**: Tax treatment - why (e.g., 'Reverse charge - US vendor')
34129
+ - **Exchange rate**: X.XX SEK/USD from Riksbank (date) - if foreign currency
34130
+ - **Reference**: Similar to V-YYY - if found similar entry
34131
+
34132
+ ## Questions
34133
+ (Only if something needs clarification - otherwise omit this section)" \\
34134
+ --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(gh pr:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)" \\
34135
+ --verbose \\
34136
+ --output-format stream-json \\
34137
+ 2>&1 | tee /tmp/claude-output.json
34138
+
34139
+ - name: Write job summary
34140
+ if: always() && steps.check.outputs.has_new == 'true'
34141
+ run: |
34142
+ echo "## \uD83E\uDD16 Claude Bookkeeping Report" >> $GITHUB_STEP_SUMMARY
34143
+ echo "" >> $GITHUB_STEP_SUMMARY
34144
+
34145
+ # Show model from init
34146
+ jq -r 'select(.type == "system" and .subtype == "init") | "**Model:** \\(.model // "claude")"' /tmp/claude-output.json >> $GITHUB_STEP_SUMMARY 2>/dev/null || true
34147
+ echo "" >> $GITHUB_STEP_SUMMARY
34148
+
34149
+ # Count tool uses
34150
+ TOOL_COUNT=$(jq -s '[.[] | select(.type == "tool_use")] | length' /tmp/claude-output.json 2>/dev/null || echo "0")
34151
+ echo "**Tool calls:** $TOOL_COUNT" >> $GITHUB_STEP_SUMMARY
34152
+ echo "" >> $GITHUB_STEP_SUMMARY
34153
+
34154
+ # List unique tools used
34155
+ echo "### Tools Used" >> $GITHUB_STEP_SUMMARY
34156
+ jq -r 'select(.type == "tool_use") | .name' /tmp/claude-output.json 2>/dev/null | sort | uniq -c | while read count name; do
34157
+ echo "- $name ($count calls)" >> $GITHUB_STEP_SUMMARY
34158
+ done
34159
+ echo "" >> $GITHUB_STEP_SUMMARY
34160
+
34161
+ # Final result
34162
+ echo "### Result" >> $GITHUB_STEP_SUMMARY
34163
+ jq -r 'select(.type == "result") | "**Status:** \\(.subtype // "completed")\\n**Duration:** \\((.duration_ms // 0) / 1000 | floor)s\\n**Turns:** \\(.num_turns // "N/A")\\n**Cost:** $\\(.total_cost_usd // 0 | tostring | .[0:6])"' /tmp/claude-output.json >> $GITHUB_STEP_SUMMARY 2>/dev/null || true
34164
+
34165
+ - name: Upload Claude execution log
34166
+ if: always() && steps.check.outputs.has_new == 'true'
34167
+ uses: actions/upload-artifact@v4
34139
34168
  with:
34140
- anthropic_api_key: \${{ secrets.ANTHROPIC_API_KEY }}
34141
- prompt: |
34142
- New documents synced to inbox/. Read AGENTS.md for instructions.
34143
-
34144
- FIRST: Check for existing open bookkeeping PRs with \\\`gh pr list\\\`.
34145
- If there's an active PR you should continue on, check out that branch and add to it.
34146
- Group related entries (e.g., same vendor, same week) in one PR when it makes sense.
34147
-
34148
- THEN:
34149
- 1. Check each new item in inbox/ (you can read PDFs directly)
34150
- 2. Find similar entries in journal-entries/ for reference
34151
- 3. Create entries with \\\`npx ledgit-cli create-entry\\\`
34152
-
34153
- ALWAYS create or update a PR:
34154
- - If you created entries successfully, PR contains the new entries
34155
- - If anything is unclear, commit the inbox files and ask questions in the PR description
34156
- - The user will answer your questions via PR comments
34157
-
34158
- For new PRs, create branch: book/{date}-{description}
34159
- claude_args: |
34160
- --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(gh pr:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)"
34161
- track_progress: true
34169
+ name: claude-execution-log
34170
+ path: /tmp/claude-output.json
34171
+ retention-days: 7
34162
34172
  `;
34163
34173
  var CLAUDE_BOOKKEEPING_WORKFLOW = `name: Claude Bookkeeping
34164
34174
  on:
@@ -34175,25 +34185,118 @@ jobs:
34175
34185
  contents: write
34176
34186
  pull-requests: write
34177
34187
  issues: write
34178
- id-token: write
34179
34188
  steps:
34189
+ - name: Get PR branch
34190
+ id: pr
34191
+ env:
34192
+ GH_TOKEN: \${{ github.token }}
34193
+ run: |
34194
+ PR_NUMBER=\${{ github.event.issue.number }}
34195
+ BRANCH=$(gh pr view $PR_NUMBER --repo \${{ github.repository }} --json headRefName --jq '.headRefName')
34196
+ echo "branch=$BRANCH" >> $GITHUB_OUTPUT
34197
+
34180
34198
  - uses: actions/checkout@v5
34181
34199
  with:
34182
- ref: \${{ github.head_ref }}
34200
+ ref: \${{ steps.pr.outputs.branch }}
34183
34201
 
34184
34202
  - uses: oven-sh/setup-bun@v2
34185
34203
 
34186
- - uses: anthropics/claude-code-action@v1
34204
+ - name: Configure git
34205
+ run: |
34206
+ git config user.name "ledgit[bot]"
34207
+ git config user.email "bot@ledgit.se"
34208
+
34209
+ - name: Get PR context
34210
+ id: context
34211
+ env:
34212
+ GH_TOKEN: \${{ github.token }}
34213
+ run: |
34214
+ PR_NUMBER=\${{ github.event.issue.number }}
34215
+
34216
+ # Fetch PR details and format as context
34217
+ {
34218
+ echo "## PR Details"
34219
+ gh pr view $PR_NUMBER --json title,body --jq '"### \\(.title)\\n\\n\\(.body)"'
34220
+ echo ""
34221
+ echo "## Comments"
34222
+ gh pr view $PR_NUMBER --json comments --jq '.comments[] | "[\\(.author.login) at \\(.createdAt)]: \\(.body)"'
34223
+ echo ""
34224
+ echo "## Files changed"
34225
+ gh pr view $PR_NUMBER --json files --jq '.files[].path'
34226
+ echo ""
34227
+ echo "---"
34228
+ echo "## Latest comment from \${{ github.event.comment.user.login }}:"
34229
+ echo "\${{ github.event.comment.body }}"
34230
+ } > /tmp/pr_context.md
34231
+
34232
+ - name: Install Claude Code
34233
+ run: |
34234
+ curl -fsSL https://claude.ai/install.sh | bash
34235
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
34236
+
34237
+ - name: Save PR context as artifact
34238
+ uses: actions/upload-artifact@v4
34187
34239
  with:
34188
- anthropic_api_key: \${{ secrets.ANTHROPIC_API_KEY }}
34189
- prompt: |
34190
- This is an ongoing bookkeeping conversation. You can see the full PR context including all previous comments.
34240
+ name: pr-context-\${{ github.event.issue.number }}
34241
+ path: /tmp/pr_context.md
34242
+ retention-days: 7
34243
+
34244
+ - name: Respond with Claude
34245
+ env:
34246
+ CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
34247
+ GH_TOKEN: \${{ github.token }}
34248
+ run: |
34249
+ PR_CONTEXT=$(cat /tmp/pr_context.md)
34250
+
34251
+ claude -p "This is an ongoing bookkeeping conversation on a GitHub PR.
34252
+
34253
+ $PR_CONTEXT
34254
+
34255
+ ---
34256
+
34257
+ Respond to the user's latest comment. If they answered your questions, continue creating the journal entries.
34258
+
34259
+ Read AGENTS.md for bookkeeping instructions.
34260
+
34261
+ When done, post your response as a PR comment using: gh pr comment \${{ github.event.issue.number }} --body 'your response'" \\
34262
+ --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(gh pr:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)" \\
34263
+ --verbose \\
34264
+ --output-format stream-json \\
34265
+ 2>&1 | tee /tmp/claude-output.json
34191
34266
 
34192
- Respond to the user's latest comment. If they answered your questions, continue creating the journal entries.
34267
+ - name: Write job summary
34268
+ if: always()
34269
+ run: |
34270
+ echo "## \uD83E\uDD16 Claude Response Report" >> $GITHUB_STEP_SUMMARY
34271
+ echo "" >> $GITHUB_STEP_SUMMARY
34272
+
34273
+ # Show model from init
34274
+ jq -r 'select(.type == "system" and .subtype == "init") | "**Model:** \\(.model // "claude")"' /tmp/claude-output.json >> $GITHUB_STEP_SUMMARY 2>/dev/null || true
34275
+ echo "" >> $GITHUB_STEP_SUMMARY
34276
+
34277
+ # Count tool uses
34278
+ TOOL_COUNT=$(jq -s '[.[] | select(.type == "tool_use")] | length' /tmp/claude-output.json 2>/dev/null || echo "0")
34279
+ echo "**Tool calls:** $TOOL_COUNT" >> $GITHUB_STEP_SUMMARY
34280
+ echo "" >> $GITHUB_STEP_SUMMARY
34281
+
34282
+ # List unique tools used
34283
+ echo "### Tools Used" >> $GITHUB_STEP_SUMMARY
34284
+ jq -r 'select(.type == "tool_use") | .name' /tmp/claude-output.json 2>/dev/null | sort | uniq -c | while read count name; do
34285
+ echo "- $name ($count calls)" >> $GITHUB_STEP_SUMMARY
34286
+ done
34287
+ echo "" >> $GITHUB_STEP_SUMMARY
34193
34288
 
34194
- Read AGENTS.md for bookkeeping instructions.
34195
- claude_args: |
34196
- --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)"
34289
+ # Final result
34290
+ echo "### Result" >> $GITHUB_STEP_SUMMARY
34291
+ jq -r 'select(.type == "result") | "**Status:** \\(.subtype // "completed")\\n**Duration:** \\((.duration_ms // 0) / 1000 | floor)s\\n**Turns:** \\(.num_turns // "N/A")\\n**Cost:** $\\(.total_cost_usd // 0 | tostring | .[0:6])"' /tmp/claude-output.json >> $GITHUB_STEP_SUMMARY 2>/dev/null || true
34292
+
34293
+ - name: Upload Claude execution log
34294
+ if: always()
34295
+ uses: actions/upload-artifact@v4
34296
+ with:
34297
+ name: claude-execution-log-pr-\${{ github.event.issue.number }}
34298
+ path: /tmp/claude-output.json
34299
+ retention-days: 7
34197
34300
  `;
34198
34301
  var SYNC_JOURNAL_WORKFLOW = `name: Sync Journal
34199
34302
  on:
@@ -34223,11 +34326,211 @@ jobs:
34223
34326
  BOKIO_COMPANY_ID: \${{ secrets.BOKIO_COMPANY_ID }}
34224
34327
  `;
34225
34328
 
34226
- // src/commands/setup-github.ts
34329
+ // src/commands/update.ts
34227
34330
  var execAsync2 = promisify6(exec6);
34331
+ var AGENTS_COMMIT_MSG = "Update AGENTS.md to latest version";
34332
+ var WORKFLOWS_COMMIT_MSG = "Update GitHub Actions workflows to latest version";
34333
+ async function hasCustomChanges(cwd, filePath, expectedMessage) {
34334
+ try {
34335
+ const result = await execAsync2(`git log -1 --format="%s" -- "${filePath}"`, { cwd });
34336
+ const lastMessage = result.stdout.trim();
34337
+ if (lastMessage === "" || lastMessage === expectedMessage) {
34338
+ return false;
34339
+ }
34340
+ return true;
34341
+ } catch {
34342
+ return false;
34343
+ }
34344
+ }
34345
+ async function getDiff(cwd, path14) {
34346
+ try {
34347
+ const result = await execAsync2(`git diff "${path14}"`, { cwd });
34348
+ return result.stdout;
34349
+ } catch {
34350
+ return "";
34351
+ }
34352
+ }
34353
+ async function revertChanges(cwd, path14) {
34354
+ try {
34355
+ await execAsync2(`git checkout "${path14}"`, { cwd });
34356
+ } catch {
34357
+ }
34358
+ }
34359
+ async function commitFiles(cwd, files, message) {
34360
+ try {
34361
+ await execAsync2(`git add ${files.map((f2) => `"${f2}"`).join(" ")}`, { cwd });
34362
+ try {
34363
+ await execAsync2("git diff --cached --quiet", { cwd });
34364
+ return false;
34365
+ } catch {
34366
+ }
34367
+ await execAsync2(`git commit -m "${message}"`, { cwd });
34368
+ return true;
34369
+ } catch {
34370
+ return false;
34371
+ }
34372
+ }
34373
+ async function updateCommand(options = {}) {
34374
+ const cwd = process.cwd();
34375
+ const agentsPath = path13.join(cwd, "AGENTS.md");
34376
+ let companyName;
34377
+ let provider;
34378
+ try {
34379
+ const content = await fs17.readFile(agentsPath, "utf-8");
34380
+ const match = content.match(/repository for (.+?) \(Swedish company\).+?integrates with (Fortnox|Bokio)/s);
34381
+ if (!match || !match[1] || !match[2]) {
34382
+ console.error("Could not parse company info from AGENTS.md");
34383
+ process.exit(1);
34384
+ }
34385
+ companyName = match[1];
34386
+ provider = match[2];
34387
+ console.log(` Company: ${companyName}`);
34388
+ console.log(` Provider: ${provider}`);
34389
+ } catch {
34390
+ console.error("AGENTS.md not found. Run this command in a ledgit repository.");
34391
+ process.exit(1);
34392
+ }
34393
+ const agentsHasCustom = await hasCustomChanges(cwd, "AGENTS.md", AGENTS_COMMIT_MSG);
34394
+ const currentAgentsContent = await fs17.readFile(agentsPath, "utf-8");
34395
+ const newContent = renderAgentsTemplate({
34396
+ companyName,
34397
+ provider
34398
+ });
34399
+ const agentsNeedsUpdate = currentAgentsContent !== newContent;
34400
+ if (agentsNeedsUpdate) {
34401
+ const updateSpinner = ora6("Updating AGENTS.md...").start();
34402
+ try {
34403
+ await fs17.writeFile(agentsPath, newContent, "utf-8");
34404
+ const claudePath = path13.join(cwd, "CLAUDE.md");
34405
+ try {
34406
+ const stat2 = await fs17.lstat(claudePath);
34407
+ if (!stat2.isSymbolicLink()) {
34408
+ await fs17.unlink(claudePath);
34409
+ await fs17.symlink("AGENTS.md", claudePath);
34410
+ }
34411
+ } catch {
34412
+ await fs17.symlink("AGENTS.md", claudePath);
34413
+ }
34414
+ updateSpinner.succeed("Updated AGENTS.md");
34415
+ } catch (error) {
34416
+ updateSpinner.fail("Failed to update AGENTS.md");
34417
+ console.error(error instanceof Error ? error.message : "Unknown error");
34418
+ process.exit(1);
34419
+ }
34420
+ } else {
34421
+ console.log(" AGENTS.md is already up to date");
34422
+ }
34423
+ if (agentsNeedsUpdate) {
34424
+ if (agentsHasCustom && !options.force) {
34425
+ const diff = await getDiff(cwd, "AGENTS.md");
34426
+ if (diff) {
34427
+ console.log(`
34428
+ AGENTS.md has custom changes. Diff:
34429
+ `);
34430
+ console.log(diff);
34431
+ const proceed = await dist_default2({
34432
+ message: "Overwrite custom changes to AGENTS.md?",
34433
+ default: false
34434
+ });
34435
+ if (!proceed) {
34436
+ await revertChanges(cwd, "AGENTS.md");
34437
+ console.log(" Reverted AGENTS.md");
34438
+ } else {
34439
+ const committed = await commitFiles(cwd, ["AGENTS.md", "CLAUDE.md"], AGENTS_COMMIT_MSG);
34440
+ if (committed) {
34441
+ console.log(` Committed: "${AGENTS_COMMIT_MSG}"`);
34442
+ }
34443
+ }
34444
+ }
34445
+ } else {
34446
+ const committed = await commitFiles(cwd, ["AGENTS.md", "CLAUDE.md"], AGENTS_COMMIT_MSG);
34447
+ if (committed) {
34448
+ console.log(` Committed: "${AGENTS_COMMIT_MSG}"`);
34449
+ }
34450
+ }
34451
+ }
34452
+ const workflowsDir = path13.join(cwd, ".github", "workflows");
34453
+ const workflowFiles = [
34454
+ { name: "sync-inbox.yml", template: SYNC_INBOX_WORKFLOW },
34455
+ { name: "claude-bookkeeping.yml", template: CLAUDE_BOOKKEEPING_WORKFLOW },
34456
+ { name: "sync-journal.yml", template: SYNC_JOURNAL_WORKFLOW }
34457
+ ];
34458
+ const workflowsToUpdate = [];
34459
+ for (const wf of workflowFiles) {
34460
+ const wfPath = path13.join(workflowsDir, wf.name);
34461
+ try {
34462
+ const currentContent = await fs17.readFile(wfPath, "utf-8");
34463
+ if (currentContent !== wf.template) {
34464
+ workflowsToUpdate.push(wf);
34465
+ }
34466
+ } catch {
34467
+ }
34468
+ }
34469
+ if (workflowsToUpdate.length === 0) {
34470
+ console.log(" Workflows are already up to date");
34471
+ console.log("");
34472
+ return;
34473
+ }
34474
+ let workflowsHaveCustom = false;
34475
+ for (const wf of workflowsToUpdate) {
34476
+ const wfPath = `.github/workflows/${wf.name}`;
34477
+ if (await hasCustomChanges(cwd, wfPath, WORKFLOWS_COMMIT_MSG)) {
34478
+ workflowsHaveCustom = true;
34479
+ break;
34480
+ }
34481
+ }
34482
+ const workflowSpinner = ora6("Updating workflows...").start();
34483
+ try {
34484
+ for (const wf of workflowsToUpdate) {
34485
+ const wfPath = path13.join(workflowsDir, wf.name);
34486
+ await fs17.writeFile(wfPath, wf.template, "utf-8");
34487
+ }
34488
+ workflowSpinner.succeed(`Updated ${workflowsToUpdate.length} workflow(s)`);
34489
+ } catch (error) {
34490
+ workflowSpinner.fail("Failed to update workflows");
34491
+ console.error(error instanceof Error ? error.message : "Unknown error");
34492
+ return;
34493
+ }
34494
+ const workflowPaths = workflowsToUpdate.map((wf) => `.github/workflows/${wf.name}`);
34495
+ if (workflowsHaveCustom && !options.force) {
34496
+ const diff = await getDiff(cwd, ".github/workflows/");
34497
+ if (diff) {
34498
+ console.log(`
34499
+ Workflows have custom changes. Diff:
34500
+ `);
34501
+ console.log(diff);
34502
+ const proceed = await dist_default2({
34503
+ message: "Overwrite custom changes to workflows?",
34504
+ default: false
34505
+ });
34506
+ if (!proceed) {
34507
+ await revertChanges(cwd, ".github/workflows/");
34508
+ console.log(" Reverted workflows");
34509
+ } else {
34510
+ const committed = await commitFiles(cwd, workflowPaths, WORKFLOWS_COMMIT_MSG);
34511
+ if (committed) {
34512
+ console.log(` Committed: "${WORKFLOWS_COMMIT_MSG}"`);
34513
+ }
34514
+ }
34515
+ }
34516
+ } else {
34517
+ const committed = await commitFiles(cwd, workflowPaths, WORKFLOWS_COMMIT_MSG);
34518
+ if (committed) {
34519
+ console.log(` Committed: "${WORKFLOWS_COMMIT_MSG}"`);
34520
+ }
34521
+ }
34522
+ console.log("");
34523
+ }
34524
+
34525
+ // src/commands/setup-github.ts
34526
+ import * as fs18 from "node:fs/promises";
34527
+ import * as path14 from "node:path";
34528
+ import { exec as exec7 } from "node:child_process";
34529
+ import { promisify as promisify7 } from "node:util";
34530
+ var execAsync3 = promisify7(exec7);
34228
34531
  async function hasGitRemote() {
34229
34532
  try {
34230
- const { stdout } = await execAsync2("git remote -v");
34533
+ const { stdout } = await execAsync3("git remote -v");
34231
34534
  return stdout.trim().length > 0;
34232
34535
  } catch {
34233
34536
  return false;
@@ -34237,25 +34540,25 @@ async function getRepoName(cwd) {
34237
34540
  return path14.basename(cwd);
34238
34541
  }
34239
34542
  async function getGitHubUsername() {
34240
- const { stdout } = await execAsync2("gh api user -q .login");
34543
+ const { stdout } = await execAsync3("gh api user -q .login");
34241
34544
  return stdout.trim();
34242
34545
  }
34243
34546
  async function createGitHubRepo(repoName) {
34244
34547
  const username = await getGitHubUsername();
34245
34548
  const fullRepoName = `${username}/${repoName}`;
34246
- await execAsync2(`gh repo create "${fullRepoName}" --private`);
34549
+ await execAsync3(`gh repo create "${fullRepoName}" --private`);
34247
34550
  const sshUrl = `git@github.com:${fullRepoName}.git`;
34248
34551
  try {
34249
- await execAsync2("git remote remove origin");
34552
+ await execAsync3("git remote remove origin");
34250
34553
  } catch {
34251
34554
  }
34252
- await execAsync2(`git remote add origin "${sshUrl}"`);
34253
- await execAsync2(`gh repo view "${fullRepoName}" --json url`);
34555
+ await execAsync3(`git remote add origin "${sshUrl}"`);
34556
+ await execAsync3(`gh repo view "${fullRepoName}" --json url`);
34254
34557
  return `https://github.com/${fullRepoName}`;
34255
34558
  }
34256
34559
  async function checkGhInstalled() {
34257
34560
  try {
34258
- await execAsync2("which gh");
34561
+ await execAsync3("which gh");
34259
34562
  return true;
34260
34563
  } catch {
34261
34564
  return false;
@@ -34263,7 +34566,7 @@ async function checkGhInstalled() {
34263
34566
  }
34264
34567
  async function checkGhWorkflowScope() {
34265
34568
  try {
34266
- const { stdout, stderr } = await execAsync2("gh auth status 2>&1");
34569
+ const { stdout, stderr } = await execAsync3("gh auth status 2>&1");
34267
34570
  const output = stdout + stderr;
34268
34571
  return output.includes("workflow");
34269
34572
  } catch {
@@ -34272,14 +34575,14 @@ async function checkGhWorkflowScope() {
34272
34575
  }
34273
34576
  async function checkGhAuthenticated() {
34274
34577
  try {
34275
- await execAsync2("gh auth status");
34578
+ await execAsync3("gh auth status");
34276
34579
  return true;
34277
34580
  } catch {
34278
34581
  return false;
34279
34582
  }
34280
34583
  }
34281
34584
  async function setGhSecret(name, value) {
34282
- await execAsync2(`echo "${value}" | gh secret set ${name}`);
34585
+ await execAsync3(`echo "${value}" | gh secret set ${name}`);
34283
34586
  }
34284
34587
  async function setupGithubCommand() {
34285
34588
  const cwd = process.cwd();
@@ -34430,22 +34733,24 @@ Make sure this repository has a GitHub remote configured.`);
34430
34733
  Creating workflow files...`);
34431
34734
  const workflowsDir = path14.join(cwd, ".github", "workflows");
34432
34735
  await fs18.mkdir(workflowsDir, { recursive: true });
34433
- await fs18.writeFile(path14.join(workflowsDir, "sync-inbox.yml"), SYNC_INBOX_WORKFLOW);
34736
+ const claudeAuthLine = authMethod === "api_key" ? "anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}" : "claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}";
34737
+ const replaceAuth = (template) => template.replace(/\{\{CLAUDE_AUTH\}\}/g, claudeAuthLine);
34738
+ await fs18.writeFile(path14.join(workflowsDir, "sync-inbox.yml"), replaceAuth(SYNC_INBOX_WORKFLOW));
34434
34739
  console.log("✓ Created .github/workflows/sync-inbox.yml");
34435
- await fs18.writeFile(path14.join(workflowsDir, "claude-bookkeeping.yml"), CLAUDE_BOOKKEEPING_WORKFLOW);
34740
+ await fs18.writeFile(path14.join(workflowsDir, "claude-bookkeeping.yml"), replaceAuth(CLAUDE_BOOKKEEPING_WORKFLOW));
34436
34741
  console.log("✓ Created .github/workflows/claude-bookkeeping.yml");
34437
34742
  await fs18.writeFile(path14.join(workflowsDir, "sync-journal.yml"), SYNC_JOURNAL_WORKFLOW);
34438
34743
  console.log("✓ Created .github/workflows/sync-journal.yml");
34439
34744
  try {
34440
- await execAsync2("git add .github/workflows");
34441
- const { stdout: status } = await execAsync2("git status --porcelain .github/workflows");
34745
+ await execAsync3("git add .github/workflows");
34746
+ const { stdout: status } = await execAsync3("git status --porcelain .github/workflows");
34442
34747
  if (status.trim()) {
34443
34748
  const shouldCommit = await dist_default2({
34444
34749
  message: "Commit workflow files?",
34445
34750
  default: true
34446
34751
  });
34447
34752
  if (shouldCommit) {
34448
- await execAsync2('git commit -m "Add GitHub Actions for automated bookkeeping"');
34753
+ await execAsync3('git commit -m "Add GitHub Actions for automated bookkeeping"');
34449
34754
  console.log(`
34450
34755
  ✓ Committed workflow files`);
34451
34756
  }
@@ -34466,9 +34771,9 @@ Warning: Could not commit files:`);
34466
34771
  try {
34467
34772
  console.log(`
34468
34773
  Pushing to GitHub...`);
34469
- const { stdout: currentBranch } = await execAsync2("git rev-parse --abbrev-ref HEAD");
34774
+ const { stdout: currentBranch } = await execAsync3("git rev-parse --abbrev-ref HEAD");
34470
34775
  const branch = currentBranch.trim();
34471
- await execAsync2(`git push -u origin ${branch}`);
34776
+ await execAsync3(`git push -u origin ${branch}`);
34472
34777
  console.log("✓ Pushed to GitHub");
34473
34778
  } catch (error) {
34474
34779
  console.error(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ledgit-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for ledgit bookkeeping repositories",
5
5
  "repository": {
6
6
  "type": "git",