ledgit-cli 0.2.1 → 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 +457 -141
  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,6 +27937,7 @@ 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!
@@ -28925,6 +28957,8 @@ async function updateEnvFile(cwd) {
28925
28957
  // src/commands/update.ts
28926
28958
  import * as fs17 from "node:fs/promises";
28927
28959
  import * as path13 from "node:path";
28960
+ import { exec as exec6 } from "node:child_process";
28961
+ import { promisify as promisify6 } from "node:util";
28928
28962
  import ora6 from "ora";
28929
28963
 
28930
28964
  // ../../node_modules/ansi-regex/index.js
@@ -34005,83 +34039,6 @@ var dist_default8 = createPrompt4((config3, done) => {
34005
34039
  `).trimEnd();
34006
34040
  return `${lines}${cursorHide4}`;
34007
34041
  });
34008
- // src/commands/update.ts
34009
- async function updateCommand(options = {}) {
34010
- const cwd = process.cwd();
34011
- const agentsPath = path13.join(cwd, "AGENTS.md");
34012
- let companyName;
34013
- let provider;
34014
- try {
34015
- const content = await fs17.readFile(agentsPath, "utf-8");
34016
- const match = content.match(/repository for (.+?) \(Swedish company\).+?integrates with (Fortnox|Bokio)/s);
34017
- if (!match || !match[1] || !match[2]) {
34018
- console.error("Could not parse company info from AGENTS.md");
34019
- process.exit(1);
34020
- }
34021
- companyName = match[1];
34022
- provider = match[2];
34023
- console.log(` Company: ${companyName}`);
34024
- console.log(` Provider: ${provider}`);
34025
- } catch {
34026
- console.error("AGENTS.md not found. Run this command in a ledgit repository.");
34027
- process.exit(1);
34028
- }
34029
- const newContent = renderAgentsTemplate({
34030
- companyName,
34031
- provider
34032
- });
34033
- if (!options.force) {
34034
- const currentContent = await fs17.readFile(agentsPath, "utf-8");
34035
- const hasCustomChanges = currentContent !== newContent;
34036
- if (hasCustomChanges) {
34037
- const proceed = await dist_default2({
34038
- message: "AGENTS.md has been modified. Overwrite with latest template?",
34039
- default: true
34040
- });
34041
- if (!proceed) {
34042
- console.log(" Update cancelled.");
34043
- return;
34044
- }
34045
- }
34046
- }
34047
- const updateSpinner = ora6("Updating AGENTS.md...").start();
34048
- try {
34049
- await fs17.writeFile(agentsPath, newContent, "utf-8");
34050
- const claudePath = path13.join(cwd, "CLAUDE.md");
34051
- try {
34052
- const stat2 = await fs17.lstat(claudePath);
34053
- if (!stat2.isSymbolicLink()) {
34054
- await fs17.unlink(claudePath);
34055
- await fs17.symlink("AGENTS.md", claudePath);
34056
- }
34057
- } catch {
34058
- await fs17.symlink("AGENTS.md", claudePath);
34059
- }
34060
- updateSpinner.succeed("Updated AGENTS.md");
34061
- } catch (error) {
34062
- updateSpinner.fail("Failed to update AGENTS.md");
34063
- console.error(error instanceof Error ? error.message : "Unknown error");
34064
- process.exit(1);
34065
- }
34066
- const commitMessage = "Update AGENTS.md to latest version";
34067
- const committed = await commitAll(cwd, commitMessage);
34068
- if (committed) {
34069
- console.log(`
34070
- Committed: "${commitMessage}"
34071
- `);
34072
- } else {
34073
- console.log(`
34074
- No changes to commit.
34075
- `);
34076
- }
34077
- }
34078
-
34079
- // src/commands/setup-github.ts
34080
- import * as fs18 from "node:fs/promises";
34081
- import * as path14 from "node:path";
34082
- import { exec as exec6 } from "node:child_process";
34083
- import { promisify as promisify6 } from "node:util";
34084
-
34085
34042
  // src/templates/workflows.ts
34086
34043
  var SYNC_INBOX_WORKFLOW = `name: Sync Inbox
34087
34044
  on:
@@ -34096,7 +34053,6 @@ jobs:
34096
34053
  contents: write
34097
34054
  pull-requests: write
34098
34055
  issues: write
34099
- id-token: write
34100
34056
  steps:
34101
34057
  - uses: actions/checkout@v5
34102
34058
 
@@ -34113,6 +34069,14 @@ jobs:
34113
34069
  BOKIO_TOKEN: \${{ secrets.BOKIO_TOKEN }}
34114
34070
  BOKIO_COMPANY_ID: \${{ secrets.BOKIO_COMPANY_ID }}
34115
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
+
34116
34080
  - name: Check for new items
34117
34081
  id: check
34118
34082
  run: |
@@ -34122,32 +34086,89 @@ jobs:
34122
34086
  echo "has_new=false" >> $GITHUB_OUTPUT
34123
34087
  fi
34124
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
+
34125
34095
  - name: Bookkeep with Claude
34126
34096
  if: steps.check.outputs.has_new == 'true'
34127
- 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
34128
34168
  with:
34129
- anthropic_api_key: \${{ secrets.ANTHROPIC_API_KEY }}
34130
- prompt: |
34131
- New documents synced to inbox/ (unstaged). Read AGENTS.md for instructions.
34132
-
34133
- YOU handle all git operations. The inbox files are NOT committed yet.
34134
-
34135
- FIRST: Check for existing open bookkeeping PRs with \\\`gh pr list\\\`.
34136
- - If there's an active PR, checkout that branch and add to it
34137
- - Otherwise, create a new branch: book/{date}-{description}
34138
- - Group related entries (same vendor, same week) in one PR when sensible
34139
-
34140
- THEN:
34141
- 1. Check each new item in inbox/ (you can read PDFs directly)
34142
- 2. Find similar entries in journal-entries/ for reference
34143
- 3. Create entries with \\\`npx ledgit-cli create-entry\\\`
34144
- 4. Stage and commit all changes (inbox files + entries)
34145
- 5. Push and create/update PR
34146
-
34147
- If anything is unclear, commit the inbox files anyway and ask questions in the PR description.
34148
- claude_args: |
34149
- --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(gh pr:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)"
34150
- track_progress: true
34169
+ name: claude-execution-log
34170
+ path: /tmp/claude-output.json
34171
+ retention-days: 7
34151
34172
  `;
34152
34173
  var CLAUDE_BOOKKEEPING_WORKFLOW = `name: Claude Bookkeeping
34153
34174
  on:
@@ -34164,25 +34185,118 @@ jobs:
34164
34185
  contents: write
34165
34186
  pull-requests: write
34166
34187
  issues: write
34167
- id-token: write
34168
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
+
34169
34198
  - uses: actions/checkout@v5
34170
34199
  with:
34171
- ref: \${{ github.head_ref }}
34200
+ ref: \${{ steps.pr.outputs.branch }}
34172
34201
 
34173
34202
  - uses: oven-sh/setup-bun@v2
34174
34203
 
34175
- - 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
34176
34239
  with:
34177
- anthropic_api_key: \${{ secrets.ANTHROPIC_API_KEY }}
34178
- prompt: |
34179
- 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.
34180
34260
 
34181
- Respond to the user's latest comment. If they answered your questions, continue creating the journal entries.
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
34182
34266
 
34183
- Read AGENTS.md for bookkeeping instructions.
34184
- claude_args: |
34185
- --allowedTools "Read,Write,Edit,Bash(npx ledgit-cli:*),Bash(git:*),Bash(ls:*),Bash(find:*),Bash(grep:*),Bash(cat:*)"
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
34288
+
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
34186
34300
  `;
34187
34301
  var SYNC_JOURNAL_WORKFLOW = `name: Sync Journal
34188
34302
  on:
@@ -34212,11 +34326,211 @@ jobs:
34212
34326
  BOKIO_COMPANY_ID: \${{ secrets.BOKIO_COMPANY_ID }}
34213
34327
  `;
34214
34328
 
34215
- // src/commands/setup-github.ts
34329
+ // src/commands/update.ts
34216
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);
34217
34531
  async function hasGitRemote() {
34218
34532
  try {
34219
- const { stdout } = await execAsync2("git remote -v");
34533
+ const { stdout } = await execAsync3("git remote -v");
34220
34534
  return stdout.trim().length > 0;
34221
34535
  } catch {
34222
34536
  return false;
@@ -34226,25 +34540,25 @@ async function getRepoName(cwd) {
34226
34540
  return path14.basename(cwd);
34227
34541
  }
34228
34542
  async function getGitHubUsername() {
34229
- const { stdout } = await execAsync2("gh api user -q .login");
34543
+ const { stdout } = await execAsync3("gh api user -q .login");
34230
34544
  return stdout.trim();
34231
34545
  }
34232
34546
  async function createGitHubRepo(repoName) {
34233
34547
  const username = await getGitHubUsername();
34234
34548
  const fullRepoName = `${username}/${repoName}`;
34235
- await execAsync2(`gh repo create "${fullRepoName}" --private`);
34549
+ await execAsync3(`gh repo create "${fullRepoName}" --private`);
34236
34550
  const sshUrl = `git@github.com:${fullRepoName}.git`;
34237
34551
  try {
34238
- await execAsync2("git remote remove origin");
34552
+ await execAsync3("git remote remove origin");
34239
34553
  } catch {
34240
34554
  }
34241
- await execAsync2(`git remote add origin "${sshUrl}"`);
34242
- 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`);
34243
34557
  return `https://github.com/${fullRepoName}`;
34244
34558
  }
34245
34559
  async function checkGhInstalled() {
34246
34560
  try {
34247
- await execAsync2("which gh");
34561
+ await execAsync3("which gh");
34248
34562
  return true;
34249
34563
  } catch {
34250
34564
  return false;
@@ -34252,7 +34566,7 @@ async function checkGhInstalled() {
34252
34566
  }
34253
34567
  async function checkGhWorkflowScope() {
34254
34568
  try {
34255
- const { stdout, stderr } = await execAsync2("gh auth status 2>&1");
34569
+ const { stdout, stderr } = await execAsync3("gh auth status 2>&1");
34256
34570
  const output = stdout + stderr;
34257
34571
  return output.includes("workflow");
34258
34572
  } catch {
@@ -34261,14 +34575,14 @@ async function checkGhWorkflowScope() {
34261
34575
  }
34262
34576
  async function checkGhAuthenticated() {
34263
34577
  try {
34264
- await execAsync2("gh auth status");
34578
+ await execAsync3("gh auth status");
34265
34579
  return true;
34266
34580
  } catch {
34267
34581
  return false;
34268
34582
  }
34269
34583
  }
34270
34584
  async function setGhSecret(name, value) {
34271
- await execAsync2(`echo "${value}" | gh secret set ${name}`);
34585
+ await execAsync3(`echo "${value}" | gh secret set ${name}`);
34272
34586
  }
34273
34587
  async function setupGithubCommand() {
34274
34588
  const cwd = process.cwd();
@@ -34419,22 +34733,24 @@ Make sure this repository has a GitHub remote configured.`);
34419
34733
  Creating workflow files...`);
34420
34734
  const workflowsDir = path14.join(cwd, ".github", "workflows");
34421
34735
  await fs18.mkdir(workflowsDir, { recursive: true });
34422
- 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));
34423
34739
  console.log("✓ Created .github/workflows/sync-inbox.yml");
34424
- 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));
34425
34741
  console.log("✓ Created .github/workflows/claude-bookkeeping.yml");
34426
34742
  await fs18.writeFile(path14.join(workflowsDir, "sync-journal.yml"), SYNC_JOURNAL_WORKFLOW);
34427
34743
  console.log("✓ Created .github/workflows/sync-journal.yml");
34428
34744
  try {
34429
- await execAsync2("git add .github/workflows");
34430
- 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");
34431
34747
  if (status.trim()) {
34432
34748
  const shouldCommit = await dist_default2({
34433
34749
  message: "Commit workflow files?",
34434
34750
  default: true
34435
34751
  });
34436
34752
  if (shouldCommit) {
34437
- await execAsync2('git commit -m "Add GitHub Actions for automated bookkeeping"');
34753
+ await execAsync3('git commit -m "Add GitHub Actions for automated bookkeeping"');
34438
34754
  console.log(`
34439
34755
  ✓ Committed workflow files`);
34440
34756
  }
@@ -34455,9 +34771,9 @@ Warning: Could not commit files:`);
34455
34771
  try {
34456
34772
  console.log(`
34457
34773
  Pushing to GitHub...`);
34458
- const { stdout: currentBranch } = await execAsync2("git rev-parse --abbrev-ref HEAD");
34774
+ const { stdout: currentBranch } = await execAsync3("git rev-parse --abbrev-ref HEAD");
34459
34775
  const branch = currentBranch.trim();
34460
- await execAsync2(`git push -u origin ${branch}`);
34776
+ await execAsync3(`git push -u origin ${branch}`);
34461
34777
  console.log("✓ Pushed to GitHub");
34462
34778
  } catch (error) {
34463
34779
  console.error(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ledgit-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for ledgit bookkeeping repositories",
5
5
  "repository": {
6
6
  "type": "git",