@kansei-link/bantou 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/data/exclusion-rules/README.md +104 -0
  4. package/data/exclusion-rules/jp-tax-baseline-v1.json +185 -0
  5. package/data/exclusion-rules-schema.json +109 -0
  6. package/data/keyword-dict/README.md +91 -0
  7. package/data/keyword-dict/jp-tax-baseline-v1.json +398 -0
  8. package/data/keyword-dict-schema.json +117 -0
  9. package/data/tax-rules/jp-tax-rules-v1.json +170 -0
  10. package/dist/adapters/csv-parser.d.ts +11 -0
  11. package/dist/adapters/csv-parser.js +133 -0
  12. package/dist/adapters/freee-csv-adapter.d.ts +14 -0
  13. package/dist/adapters/freee-csv-adapter.js +67 -0
  14. package/dist/adapters/generic-adapter.d.ts +20 -0
  15. package/dist/adapters/generic-adapter.js +73 -0
  16. package/dist/adapters/index.d.ts +23 -0
  17. package/dist/adapters/index.js +386 -0
  18. package/dist/adapters/types.d.ts +111 -0
  19. package/dist/adapters/types.js +9 -0
  20. package/dist/adapters/yayoi-adapter.d.ts +46 -0
  21. package/dist/adapters/yayoi-adapter.js +181 -0
  22. package/dist/bin/freee-doctor.d.ts +3 -0
  23. package/dist/bin/freee-doctor.js +15 -0
  24. package/dist/classifier/claude-classifier.d.ts +24 -0
  25. package/dist/classifier/claude-classifier.js +154 -0
  26. package/dist/classifier/keyword-classifier.d.ts +22 -0
  27. package/dist/classifier/keyword-classifier.js +124 -0
  28. package/dist/classifier/keyword-match.d.ts +21 -0
  29. package/dist/classifier/keyword-match.js +57 -0
  30. package/dist/classifier/normalize.d.ts +3 -0
  31. package/dist/classifier/normalize.js +27 -0
  32. package/dist/classifier/two-stage-classifier.d.ts +21 -0
  33. package/dist/classifier/two-stage-classifier.js +51 -0
  34. package/dist/classifier/types.d.ts +31 -0
  35. package/dist/classifier/types.js +3 -0
  36. package/dist/connectors/freee.d.ts +115 -0
  37. package/dist/connectors/freee.js +177 -0
  38. package/dist/exclusion/exclusion-checker.d.ts +10 -0
  39. package/dist/exclusion/exclusion-checker.js +162 -0
  40. package/dist/freee-doctor.d.ts +26 -0
  41. package/dist/freee-doctor.js +82 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +656 -0
  44. package/dist/memory/cockpit-memory.d.ts +73 -0
  45. package/dist/memory/cockpit-memory.js +473 -0
  46. package/dist/memory/types.d.ts +114 -0
  47. package/dist/memory/types.js +11 -0
  48. package/dist/pipeline/confidence-router.d.ts +38 -0
  49. package/dist/pipeline/confidence-router.js +129 -0
  50. package/dist/pipeline/nightly-pipeline.d.ts +44 -0
  51. package/dist/pipeline/nightly-pipeline.js +497 -0
  52. package/dist/pipeline/types.d.ts +84 -0
  53. package/dist/pipeline/types.js +12 -0
  54. package/dist/reports/monthly-report.d.ts +64 -0
  55. package/dist/reports/monthly-report.js +230 -0
  56. package/dist/secrets.d.ts +14 -0
  57. package/dist/secrets.js +86 -0
  58. package/dist/tax-rules/tax-rule-engine.d.ts +103 -0
  59. package/dist/tax-rules/tax-rule-engine.js +449 -0
  60. package/dist/tax-rules/types.d.ts +103 -0
  61. package/dist/tax-rules/types.js +7 -0
  62. package/package.json +74 -0
