@flydocs/cli 0.6.0-alpha.11 → 0.6.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ var CLI_VERSION, CLI_NAME, PACKAGE_NAME, POSTHOG_API_KEY;
15
15
  var init_constants = __esm({
16
16
  "src/lib/constants.ts"() {
17
17
  "use strict";
18
- CLI_VERSION = "0.6.0-alpha.11";
18
+ CLI_VERSION = "0.6.0-alpha.12";
19
19
  CLI_NAME = "flydocs";
20
20
  PACKAGE_NAME = "@flydocs/cli";
21
21
  POSTHOG_API_KEY = "phc_v1MSJTQDFkMS90CBh3mxIz3v8bYCCnKU6v1ir6bz0Xn";
@@ -294,6 +294,7 @@ function extractPreservedValues(config) {
294
294
  tier: config.tier,
295
295
  setupComplete: config.setupComplete ?? false,
296
296
  workspaceId: config.workspaceId ?? null,
297
+ configVersion: config.configVersion,
297
298
  workspace: config.workspace ?? {},
298
299
  issueLabels: config.issueLabels ?? {},
299
300
  detectedStack: config.detectedStack ?? {},
@@ -312,6 +313,9 @@ async function mergeConfig(templateDir, version, tierFlag, preserved) {
312
313
  config.tier = tierFlag ?? preserved.tier;
313
314
  config.setupComplete = preserved.setupComplete;
314
315
  config.workspaceId = preserved.workspaceId;
316
+ if (preserved.configVersion !== void 0) {
317
+ config.configVersion = preserved.configVersion;
318
+ }
315
319
  if (Object.keys(preserved.workspace).length > 0) {
316
320
  config.workspace = preserved.workspace;
317
321
  }
@@ -1523,6 +1527,8 @@ var init_gitignore = __esm({
1523
1527
  ".flydocs/session.json",
1524
1528
  ".flydocs/logs/",
1525
1529
  ".flydocs/backup-*/",
1530
+ ".flydocs/me.json",
1531
+ ".flydocs/validation-cache.json",
1526
1532
  "flydocs/context/graph.json"
1527
1533
  ];
1528
1534
  FULL_GITIGNORE_TEMPLATE = `# Environment
@@ -1534,6 +1540,8 @@ var init_gitignore = __esm({
1534
1540
  .flydocs/session.json
1535
1541
  .flydocs/logs/
1536
1542
  .flydocs/backup-*/
1543
+ .flydocs/me.json
1544
+ .flydocs/validation-cache.json
1537
1545
  flydocs/context/graph.json
1538
1546
 
1539
1547
  # Dependencies
@@ -2881,6 +2889,7 @@ var init_update = __esm({
2881
2889
  tier: "cloud",
2882
2890
  setupComplete: false,
2883
2891
  workspaceId: null,
2892
+ configVersion: void 0,
2884
2893
  workspace: {},
2885
2894
  issueLabels: {},
2886
2895
  detectedStack: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.11",
3
+ "version": "0.6.0-alpha.12",
4
4
  "type": "module",
5
5
  "description": "FlyDocs AI CLI — install, setup, and manage FlyDocs projects",
6
6
  "bin": {
@@ -470,17 +470,38 @@ Ask about product metadata:
470
470
  from project.md)
471
471
  - **Icon and color** — optional, ask if they have preferences
472
472
 
473
- **Step 8: Save to config.**
473
+ **Step 8: Generate config from relay.**
474
474
 
475
- Update `.flydocs/config.json`:
475
+ The setup scripts in Steps 2-7 all POST to the relay — they no longer write
476
+ to local config. After all setup steps complete, pull the canonical config:
476
477
 
477
- - `provider.type` — set by `set_provider.py`
478
- - `provider.teamId` — selected team ID (set by `set_team.py`)
479
- - `labels.defaults` — default label names (set by `set_labels.py`)
480
- - `labels.typeMap` — type-to-label mapping (set by `set_labels.py`)
481
- - `statusMapping` FlyDocs-to-provider status mapping (set by `set_status_mapping.py`)
482
- - `workspace.activeProjects` add the project ID
483
- - `workspace.product.name` — product name
478
+ ```bash
479
+ python3 .claude/skills/flydocs-cloud/scripts/generate_config.py
480
+ ```
481
+
482
+ This calls `GET /config/generate` which returns all server-owned fields
483
+ (`workspaceId`, `setupComplete`, `workspace`, `issueLabels`). The script
484
+ merges these with local-only fields (`tier`, `detectedStack`, `skills`,
485
+ `designSystem`, `aiLabor`, `paths`) and writes `.flydocs/config.json`.
486
+
487
+ It also cleans up ghost fields (`provider`, `statusMapping`, `labels`) that
488
+ were previously written by individual setup scripts but aren't in the TS
489
+ type system.
490
+
491
+ If the response includes `missing` or `warnings`, show them to the user
492
+ but don't block — they can fix in the dashboard and re-run later.
493
+
494
+ **Step 9: Fetch user identity.**
495
+
496
+ ```bash
497
+ python3 .claude/skills/flydocs-cloud/scripts/get_me.py
498
+ ```
499
+
500
+ This calls `GET /auth/me` (no workspace required) and writes
501
+ `.flydocs/me.json` with `displayName`, `email`, `providerId`, and
502
+ `providerIdentities`. The file is gitignored — each developer gets their own.
503
+
504
+ This enables `--mine` filters and personalizes session output.
484
505
 
485
506
  ---
486
507
 
@@ -677,9 +698,14 @@ Tier: [local / cloud]
677
698
 
678
699
  **Mark setup as complete.**
679
700
 
680
- Update `.flydocs/config.json` to set `setupComplete` to `true`. This disables
681
- the setup reminder in the prompt hook. Read the config, set the field, and
682
- write it back:
701
+ For **cloud tier**: Setup completion is determined by the relay. The
702
+ `generate_config.py` call in Phase 2 Step 8 already sets `setupComplete`
703
+ based on the relay's validation result. If the relay returns `valid: true`,
704
+ config will have `setupComplete: true`. If not, run `validate_setup.py` to
705
+ check what's still missing.
706
+
707
+ For **local tier**: Update `.flydocs/config.json` to set `setupComplete` to
708
+ `true`. Read the config, set the field, and write it back:
683
709
 
684
710
  ```json
685
711
  { "setupComplete": true }
@@ -59,26 +59,29 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
59
59
 
60
60
  ### Workspace Scripts
61
61
 
62
- | Script | Usage | Output |
63
- | ----------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
64
- | `list_providers.py` | (no args) | `[{type, name, connected}]` |
65
- | `set_provider.py` | `<provider_type>` (`linear` or `jira`) | `{success}` — updates relay routing and local config `provider.type` |
66
- | `list_teams.py` | (no args) | `[{id, name, key}]` — returns Linear teams or Jira projects (relay normalizes) |
67
- | `create_team.py` | `--name "..." [--key KEY] [--description "..."] [--parent <team_id>]` | `{id, name, key}` — `--parent` is Linear-only (sub-teams) |
68
- | `set_team.py` | `<team_id>` | `{success}` — updates relay preference and local config; for Jira, sets the active Jira project |
69
- | `list_labels.py` | (no args) | `[{id, name, color}]` — requires team to be set first |
70
- | `set_labels.py` | `--defaults '["a"]' --type-map '{"feature":["F"],...}' \| stdin` | `{success, validated, defaults, typeMap}` — stores label config on relay |
71
- | `list_statuses.py` | (no args) | `{states, currentMapping, flydocsStatuses}` — provider workflow states and current mapping |
72
- | `set_status_mapping.py` | `--auto \| --mapping '{"BACKLOG":"Backlog",...}' \| stdin` | `{success, mapping, matched, total}` — stores status mapping on relay |
73
- | `set_identity.py` | `<provider> <provider-user-id>` | `{success, provider, providerId}` — binds provider user ID for `--mine` resolution |
74
- | `set_preferences.py` | `[--workspace ID] [--assignee self\|ID] [--display JSON]` | `{success, preferences}` — no flags = GET current; with flags = POST update |
75
- | `get_estimate_scale.py` | (no args) | `{scale, type}` — provider's valid estimate values (fixed or freeform) |
76
- | `refresh_labels.py` | `[--fix]` | `{valid, stale, details}` — validates config label IDs against relay; `--fix` updates stale IDs |
77
- | `validate_setup.py` | (no args) | `{valid, checks, passed[], missing[], warnings[]}` — reads relay validation, caches result, sets setupComplete |
62
+ | Script | Usage | Output |
63
+ | ----------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
64
+ | `list_providers.py` | (no args) | `[{type, name, connected}]` |
65
+ | `set_provider.py` | `<provider_type>` (`linear` or `jira`) | `{success}` — updates relay routing (no local config write) |
66
+ | `list_teams.py` | (no args) | `[{id, name, key}]` — returns Linear teams or Jira projects (relay normalizes) |
67
+ | `create_team.py` | `--name "..." [--key KEY] [--description "..."] [--parent <team_id>]` | `{id, name, key}` — `--parent` is Linear-only (sub-teams) |
68
+ | `set_team.py` | `<team_id>` | `{success}` — updates relay preference (no local config write); for Jira, sets the active Jira project |
69
+ | `list_labels.py` | (no args) | `[{id, name, color}]` — requires team to be set first |
70
+ | `set_labels.py` | `--defaults '["a"]' --type-map '{"feature":["F"],...}' \| stdin` | `{success, validated, defaults, typeMap}` — stores label config on relay |
71
+ | `list_statuses.py` | (no args) | `{states, currentMapping, flydocsStatuses}` — provider workflow states and current mapping |
72
+ | `set_status_mapping.py` | `--auto \| --mapping '{"BACKLOG":"Backlog",...}' \| stdin` | `{success, mapping, matched, total}` — stores status mapping on relay |
73
+ | `set_identity.py` | `<provider> <provider-user-id>` | `{success, provider, providerId, meJson}` — binds provider user ID for `--mine` resolution; writes `.flydocs/me.json` |
74
+ | `set_preferences.py` | `[--workspace ID] [--assignee self\|ID] [--display JSON]` | `{success, preferences}` — no flags = GET current; with flags = POST update |
75
+ | `get_estimate_scale.py` | (no args) | `{scale, type}` — provider's valid estimate values (fixed or freeform) |
76
+ | `refresh_labels.py` | `[--fix]` | `{valid, stale, details}` — validates config label IDs against relay; `--fix` updates stale IDs |
77
+ | `generate_config.py` | `[--dry-run]` | `{success, configVersion, valid, missing[], warnings[]}` — pulls server config, merges with local, writes config.json |
78
+ | `get_me.py` | (no args) | `{success, displayName, email, provider, meJson}` — fetches user identity, writes `.flydocs/me.json` (no workspace) |
79
+ | `validate_setup.py` | (no args) | `{valid, checks, passed[], missing[], warnings[]}` — reads relay validation, caches result, sets setupComplete |
78
80
 
79
81
  ### Script Notes
80
82
 
81
83
  - **How it works**: Scripts call the FlyDocs Relay REST API, which translates to the provider (Linear, Jira) server-side. Same interface and output as before — the transport changed from direct GraphQL to managed REST. For Jira, the relay handles Markdown-to-ADF (Atlassian Document Format) conversion automatically.
84
+ - **Config cascade**: Setup scripts (`set_provider`, `set_team`, `set_labels`, `set_status_mapping`) only POST to the relay — they do not write local config. After all setup steps, run `generate_config.py` to pull the canonical server config and merge with local-only fields. This eliminates ghost fields and ensures config.json stays in sync with the relay.
82
85
  - **`list_issues.py --active`**: Returns all non-terminal issues (excludes Done, Archived, Canceled, Duplicate).
83
86
  - **`list_issues.py --status`**: Accepts comma-separated statuses: `--status READY,IMPLEMENTING,BLOCKED`
84
87
  - **`get_issue.py --fields basic`**: Skips comment fetch for faster responses.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Generate config.json from the relay's canonical workspace config.
3
+
4
+ Calls GET /config/generate which returns server-owned fields (workspace,
5
+ issueLabels, provider, statusMapping, etc.). Merges with local-only fields
6
+ (tier, detectedStack, skills, designSystem, aiLabor, paths) to produce
7
+ the final .flydocs/config.json.
8
+
9
+ This replaces the pattern where each setup script (set_provider, set_team,
10
+ set_labels, set_status_mapping) independently wrote to local config.
11
+ Now those scripts only POST to the relay, and this script pulls the
12
+ merged result once at the end of setup.
13
+
14
+ Usage:
15
+ python3 generate_config.py
16
+ python3 generate_config.py --dry-run # Print merged config without writing
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ sys.path.insert(0, str(Path(__file__).parent))
25
+ from flydocs_api import get_client, output_json, fail
26
+
27
+ # Fields owned by the server — overwritten from generate response
28
+ SERVER_OWNED_FIELDS = {
29
+ "workspaceId",
30
+ "setupComplete",
31
+ "workspace",
32
+ "issueLabels",
33
+ }
34
+
35
+ # Fields owned locally — never overwritten by generate
36
+ LOCAL_ONLY_FIELDS = {
37
+ "version",
38
+ "sourceRepo",
39
+ "tier",
40
+ "paths",
41
+ "detectedStack",
42
+ "skills",
43
+ "designSystem",
44
+ "aiLabor",
45
+ }
46
+
47
+
48
+ def main() -> None:
49
+ parser = argparse.ArgumentParser(description="Generate config from relay")
50
+ parser.add_argument(
51
+ "--dry-run",
52
+ action="store_true",
53
+ help="Print merged config without writing to disk",
54
+ )
55
+ args = parser.parse_args()
56
+
57
+ client = get_client()
58
+
59
+ # Fetch server-owned config from relay
60
+ response = client.get("/config/generate")
61
+
62
+ if not response.get("valid", False):
63
+ missing = response.get("missing", [])
64
+ warnings = response.get("warnings", [])
65
+ parts = []
66
+ if missing:
67
+ parts.append(f"missing: {', '.join(missing)}")
68
+ if warnings:
69
+ parts.append(f"warnings: {', '.join(warnings)}")
70
+ detail = "; ".join(parts) if parts else "unknown reason"
71
+ print(f"WARNING: Config not fully valid — {detail}", file=sys.stderr)
72
+
73
+ server_config = response.get("config", {})
74
+
75
+ # Read existing local config
76
+ config_path = client.config_path
77
+ if config_path.exists():
78
+ with open(config_path, "r") as f:
79
+ local_config = json.load(f)
80
+ else:
81
+ local_config = {}
82
+
83
+ # Merge: server-owned fields overwrite, local-only fields preserved
84
+ merged = dict(local_config)
85
+
86
+ # Apply server-owned fields
87
+ if "workspaceId" in server_config:
88
+ merged["workspaceId"] = server_config["workspaceId"]
89
+ if "setupComplete" in server_config:
90
+ merged["setupComplete"] = server_config["setupComplete"]
91
+ if "workspace" in server_config:
92
+ merged["workspace"] = server_config["workspace"]
93
+ if "issueLabels" in server_config:
94
+ merged["issueLabels"] = server_config["issueLabels"]
95
+
96
+ # Store configVersion for freshness tracking
97
+ if "configVersion" in response:
98
+ merged["configVersion"] = response["configVersion"]
99
+
100
+ # Clean up ghost fields that are now server-owned
101
+ # These were written by old setup scripts but aren't in the TS type system
102
+ for ghost in ("provider", "statusMapping", "labels"):
103
+ merged.pop(ghost, None)
104
+
105
+ if args.dry_run:
106
+ output_json(merged)
107
+ return
108
+
109
+ # Write merged config
110
+ config_path.parent.mkdir(parents=True, exist_ok=True)
111
+ with open(config_path, "w") as f:
112
+ json.dump(merged, f, indent=2)
113
+ f.write("\n")
114
+
115
+ output_json({
116
+ "success": True,
117
+ "configVersion": response.get("configVersion"),
118
+ "valid": response.get("valid", False),
119
+ "missing": response.get("missing", []),
120
+ "warnings": response.get("warnings", []),
121
+ })
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch current user identity from the FlyDocs Relay API.
3
+
4
+ Calls GET /auth/me (no X-Workspace needed — user-scoped endpoint).
5
+ Writes .flydocs/me.json for local identity resolution (--mine filters,
6
+ display name in comments, etc.). This file is gitignored.
7
+
8
+ Usage:
9
+ python3 get_me.py
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).parent))
18
+ from flydocs_api import find_project_root, output_json, fail
19
+
20
+
21
+ def load_api_key(project_root: Path) -> str | None:
22
+ """Load API key from environment or .env files."""
23
+ if os.environ.get("FLYDOCS_API_KEY"):
24
+ return os.environ["FLYDOCS_API_KEY"]
25
+ for name in [".env.local", ".env"]:
26
+ env_file = project_root / name
27
+ if env_file.exists():
28
+ with open(env_file, "r") as f:
29
+ for line in f:
30
+ line = line.strip()
31
+ if line.startswith("#") or "=" not in line:
32
+ continue
33
+ k, _, v = line.partition("=")
34
+ if k.strip() == "FLYDOCS_API_KEY":
35
+ v = v.strip().strip("\"'")
36
+ return v if v else None
37
+ return None
38
+
39
+
40
+ def resolve_base_url() -> str:
41
+ """Resolve relay base URL from environment."""
42
+ env_url = os.environ.get("FLYDOCS_RELAY_URL")
43
+ if env_url:
44
+ return env_url.rstrip("/")
45
+ return "https://app.flydocs.ai/api/relay"
46
+
47
+
48
+ def main() -> None:
49
+ import urllib.request
50
+ import urllib.error
51
+
52
+ project_root = find_project_root()
53
+ api_key = load_api_key(project_root)
54
+ if not api_key:
55
+ fail("FLYDOCS_API_KEY not found. Set in environment or .env/.env.local file")
56
+
57
+ base_url = resolve_base_url()
58
+ url = f"{base_url}/auth/me"
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {api_key}",
62
+ "Accept": "application/json",
63
+ }
64
+
65
+ try:
66
+ req = urllib.request.Request(url, headers=headers, method="GET")
67
+ with urllib.request.urlopen(req, timeout=15) as resp:
68
+ result = json.loads(resp.read().decode("utf-8"))
69
+ except urllib.error.HTTPError as e:
70
+ error_body = e.read().decode("utf-8") if e.fp else ""
71
+ try:
72
+ error_data = json.loads(error_body)
73
+ except json.JSONDecodeError:
74
+ error_data = {"error": error_body}
75
+ fail(f"API error ({e.code}): {error_data.get('error', 'Unknown')}")
76
+ except (urllib.error.URLError, TimeoutError):
77
+ fail("Network error: unable to reach relay API")
78
+
79
+ # Write me.json
80
+ me_data = {
81
+ "displayName": result.get("displayName"),
82
+ "email": result.get("email"),
83
+ "providerId": result.get("providerId"),
84
+ "provider": result.get("provider"),
85
+ "providerIdentities": result.get("providerIdentities", []),
86
+ "preferences": result.get("preferences", {}),
87
+ }
88
+
89
+ me_path = project_root / ".flydocs" / "me.json"
90
+ me_path.parent.mkdir(parents=True, exist_ok=True)
91
+ me_path.write_text(json.dumps(me_data, indent=2) + "\n")
92
+
93
+ output_json({
94
+ "success": True,
95
+ "displayName": me_data["displayName"],
96
+ "email": me_data["email"],
97
+ "provider": me_data["provider"],
98
+ "meJson": str(me_path),
99
+ })
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -3,8 +3,12 @@
3
3
 
4
4
  Binds the user's provider-specific ID to their FlyDocs user record.
5
5
  Once set, ?mine=true resolves via exact provider ID matching.
6
+
7
+ Also writes .flydocs/me.json for local identity resolution (e.g. --mine
8
+ in list_issues.py). This file is gitignored — each developer has their own.
6
9
  """
7
10
 
11
+ import json
8
12
  import sys
9
13
  from pathlib import Path
10
14
 
@@ -31,8 +35,20 @@ result = client.post("/auth/identity", {
31
35
  "providerId": provider_id,
32
36
  })
33
37
 
34
- output_json({
35
- "success": result.get("success", True),
38
+ # Write me.json for local identity resolution
39
+ me_data = {
36
40
  "provider": result.get("provider", provider),
37
41
  "providerId": result.get("providerId", provider_id),
42
+ "displayName": result.get("displayName"),
43
+ "email": result.get("email"),
44
+ }
45
+ me_path = Path(".flydocs/me.json")
46
+ me_path.parent.mkdir(parents=True, exist_ok=True)
47
+ me_path.write_text(json.dumps(me_data, indent=2) + "\n")
48
+
49
+ output_json({
50
+ "success": result.get("success", True),
51
+ "provider": me_data["provider"],
52
+ "providerId": me_data["providerId"],
53
+ "meJson": str(me_path),
38
54
  })
@@ -47,20 +47,6 @@ def main():
47
47
 
48
48
  client = get_client()
49
49
  result = client.post("/auth/labels", body)
50
-
51
- # Store label config in local config as reference
52
- config_path = client.config_path
53
- if config_path.exists():
54
- with open(config_path, "r") as f:
55
- config = json.load(f)
56
- config["labels"] = {
57
- "defaults": body.get("defaults", []),
58
- "typeMap": body.get("typeMap", {}),
59
- }
60
- with open(config_path, "w") as f:
61
- json.dump(config, f, indent=2)
62
- f.write("\n")
63
-
64
50
  output_json(result)
65
51
 
66
52
 
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """Set provider preference via the FlyDocs Relay API.
3
3
 
4
- Stores the provider type on the relay (for server-side routing)
5
- and updates the local config (for display/reference).
4
+ Stores the provider type on the relay for server-side routing.
5
+ Local config is updated by generate_config.py after all setup steps complete.
6
6
  """
7
7
 
8
8
  import argparse
9
- import json
10
9
  import sys
11
10
  from pathlib import Path
12
11
 
@@ -25,20 +24,6 @@ def main():
25
24
 
26
25
  client = get_client()
27
26
  result = client.post("/auth/provider", {"providerType": args.provider_type})
28
-
29
- # Update local config with provider type
30
- config_path = client.config_path
31
- if config_path.exists():
32
- with open(config_path, "r") as f:
33
- config = json.load(f)
34
- if "provider" not in config:
35
- config["provider"] = {"type": args.provider_type, "teamId": None}
36
- else:
37
- config["provider"]["type"] = args.provider_type
38
- with open(config_path, "w") as f:
39
- json.dump(config, f, indent=2)
40
- f.write("\n")
41
-
42
27
  output_json(result)
43
28
 
44
29
 
@@ -50,18 +50,6 @@ def main():
50
50
 
51
51
  client = get_client()
52
52
  result = client.post("/auth/statuses", body)
53
-
54
- # Store status mapping in local config as reference
55
- config_path = client.config_path
56
- if config_path.exists():
57
- with open(config_path, "r") as f:
58
- config = json.load(f)
59
- if isinstance(result.get("mapping"), dict):
60
- config["statusMapping"] = result["mapping"]
61
- with open(config_path, "w") as f:
62
- json.dump(config, f, indent=2)
63
- f.write("\n")
64
-
65
53
  output_json(result)
66
54
 
67
55
 
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env python3
2
2
  """Set team/project preference via the FlyDocs Relay API.
3
3
 
4
- Stores the team preference on the relay (for server-side scoping)
5
- and updates the local config (for display/reference).
4
+ Stores the team preference on the relay for server-side scoping.
5
+ Local config is updated by generate_config.py after all setup steps complete.
6
6
  For Jira, this sets the active Jira project.
7
7
  """
8
8
 
9
9
  import argparse
10
- import json
11
10
  import sys
12
11
  from pathlib import Path
13
12
 
@@ -22,19 +21,6 @@ def main():
22
21
 
23
22
  client = get_client()
24
23
  result = client.post("/auth/team", {"teamId": args.team_id})
25
-
26
- # Update local config with team ID
27
- config_path = client.config_path
28
- if config_path.exists():
29
- with open(config_path, "r") as f:
30
- config = json.load(f)
31
- if "provider" not in config:
32
- config["provider"] = {"type": None, "teamId": None}
33
- config["provider"]["teamId"] = args.team_id
34
- with open(config_path, "w") as f:
35
- json.dump(config, f, indent=2)
36
- f.write("\n")
37
-
38
24
  output_json(result)
39
25
 
40
26
 
@@ -12,6 +12,22 @@ from flydocs_api import list_issues
12
12
 
13
13
  TERMINAL_STATUSES = {"COMPLETE", "ARCHIVED", "CANCELED", "DUPLICATE"}
14
14
 
15
+
16
+ def resolve_identity() -> str:
17
+ """Resolve current user identity from me.json, falling back to $USER."""
18
+ me_path = Path(".flydocs/me.json")
19
+ if me_path.exists():
20
+ try:
21
+ me = json.loads(me_path.read_text())
22
+ # Prefer displayName for local file matching, fall back to email
23
+ name = me.get("displayName") or me.get("email") or ""
24
+ if name:
25
+ return name
26
+ except (json.JSONDecodeError, OSError):
27
+ pass
28
+ return os.environ.get("USER", os.environ.get("USERNAME", ""))
29
+
30
+
15
31
  parser = argparse.ArgumentParser(description="List issues")
16
32
  parser.add_argument("--status", default="")
17
33
  parser.add_argument("--active", action="store_true",
@@ -24,7 +40,7 @@ try:
24
40
  args = parser.parse_args()
25
41
  assignee = args.assignee
26
42
  if args.mine:
27
- assignee = os.environ.get("USER", os.environ.get("USERNAME", ""))
43
+ assignee = resolve_identity()
28
44
  result = list_issues(status=args.status.upper() if args.status else "", assignee=assignee, limit=args.limit)
29
45
  if args.active:
30
46
  result = [r for r in result if r.get("status") not in TERMINAL_STATUSES]
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.11",
2
+ "version": "0.6.0-alpha.12",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
4
  "tier": "local",
5
5
  "setupComplete": false,
@@ -205,6 +205,43 @@ def get_setup_nudge() -> str | None:
205
205
  return None
206
206
 
207
207
 
208
+ def get_config_freshness_nudge() -> str | None:
209
+ """Nudge if validation cache is stale (>24h old).
210
+
211
+ Only applies to cloud tier with setupComplete=true. Encourages
212
+ periodic re-validation so config stays in sync with the server.
213
+ """
214
+ config_file = Path('.flydocs/config.json')
215
+ if not config_file.exists():
216
+ return None
217
+ try:
218
+ config = json.loads(config_file.read_text())
219
+ # Only check freshness for cloud tier with completed setup
220
+ if config.get('tier') != 'cloud' or config.get('setupComplete') is not True:
221
+ return None
222
+
223
+ cache_file = Path('.flydocs/validation-cache.json')
224
+ if not cache_file.exists():
225
+ return '[Config not validated — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
226
+
227
+ from datetime import datetime, timezone
228
+ cache = json.loads(cache_file.read_text())
229
+ timestamp_str = cache.get('timestamp')
230
+ if not timestamp_str:
231
+ return None
232
+
233
+ # Parse ISO timestamp
234
+ cached_at = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
235
+ now = datetime.now(timezone.utc)
236
+ age_hours = (now - cached_at).total_seconds() / 3600
237
+
238
+ if age_hours > 24:
239
+ return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
240
+ except (json.JSONDecodeError, OSError, IOError, ValueError):
241
+ pass
242
+ return None
243
+
244
+
208
245
  def main() -> None:
209
246
  """Main hook execution."""
210
247
  debug_log('=== Hook invoked ===')
@@ -268,10 +305,14 @@ def main() -> None:
268
305
  if ac_progress:
269
306
  context_parts.append(ac_progress)
270
307
 
271
- # Setup completion nudge
308
+ # Setup completion nudge OR config freshness nudge (mutually exclusive)
272
309
  setup_nudge = get_setup_nudge()
273
310
  if setup_nudge:
274
311
  context_parts.append(setup_nudge)
312
+ else:
313
+ freshness_nudge = get_config_freshness_nudge()
314
+ if freshness_nudge:
315
+ context_parts.append(freshness_nudge)
275
316
 
276
317
  # FlyDocs version
277
318
  version = get_flydocs_version()
@@ -1 +1 @@
1
- 0.6.0-alpha.11
1
+ 0.6.0-alpha.12
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.11",
2
+ "version": "0.6.0-alpha.12",
3
3
  "description": "FlyDocs Core - Manifest of all managed files",
4
4
  "repository": "github.com/plastrlab/flydocs-core",
5
5