@@ -0,0 +1,51 @@
1
+ // Two-stage classifier orchestrator.
2
+ //
3
+ // Stage 1: keyword dict (= fast, deterministic, free)
4
+ // Stage 2: Claude API fallback (= slow-ish, AI, paid)
5
+ //
6
+ // If Stage 1 matches → return immediately (= ~95% of typical transactions).
7
+ // If no match → try Stage 2 (= remaining ~5%). If both miss → return
8
+ // unclassified (= human review queue).
9
+ export class TwoStageClassifier {
10
+ stage1;
11
+ stage2;
12
+ constructor(stage1, stage2) {
13
+ this.stage1 = stage1;
14
+ this.stage2 = stage2 || null;
15
+ }
16
+ async classify(tx) {
17
+ // Stage 1: keyword match
18
+ const s1 = this.stage1.classify(tx);
19
+ if (s1.classified) {
20
+ return { ...s1, stage: 1 };
21
+ }
22
+ // Stage 2: Claude API fallback (= only if configured)
23
+ if (this.stage2) {
24
+ const s2 = await this.stage2.classify(tx);
25
+ if (s2.classified) {
26
+ return { ...s2, stage: 2 };
27
+ }
28
+ // Stage 2 also failed → return its failure reason
29
+ return { ...s2, stage: 'unclassified' };
30
+ }
31
+ // No Stage 2 configured → return Stage 1 result (unclassified)
32
+ return { ...s1, stage: 'unclassified' };
33
+ }
34
+ hasStage2() {
35
+ return this.stage2 !== null;
36
+ }
37
+ getStage1() {
38
+ return this.stage1;
39
+ }
40
+ getStage2() {
41
+ return this.stage2;
42
+ }
43
+ }
44
+ /**
45
+ * Helper: extract category metadata from keyword dict for ClaudeClassifier construction.
46
+ * Reads from KeywordClassifier's internal data via a getCategories() method.
47
+ */
48
+ export function extractCategoryMeta(classifier) {
49
+ return classifier.getCategoriesMeta();
50
+ }
51
+ //# sourceMappingURL=two-stage-classifier.js.map
@@ -0,0 +1,31 @@
1
+ export interface Transaction {
2
+ amount: number;
3
+ memo: string;
4
+ date: string;
5
+ partner_name?: string;
6
+ company_id?: number;
7
+ }
8
+ export interface ClassificationResult {
9
+ classified: boolean;
10
+ category_id?: string;
11
+ category_name_ja?: string;
12
+ freee_account_code?: number;
13
+ tax_code?: number;
14
+ confidence: 'high' | 'medium' | 'low' | 'none';
15
+ matched_keyword?: string;
16
+ match_reason: string;
17
+ classifier_version: string;
18
+ amount_override_redirect?: string;
19
+ special_pattern?: string;
20
+ }
21
+ export interface ExclusionResult {
22
+ excluded: boolean;
23
+ rule_id?: string;
24
+ rule_name_ja?: string;
25
+ reason?: string;
26
+ suggested_next_step?: string;
27
+ action_type?: 'human_review' | 'alternative_workflow' | 'skip_silently';
28
+ alternative_workflow_id?: string;
29
+ matched_keyword?: string;
30
+ }
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,3 @@
1
+ // Shared types for keyword classifier + exclusion checker.
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,115 @@
1
+ import { FreeeSecrets } from '../secrets.js';
2
+ export interface FreeeCompany {
3
+ id: number;
4
+ display_name: string;
5
+ tax_at_source_calc_type: number;
6
+ contact_name: string;
7
+ fiscal_yearmonth?: string;
8
+ tax_method?: number;
9
+ accounting_period_start?: string;
10
+ }
11
+ export interface FreeeDeal {
12
+ id: number;
13
+ company_id: number;
14
+ issue_date: string;
15
+ type: 'income' | 'expense';
16
+ partner_id: number | null;
17
+ partner_name?: string;
18
+ account_item_id?: number;
19
+ tax_code?: number;
20
+ ref_number?: string;
21
+ amount: number;
22
+ due_amount: number;
23
+ status: 'unsettled' | 'settled';
24
+ details: FreeeDealDetail[];
25
+ description?: string;
26
+ memo?: string;
27
+ }
28
+ export interface FreeeDealDetail {
29
+ id: number;
30
+ account_item_id: number;
31
+ tax_code: number;
32
+ amount: number;
33
+ description?: string;
34
+ }
35
+ export interface FreeePartner {
36
+ id: number;
37
+ name: string;
38
+ shortcut1?: string;
39
+ shortcut2?: string;
40
+ }
41
+ export interface FreeeAccountItem {
42
+ id: number;
43
+ name: string;
44
+ shortcut?: string;
45
+ account_category?: string;
46
+ account_category_id?: number;
47
+ tax_code?: number;
48
+ group_name?: string;
49
+ available?: boolean;
50
+ }
51
+ export interface CreateDealInput {
52
+ issue_date: string;
53
+ type: 'income' | 'expense';
54
+ amount: number;
55
+ account_item_id: number;
56
+ tax_code?: number;
57
+ partner_id?: number;
58
+ ref_number?: string;
59
+ description?: string;
60
+ }
61
+ export declare class FreeeConnector {
62
+ private secrets;
63
+ constructor(secrets: FreeeSecrets, opts?: {
64
+ skipExpiryCheck?: boolean;
65
+ });
66
+ private request;
67
+ /**
68
+ * List all companies accessible by the current OAuth token.
69
+ * Multi-company method: a single token grants access to 60+ companies.
70
+ * freee API: GET /api/1/companies
71
+ */
72
+ listCompanies(): Promise<FreeeCompany[]>;
73
+ getCompany(companyId?: number): Promise<FreeeCompany>;
74
+ /**
75
+ * List deals (取引) with optional filters.
76
+ *
77
+ * @param opts.company_id Override company (default: from secrets). Required for multi-company batch.
78
+ * @param opts.status Filter by 'unsettled' (未処理) or 'settled' (処理済み).
79
+ * Multi-company method: always fetch 'unsettled' to avoid reprocessing.
80
+ */
81
+ listDeals(opts?: {
82
+ company_id?: number;
83
+ type?: 'income' | 'expense';
84
+ status?: 'unsettled' | 'settled';
85
+ start_issue_date?: string;
86
+ end_issue_date?: string;
87
+ limit?: number;
88
+ offset?: number;
89
+ }): Promise<FreeeDeal[]>;
90
+ /**
91
+ * List partners (取引先) for a company.
92
+ * @param opts.company_id Override company (default: from secrets).
93
+ */
94
+ listPartners(opts?: {
95
+ company_id?: number;
96
+ limit?: number;
97
+ offset?: number;
98
+ }): Promise<FreeePartner[]>;
99
+ listAccountItems(opts?: {
100
+ limit?: number;
101
+ offset?: number;
102
+ }): Promise<FreeeAccountItem[]>;
103
+ createPartner(name: string): Promise<FreeePartner>;
104
+ createDeal(input: CreateDealInput): Promise<FreeeDeal>;
105
+ getWalletTxns(opts?: {
106
+ walletable_type?: 'bank_account' | 'credit_card' | 'wallet';
107
+ start_date?: string;
108
+ end_date?: string;
109
+ limit?: number;
110
+ offset?: number;
111
+ }): Promise<any[]>;
112
+ get companyId(): number;
113
+ get companyName(): string;
114
+ }
115
+ //# sourceMappingURL=freee.d.ts.map
@@ -0,0 +1,177 @@
1
+ // freee API connector for Cockpit MCP.
2
+ //
3
+ // Wraps the freee Accounting API. Auth via Bearer token from secrets.ts.
4
+ // All API calls return parsed JSON or throw with sanitized error messages
5
+ // (= NEVER includes the access token).
6
+ import https from 'node:https';
7
+ import { isTokenExpired } from '../secrets.js';
8
+ const FREEE_API_BASE = 'api.freee.co.jp';
9
+ const FREEE_API_VERSION = '2020-06-15';
10
+ export class FreeeConnector {
11
+ secrets;
12
+ constructor(secrets, opts = {}) {
13
+ this.secrets = secrets;
14
+ if (!opts.skipExpiryCheck && isTokenExpired(secrets)) {
15
+ throw new Error('freee access token has expired or is about to expire. ' +
16
+ 'Please refresh manually until OAuth refresh flow is implemented. ' +
17
+ 'Re-issue token at https://developer.freee.co.jp/ and update FREEE_ACCESS_TOKEN ' +
18
+ '(or ~/.claude/secrets/freee-cockpit-dev.json).');
19
+ }
20
+ }
21
+ request(method, path, body) {
22
+ const options = {
23
+ hostname: FREEE_API_BASE,
24
+ port: 443,
25
+ path,
26
+ method,
27
+ headers: {
28
+ 'Authorization': `Bearer ${this.secrets.access_token}`,
29
+ 'Accept': 'application/json',
30
+ 'X-Api-Version': FREEE_API_VERSION,
31
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
32
+ },
33
+ timeout: 30000,
34
+ };
35
+ return new Promise((resolve, reject) => {
36
+ const req = https.request(options, (res) => {
37
+ let chunks = '';
38
+ res.on('data', (chunk) => { chunks += chunk; });
39
+ res.on('end', () => {
40
+ try {
41
+ const data = chunks ? JSON.parse(chunks) : {};
42
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
43
+ resolve(data);
44
+ }
45
+ else {
46
+ // Sanitize error: never echo body if it might contain sensitive data
47
+ const errMessage = data.errors
48
+ ? JSON.stringify(data.errors).slice(0, 300)
49
+ : `HTTP ${res.statusCode}`;
50
+ const hint = res.statusCode === 401
51
+ ? ' [401 — access token likely expired/invalid; re-issue at https://developer.freee.co.jp/ and update FREEE_ACCESS_TOKEN]'
52
+ : '';
53
+ reject(new Error(`freee API ${method} ${path} failed: ${errMessage}${hint}`));
54
+ }
55
+ }
56
+ catch (e) {
57
+ reject(new Error(`freee API parse error: ${e.message}`));
58
+ }
59
+ });
60
+ });
61
+ req.on('error', (e) => reject(new Error(`freee API request error: ${e.message}`)));
62
+ req.on('timeout', () => {
63
+ req.destroy();
64
+ reject(new Error('freee API timeout (30s)'));
65
+ });
66
+ if (body)
67
+ req.write(JSON.stringify(body));
68
+ req.end();
69
+ });
70
+ }
71
+ /**
72
+ * List all companies accessible by the current OAuth token.
73
+ * Multi-company method: a single token grants access to 60+ companies.
74
+ * freee API: GET /api/1/companies
75
+ */
76
+ async listCompanies() {
77
+ const data = await this.request('GET', '/api/1/companies');
78
+ return data.companies;
79
+ }
80
+ async getCompany(companyId) {
81
+ const id = companyId ?? this.secrets.company_id;
82
+ const data = await this.request('GET', `/api/1/companies/${id}`);
83
+ return data.company;
84
+ }
85
+ /**
86
+ * List deals (取引) with optional filters.
87
+ *
88
+ * @param opts.company_id Override company (default: from secrets). Required for multi-company batch.
89
+ * @param opts.status Filter by 'unsettled' (未処理) or 'settled' (処理済み).
90
+ * Multi-company method: always fetch 'unsettled' to avoid reprocessing.
91
+ */
92
+ async listDeals(opts = {}) {
93
+ const params = new URLSearchParams({
94
+ company_id: String(opts.company_id ?? this.secrets.company_id),
95
+ ...(opts.type ? { type: opts.type } : {}),
96
+ ...(opts.status ? { status: opts.status } : {}),
97
+ ...(opts.start_issue_date ? { start_issue_date: opts.start_issue_date } : {}),
98
+ ...(opts.end_issue_date ? { end_issue_date: opts.end_issue_date } : {}),
99
+ limit: String(opts.limit ?? 20),
100
+ offset: String(opts.offset ?? 0),
101
+ });
102
+ const data = await this.request('GET', `/api/1/deals?${params}`);
103
+ return data.deals;
104
+ }
105
+ /**
106
+ * List partners (取引先) for a company.
107
+ * @param opts.company_id Override company (default: from secrets).
108
+ */
109
+ async listPartners(opts = {}) {
110
+ const params = new URLSearchParams({
111
+ company_id: String(opts.company_id ?? this.secrets.company_id),
112
+ limit: String(opts.limit ?? 20),
113
+ offset: String(opts.offset ?? 0),
114
+ });
115
+ const data = await this.request('GET', `/api/1/partners?${params}`);
116
+ return data.partners;
117
+ }
118
+ async listAccountItems(opts = {}) {
119
+ const params = new URLSearchParams({
120
+ company_id: String(this.secrets.company_id),
121
+ });
122
+ if (opts.limit !== undefined)
123
+ params.set('limit', String(opts.limit));
124
+ if (opts.offset !== undefined)
125
+ params.set('offset', String(opts.offset));
126
+ const data = await this.request('GET', `/api/1/account_items?${params}`);
127
+ return data.account_items;
128
+ }
129
+ async createPartner(name) {
130
+ const body = {
131
+ company_id: this.secrets.company_id,
132
+ name,
133
+ };
134
+ const data = await this.request('POST', '/api/1/partners', body);
135
+ return data.partner;
136
+ }
137
+ async createDeal(input) {
138
+ const body = {
139
+ company_id: this.secrets.company_id,
140
+ issue_date: input.issue_date,
141
+ type: input.type,
142
+ details: [
143
+ {
144
+ account_item_id: input.account_item_id,
145
+ tax_code: input.tax_code ?? 0,
146
+ amount: input.amount,
147
+ ...(input.description ? { description: input.description } : {}),
148
+ },
149
+ ],
150
+ };
151
+ if (input.partner_id)
152
+ body.partner_id = input.partner_id;
153
+ if (input.ref_number)
154
+ body.ref_number = input.ref_number;
155
+ const data = await this.request('POST', '/api/1/deals', body);
156
+ return data.deal;
157
+ }
158
+ async getWalletTxns(opts = {}) {
159
+ const params = new URLSearchParams({
160
+ company_id: String(this.secrets.company_id),
161
+ ...(opts.walletable_type ? { walletable_type: opts.walletable_type } : {}),
162
+ ...(opts.start_date ? { start_date: opts.start_date } : {}),
163
+ ...(opts.end_date ? { end_date: opts.end_date } : {}),
164
+ limit: String(opts.limit ?? 20),
165
+ offset: String(opts.offset ?? 0),
166
+ });
167
+ const data = await this.request('GET', `/api/1/wallet_txns?${params}`);
168
+ return data.wallet_txns;
169
+ }
170
+ get companyId() {
171
+ return this.secrets.company_id;
172
+ }
173
+ get companyName() {
174
+ return this.secrets.company_name;
175
+ }
176
+ }
177
+ //# sourceMappingURL=freee.js.map
@@ -0,0 +1,10 @@
1
+ import { Transaction, ExclusionResult } from '../classifier/types.js';
2
+ export declare class ExclusionChecker {
3
+ private rules;
4
+ private rulesFile;
5
+ constructor(rulesFile?: string, dataDir?: string);
6
+ check(tx: Transaction, employees?: string[]): ExclusionResult;
7
+ getVersion(): string;
8
+ getRulesCount(): number;
9
+ }
10
+ //# sourceMappingURL=exclusion-checker.d.ts.map
@@ -0,0 +1,162 @@
1
+ // Stage 0 exclusion checker.
2
+ //
3
+ // Reads jp-tax-baseline-v1.json exclusion rules and checks if a transaction
4
+ // should be escalated to human review (= NOT auto-classified).
5
+ //
6
+ // Match types:
7
+ // - regex: pattern match against field
8
+ // - any_keyword: substring list match
9
+ // - any_keyword_or_pattern: keyword + special detector (e.g., transfer_to_employee)
10
+ //
11
+ // Returns excluded:true with rule_id + reason if matched.
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { normalizeMemo } from '../classifier/normalize.js';
16
+ import { keywordMatches } from '../classifier/keyword-match.js';
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+ function defaultDataDir() {
20
+ const envDir = process.env.COCKPIT_DATA_DIR;
21
+ if (envDir)
22
+ return envDir;
23
+ return path.resolve(__dirname, '../../../../data');
24
+ }
25
+ export class ExclusionChecker {
26
+ rules;
27
+ rulesFile;
28
+ constructor(rulesFile, dataDir) {
29
+ const dir = dataDir || defaultDataDir();
30
+ this.rulesFile = rulesFile || path.join(dir, 'exclusion-rules', 'jp-tax-baseline-v1.json');
31
+ if (!fs.existsSync(this.rulesFile)) {
32
+ throw new Error(`Exclusion rules not found at ${this.rulesFile}. ` +
33
+ `Set COCKPIT_DATA_DIR env var or place data files at the expected path.`);
34
+ }
35
+ const raw = fs.readFileSync(this.rulesFile, 'utf8');
36
+ this.rules = JSON.parse(raw);
37
+ // Sort by priority
38
+ this.rules.rules.sort((a, b) => a.priority - b.priority);
39
+ }
40
+ check(tx, employees) {
41
+ const memo = tx.memo || '';
42
+ const normalizedMemo = normalizeMemo(memo);
43
+ for (const rule of this.rules.rules) {
44
+ const fieldValue = (() => {
45
+ switch (rule.match.field) {
46
+ case 'memo': return memo;
47
+ case 'amount': return String(tx.amount);
48
+ case 'partner_name': return tx.partner_name || '';
49
+ case 'date': return tx.date;
50
+ default: return '';
51
+ }
52
+ })();
53
+ const normalizedField = normalizeMemo(fieldValue);
54
+ let matchedKeyword;
55
+ let matched = false;
56
+ switch (rule.match.type) {
57
+ case 'regex': {
58
+ if (rule.match.pattern) {
59
+ try {
60
+ const re = new RegExp(rule.match.pattern, rule.match.flags);
61
+ if (re.test(fieldValue)) {
62
+ matched = true;
63
+ matchedKeyword = `regex:${rule.match.pattern}`;
64
+ }
65
+ }
66
+ catch {
67
+ // invalid regex, skip
68
+ }
69
+ }
70
+ // Also check alternative_patterns
71
+ if (!matched && rule.alternative_patterns) {
72
+ for (const alt of rule.alternative_patterns) {
73
+ if (keywordMatches(normalizedField, normalizeMemo(alt))) {
74
+ matched = true;
75
+ matchedKeyword = alt;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ break;
81
+ }
82
+ case 'any_keyword': {
83
+ if (rule.match.keywords) {
84
+ for (const kw of rule.match.keywords) {
85
+ if (keywordMatches(normalizedField, normalizeMemo(kw))) {
86
+ matched = true;
87
+ matchedKeyword = kw;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ break;
93
+ }
94
+ case 'any_keyword_or_pattern': {
95
+ // First check keywords
96
+ if (rule.match.keywords) {
97
+ for (const kw of rule.match.keywords) {
98
+ if (keywordMatches(normalizedField, normalizeMemo(kw))) {
99
+ matched = true;
100
+ matchedKeyword = kw;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ // Then check patterns (= special detectors)
106
+ if (!matched && rule.match.patterns) {
107
+ for (const pattern of rule.match.patterns) {
108
+ if (pattern.type === 'transfer_to_employee' && employees && employees.length > 0) {
109
+ // Check if memo contains 振込 + employee name
110
+ if (/振込|振替/.test(memo)) {
111
+ for (const emp of employees) {
112
+ if (keywordMatches(normalizedField, normalizeMemo(emp))) {
113
+ matched = true;
114
+ matchedKeyword = `transfer_to_employee:${emp}`;
115
+ break;
116
+ }
117
+ }
118
+ if (matched)
119
+ break;
120
+ }
121
+ }
122
+ // Other pattern types could be added here
123
+ }
124
+ }
125
+ break;
126
+ }
127
+ case 'exact': {
128
+ if (rule.match.keywords) {
129
+ for (const kw of rule.match.keywords) {
130
+ if (normalizedField === normalizeMemo(kw)) {
131
+ matched = true;
132
+ matchedKeyword = kw;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ break;
138
+ }
139
+ }
140
+ if (matched) {
141
+ return {
142
+ excluded: true,
143
+ rule_id: rule.id,
144
+ rule_name_ja: rule.name_ja,
145
+ reason: rule.action.reason_template,
146
+ suggested_next_step: rule.action.suggested_next_step,
147
+ action_type: rule.action.type,
148
+ alternative_workflow_id: rule.action.alternative_workflow_id,
149
+ matched_keyword: matchedKeyword,
150
+ };
151
+ }
152
+ }
153
+ return { excluded: false };
154
+ }
155
+ getVersion() {
156
+ return this.rules.version;
157
+ }
158
+ getRulesCount() {
159
+ return this.rules.rules.length;
160
+ }
161
+ }
162
+ //# sourceMappingURL=exclusion-checker.js.map
@@ -0,0 +1,26 @@
1
+ export interface FreeeDoctorReport {
2
+ ok: boolean;
3
+ stage?: string;
4
+ token_source: {
5
+ env_FREEE_ACCESS_TOKEN: 'set' | 'not set';
6
+ env_FREEE_COMPANY_ID: string;
7
+ env_FREEE_TOKEN_EXPIRES_AT: string;
8
+ secrets_file: string;
9
+ };
10
+ token_expires_at?: string;
11
+ token_expires_at_parseable?: boolean;
12
+ token_expired_or_near?: boolean;
13
+ note?: string;
14
+ configured_company_id?: number | null;
15
+ live_connection?: boolean;
16
+ accessible_companies?: Array<{
17
+ id: number;
18
+ display_name: string;
19
+ }>;
20
+ live_error?: string;
21
+ verdict?: string | null;
22
+ action_required?: string | null;
23
+ error?: string;
24
+ }
25
+ export declare function runFreeeDoctor(): Promise<FreeeDoctorReport>;
26
+ //# sourceMappingURL=freee-doctor.d.ts.map
@@ -0,0 +1,82 @@
1
+ // freee connection doctor — one-shot diagnostic for the three freee trip-wires:
2
+ // ① token source — FREEE_ACCESS_TOKEN env var vs the secrets file
3
+ // ② token expiry — 24h TTL, no auto-refresh
4
+ // ③ which company_id — the "I have 4-5 test 事業所, which one is live?" problem
5
+ //
6
+ // Reads credentials at runtime; NEVER prints the access token.
7
+ import { loadFreeeSecrets, isTokenExpired } from './secrets.js';
8
+ import { FreeeConnector } from './connectors/freee.js';
9
+ export async function runFreeeDoctor() {
10
+ const env = process.env;
11
+ const token_source = {
12
+ env_FREEE_ACCESS_TOKEN: (env.FREEE_ACCESS_TOKEN ? 'set' : 'not set'),
13
+ env_FREEE_COMPANY_ID: env.FREEE_COMPANY_ID || 'not set',
14
+ // An env var here OVERRIDES the file (secrets.ts) — so a stale/malformed value set in the
15
+ // shell can silently "revive" after you fix the file. Surfaced so that case is diagnosable.
16
+ env_FREEE_TOKEN_EXPIRES_AT: env.FREEE_TOKEN_EXPIRES_AT || 'not set',
17
+ secrets_file: '~/.claude/secrets/freee-cockpit-dev.json',
18
+ };
19
+ // ① — can we source a token at all? (company_id NOT required here — that's the whole point:
20
+ // you must be able to discover the right company_id WITHOUT already having one.)
21
+ let secrets;
22
+ try {
23
+ secrets = loadFreeeSecrets(undefined, { requireCompanyId: false });
24
+ }
25
+ catch (err) {
26
+ return {
27
+ ok: false,
28
+ stage: 'token_load',
29
+ token_source,
30
+ error: err?.message ?? String(err),
31
+ action_required: 'No usable access token. Set FREEE_ACCESS_TOKEN (env / plugin settings) or create the secrets file, then re-run.',
32
+ };
33
+ }
34
+ // ② — is the token alive?
35
+ const expired = isTokenExpired(secrets);
36
+ const expiresParseable = !!secrets.token_expires_at && !Number.isNaN(new Date(secrets.token_expires_at).getTime());
37
+ const configured = Number.isFinite(secrets.company_id) ? secrets.company_id : null;
38
+ const report = {
39
+ ok: true,
40
+ token_source,
41
+ token_expires_at: secrets.token_expires_at || '(not recorded — cannot pre-check expiry)',
42
+ token_expires_at_parseable: expiresParseable,
43
+ token_expired_or_near: expired,
44
+ configured_company_id: configured,
45
+ };
46
+ if (secrets.token_expires_at && !expiresParseable) {
47
+ report.note =
48
+ 'token_expires_at is present but NOT a parseable date (check for stray spaces, e.g. "16: 00: 00"). ' +
49
+ 'A malformed expiry silently defeats the pre-flight expiry check — fix the timestamp or just re-issue the token.';
50
+ }
51
+ // ③ — list every accessible 事業所 and validate the configured id against reality.
52
+ try {
53
+ const conn = new FreeeConnector(secrets, { skipExpiryCheck: true });
54
+ const companies = await conn.listCompanies();
55
+ report.live_connection = true;
56
+ report.accessible_companies = companies.map((c) => ({ id: c.id, display_name: c.display_name }));
57
+ if (configured === null) {
58
+ report.action_required =
59
+ 'No company_id configured. Pick an id from accessible_companies and set FREEE_COMPANY_ID ' +
60
+ '(or company_id in the secrets file).';
61
+ }
62
+ else if (!companies.some((c) => c.id === configured)) {
63
+ report.action_required =
64
+ `Configured company_id ${configured} is NOT accessible by this token — likely a dead/duplicate ` +
65
+ `test company. Switch to one of the ids listed in accessible_companies.`;
66
+ }
67
+ else {
68
+ report.verdict = `OK — token is live and company_id ${configured} is valid.`;
69
+ report.action_required = null;
70
+ }
71
+ }
72
+ catch (err) {
73
+ report.ok = false;
74
+ report.live_connection = false;
75
+ report.live_error = err?.message ?? String(err);
76
+ report.action_required = expired
77
+ ? 'Token is expired/near-expiry. Re-issue at https://developer.freee.co.jp/ and update FREEE_ACCESS_TOKEN (or the secrets file).'
78
+ : 'Could not list companies. If the error is 401, the token is invalid/expired — re-issue at https://developer.freee.co.jp/.';
79
+ }
80
+ return report;
81
+ }
82
+ //# sourceMappingURL=freee-doctor.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map